diff --git a/README.md b/README.md index 98bcd0f..f288675 100644 --- a/README.md +++ b/README.md @@ -23,25 +23,20 @@ Add `obsidianmd` to the plugins section of your `.eslintrc` configuration file. ```json { - "plugins": [ - "obsidianmd" - ] + "plugins": ["obsidianmd"] } ``` - Then configure the rules you want to use under the rules section. ```json { - "rules": { - "obsidian/rule-name": 2 - } + "rules": { + "obsidian/rule-name": 2 + } } ``` - - ## Configurations @@ -52,8 +47,6 @@ Then configure the rules you want to use under the rules section. - - ## Rules @@ -62,18 +55,19 @@ Then configure the rules you want to use under the rules section. ✅ Set in the `recommended` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name                         | Description | 💼 | 🔧 | -| :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | :- | :- | -| [commands](docs/rules/commands.md) | Command guidelines | ✅ | | -| [detach-leaves](docs/rules/detach-leaves.md) | Don't detach leaves in onunload. | ✅ | 🔧 | -| [hardcoded-config-path](docs/rules/hardcoded-config-path.md) | test | ✅ | | -| [no-tfile-tfolder-cast](docs/rules/no-tfile-tfolder-cast.md) | Disallow type casting to TFile or TFolder, suggesting instanceof checks instead. | ✅ | | -| [no-view-references-in-plugin](docs/rules/no-view-references-in-plugin.md) | Disallow storing references to custom views directly in the plugin, which can cause memory leaks. | ✅ | | -| [object-assign](docs/rules/object-assign.md) | Object.assign with two parameters instead of 3. | ✅ | | -| [platform](docs/rules/platform.md) | Disallow use of navigator API for OS detection | ✅ | | -| [regex-lookbehind](docs/rules/regex-lookbehind.md) | Using lookbehinds in Regex is not supported in some iOS versions | ✅ | | -| [sample-names](docs/rules/sample-names.md) | Rename sample plugin class names | ✅ | | -| [settings-tab](docs/rules/settings-tab.md) | Discourage common anti-patterns in plugin settings tabs. | ✅ | 🔧 | -| [vault-iterate](docs/rules/vault-iterate.md) | Avoid iterating all files to find a file by its path
| ✅ | 🔧 | +| Name                          | Description | 💼 | 🔧 | +| :--------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- | :- | :- | +| [commands](docs/rules/commands.md) | Command guidelines | ✅ | | +| [detach-leaves](docs/rules/detach-leaves.md) | Don't detach leaves in onunload. | ✅ | 🔧 | +| [hardcoded-config-path](docs/rules/hardcoded-config-path.md) | test | ✅ | | +| [no-tfile-tfolder-cast](docs/rules/no-tfile-tfolder-cast.md) | Disallow type casting to TFile or TFolder, suggesting instanceof checks instead. | ✅ | | +| [no-view-references-in-plugin](docs/rules/no-view-references-in-plugin.md) | Disallow storing references to custom views directly in the plugin, which can cause memory leaks. | ✅ | | +| [object-assign](docs/rules/object-assign.md) | Object.assign with two parameters instead of 3. | ✅ | | +| [platform](docs/rules/platform.md) | Disallow use of navigator API for OS detection | ✅ | | +| [prefer-abstract-input-suggest](docs/rules/prefer-abstract-input-suggest.md) | Disallow Liam's frequently copied `TextInputSuggest` implementation in favor of the built-in `AbstractInputSuggest`. | ✅ | | +| [regex-lookbehind](docs/rules/regex-lookbehind.md) | Using lookbehinds in Regex is not supported in some iOS versions | ✅ | | +| [sample-names](docs/rules/sample-names.md) | Rename sample plugin class names | ✅ | | +| [settings-tab](docs/rules/settings-tab.md) | Discourage common anti-patterns in plugin settings tabs. | ✅ | 🔧 | +| [vault-iterate](docs/rules/vault-iterate.md) | Avoid iterating all files to find a file by its path
| ✅ | 🔧 | diff --git a/docs/rules/prefer-abstract-input-suggest.md b/docs/rules/prefer-abstract-input-suggest.md new file mode 100644 index 0000000..f9d9e35 --- /dev/null +++ b/docs/rules/prefer-abstract-input-suggest.md @@ -0,0 +1,5 @@ +# Disallow Liam's frequently copied `TextInputSuggest` implementation in favor of the built-in `AbstractInputSuggest` (`obsidianmd/prefer-abstract-input-suggest`) + +💼 This rule is enabled in the ✅ `recommended` config. + + diff --git a/eslint.config.mjs b/eslint.config.mjs index 1f3ba0a..bfa9844 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,6 +5,7 @@ import noTFileTFolderCast from "./lib/rules/noTFileTFolderCast.ts"; import noViewReferencesInPlugin from "./lib/rules/noViewReferencesInPlugin.ts"; import objectAssign from "./lib/rules/objectAssign.ts"; import platform from "./lib/rules/platform.ts"; +import preferAbstractInputSuggest from "./lib/rules/preferAbstractInputSuggest.ts"; import regexLookbehind from "./lib/rules/regexLookbehind.ts"; import sampleNames from "./lib/rules/sampleNames.ts"; import settingsTab from "./lib/rules/settingsTab.ts"; @@ -31,6 +32,7 @@ export default [ "no-view-references-in-plugin": noViewReferencesInPlugin, "object-assign": objectAssign, platform: platform, + "prefer-abstract-input-suggest": preferAbstractInputSuggest, "regex-lookbehind": regexLookbehind, "sample-names": sampleNames, "settings-tab": settingsTab, @@ -46,6 +48,7 @@ export default [ "obsidianmd/no-view-references-in-plugin": "error", "obsidianmd/object-assign": "error", "obsidianmd/platform": "error", + "obsidianmd/prefer-abstract-input-suggest": "error", "obsidianmd/regex-lookbehind": "error", "obsidianmd/sample-names": "error", "obsidianmd/settings-tab": "error", diff --git a/lib/index.ts b/lib/index.ts index fc11b05..e5a6500 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -5,6 +5,7 @@ import noTFileTFolderCast from "./rules/noTFileTFolderCast.js"; import noViewReferencesInPlugin from "./rules/noViewReferencesInPlugin.js"; import objectAssign from "./rules/objectAssign.js"; import platform from "./rules/platform.js"; +import preferAbstractInputSuggest from "./rules/preferAbstractInputSuggest.js"; import regexLookbehind from "./rules/regexLookbehind.js"; import sampleNames from "./rules/sampleNames.js"; import settingsTab from "./rules/settingsTab.js"; @@ -24,6 +25,7 @@ export default { "no-view-references-in-plugin": noViewReferencesInPlugin, "object-assign": objectAssign, platform: platform, + "prefer-abstract-input-suggest": preferAbstractInputSuggest, "regex-lookbehind": regexLookbehind, "sample-names": sampleNames, "settings-tab": settingsTab, @@ -150,6 +152,7 @@ export default { "obsidianmd/no-view-references-in-plugin": "error", "obsidianmd/object-assign": "error", "obsidianmd/platform": "error", + "obsidianmd/prefer-abstract-input-suggest": "error", "obsidianmd/regex-lookbehind": "error", "obsidianmd/sample-names": "error", "obsidianmd/settings-tab": "error", diff --git a/lib/rules/preferAbstractInputSuggest.ts b/lib/rules/preferAbstractInputSuggest.ts new file mode 100644 index 0000000..091e3b9 --- /dev/null +++ b/lib/rules/preferAbstractInputSuggest.ts @@ -0,0 +1,87 @@ +import { TSESLint, TSESTree } from "@typescript-eslint/utils"; + +export default { + name: "no-deprecated-text-input-suggest", + meta: { + type: "suggestion" as const, + docs: { + description: + "Disallow Liam's frequently copied `TextInputSuggest` implementation in favor of the built-in `AbstractInputSuggest`.", + recommended: true, + }, + schema: [], + messages: { + preferAbstractInputSuggest: + "This appears to be a custom `TextInputSuggest` implementation. Please use the built-in `AbstractInputSuggest` API instead.", + }, + }, + defaultOptions: [], + create( + context: TSESLint.RuleContext<"preferAbstractInputSuggest", []>, + ): TSESLint.RuleListener { + return { + // We start by looking for any call to a function named `createPopper`. + "CallExpression[callee.name='createPopper']"( + node: TSESTree.CallExpression, + ) { + // The options object is the 3rd argument. + const options = node.arguments[2]; + if (!options || options.type !== "ObjectExpression") { + return; + } + + // Find the `modifiers` property within the options. + const modifiersProp = options.properties.find( + ( + prop, + ): prop is TSESTree.Property & { + key: TSESTree.Identifier; + } => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "modifiers", + ); + + if ( + !modifiersProp || + modifiersProp.value.type !== "ArrayExpression" + ) { + return; + } + + // Check if any modifier in the array has the name "sameWidth". + const hasSameWidthModifier = modifiersProp.value.elements.some( + (element) => { + if (!element || element.type !== "ObjectExpression") { + return false; + } + // Find the `name` property of the modifier object. + const nameProp = element.properties.find( + ( + prop, + ): prop is TSESTree.Property & { + key: TSESTree.Identifier; + } => + prop.type === "Property" && + prop.key.type === "Identifier" && + prop.key.name === "name", + ); + // Check if its value is the literal string "sameWidth". + return ( + nameProp && + nameProp.value.type === "Literal" && + nameProp.value.value === "sameWidth" + ); + }, + ); + + if (hasSameWidthModifier) { + context.report({ + node, + messageId: "preferAbstractInputSuggest", + }); + } + }, + }; + }, +}; diff --git a/tests/all-rules.test.ts b/tests/all-rules.test.ts index 231d049..14ff666 100644 --- a/tests/all-rules.test.ts +++ b/tests/all-rules.test.ts @@ -10,4 +10,5 @@ import "./hardcodedConfigPath.test"; import "./vaultIterate.test"; import "./detachLeaves.test"; import "./noTFileTFolderCast.test"; +import "./preferAbstractInputSuggest.test"; import "./noViewReferencesInPlugin.test"; diff --git a/tests/preferAbstractInputSuggest.test.ts b/tests/preferAbstractInputSuggest.test.ts new file mode 100644 index 0000000..881f0b4 --- /dev/null +++ b/tests/preferAbstractInputSuggest.test.ts @@ -0,0 +1,52 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import noDeprecatedSuggestRule from "../lib/rules/preferAbstractInputSuggest.js"; + +const ruleTester = new RuleTester(); + +ruleTester.run("no-deprecated-text-input-suggest", noDeprecatedSuggestRule, { + valid: [ + // Valid: A standard popperjs call without the custom modifier. + { + code: ` + import { createPopper } from '@popperjs/core'; + createPopper(button, tooltip, { + placement: 'top', + }); + `, + }, + // Valid: A popperjs call with other modifiers. + { + code: ` + import { createPopper } from '@popperjs/core'; + createPopper(button, tooltip, { + modifiers: [{ name: 'offset', options: { offset: [0, 8] } }], + }); + `, + }, + // Valid: A call to a different function. + { + code: "someOtherFunction();", + }, + ], + invalid: [ + // Invalid: The exact pattern from the deprecated implementation. + { + code: ` + import { createPopper } from '@popperjs/core'; + createPopper(inputEl, suggestEl, { + placement: "bottom-start", + modifiers: [ + { + name: "sameWidth", + enabled: true, + fn: () => {}, + phase: "beforeWrite", + requires: ["computeStyles"], + }, + ], + }); + `, + errors: [{ messageId: "preferAbstractInputSuggest" }], + }, + ], +});