From 4040f8f2d30798ca4ce87bafd8159f5bc11f2288 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Thu, 21 Nov 2024 19:27:15 +0900 Subject: [PATCH 1/3] feat: support linting html in template literals in lowercase --- packages/eslint-plugin/lib/rules/lowercase.js | 36 +++++++------- .../lib/rules/no-inline-styles.js | 47 ++++++------------- .../eslint-plugin/lib/rules/utils/visitors.js | 36 ++++++++++++++ packages/eslint-plugin/lib/types.d.ts | 12 +++-- packages/eslint-plugin/package.json | 3 ++ .../tests/rules/lowercase.test.js | 32 +++++++++++++ .../tests/rules/no-inline-styles.test.js | 2 +- packages/template-parser/lib/traverser.js | 6 ++- packages/template-parser/lib/types.d.ts | 9 +++- .../tests/template-parser.test.js | 11 +++++ 10 files changed, 137 insertions(+), 57 deletions(-) create mode 100644 packages/eslint-plugin/lib/rules/utils/visitors.js diff --git a/packages/eslint-plugin/lib/rules/lowercase.js b/packages/eslint-plugin/lib/rules/lowercase.js index d6234f6c..6a8c3e9c 100644 --- a/packages/eslint-plugin/lib/rules/lowercase.js +++ b/packages/eslint-plugin/lib/rules/lowercase.js @@ -3,11 +3,13 @@ * @typedef { import("../types").TagNode } TagNode * @typedef { import("../types").StyleTagNode } StyleTagNode * @typedef { import("../types").ScriptTagNode } ScriptTagNode + * @typedef { import("../types").RuleListener } RuleListener */ const { NODE_TYPES } = require("@html-eslint/parser"); const { RULE_CATEGORY } = require("../constants"); const SVG_CAMEL_CASE_ATTRIBUTES = require("../constants/svg-camel-case-attributes"); +const { createVisitors } = require("./utils/visitors"); const MESSAGE_IDS = { UNEXPECTED: "unexpected", @@ -120,23 +122,23 @@ module.exports = { } } - return { - Tag(node) { - if (node.name.toLocaleLowerCase() === "svg") { - enterSvg(node); - } - check(node); - }, - /** - * @param {TagNode} node - */ - "Tag:exit"(node) { - if (node.name.toLocaleLowerCase() === "svg") { - exitSvg(); - } + return createVisitors( + { + Tag(node) { + if (node.name.toLocaleLowerCase() === "svg") { + enterSvg(node); + } + check(node); + }, + "Tag:exit"(node) { + if (node.name.toLocaleLowerCase() === "svg") { + exitSvg(); + } + }, + StyleTag: check, + ScriptTag: check, }, - StyleTag: check, - ScriptTag: check, - }; + context + ); }, }; diff --git a/packages/eslint-plugin/lib/rules/no-inline-styles.js b/packages/eslint-plugin/lib/rules/no-inline-styles.js index 8a0e558c..028409d7 100644 --- a/packages/eslint-plugin/lib/rules/no-inline-styles.js +++ b/packages/eslint-plugin/lib/rules/no-inline-styles.js @@ -5,12 +5,7 @@ const { RULE_CATEGORY } = require("../constants"); const { findAttr } = require("./utils/node"); -const { parse } = require("@html-eslint/template-parser"); -const { - shouldCheckTaggedTemplateExpression, - shouldCheckTemplateLiteral, -} = require("./utils/settings"); -const { getSourceCode } = require("./utils/source-code"); +const { createVisitors } = require("./utils/visitors"); const MESSAGE_IDS = { INLINE_STYLE: "unexpectedInlineStyle", }; @@ -36,33 +31,19 @@ module.exports = { }, create(context) { - /** - * @type {RuleListener} - */ - const visitors = { - Tag(node) { - const styleAttr = findAttr(node, "style"); - if (styleAttr) { - context.report({ - node: styleAttr, - messageId: MESSAGE_IDS.INLINE_STYLE, - }); - } + return createVisitors( + { + Tag(node) { + const styleAttr = findAttr(node, "style"); + if (styleAttr) { + context.report({ + node: styleAttr, + messageId: MESSAGE_IDS.INLINE_STYLE, + }); + } + }, }, - }; - - return { - ...visitors, - TaggedTemplateExpression(node) { - if (shouldCheckTaggedTemplateExpression(node, context)) { - parse(node.quasi, getSourceCode(context), visitors); - } - }, - TemplateLiteral(node) { - if (shouldCheckTemplateLiteral(node, context)) { - parse(node, getSourceCode(context), visitors); - } - }, - }; + context + ); }, }; diff --git a/packages/eslint-plugin/lib/rules/utils/visitors.js b/packages/eslint-plugin/lib/rules/utils/visitors.js new file mode 100644 index 00000000..0c61f39d --- /dev/null +++ b/packages/eslint-plugin/lib/rules/utils/visitors.js @@ -0,0 +1,36 @@ +/** + * @typedef { import("../../types").RuleListener } RuleListener + * @typedef { import("../../types").Context } Context + */ + +const { + shouldCheckTaggedTemplateExpression, + shouldCheckTemplateLiteral, +} = require("./settings"); +const { parse } = require("@html-eslint/template-parser"); +const { getSourceCode } = require("./source-code"); + +/** + * @param {RuleListener} visitors + * @param {Context} context + * @returns {RuleListener} + */ +function createVisitors(visitors, context) { + return { + ...visitors, + TaggedTemplateExpression(node) { + if (shouldCheckTaggedTemplateExpression(node, context)) { + parse(node.quasi, getSourceCode(context), visitors); + } + }, + TemplateLiteral(node) { + if (shouldCheckTemplateLiteral(node, context)) { + parse(node, getSourceCode(context), visitors); + } + }, + }; +} + +module.exports = { + createVisitors, +}; diff --git a/packages/eslint-plugin/lib/types.d.ts b/packages/eslint-plugin/lib/types.d.ts index b0e63952..b84211de 100644 --- a/packages/eslint-plugin/lib/types.d.ts +++ b/packages/eslint-plugin/lib/types.d.ts @@ -173,7 +173,13 @@ interface LineNode extends BaseNode { value: string; } -interface RuleListener { +type PostFix = { + [K in keyof T as `${K & string}${S}`]: T[K]; +}; + +export type RuleListener = BaseRuleListenr & PostFix; + +interface BaseRuleListenr { Program?: (node: ProgramNode) => void; AttributeKey?: (node: AttributeKeyNode) => void; Text?: (node: TextNode) => void; @@ -267,8 +273,8 @@ export interface Context extends Omit { export type ChildType = T extends ProgramNode ? T["body"][number] : T extends TagNode - ? T["children"][number] - : never; + ? T["children"][number] + : never; export type ContentNode = | CommentNode diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 52cc8b44..15b1e724 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -44,6 +44,9 @@ "lint", "accessibility" ], + "dependencies": { + "@html-eslint/template-parser": "^0.27.0" + }, "devDependencies": { "@html-eslint/parser": "^0.27.0", "@html-eslint/template-parser": "^0.27.0", diff --git a/packages/eslint-plugin/tests/rules/lowercase.test.js b/packages/eslint-plugin/tests/rules/lowercase.test.js index a461c57b..0c0a4de2 100644 --- a/packages/eslint-plugin/tests/rules/lowercase.test.js +++ b/packages/eslint-plugin/tests/rules/lowercase.test.js @@ -2,6 +2,7 @@ const createRuleTester = require("../rule-tester"); const rule = require("../../lib/rules/lowercase"); const ruleTester = createRuleTester(); +const templateRuleTester = createRuleTester("espree"); ruleTester.run("lowercase", rule, { valid: [ @@ -101,3 +102,34 @@ ruleTester.run("lowercase", rule, { }, ], }); + +templateRuleTester.run("[template] lowercase", rule, { + valid: [ + { + code: `html\`\``, + }, + { + code: `const code = /* html */\`\``, + }, + ], + invalid: [ + { + code: `html\`\``, + output: `html\`\``, + errors: [ + { + message: "'STYLE' is not in lowercase.", + }, + ], + }, + { + code: `const code = /* html */\`\``, + output: `const code = /* html */\`\``, + errors: [ + { + message: "'STYLE' is not in lowercase.", + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-inline-styles.test.js b/packages/eslint-plugin/tests/rules/no-inline-styles.test.js index 4e88f7a4..c76aa207 100644 --- a/packages/eslint-plugin/tests/rules/no-inline-styles.test.js +++ b/packages/eslint-plugin/tests/rules/no-inline-styles.test.js @@ -35,7 +35,7 @@ ruleTester.run("no-inline-styles", rule, { ], }); -templateRuleTester.run("[template]no-inline-styles", rule, { +templateRuleTester.run("[template] no-inline-styles", rule, { valid: [ { code: ` diff --git a/packages/template-parser/lib/traverser.js b/packages/template-parser/lib/traverser.js index 0ff1b3f1..c4243a1b 100644 --- a/packages/template-parser/lib/traverser.js +++ b/packages/template-parser/lib/traverser.js @@ -58,8 +58,8 @@ const visitorKeys = { * @param {TemplateHTMLVisitor} visitors */ function traverse(node, visitors) { - const visitor = visitors[node.type]; - visitor && visitor(node); + const enterVisitor = visitors[node.type]; + enterVisitor && enterVisitor(node); const nextKeys = visitorKeys[node.type]; nextKeys.forEach((key) => { @@ -70,6 +70,8 @@ function traverse(node, visitors) { traverse(next, visitors); } }); + const exitVisitor = visitors[`${node.type}:exit`]; + exitVisitor && exitVisitor(node); } module.exports = { diff --git a/packages/template-parser/lib/types.d.ts b/packages/template-parser/lib/types.d.ts index 244335fc..22a77a50 100644 --- a/packages/template-parser/lib/types.d.ts +++ b/packages/template-parser/lib/types.d.ts @@ -34,7 +34,14 @@ import type { } from "es-html-parser"; import { Comment } from "estree"; -export type TemplateHTMLVisitor = Partial<{ +type PostFix = { + [K in keyof T as `${K & string}${S}`]: T[K]; +}; +export type TemplateHTMLVisitor = BaseVisiter & PostFix; + +declare const a: TemplateHTMLVisitor; + +type BaseVisiter = Partial<{ [NodeTypes.Document]: (node: DocumentNode) => void; [NodeTypes.Attribute]: (node: AttributeNode) => void; [NodeTypes.AttributeKey]: (node: AttributeKeyNode) => void; diff --git a/packages/template-parser/tests/template-parser.test.js b/packages/template-parser/tests/template-parser.test.js index d03424a7..92337e92 100644 --- a/packages/template-parser/tests/template-parser.test.js +++ b/packages/template-parser/tests/template-parser.test.js @@ -23,6 +23,7 @@ const createSourceCode = (code, ast) => const visitors = { Tag: jest.fn(), + "Tag:exit": jest.fn(), OpenTagStart: jest.fn(), CloseTag: jest.fn(), AttributeValue: jest.fn(), @@ -59,6 +60,16 @@ describe("parseTemplate", () => { }, }) ); + expect(visitors["Tag:exit"]).toHaveBeenCalledWith( + expect.objectContaining({ + type: NodeTypes.Tag, + range: [1, 12], + loc: { + start: { line: 1, column: 1 }, + end: { line: 1, column: 12 }, + }, + }) + ); }); test("multiline", () => { From ba5022df4159d2ad4f9237b3fad91336eeffd554 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Thu, 21 Nov 2024 19:28:05 +0900 Subject: [PATCH 2/3] fix format --- packages/eslint-plugin/lib/types.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/lib/types.d.ts b/packages/eslint-plugin/lib/types.d.ts index b84211de..244976e7 100644 --- a/packages/eslint-plugin/lib/types.d.ts +++ b/packages/eslint-plugin/lib/types.d.ts @@ -273,8 +273,8 @@ export interface Context extends Omit { export type ChildType = T extends ProgramNode ? T["body"][number] : T extends TagNode - ? T["children"][number] - : never; + ? T["children"][number] + : never; export type ContentNode = | CommentNode From 08b6512b883da2f36cc3a0777ac58a7802544619 Mon Sep 17 00:00:00 2001 From: YeonJuan Date: Thu, 21 Nov 2024 19:29:40 +0900 Subject: [PATCH 3/3] typo --- packages/eslint-plugin/lib/types.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/lib/types.d.ts b/packages/eslint-plugin/lib/types.d.ts index 244976e7..c2ab3fa0 100644 --- a/packages/eslint-plugin/lib/types.d.ts +++ b/packages/eslint-plugin/lib/types.d.ts @@ -177,9 +177,10 @@ type PostFix = { [K in keyof T as `${K & string}${S}`]: T[K]; }; -export type RuleListener = BaseRuleListenr & PostFix; +export type RuleListener = BaseRuleListener & + PostFix; -interface BaseRuleListenr { +interface BaseRuleListener { Program?: (node: ProgramNode) => void; AttributeKey?: (node: AttributeKeyNode) => void; Text?: (node: TextNode) => void;