@html-eslint/eslint-plugin
Version:
ESLint plugin for html
146 lines (134 loc) • 3.59 kB
JavaScript
/**
* @typedef { import("@html-eslint/types").Tag } Tag
* @typedef { import("@html-eslint/types").ScriptTag } ScriptTag
* @typedef { import("@html-eslint/types").StyleTag } StyleTag
*
* @typedef {Object} Option
* @property {string} tag
* @property {string} attr
* @property {string} [value]
*
* @typedef { import("../types").RuleModule<Option[]> } RuleModule
*
*/
const { NODE_TYPES } = require("@html-eslint/parser");
const { RULE_CATEGORY } = require("../constants");
const { createVisitors } = require("./utils/visitors");
const MESSAGE_IDS = {
MISSING: "missing",
UNEXPECTED: "unexpected",
};
/**
* @type {RuleModule}
*/
module.exports = {
meta: {
type: "code",
docs: {
description: "Require specified attributes",
category: RULE_CATEGORY.BEST_PRACTICE,
recommended: false,
},
fixable: null,
schema: {
type: "array",
items: {
type: "object",
properties: {
tag: { type: "string" },
attr: { type: "string" },
value: { type: "string" },
},
required: ["tag", "attr"],
additionalProperties: false,
},
},
messages: {
[MESSAGE_IDS.MISSING]: "Missing '{{attr}}' attributes for '{{tag}}' tag",
[MESSAGE_IDS.UNEXPECTED]:
"Unexpected '{{attr}}' attributes value. '{{expected}}' is expected",
},
},
create(context) {
/**
* @type {Option[]}
*/
const options = context.options || [];
/**
* @type {Map<string, { tag: string, attr: string, value?: string}[]>}
*/
const tagOptionsMap = new Map();
options.forEach((option) => {
const tagName = option.tag.toLowerCase();
if (tagOptionsMap.has(tagName)) {
tagOptionsMap.set(tagName, [
...(tagOptionsMap.get(tagName) || []),
option,
]);
} else {
tagOptionsMap.set(tagName, [option]);
}
});
/**
* @param {StyleTag | ScriptTag | Tag} node
* @param {string} tagName
*/
function check(node, tagName) {
const tagOptions = tagOptionsMap.get(tagName) || [];
const attributes = node.attributes || [];
tagOptions.forEach((option) => {
const attrName = option.attr;
const attr = attributes.find(
(attr) => attr.key && attr.key.value === attrName
);
if (!attr) {
context.report({
messageId: MESSAGE_IDS.MISSING,
node,
data: {
attr: attrName,
tag: tagName,
},
});
} else if (
typeof option.value === "string" &&
(!attr.value || attr.value.value !== option.value)
) {
context.report({
messageId: MESSAGE_IDS.UNEXPECTED,
node: attr,
data: {
attr: attrName,
expected: option.value,
},
});
}
});
}
/**
* @param {StyleTag | ScriptTag} node
*/
function checkStyleOrScript(node) {
const tagName = node.type === NODE_TYPES.StyleTag ? "style" : "script";
if (!tagOptionsMap.has(tagName)) {
return;
}
check(node, tagName);
}
/**
* @param {Tag} node
*/
function checkTag(node) {
const tagName = node.name.toLowerCase();
if (!tagOptionsMap.has(tagName)) {
return;
}
check(node, tagName);
}
return createVisitors(context, {
StyleTag: checkStyleOrScript,
ScriptTag: checkStyleOrScript,
Tag: checkTag,
});
},
};