From d728126bcc5a1400da29a0ad4730165e3b7810fb Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 10 Sep 2025 19:16:57 -0400 Subject: [PATCH] Custom eslint plugin --- eslint.config.mjs | 8 +- eslint_plugins/index.js | 7 ++ .../rules/aligned-useState-pairs.js | 104 ++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 eslint_plugins/index.js create mode 100644 eslint_plugins/rules/aligned-useState-pairs.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 25d0395c7..73644d633 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,13 +1,14 @@ import react from "eslint-plugin-react"; import jest from "eslint-plugin-jest"; import globals from "globals"; +import localPlugin from "./eslint_plugins/index.js"; export default [{ ignores: ["build/"] }, { files : ['**/*.js', '**/*.jsx'], - plugins : { react, jest }, + plugins : { react, jest, local: localPlugin }, languageOptions : { ecmaVersion : "latest", sourceType : "module", @@ -65,7 +66,10 @@ export default [{ "key-spacing" : ["warn", { multiLine : { beforeColon: true, afterColon: true, align: "colon" }, singleLine : { beforeColon: false, afterColon: true } - }] + }], + + "local/aligned-useState-pairs": "warn" + } } ]; \ No newline at end of file diff --git a/eslint_plugins/index.js b/eslint_plugins/index.js new file mode 100644 index 000000000..6961f216a --- /dev/null +++ b/eslint_plugins/index.js @@ -0,0 +1,7 @@ +import alignedUseStatePairs from './rules/aligned-useState-pairs.js'; + +export default { + rules: { + 'aligned-useState-pairs': alignedUseStatePairs + } +}; \ No newline at end of file diff --git a/eslint_plugins/rules/aligned-useState-pairs.js b/eslint_plugins/rules/aligned-useState-pairs.js new file mode 100644 index 000000000..53b4e8749 --- /dev/null +++ b/eslint_plugins/rules/aligned-useState-pairs.js @@ -0,0 +1,104 @@ +export default { + meta: { + type: "layout", + docs: { + description: "Enforce alignment of adjacent useState variable pairs", + }, + fixable: "whitespace", + schema: [], + }, + create(context) { + const sourceCode = context.getSourceCode(); + const useStateDeclarations = []; + + return { + VariableDeclaration(node) { + for (const decl of node.declarations) { + const init = decl.init; + if ( + init && + init.type === "CallExpression" && + init.callee.name === "useState" && + decl.id.type === "ArrayPattern" + ) { + useStateDeclarations.push(decl); + } + } + }, + "Program:exit"() { + if (useStateDeclarations.length < 2) return; + + // Sort by line number + useStateDeclarations.sort( + (a, b) => a.loc.start.line - b.loc.start.line + ); + + // Group adjacent lines + const groups = []; + let currentGroup = [useStateDeclarations[0]]; + + for (let i = 1; i < useStateDeclarations.length; i++) { + const prev = useStateDeclarations[i - 1]; + const curr = useStateDeclarations[i]; + + if (curr.loc.start.line === prev.loc.end.line + 1) { + currentGroup.push(curr); + } else { + if (currentGroup.length > 1) groups.push(currentGroup); + currentGroup = [curr]; + } + } + if (currentGroup.length > 1) groups.push(currentGroup); + + // Analyze each group + for (const group of groups) { + const positions = group.map((decl) => { + const text = sourceCode.getText(decl); + const commaIndex = text.indexOf(","); + const closingBracketIndex = text.lastIndexOf("]"); + return { + node: decl, + comma: commaIndex, + closing: closingBracketIndex, + }; + }); + + const maxComma = Math.max(...positions.map((p) => p.comma)); + const maxClosing = Math.max( + ...positions.map((p) => p.closing) + ); + + for (const pos of positions) { + if ( + pos.comma !== maxComma || + pos.closing !== maxClosing + ) { + context.report({ + node: pos.node, + message: "useState pair is not aligned with others in its block.", + fix(fixer) { + const text = sourceCode.getText(pos.node); + const parts = text.match(/^\[\s*(.+?)\s*,\s*(.+?)\s*\]\s*=\s*useState\((.+)\)$/); + if (!parts) return null; + + const [_, left, right, value] = parts; + + const paddedLeft = left.padEnd(maxComma - 1); + const paddedRight = right.padEnd(maxClosing - maxComma - 2); + const aligned = `[${paddedLeft}, ${paddedRight}] = useState(${value})`; + console.log("Pre: " + text); + console.log("Post: " + aligned); + return [ + fixer.replaceText(pos.node, aligned), + fixer.insertTextBefore(pos.node.parent, ""), + fixer.insertTextAfter(pos.node.parent, "") + ]; + } + }); + } + } + } + }, + }; + }, +}; \ No newline at end of file