From 4308736bf3133b6f74661abc31753525705592c3 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 6 Jun 2025 19:47:09 +0200 Subject: [PATCH 1/4] Rule: 'no-static-styles-assignment' --- eslint.config.mjs | 3 + lib/index.ts | 3 + lib/rules/noStaticStylesAssignment.ts | 116 +++++++++++++++++++++++++ tests/all-rules.test.ts | 1 + tests/noStaticStylesAssignment.test.ts | 73 ++++++++++++++++ 5 files changed, 196 insertions(+) create mode 100644 lib/rules/noStaticStylesAssignment.ts create mode 100644 tests/noStaticStylesAssignment.test.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index b751ed4..7218994 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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 noStaticStylesAssignment from "./lib/rules/noStaticStylesAssignment.ts"; import objectAssign from "./lib/rules/objectAssign.ts"; import platform from "./lib/rules/platform.ts"; import regexLookbehind from "./lib/rules/regexLookbehind.ts"; @@ -25,6 +26,7 @@ export default [ commands: commands, "detach-leaves": detachLeaves, "hardcoded-config-path": hardcodedConfigPath, + "no-static-styles-assignment": noStaticStylesAssignment, "object-assign": objectAssign, platform: platform, "regex-lookbehind": regexLookbehind, @@ -38,6 +40,7 @@ export default [ "obsidianmd/commands": "error", "obsidianmd/detach-leaves": "error", "obsidianmd/hardcoded-config-path": "error", + "obsidianmd/no-static-styles-assignment": "error", "obsidianmd/object-assign": "error", "obsidianmd/platform": "error", "obsidianmd/regex-lookbehind": "error", diff --git a/lib/index.ts b/lib/index.ts index 49d4355..1cf2297 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,6 +1,7 @@ import commands from "./rules/commands.js"; import detachLeaves from "./rules/detachLeaves.js"; import hardcodedConfigPath from "./rules/hardcodedConfigPath.js"; +import noStaticStylesAssignment from "./rules/noStaticStylesAssignment.js"; import objectAssign from "./rules/objectAssign.js"; import platform from "./rules/platform.js"; import regexLookbehind from "./rules/regexLookbehind.js"; @@ -18,6 +19,7 @@ export default { commands: commands, "detach-leaves": detachLeaves, "hardcoded-config-path": hardcodedConfigPath, + "no-static-styles-assignment": noStaticStylesAssignment, "object-assign": objectAssign, platform: platform, "regex-lookbehind": regexLookbehind, @@ -103,6 +105,7 @@ export default { "obsidianmd/hardcoded-config-path": "error", "obsidianmd/no-document-write": "error", "obsidianmd/no-inner-html": "error", + "obsidianmd/no-static-styles-assignment": "error", "obsidianmd/object-assign": "error", "obsidianmd/platform": "error", "obsidianmd/regex-lookbehind": "error", diff --git a/lib/rules/noStaticStylesAssignment.ts b/lib/rules/noStaticStylesAssignment.ts new file mode 100644 index 0000000..8df1bdb --- /dev/null +++ b/lib/rules/noStaticStylesAssignment.ts @@ -0,0 +1,116 @@ +import { TSESLint, TSESTree } from "@typescript-eslint/utils"; + +// This rule will flag: +// +// - element.style.color = 'red' +// - element.style.setProperty('color', 'red') +// - element.style.cssText = 'color: red;' +// - element.setAttribute('style', 'color: red;') +// +// This rule will not flag: +// +// - element.style.width = myWidth; (assignment from a variable) +// - element.style.transform = \translateX(${offset}px)`;` (assignment from a template literal with expressions) + +// Checks if a node is a MemberExpression accessing the 'style' property. +// e.g., `el.style` or `this.containerEl.style` +function isStyleMemberExpression( + node: TSESTree.Node, +): node is TSESTree.MemberExpression { + return ( + node.type === "MemberExpression" && + !node.computed && + node.property.type === "Identifier" && + node.property.name === "style" + ); +} + +export default { + name: "no-static-styles-assignment", + meta: { + type: "suggestion" as const, + docs: { + description: + "Disallow setting styles directly on DOM elements, favoring CSS classes instead.", + recommended: true, + }, + schema: [], + messages: { + avoidStyleAssignment: + "Avoid setting styles directly via `{{property}}`. Use CSS classes for better theming and maintainability.", + }, + }, + defaultOptions: [], + create( + context: TSESLint.RuleContext<"avoidStyleAssignment", []>, + ): TSESLint.RuleListener { + return { + // Catches `el.style.color = 'red'` and `el.style.cssText = '...'` + AssignmentExpression(node: TSESTree.AssignmentExpression) { + const left = node.left; + // We only care about static assignments (literals) + if (node.right.type !== "Literal") { + return; + } + + if ( + left.type === "MemberExpression" && + isStyleMemberExpression(left.object) + ) { + context.report({ + node, + messageId: "avoidStyleAssignment", + data: { + property: `element.style.${(left.property as TSESTree.Identifier).name}`, + }, + }); + } + }, + + // Catches `el.style.setProperty(...)` and `el.setAttribute('style', ...)` + CallExpression(node: TSESTree.CallExpression) { + const callee = node.callee; + if (callee.type !== "MemberExpression") { + return; + } + + const propertyName = (callee.property as TSESTree.Identifier) + .name; + + // Case 1: `el.style.setProperty('color', 'red')` + if ( + propertyName === "setProperty" && + isStyleMemberExpression(callee.object) + ) { + // Check if the second argument is a literal + if ( + node.arguments.length > 1 && + node.arguments[1].type === "Literal" + ) { + context.report({ + node, + messageId: "avoidStyleAssignment", + data: { property: "element.style.setProperty" }, + }); + } + } + + // Case 2: `el.setAttribute('style', '...')` + if (propertyName === "setAttribute") { + if ( + node.arguments.length > 1 && + node.arguments[0].type === "Literal" && + node.arguments[0].value === "style" && + node.arguments[1].type === "Literal" + ) { + context.report({ + node, + messageId: "avoidStyleAssignment", + data: { property: "element.setAttribute" }, + }); + } + } + }, + }; + }, +}; diff --git a/tests/all-rules.test.ts b/tests/all-rules.test.ts index f8ed9b3..e8786b7 100644 --- a/tests/all-rules.test.ts +++ b/tests/all-rules.test.ts @@ -9,3 +9,4 @@ import "./settingsTab.test"; import "./hardcodedConfigPath.test"; import "./vaultIterate.test"; import "./detachLeaves.test"; +import "./noStaticStylesAssignment.test"; diff --git a/tests/noStaticStylesAssignment.test.ts b/tests/noStaticStylesAssignment.test.ts new file mode 100644 index 0000000..0a273c3 --- /dev/null +++ b/tests/noStaticStylesAssignment.test.ts @@ -0,0 +1,73 @@ +import { RuleTester } from "@typescript-eslint/rule-tester"; +import noInlineStylesRule from "../lib/rules/noStaticStylesAssignment.js"; + +const ruleTester = new RuleTester(); + +ruleTester.run("no-static-styles-assignment", noInlineStylesRule, { + valid: [ + // Correct: Using classList + { code: "el.classList.add('my-class');" }, + // Allowed: Dynamic style assignment from a variable + { code: "const myWidth = '100px'; el.style.width = myWidth;" }, + // Allowed: Dynamic style assignment from a template literal + { code: "el.style.transform = `translateX(${offset}px)`;" }, + // Allowed: Dynamic setProperty + { code: "el.style.setProperty('--my-var', someValue);" }, + // Allowed: setAttribute for other attributes + { code: "el.setAttribute('data-id', '123');" }, + // Allowed: Reading a style is fine + { code: "const color = el.style.color;" }, + ], + invalid: [ + // Invalid: Direct property assignment with a literal + { + code: "el.style.color = 'red';", + errors: [ + { + messageId: "avoidStyleAssignment", + data: { property: "element.style.color" }, + }, + ], + }, + // Invalid: cssText assignment with a literal + { + code: "el.style.cssText = 'font-weight: bold;';", + errors: [ + { + messageId: "avoidStyleAssignment", + data: { property: "element.style.cssText" }, + }, + ], + }, + // Invalid: setProperty with a literal value + { + code: "el.style.setProperty('background', 'blue');", + errors: [ + { + messageId: "avoidStyleAssignment", + data: { property: "element.style.setProperty" }, + }, + ], + }, + // Invalid: setAttribute('style', ...) with a literal value + { + code: "el.setAttribute('style', 'padding: 10px;');", + errors: [ + { + messageId: "avoidStyleAssignment", + data: { property: "element.setAttribute" }, + }, + ], + }, + // Invalid: Chained member expression + { + code: "this.containerEl.style.border = '1px solid black';", + errors: [ + { + messageId: "avoidStyleAssignment", + data: { property: "element.style.border" }, + }, + ], + }, + ], +}); From f4a8251067dfbd6a47218e8024631997911d31b4 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 6 Jun 2025 19:48:17 +0200 Subject: [PATCH 2/4] Regenerated documentation --- README.md | 23 ++++++++++++----------- docs/rules/no-static-styles-assignment.md | 5 +++++ 2 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 docs/rules/no-static-styles-assignment.md diff --git a/README.md b/README.md index 479b605..c20d7b2 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,17 @@ 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 | ✅ | | -| [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-static-styles-assignment](docs/rules/no-static-styles-assignment.md) | Disallow setting styles directly on DOM elements, favoring CSS classes 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
| ✅ | 🔧 | diff --git a/docs/rules/no-static-styles-assignment.md b/docs/rules/no-static-styles-assignment.md new file mode 100644 index 0000000..50789a3 --- /dev/null +++ b/docs/rules/no-static-styles-assignment.md @@ -0,0 +1,5 @@ +# Disallow setting styles directly on DOM elements, favoring CSS classes instead (`obsidianmd/no-static-styles-assignment`) + +💼 This rule is enabled in the ✅ `recommended` config. + + From 5b99314f55ab833863219d6c16145c0f02521d99 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 6 Jun 2025 19:52:29 +0200 Subject: [PATCH 3/4] Fixed typo --- lib/rules/noStaticStylesAssignment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/noStaticStylesAssignment.ts b/lib/rules/noStaticStylesAssignment.ts index 8df1bdb..ced2d7c 100644 --- a/lib/rules/noStaticStylesAssignment.ts +++ b/lib/rules/noStaticStylesAssignment.ts @@ -10,7 +10,7 @@ import { TSESLint, TSESTree } from "@typescript-eslint/utils"; // This rule will not flag: // // - element.style.width = myWidth; (assignment from a variable) -// - element.style.transform = \translateX(${offset}px)`;` (assignment from a template literal with expressions) +// - element.style.transform = `translateX(${offset}px)`; (assignment from a template literal with expressions) // Checks if a node is a MemberExpression accessing the 'style' property. // e.g., `el.style` or `this.containerEl.style` From b774f9ac9bfe4285d2df6d1351472fcc8b344f39 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Fri, 6 Jun 2025 22:22:50 +0200 Subject: [PATCH 4/4] Regenerated documentation --- README.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c95c558..1eb5e2d 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-static-styles-assignment](docs/rules/no-static-styles-assignment.md) | Disallow setting styles directly on DOM elements, favoring CSS classes instead. | ✅ | | +| [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
| ✅ | 🔧 |