From 22325ef7aa344147fe1715b263b43f6e59f2234d Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Sat, 14 Dec 2024 19:36:20 +0900 Subject: [PATCH] feat: implement tagChildrenIndent options in indent (#248) * feat: implement tagChildrenIndent options in indent * fix * Update indent.md * add tests --- docs/rules/indent.md | 9 +- .../eslint-plugin/lib/rules/indent/indent.js | 178 +++++++++++------- .../eslint-plugin/lib/rules/utils/node.js | 9 + .../eslint-plugin/tests/rules/indent.test.js | 113 +++++++++++ .../template-parser/lib/template-parser.js | 2 +- packages/template-parser/lib/traverser.js | 8 +- 6 files changed, 249 insertions(+), 70 deletions(-) diff --git a/docs/rules/indent.md b/docs/rules/indent.md index 2063ece..f8814dc 100644 --- a/docs/rules/indent.md +++ b/docs/rules/indent.md @@ -115,10 +115,17 @@ This rule has an object option: "error", 2, { - "Attribute": 2 + "Attribute": 2, + "tagChildrenIndent": { + "html": 0, + "div": 1 + // ... + } } ] } ``` - `Attribute` (default: 1): enforces indentation level for attributes. e.g. indent of 2 spaces with `Attribute` set to `2` will indent the attributes with `4` spaces (2 x 2). + +- `tagChildrenIndent` (default: `{}`): specifies the indent increment of the child tags of the specified tag. e.g. For example, `"tagChildIndent": { "html": 0 }` will set the `` tag children to 0 indent (2 x 0). diff --git a/packages/eslint-plugin/lib/rules/indent/indent.js b/packages/eslint-plugin/lib/rules/indent/indent.js index dde2bc1..69ce6f3 100644 --- a/packages/eslint-plugin/lib/rules/indent/indent.js +++ b/packages/eslint-plugin/lib/rules/indent/indent.js @@ -2,7 +2,9 @@ * @typedef { import("../../types").RuleModule } RuleModule * @typedef { import("../../types").AnyNode } AnyNode * @typedef { import("../../types").Line } Line + * @typedef { import("../../types").Tag } Tag * @typedef { import("../../types").RuleListener } RuleListener + * @typedef { import("../../types").Context } Context * @typedef { import("eslint").AST.Token } Token * @typedef { import("eslint").SourceCode } SourceCode * @typedef { import("eslint").AST.Range } Range @@ -15,11 +17,15 @@ * @property {"space"} SPACE * @typedef {Object} MessageId * @property {"wrongIndent"} WRONG_INDENT + * @typedef {Object} IndentOptionInfo + * @property {IndentType["TAB"] | IndentType["SPACE"]} indentType + * @property {number} indentSize + * @property {string} indentChar */ const { parse } = require("@html-eslint/template-parser"); const { RULE_CATEGORY } = require("../../constants"); -const { splitToLineNodes } = require("../utils/node"); +const { splitToLineNodes, isLine, isTag } = require("../utils/node"); const { shouldCheckTaggedTemplateExpression, shouldCheckTemplateLiteral, @@ -74,6 +80,17 @@ module.exports = { minimum: 1, default: 1, }, + tagChildrenIndent: { + default: {}, + type: "object", + patternProperties: { + "^[a-z]+$": { + type: "integer", + minimum: 0, + }, + }, + additionalProperties: false, + }, }, }, ], @@ -86,39 +103,44 @@ module.exports = { const sourceCode = getSourceCode(context); const indentLevelOptions = (context.options && context.options[1]) || {}; const lines = sourceCode.getLines(); - const { indentType, indentSize, indentChar } = (function () { - const options = context.options; - /** - * @type {IndentType['SPACE'] | IndentType['TAB']} - */ - let indentType = INDENT_TYPES.SPACE; - let indentSize = 4; - if (options.length) { - if (options[0] === INDENT_TYPES.TAB) { - indentType = INDENT_TYPES.TAB; - } else { - indentSize = options[0]; - } - } - const indentChar = - indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t"; - return { indentType, indentSize, indentChar }; - })(); + const { indentType, indentSize, indentChar } = getIndentOptionInfo(context); /** - * @param {string} str - * @returns {number} + * @param {Tag} node + * @return {number} */ - function countLeftPadding(str) { - return str.length - str.replace(/^[\s\t]+/, "").length; + function getTagIncreasingLevel(node) { + if ( + node.parent && + isTag(node.parent) && + indentLevelOptions && + typeof indentLevelOptions.tagChildrenIndent === "object" && + indentLevelOptions.tagChildrenIndent + ) { + const option = + indentLevelOptions.tagChildrenIndent[node.parent.name.toLowerCase()]; + if (typeof option === "number") { + return option; + } + } + + return 1; } /** * @param {AnyNode} node - * @returns {node is Line} + * @return {number} */ - function isLineNode(node) { - return node.type === "Line"; + function getIncreasingLevel(node) { + if (isLine(node)) { + return 1; + } + if (isTag(node)) { + return getTagIncreasingLevel(node); + } + return typeof indentLevelOptions[node.type] === "number" + ? indentLevelOptions[node.type] + : 1; } /** @@ -144,18 +166,14 @@ module.exports = { */ function createIndentVisitor(baseLevel) { const indentLevel = new IndentLevel({ - getIncreasingLevel(node) { - return typeof indentLevelOptions[node.type] === "number" - ? indentLevelOptions[node.type] - : 1; - }, + getIncreasingLevel, }); indentLevel.setBase(baseLevel); let parentIgnoringChildCount = 0; /** - * @param {AnyNode} node + * @param {AnyNode | Line} node * @returns {string} */ function getActualIndent(node) { @@ -163,7 +181,7 @@ module.exports = { const line = lines[node.loc.start.line - 1]; let column = node.loc.start.column; - if (isLineNode(node)) { + if (isLine(node)) { column += countLeftPadding(node.value); } @@ -177,33 +195,6 @@ module.exports = { return indentChar.repeat(indentLevel.value()); } - /** - * @param {AnyNode} node - * @param {string} actualIndent - * @return {{ range: Range, loc: SourceLocation }} - */ - function getIndentNodeToReport(node, actualIndent) { - let rangeStart = node.range[0]; - - if (node.type !== "Line") { - rangeStart -= actualIndent.length; - } - - return { - range: [rangeStart, rangeStart + actualIndent.length], - loc: { - start: { - column: 0, - line: node.loc.start.line, - }, - end: { - column: actualIndent.length, - line: node.loc.start.line, - }, - }, - }; - } - /** * @param {string} actualIndent * @param {number} expectedIndentSize @@ -237,7 +228,7 @@ module.exports = { } /** - * @param {AnyNode} node + * @param {AnyNode | Line} node */ function checkIndent(node) { if (parentIgnoringChildCount > 0) { @@ -289,7 +280,9 @@ module.exports = { OpenStyleTagStart: checkIndent, OpenStyleTagEnd: checkIndent, OpenTagStart: checkIndent, - OpenTagEnd: checkIndent, + OpenTagEnd(node) { + checkIndent(node); + }, CloseTag: checkIndent, "Tag:exit"(node) { if (IGNORING_NODES.includes(node.name)) { @@ -298,7 +291,6 @@ module.exports = { indentLevel.dedent(node); }, - // Attribute Attribute(node) { indentLevel.indent(node); }, @@ -307,8 +299,6 @@ module.exports = { "Attribute:exit"(node) { indentLevel.dedent(node); }, - - // Text Text(node) { indentLevel.indent(node); const lineNodes = splitToLineNodes(node); @@ -369,3 +359,61 @@ module.exports = { }; }, }; + +/** + * @param {AnyNode | Line} node + * @param {string} actualIndent + * @return {{range: Range; loc: SourceLocation}} + */ +function getIndentNodeToReport(node, actualIndent) { + let rangeStart = node.range[0]; + + if (!isLine(node)) { + rangeStart -= actualIndent.length; + } + + return { + range: [rangeStart, rangeStart + actualIndent.length], + loc: { + start: { + column: 0, + line: node.loc.start.line, + }, + end: { + column: actualIndent.length, + line: node.loc.start.line, + }, + }, + }; +} + +/** + * @param {string} str + * @returns {number} + */ +function countLeftPadding(str) { + return str.length - str.replace(/^[\s\t]+/, "").length; +} + +/** + * @param {Context} context + * @return {IndentOptionInfo} + */ +function getIndentOptionInfo(context) { + const options = context.options; + /** + * @type {IndentType['SPACE'] | IndentType['TAB']} + */ + let indentType = INDENT_TYPES.SPACE; + let indentSize = 4; + if (options.length) { + if (options[0] === INDENT_TYPES.TAB) { + indentType = INDENT_TYPES.TAB; + } else { + indentSize = options[0]; + } + } + const indentChar = + indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t"; + return { indentType, indentSize, indentChar }; +} diff --git a/packages/eslint-plugin/lib/rules/utils/node.js b/packages/eslint-plugin/lib/rules/utils/node.js index 1bc1536..5db6f2b 100644 --- a/packages/eslint-plugin/lib/rules/utils/node.js +++ b/packages/eslint-plugin/lib/rules/utils/node.js @@ -176,6 +176,14 @@ function isText(node) { return node.type === NODE_TYPES.Text; } +/** + * @param {AnyNode | Line} node + * @returns {node is Line} + */ +function isLine(node) { + return node.type === "Line"; +} + const lineBreakPattern = /\r\n|[\r\n\u2028\u2029]/u; const lineEndingPattern = new RegExp(lineBreakPattern.source, "gu"); /** @@ -214,6 +222,7 @@ module.exports = { isTag, isComment, isText, + isLine, isOverlapWithTemplates, codeToLines, isRangesOverlap, diff --git a/packages/eslint-plugin/tests/rules/indent.test.js b/packages/eslint-plugin/tests/rules/indent.test.js index 264add8..36d33a2 100644 --- a/packages/eslint-plugin/tests/rules/indent.test.js +++ b/packages/eslint-plugin/tests/rules/indent.test.js @@ -337,6 +337,43 @@ function createTests() { }, ], }, + { + code: ` + + + + + + + `, + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, + { + code: ` + + +
+ text + + + `, + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, ], invalid: [ { @@ -993,6 +1030,29 @@ id="bar" `, }, + { + code: ` + + + + + `, + output: ` + + + + + `, + errors: wrongIndentErrors(1), + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, ], }; } @@ -1043,6 +1103,27 @@ return "
" } `, }, + { + code: ` +const code = html\` + + + + + + + +\`; + `, + options: [ + 2, + { + tagChildrenIndent: { + html: 0, + }, + }, + ], + }, ], invalid: [ { @@ -1285,5 +1366,37 @@ class Component extends LitElement { options: [2, { Attribute: 2 }], errors: wrongIndentErrors(2), }, + { + code: ` +class Component extends LitElement { +\trender() { +\t\treturn html\` +\t\t\t
+\t\t\t\t +\t\t\t
+\t\t\t +\t\t\t\t
+\t\t\t
+ \`; + } +} + `, + output: ` +class Component extends LitElement { +\trender() { +\t\treturn html\` +\t\t\t
+\t\t\t\t +\t\t\t
+\t\t\t +\t\t\t\t\t
+\t\t\t
+ \`; + } +} + `, + options: ["tab", { Attribute: 2, tagChildrenIndent: { span: 2 } }], + errors: wrongIndentErrors(1), + }, ], }); diff --git a/packages/template-parser/lib/template-parser.js b/packages/template-parser/lib/template-parser.js index 3956848..77c60af 100644 --- a/packages/template-parser/lib/template-parser.js +++ b/packages/template-parser/lib/template-parser.js @@ -63,7 +63,7 @@ function parse(node, sourceCode, visitors) { }, }, }); - traverse(ast, visitors); + traverse(ast, visitors, null); return { ast, html, tokens }; } diff --git a/packages/template-parser/lib/traverser.js b/packages/template-parser/lib/traverser.js index c4243a1..01398b2 100644 --- a/packages/template-parser/lib/traverser.js +++ b/packages/template-parser/lib/traverser.js @@ -56,18 +56,20 @@ const visitorKeys = { * * @param {AnyNode} node * @param {TemplateHTMLVisitor} visitors + * @param {any} parent */ -function traverse(node, visitors) { +function traverse(node, visitors, parent) { const enterVisitor = visitors[node.type]; + node.parent = parent; enterVisitor && enterVisitor(node); const nextKeys = visitorKeys[node.type]; nextKeys.forEach((key) => { const next = node[key]; if (Array.isArray(next)) { - next.forEach((n) => traverse(n, visitors)); + next.forEach((n) => traverse(n, visitors, node)); } else if (next) { - traverse(next, visitors); + traverse(next, visitors, node); } }); const exitVisitor = visitors[`${node.type}:exit`];