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" }],
+ },
+ ],
+});