Skip to content

Commit

Permalink
feat: support linting html in template literals (#238)
Browse files Browse the repository at this point in the history
* feat: support linting html in templateliterals

* require-button-type, require-closing-tags

* templates

* require-attrs

* sort-attrs

* no-duplicate-id

* no-restricted-attrs

* no-restricted-attr-values

* typo

* fix format
  • Loading branch information
yeonjuan authored Dec 1, 2024
1 parent c9651f9 commit ed2a3b3
Show file tree
Hide file tree
Showing 32 changed files with 806 additions and 203 deletions.
32 changes: 18 additions & 14 deletions packages/eslint-plugin/lib/rules/no-accesskey-attrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

const { RULE_CATEGORY } = require("../constants");
const { findAttr } = require("./utils/node");
const { createVisitors } = require("./utils/visitors");

const MESSAGE_IDS = {
UNEXPECTED: "unexpected",
Expand All @@ -33,19 +34,22 @@ module.exports = {
},

create(context) {
return {
/**
* @param {TagNode | ScriptTagNode | StyleTagNode} node
*/
[["Tag", "ScriptTag", "StyleTag"].join(",")](node) {
const accessKeyAttr = findAttr(node, "accesskey");
if (accessKeyAttr) {
context.report({
node: accessKeyAttr,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
},
};
/**
* @param {TagNode | ScriptTagNode | StyleTagNode} node
*/
function check(node) {
const accessKeyAttr = findAttr(node, "accesskey");
if (accessKeyAttr) {
context.report({
node: accessKeyAttr,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
}
return createVisitors(context, {
Tag: check,
ScriptTag: check,
StyleTag: check,
});
},
};
30 changes: 18 additions & 12 deletions packages/eslint-plugin/lib/rules/no-aria-hidden-body.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

const { RULE_CATEGORY } = require("../constants");
const { findAttr } = require("./utils/node");
const { createVisitors } = require("./utils/visitors");

const MESSAGE_IDS = {
UNEXPECTED: "unexpected",
Expand Down Expand Up @@ -31,24 +32,29 @@ module.exports = {
},

create(context) {
return {
return createVisitors(context, {
Tag(node) {
if (node.name !== "body") {
return;
}
const ariaHiddenAttr = findAttr(node, "aria-hidden");
if (ariaHiddenAttr) {
if (
(ariaHiddenAttr.value && ariaHiddenAttr.value.value !== "false") ||
!ariaHiddenAttr.value
) {
context.report({
node: ariaHiddenAttr,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
if (!ariaHiddenAttr) {
return;
}
if (ariaHiddenAttr.value && ariaHiddenAttr.value.templates.length) {
return;
}

if (
(ariaHiddenAttr.value && ariaHiddenAttr.value.value !== "false") ||
!ariaHiddenAttr.value
) {
context.report({
node: ariaHiddenAttr,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
},
};
});
},
};
83 changes: 62 additions & 21 deletions packages/eslint-plugin/lib/rules/no-duplicate-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
* @typedef { import("../types").TagNode } TagNode
* @typedef { import("../types").StyleTagNode } StyleTagNode
* @typedef { import("../types").ScriptTagNode } ScriptTagNode
* @typedef { import("es-html-parser").AttributeValueNode } AttributeValueNode
*/

const { parse } = require("@html-eslint/template-parser");
const { RULE_CATEGORY } = require("../constants");
const { findAttr } = require("./utils/node");
const {
shouldCheckTaggedTemplateExpression,
shouldCheckTemplateLiteral,
} = require("./utils/settings");
const { getSourceCode } = require("./utils/source-code");

const MESSAGE_IDS = {
DUPLICATE_ID: "duplicateId",
Expand All @@ -33,37 +40,71 @@ module.exports = {
},

create(context) {
const IdAttrsMap = new Map();
return {
const htmlIdAttrsMap = new Map();
/**
* @param {Map<string, AttributeValueNode[]>} map
*/
function createTagVisitor(map) {
/**
* @param {TagNode | ScriptTagNode | StyleTagNode} node
* @returns
* @param {TagNode} node
*/
Tag(node) {
return function (node) {
if (!node.attributes || node.attributes.length <= 0) {
return;
}
const idAttr = findAttr(node, "id");
if (idAttr && idAttr.value) {
if (!IdAttrsMap.has(idAttr.value.value)) {
IdAttrsMap.set(idAttr.value.value, []);
if (!map.has(idAttr.value.value)) {
map.set(idAttr.value.value, []);
}
const nodes = map.get(idAttr.value.value);
if (nodes) {
nodes.push(idAttr.value);
}
const nodes = IdAttrsMap.get(idAttr.value.value);
nodes.push(idAttr.value);
}
},
"Program:exit"() {
IdAttrsMap.forEach((attrs) => {
if (Array.isArray(attrs) && attrs.length > 1) {
attrs.forEach((attr) => {
context.report({
node: attr,
data: { id: attr.value },
messageId: MESSAGE_IDS.DUPLICATE_ID,
});
};
}

/**
*
* @param {Map<string, AttributeValueNode[]>} map
*/
function report(map) {
map.forEach((attrs) => {
if (Array.isArray(attrs) && attrs.length > 1) {
attrs.forEach((attr) => {
context.report({
node: attr,
data: { id: attr.value },
messageId: MESSAGE_IDS.DUPLICATE_ID,
});
}
});
});
}
});
}

return {
Tag: createTagVisitor(htmlIdAttrsMap),
"Program:exit"() {
report(htmlIdAttrsMap);
},
TaggedTemplateExpression(node) {
const idAttrsMap = new Map();
if (shouldCheckTaggedTemplateExpression(node, context)) {
parse(node.quasi, getSourceCode(context), {
Tag: createTagVisitor(idAttrsMap),
});
}
report(idAttrsMap);
},
TemplateLiteral(node) {
const idAttrsMap = new Map();
if (shouldCheckTemplateLiteral(node, context)) {
parse(node, getSourceCode(context), {
Tag: createTagVisitor(idAttrsMap),
});
}
report(idAttrsMap);
},
};
},
Expand Down
42 changes: 24 additions & 18 deletions packages/eslint-plugin/lib/rules/no-positive-tabindex.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

const { RULE_CATEGORY } = require("../constants");
const { findAttr } = require("./utils/node");
const { createVisitors } = require("./utils/visitors");

const MESSAGE_IDS = {
UNEXPECTED: "unexpected",
Expand All @@ -33,23 +34,28 @@ module.exports = {
},

create(context) {
return {
/**
* @param {TagNode | StyleTagNode | ScriptTagNode} node
*/
[["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
const tabIndexAttr = findAttr(node, "tabindex");
if (
tabIndexAttr &&
tabIndexAttr.value &&
parseInt(tabIndexAttr.value.value, 10) > 0
) {
context.report({
node: tabIndexAttr,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
},
};
/**
* @param {TagNode | StyleTagNode | ScriptTagNode} node
*/
function check(node) {
const tabIndexAttr = findAttr(node, "tabindex");
if (
tabIndexAttr &&
tabIndexAttr.value &&
!tabIndexAttr.value.templates.length &&
parseInt(tabIndexAttr.value.value, 10) > 0
) {
context.report({
node: tabIndexAttr,
messageId: MESSAGE_IDS.UNEXPECTED,
});
}
}

return createVisitors(context, {
Tag: check,
StyleTag: check,
ScriptTag: check,
});
},
};
98 changes: 51 additions & 47 deletions packages/eslint-plugin/lib/rules/no-restricted-attr-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

const { RULE_CATEGORY } = require("../constants");
const { createVisitors } = require("./utils/visitors");

const MESSAGE_IDS = {
RESTRICTED: "restricted",
Expand Down Expand Up @@ -64,54 +65,57 @@ module.exports = {
*/
const options = context.options;
const checkers = options.map((option) => new PatternChecker(option));

return {
/**
* @param {TagNode | StyleTagNode | ScriptTagNode} node
*/
[["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
node.attributes.forEach((attr) => {
if (
!attr.key ||
!attr.key.value ||
!attr.value ||
typeof attr.value.value !== "string"
) {
return;
}

const matched = checkers.find(
(checker) =>
attr.value && checker.test(attr.key.value, attr.value.value)
);

if (!matched) {
return;
}

/**
* @type {{node: AttributeNode, message: string, messageId?: string}}
*/
const result = {
node: attr,
message: "",
};

const customMessage = matched.getMessage();

if (customMessage) {
result.message = customMessage;
} else {
result.messageId = MESSAGE_IDS.RESTRICTED;
}

context.report({
...result,
data: { attrValuePatterns: attr.value.value },
});
/**
* @param {TagNode | StyleTagNode | ScriptTagNode} node
*/
function check(node) {
node.attributes.forEach((attr) => {
if (
!attr.key ||
!attr.key.value ||
!attr.value ||
typeof attr.value.value !== "string"
) {
return;
}

const matched = checkers.find(
(checker) =>
attr.value && checker.test(attr.key.value, attr.value.value)
);

if (!matched) {
return;
}

/**
* @type {{node: AttributeNode, message: string, messageId?: string}}
*/
const result = {
node: attr,
message: "",
};

const customMessage = matched.getMessage();

if (customMessage) {
result.message = customMessage;
} else {
result.messageId = MESSAGE_IDS.RESTRICTED;
}

context.report({
...result,
data: { attrValuePatterns: attr.value.value },
});
},
};
});
}

return createVisitors(context, {
Tag: check,
StyleTag: check,
ScriptTag: check,
});
},
};

Expand Down
Loading

0 comments on commit ed2a3b3

Please sign in to comment.