From 7a514f87670dbb2fdd25d4d16a8dc45bc36d520a Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Sun, 8 Jun 2025 20:26:04 +0200 Subject: [PATCH 1/4] Detect Liam's implementation using fingerprinting --- eslint.config.mjs | 3 + lib/index.ts | 16 ++--- lib/rules/preferAbstractInputSuggest.ts | 86 ++++++++++++++++++++++++ tests/all-rules.test.ts | 1 + tests/preferAbstractInputSuggest.test.ts | 52 ++++++++++++++ 5 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 lib/rules/preferAbstractInputSuggest.ts create mode 100644 tests/preferAbstractInputSuggest.test.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index b037f57..0dadff2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,6 +4,7 @@ import hardcodedConfigPath from "./lib/rules/hardcodedConfigPath.ts"; import noTFileTFolderCast from "./lib/rules/noTFileTFolderCast.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"; @@ -29,6 +30,7 @@ export default [ "no-tfile-tfolder-cast": noTFileTFolderCast, "object-assign": objectAssign, platform: platform, + "prefer-abstract-input-suggest": preferAbstractInputSuggest, "regex-lookbehind": regexLookbehind, "sample-names": sampleNames, "settings-tab": settingsTab, @@ -43,6 +45,7 @@ export default [ "obsidianmd/no-tfile-tfolder-cast": "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 489d7e2..3e59792 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -4,6 +4,7 @@ import hardcodedConfigPath from "./rules/hardcodedConfigPath.js"; import noTFileTFolderCast from "./rules/noTFileTFolderCast.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"; @@ -22,6 +23,7 @@ export default { "no-tfile-tfolder-cast": noTFileTFolderCast, "object-assign": objectAssign, platform: platform, + "prefer-abstract-input-suggest": preferAbstractInputSuggest, "regex-lookbehind": regexLookbehind, "sample-names": sampleNames, "settings-tab": settingsTab, @@ -91,6 +93,11 @@ export default { message: "Use the built-in `requestUrl` function instead of `node-fetch`.", }, + { + name: "moment", + message: + "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.", + }, ], "no-alert": "error", "no-undef": "error", @@ -135,14 +142,6 @@ export default { "import/no-nodejs-modules": manifest && manifest.isDesktopOnly ? "off" : "error", "import/no-extraneous-dependencies": "error", - "no-restricted-imports": [ - "error", - { - name: "moment", - message: - "The 'moment' package is bundled with Obsidian. Please import it from 'obsidian' instead.", - }, - ], "obsidianmd/commands": "error", "obsidianmd/detach-leaves": "error", @@ -150,6 +149,7 @@ export default { "obsidianmd/no-tfile-tfolder-cast": "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..fd8a437 --- /dev/null +++ b/lib/rules/preferAbstractInputSuggest.ts @@ -0,0 +1,86 @@ +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: { + useAbstractInputSuggest: + "This appears to be a custom `TextInputSuggest` implementation. Please use the built-in `AbstractInputSuggest` API instead.", + }, + }, + defaultOptions: [], + create( + context: TSESLint.RuleContext<"useAbstractInputSuggest", []>, + ): 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?.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?.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?.value.type === "Literal" && + nameProp.value.value === "sameWidth" + ); + }, + ); + + if (hasSameWidthModifier) { + context.report({ + node, + messageId: "useAbstractInputSuggest", + }); + } + }, + }; + }, +}; diff --git a/tests/all-rules.test.ts b/tests/all-rules.test.ts index 9153c65..14c4feb 100644 --- a/tests/all-rules.test.ts +++ b/tests/all-rules.test.ts @@ -10,3 +10,4 @@ import "./hardcodedConfigPath.test"; import "./vaultIterate.test"; import "./detachLeaves.test"; import "./noTFileTFolderCast.test"; +import "./preferAbstractInputSuggest.test"; diff --git a/tests/preferAbstractInputSuggest.test.ts b/tests/preferAbstractInputSuggest.test.ts new file mode 100644 index 0000000..1963199 --- /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: "useAbstractInputSuggest" }], + }, + ], +}); From b8bcef4bf2ecbdbf1d8d08dcf8484399876e1f58 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Sun, 8 Jun 2025 20:28:16 +0200 Subject: [PATCH 2/4] Regenerated documentation --- README.md | 25 +++++++++++---------- docs/rules/prefer-abstract-input-suggest.md | 5 +++++ 2 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 docs/rules/prefer-abstract-input-suggest.md diff --git a/README.md b/README.md index c95c558..4a04acd 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,18 @@ 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. | ✅ | | -| [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. | ✅ | | +| [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 the deprecated `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..7240186 --- /dev/null +++ b/docs/rules/prefer-abstract-input-suggest.md @@ -0,0 +1,5 @@ +# Disallow the deprecated `TextInputSuggest` implementation in favor of the built-in `AbstractInputSuggest` (`obsidianmd/prefer-abstract-input-suggest`) + +💼 This rule is enabled in the ✅ `recommended` config. + + From 5812c200200d577e740a8e2d6f2b920abae07c17 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Sun, 8 Jun 2025 21:47:26 +0200 Subject: [PATCH 3/4] Cleaned up rule --- lib/rules/preferAbstractInputSuggest.ts | 13 +++++++------ tests/preferAbstractInputSuggest.test.ts | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/rules/preferAbstractInputSuggest.ts b/lib/rules/preferAbstractInputSuggest.ts index fd8a437..091e3b9 100644 --- a/lib/rules/preferAbstractInputSuggest.ts +++ b/lib/rules/preferAbstractInputSuggest.ts @@ -11,13 +11,13 @@ export default { }, schema: [], messages: { - useAbstractInputSuggest: + preferAbstractInputSuggest: "This appears to be a custom `TextInputSuggest` implementation. Please use the built-in `AbstractInputSuggest` API instead.", }, }, defaultOptions: [], create( - context: TSESLint.RuleContext<"useAbstractInputSuggest", []>, + context: TSESLint.RuleContext<"preferAbstractInputSuggest", []>, ): TSESLint.RuleListener { return { // We start by looking for any call to a function named `createPopper`. @@ -26,7 +26,7 @@ export default { ) { // The options object is the 3rd argument. const options = node.arguments[2]; - if (options?.type !== "ObjectExpression") { + if (!options || options.type !== "ObjectExpression") { return; } @@ -52,7 +52,7 @@ export default { // Check if any modifier in the array has the name "sameWidth". const hasSameWidthModifier = modifiersProp.value.elements.some( (element) => { - if (element?.type !== "ObjectExpression") { + if (!element || element.type !== "ObjectExpression") { return false; } // Find the `name` property of the modifier object. @@ -68,7 +68,8 @@ export default { ); // Check if its value is the literal string "sameWidth". return ( - nameProp?.value.type === "Literal" && + nameProp && + nameProp.value.type === "Literal" && nameProp.value.value === "sameWidth" ); }, @@ -77,7 +78,7 @@ export default { if (hasSameWidthModifier) { context.report({ node, - messageId: "useAbstractInputSuggest", + messageId: "preferAbstractInputSuggest", }); } }, diff --git a/tests/preferAbstractInputSuggest.test.ts b/tests/preferAbstractInputSuggest.test.ts index 1963199..881f0b4 100644 --- a/tests/preferAbstractInputSuggest.test.ts +++ b/tests/preferAbstractInputSuggest.test.ts @@ -46,7 +46,7 @@ ruleTester.run("no-deprecated-text-input-suggest", noDeprecatedSuggestRule, { ], }); `, - errors: [{ messageId: "useAbstractInputSuggest" }], + errors: [{ messageId: "preferAbstractInputSuggest" }], }, ], }); From 815ea169c58f9e76c2367ee8c467f6f1b1799db2 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Sun, 8 Jun 2025 21:47:35 +0200 Subject: [PATCH 4/4] Regenerated documentation --- README.md | 26 ++++++++++----------- docs/rules/prefer-abstract-input-suggest.md | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4a04acd..a3ae33f 100644 --- a/README.md +++ b/README.md @@ -62,18 +62,18 @@ 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. | ✅ | | -| [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 the deprecated `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
| ✅ | 🔧 | +| 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. | ✅ | | +| [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 index 7240186..f9d9e35 100644 --- a/docs/rules/prefer-abstract-input-suggest.md +++ b/docs/rules/prefer-abstract-input-suggest.md @@ -1,4 +1,4 @@ -# Disallow the deprecated `TextInputSuggest` implementation in favor of the built-in `AbstractInputSuggest` (`obsidianmd/prefer-abstract-input-suggest`) +# 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.