@yusufkandemir/eslint-plugin-lodash-template
Version:
ESLint plugin for John Resig-style micro template, Lodash's template, Underscore's template and EJS.
314 lines (296 loc) • 9.74 kB
JavaScript
// @ts-check
;
/**
* @typedef {import('../services/micro-template-service')} MicroTemplateService
*/
const COMMENT_DIRECTIVE_B =
/^\s*(eslint-(?:en|dis)able)(?:\s+(\S|\S[\s\S]*\S))?\s*$/u;
const COMMENT_DIRECTIVE_L =
/^\s*(eslint-disable(?:-next)?-line)(?:\s+(\S|\S[\s\S]*\S))?\s*$/u;
/**
* Remove the ignored part from a given directive comment and trim it.
* @param {string} value The comment text to strip.
* @returns {string} The stripped text.
*/
function stripDirectiveComment(value) {
return value.split(/\s-{2,}\s/u)[0].trim();
}
/**
* The comment directive context
*/
class CommentDirectiveContext {
/**
* constructor
*/
constructor() {
this._directives = [];
}
/**
* Enable rules.
* @param {{line:number,column:number}} loc The location information to enable.
* @param {string} group The group to enable.
* @param {string[]} rules The rule IDs to enable.
* @returns {void}
*/
enable(loc, group, rules) {
this._directives.push({
type: "enable",
loc,
group,
rules,
});
}
/**
* Disable rules.
* @param {{line:number,column:number}} loc The location information to disable.
* @param {string} group The group to disable.
* @param {string[]} rules The rule IDs to disable.
* @returns {void}
*/
disable(loc, group, rules) {
this._directives.push({
type: "disable",
loc,
group,
rules,
});
}
/**
* The post-process.
* @returns {void}
*/
postprocess() {
this._directives.sort((a, b) => {
if (a.loc.line < b.loc.line) {
return -1;
}
if (b.loc.line < a.loc.line) {
return 1;
}
if (a.loc.column < b.loc.column) {
return -1;
}
if (b.loc.column < a.loc.column) {
return 1;
}
if (a.type !== b.type) {
return a.type === "enable" ? -1 : 1;
}
return 0;
});
const state = {
block: {
disableAll: undefined,
disableRules: new Map(),
},
line: {
disableAll: undefined,
disableRules: new Map(),
},
};
const directiveLocations = [];
for (const directive of this._directives) {
const rules = directive.rules;
const type = directive.type;
const group = directive.group;
const loc = directive.loc;
switch (type) {
case "disable":
if (!rules.length) {
if (!state[group].disableAll) {
const obj = {
loc: {
start: loc,
},
};
state[group].disableAll = obj;
directiveLocations.push(obj);
}
} else {
for (const rule of rules) {
if (!state[group].disableRules.has(rule)) {
const obj = {
rule,
loc: {
start: loc,
},
};
state[group].disableRules.set(rule, obj);
directiveLocations.push(obj);
}
}
}
break;
case "enable":
if (!rules.length) {
const obj = state[group].disableAll;
if (obj) {
obj.loc.end = loc;
state[group].disableAll = undefined;
}
} else {
for (const rule of rules) {
const obj = state[group].disableRules.get(rule);
if (obj) {
obj.loc.end = loc;
state[group].disableRules.delete(rule);
}
}
}
break;
default:
break;
}
}
this.directiveLocations = directiveLocations;
}
/**
* Check whether the message is disable rule.
* @param {object} message The message.
* @returns {boolean} `true` if the message is disable rule.
*/
isDisableMessage(message) {
if (!this.directiveLocations) {
return false;
}
for (const directiveLocation of this.directiveLocations) {
if (
!directiveLocation.rule ||
directiveLocation.rule === message.ruleId
) {
if (
locationInRangeLoc(
message,
directiveLocation.loc.start,
directiveLocation.loc.end || {
line: message.line,
column: message.column,
},
)
) {
return true;
}
}
}
return false;
}
}
/**
* Check whether the location is in range location.
* @param {object} loc The location.
* @param {object} start The start location.
* @param {object} end The end location.
* @returns {boolean} `true` if the location is in range location.
*/
function locationInRangeLoc(loc, start, end) {
if (loc.line < start.line || end.line < loc.line) {
return false;
}
if (loc.line === start.line) {
if (start.column > loc.column) {
return false;
}
}
if (loc.line === end.line) {
if (loc.column >= end.column) {
return false;
}
}
return true;
}
/**
* Parse a given comment.
* @param {RegExp} pattern The RegExp pattern to parse.
* @param {string} comment The comment value to parse.
* @returns {({type:string,rules:string[]})|null} The parsing result.
*/
function parse(pattern, comment) {
const match = pattern.exec(stripDirectiveComment(comment));
if (match == null) {
return null;
}
const type = match[1];
const rules = (match[2] || "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
return { type, rules };
}
/**
* Process a given comment token.
* If the comment is `eslint-disable` or `eslint-enable` then it reports the comment.
* @param {CommentDirectiveContext} context The comment directive context.
* @param {Token} comment The comment token to process.
* @returns {void}
*/
function processBlock(context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_B, comment.value);
if (parsed != null) {
if (parsed.type === "eslint-disable") {
context.disable(comment.loc.start, "block", parsed.rules);
} else {
context.enable(comment.loc.end, "block", parsed.rules);
}
}
}
/**
* Process a given comment token.
* If the comment is `eslint-disable-line` or `eslint-disable-next-line` then it reports the comment.
* @param {CommentDirectiveContext} context The comment directive context.
* @param {Token} comment The comment token to process.
* @returns {void}
*/
function processLine(context, comment) {
const parsed = parse(COMMENT_DIRECTIVE_L, comment.value);
if (parsed != null && comment.loc.start.line === comment.loc.end.line) {
const line =
comment.loc.start.line +
(parsed.type === "eslint-disable-line" ? 0 : 1);
const column = -1;
context.disable({ line, column }, "line", parsed.rules);
context.enable({ line: line + 1, column }, "line", parsed.rules);
}
}
module.exports = {
/**
* Create the HTML comment directive context.
*
* @param {boolean} needCheck need check flg.
* @param {string} html The html.
* @param {MicroTemplateService} microTemplateService MicroTemplateService.
* @returns {CommentDirectiveContext} The comment directive context.
*/
createHtmlCommentDirectiveContext(needCheck, html, microTemplateService) {
const context = new CommentDirectiveContext();
if (needCheck) {
if (html.indexOf("eslint-") < 0) {
return context;
}
}
microTemplateService.traverseDocumentNodes({
HTMLComment(comment) {
processBlock(context, comment);
processLine(context, comment);
},
});
context.postprocess();
return context;
},
/**
* Create the EJS comment directive context.
*
* @param {MicroTemplateService} microTemplateService MicroTemplateService.
* @returns {CommentDirectiveContext} The comment directive context.
*/
createEjsCommentDirectiveContext(microTemplateService) {
const context = new CommentDirectiveContext();
microTemplateService.traverseMicroTemplates({
MicroTemplateComment(comment) {
processBlock(context, comment);
processLine(context, comment);
},
});
context.postprocess();
return context;
},
};