Skip to content

Rule: detect usage of Liam's implementation using fingerprinting #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 18 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- begin auto-generated configs list -->
Expand All @@ -52,8 +47,6 @@ Then configure the rules you want to use under the rules section.

<!-- end auto-generated configs list -->



## Rules

<!-- begin auto-generated rules list -->
Expand All @@ -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<br/> | ✅ | 🔧 |
| 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<br/> | ✅ | 🔧 |

<!-- end auto-generated rules list -->
5 changes: 5 additions & 0 deletions docs/rules/prefer-abstract-input-suggest.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- end auto-generated rule header -->
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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",
Expand Down
87 changes: 87 additions & 0 deletions lib/rules/preferAbstractInputSuggest.ts
Original file line number Diff line number Diff line change
@@ -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",
});
}
},
};
},
};
1 change: 1 addition & 0 deletions tests/all-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import "./hardcodedConfigPath.test";
import "./vaultIterate.test";
import "./detachLeaves.test";
import "./noTFileTFolderCast.test";
import "./preferAbstractInputSuggest.test";
import "./noViewReferencesInPlugin.test";
52 changes: 52 additions & 0 deletions tests/preferAbstractInputSuggest.test.ts
Original file line number Diff line number Diff line change
@@ -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" }],
},
],
});