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`];