@html-eslint/eslint-plugin
Version:
ESLint plugin for HTML
208 lines (184 loc) • 4.96 kB
JavaScript
/**
* @import {
* SourceCode,
* TraversalStep
* } from "@eslint/core"
* @import {
* DirectiveType,
* SourceLocation
* } from "@eslint/plugin-kit"
* @import {
* AnyHTMLNode,
* CommentContent
* } from "@html-eslint/types"
* @import {AST} from "eslint"
* @import {BaseNode} from "../types"
*/
const {
TextSourceCodeBase,
ConfigCommentParser,
Directive,
} = require("@eslint/plugin-kit");
const { HTMLTraversalStep, STEP_PHASE } = require("./html-traversal-step");
const { visitorKeys } = require("@html-eslint/parser");
const lineBreakPattern = /\r\n|[\r\n\u2028\u2029]/u;
function createGlobalLinebreakMatcher() {
return new RegExp(lineBreakPattern.source, "gu");
}
const INLINE_CONFIG =
/^\s*(?:eslint(?:-enable|-disable(?:(?:-next)?-line)?)?)(?:\s|$)/u;
const commentParser = new ConfigCommentParser();
class HTMLSourceCode extends TextSourceCodeBase {
/** @param {{ ast: AST.Program; text: string; comments: CommentContent[] }} config */
constructor({ ast, text, comments }) {
super({ ast, text });
/** @property */
this.ast = ast;
/** @property */
this.comments = comments;
this.parentsMap = new Map();
this.lineStartIndices = [0];
const lineEndingPattern = createGlobalLinebreakMatcher();
let match;
while ((match = lineEndingPattern.exec(this.text))) {
this.lineStartIndices.push(match.index + match[0].length);
}
}
/**
* @param {BaseNode} node
* @returns {[number, number]}
*/
getRange(node) {
return node.range;
}
/**
* @param {BaseNode} node
* @returns {import("@eslint/plugin-kit").SourceLocation}
*/
getLoc(node) {
return node.loc;
}
getLines() {
return this.lines;
}
getInlineConfigNodes() {
return this.comments.filter((comment) => INLINE_CONFIG.test(comment.value));
}
getDisableDirectives() {
/**
* @type {{
* ruleId: null | string;
* message: string;
* loc: SourceLocation;
* }[]}
*/
const problems = [];
/** @type {Directive[]} */
const directives = [];
this.getInlineConfigNodes().forEach((comment) => {
const parsed = commentParser.parseDirective(comment.value);
if (!parsed) return;
const { label, value, justification } = parsed;
// `eslint-disable-line` directives are not allowed to span multiple lines as it would be confusing to which lines they apply
if (
label === "eslint-disable-line" &&
comment.loc.start.line !== comment.loc.end.line
) {
const message = `${label} comment should not span multiple lines.`;
problems.push({
ruleId: null,
message,
loc: comment.loc,
});
return;
}
switch (label) {
case "eslint-disable":
case "eslint-enable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = label.slice("eslint-".length);
directives.push(
new Directive({
type: /** @type {DirectiveType} */ (directiveType),
node: comment,
value,
justification,
})
);
}
}
});
return { problems, directives };
}
traverse() {
/** @type {TraversalStep[]} */
const steps = [];
/**
* @param {AnyHTMLNode | AST.Program} node
* @param {AnyHTMLNode | AST.Program | null} parent
*/
const visit = (node, parent) => {
this.parentsMap.set(node, parent);
// @ts-ignore
node.parent = parent;
steps.push(
new HTMLTraversalStep({
target: node,
phase: STEP_PHASE.ENTER,
args: [node, parent],
})
);
// @ts-ignore
for (const key of visitorKeys[node.type] || []) {
// @ts-ignore
const child = node[key];
if (child) {
if (Array.isArray(child)) {
child.forEach((grandchild) => {
visit(grandchild, node);
});
} else {
visit(child, node);
}
}
}
steps.push(
new HTMLTraversalStep({
target: node,
phase: STEP_PHASE.EXIT,
args: [node, parent],
})
);
};
visit(this.ast, null);
return steps;
}
/**
* @param {AnyHTMLNode} node
* @returns
*/
getParent(node) {
return this.parentsMap.get(node);
}
}
/**
* @param {{ ast: AST.Program; text: string; comments: CommentContent[] }} config
* @returns {TextSourceCodeBase<any> & {
* getDisableDirectives(): {
* problems: {
* ruleId: null | string;
* message: string;
* loc: SourceLocation;
* }[];
* directives: Directive[];
* };
* getInlineConfigNodes(): CommentContent[];
* }}
*/
function createHTMLSourceCode(config) {
return new HTMLSourceCode(config);
}
module.exports = {
createHTMLSourceCode,
};