Skip to content

Rule: 'no-static-styles-assignment' #33

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 7 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-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. | ✅ | |
| [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
5 changes: 5 additions & 0 deletions docs/rules/no-static-styles-assignment.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- 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 noStaticStylesAssignment from "./lib/rules/noStaticStylesAssignment.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-static-styles-assignment": noStaticStylesAssignment,
"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-static-styles-assignment": "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 noStaticStylesAssignment from "./rules/noStaticStylesAssignment.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-static-styles-assignment": noStaticStylesAssignment,
"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-static-styles-assignment": "error",
"obsidianmd/no-tfile-tfolder-cast": "error",
"obsidianmd/no-view-references-in-plugin": "error",
"obsidianmd/object-assign": "error",
Expand Down
116 changes: 116 additions & 0 deletions lib/rules/noStaticStylesAssignment.ts
Original file line number Diff line number Diff line change
@@ -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" },
});
}
}
},
};
},
};
1 change: 1 addition & 0 deletions tests/all-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ import "./settingsTab.test";
import "./hardcodedConfigPath.test";
import "./vaultIterate.test";
import "./detachLeaves.test";
import "./noStaticStylesAssignment.test";
import "./noTFileTFolderCast.test";
import "./noViewReferencesInPlugin.test";
73 changes: 73 additions & 0 deletions tests/noStaticStylesAssignment.test.ts
Original file line number Diff line number Diff line change
@@ -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" },
},
],
},
],
});