Skip to content

Rule: 'no-sample-code' with --fix mapped to remove it #42

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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Then configure the rules you want to use under the rules section.
| [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-sample-code](docs/rules/no-sample-code.md) | Disallow sample code snippets from the Obsidian plugin template. | ✅ | 🔧 |
| [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. | ✅ | |
Expand Down
7 changes: 7 additions & 0 deletions docs/rules/no-sample-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Disallow sample code snippets from the Obsidian plugin template (`obsidianmd/no-sample-code`)

💼 This rule is enabled in the ✅ `recommended` config.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import commands from "./lib/rules/commands.ts";
import detachLeaves from "./lib/rules/detachLeaves.ts";
import hardcodedConfigPath from "./lib/rules/hardcodedConfigPath.ts";
import noSampleCode from "./lib/rules/noSampleCode.ts";
import noTFileTFolderCast from "./lib/rules/noTFileTFolderCast.ts";
import noViewReferencesInPlugin from "./lib/rules/noViewReferencesInPlugin.ts";
import objectAssign from "./lib/rules/objectAssign.ts";
Expand All @@ -27,6 +28,7 @@ export default [
commands: commands,
"detach-leaves": detachLeaves,
"hardcoded-config-path": hardcodedConfigPath,
"no-sample-code": noSampleCode,
"no-tfile-tfolder-cast": noTFileTFolderCast,
"no-view-references-in-plugin": noViewReferencesInPlugin,
"object-assign": objectAssign,
Expand All @@ -42,6 +44,7 @@ export default [
"obsidianmd/commands": "error",
"obsidianmd/detach-leaves": "error",
"obsidianmd/hardcoded-config-path": "error",
"obsidianmd/no-sample-code": "error",
"obsidianmd/no-tfile-tfolder-cast": "error",
"obsidianmd/no-view-references-in-plugin": "error",
"obsidianmd/object-assign": "error",
Expand Down
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import commands from "./rules/commands.js";
import detachLeaves from "./rules/detachLeaves.js";
import hardcodedConfigPath from "./rules/hardcodedConfigPath.js";
import noSampleCode from "./rules/noSampleCode.js";
import noTFileTFolderCast from "./rules/noTFileTFolderCast.js";
import noViewReferencesInPlugin from "./rules/noViewReferencesInPlugin.js";
import objectAssign from "./rules/objectAssign.js";
Expand All @@ -20,6 +21,7 @@ export default {
commands: commands,
"detach-leaves": detachLeaves,
"hardcoded-config-path": hardcodedConfigPath,
"no-sample-code": noSampleCode,
"no-tfile-tfolder-cast": noTFileTFolderCast,
"no-view-references-in-plugin": noViewReferencesInPlugin,
"object-assign": objectAssign,
Expand Down Expand Up @@ -146,6 +148,7 @@ export default {
"obsidianmd/commands": "error",
"obsidianmd/detach-leaves": "error",
"obsidianmd/hardcoded-config-path": "error",
"obsidianmd/no-sample-code": "error",
"obsidianmd/no-tfile-tfolder-cast": "error",
"obsidianmd/no-view-references-in-plugin": "error",
"obsidianmd/object-assign": "error",
Expand Down
149 changes: 149 additions & 0 deletions lib/rules/noSampleCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { TSESLint, TSESTree } from "@typescript-eslint/utils";

/**
* Checks if a node is the sample `registerInterval` call.
* `this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));`
*/
function isSampleIntervalCall(node: TSESTree.CallExpression): boolean {
// Check for `this.registerInterval(...)`
if (
node.callee.type !== "MemberExpression" ||
node.callee.object.type !== "ThisExpression" ||
node.callee.property.type !== "Identifier" ||
node.callee.property.name !== "registerInterval"
) {
return false;
}

// Check for `window.setInterval(...)` as the first argument
const setIntervalCall = node.arguments[0];
if (
!setIntervalCall ||
setIntervalCall.type !== "CallExpression" ||
setIntervalCall.callee.type !== "MemberExpression" ||
(setIntervalCall.callee.object as TSESTree.Identifier)?.name !==
"window" ||
(setIntervalCall.callee.property as TSESTree.Identifier)?.name !==
"setInterval"
) {
return false;
}

// Check for `() => console.log('setInterval')` as the first argument to setInterval
const callback = setIntervalCall.arguments[0];
if (
!callback ||
callback.type !== "ArrowFunctionExpression" ||
callback.body.type !== "CallExpression" ||
(callback.body.callee as TSESTree.MemberExpression)?.property.type !==
"Identifier" ||
(
(callback.body.callee as TSESTree.MemberExpression)
?.property as TSESTree.Identifier
)?.name !== "log" ||
(callback.body.arguments[0] as TSESTree.Literal)?.value !==
"setInterval"
) {
return false;
}

return true;
}

/**
* Checks if a node is the sample `registerDomEvent` call.
* `this.registerDomEvent(document, 'click', (evt: MouseEvent) => { console.log('click', evt); });`
*/
function isSampleDomEventCall(node: TSESTree.CallExpression): boolean {
// Check for `this.registerDomEvent(...)`
if (
node.callee.type !== "MemberExpression" ||
node.callee.object.type !== "ThisExpression" ||
node.callee.property.type !== "Identifier" ||
node.callee.property.name !== "registerDomEvent"
) {
return false;
}

// Check for `document` and `'click'` as the first two arguments
if (
(node.arguments[0] as TSESTree.Identifier)?.name !== "document" ||
(node.arguments[1] as TSESTree.Literal)?.value !== "click"
) {
return false;
}

// Check for the specific callback function
const callback = node.arguments[2];
if (
!callback ||
callback.type !== "ArrowFunctionExpression" ||
callback.body.type !== "BlockStatement"
) {
return false;
}

const firstStatement = callback.body.body[0];
if (
!firstStatement ||
firstStatement.type !== "ExpressionStatement" ||
firstStatement.expression.type !== "CallExpression" ||
(firstStatement.expression.callee as TSESTree.MemberExpression)
?.property.type !== "Identifier" ||
(
(firstStatement.expression.callee as TSESTree.MemberExpression)
?.property as TSESTree.Identifier
)?.name !== "log" ||
(firstStatement.expression.arguments[0] as TSESTree.Literal)?.value !==
"click"
) {
return false;
}

return true;
}

export default {
name: "no-sample-code",
meta: {
type: "problem" as const,
docs: {
description:
"Disallow sample code snippets from the Obsidian plugin template.",
recommended: true,
},
schema: [],
messages: {
removeSampleInterval:
"Remove the sample `registerInterval` call from the plugin template.",
removeSampleDomEvent:
"Remove the sample `registerDomEvent` call from the plugin template.",
},
fixable: "code" as const,
},
defaultOptions: [],
create(
context: TSESLint.RuleContext<
"removeSampleInterval" | "removeSampleDomEvent",
[]
>,
) {
return {
CallExpression(node: TSESTree.CallExpression) {
if (isSampleIntervalCall(node)) {
context.report({
node,
messageId: "removeSampleInterval",
fix: (fixer) => fixer.remove(node.parent), // Remove the entire ExpressionStatement
});
} else if (isSampleDomEventCall(node)) {
context.report({
node,
messageId: "removeSampleDomEvent",
fix: (fixer) => fixer.remove(node.parent), // Remove the entire ExpressionStatement
});
}
},
};
},
};
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 "./noSampleCode.test";
import "./noViewReferencesInPlugin.test";
33 changes: 33 additions & 0 deletions tests/noSampleCode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { RuleTester } from "@typescript-eslint/rule-tester";
import noSampleCode from "../lib/rules/noSampleCode.js";

const ruleTester = new RuleTester();

ruleTester.run("no-sample-code", noSampleCode, {
valid: [
// Valid: A different, legitimate registerInterval call
{
code: "this.registerInterval(window.setInterval(() => this.doSomething(), 1000));",
},
// Valid: A different, legitimate registerDomEvent call
{
code: "this.registerDomEvent(this.containerEl, 'click', () => this.onClick());",
},
],
invalid: [
// Invalid: The sample setInterval call
{
code: "this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));",
errors: [{ messageId: "removeSampleInterval" }],
output: "", // The auto-fix should remove the entire line
},
// Invalid: The sample registerDomEvent call
{
code: `this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
});`,
errors: [{ messageId: "removeSampleDomEvent" }],
output: "", // The auto-fix should remove the entire block
},
],
});