/** @typedef {import("../shared/types").Environment} Environment */
/** @typedef {import("../shared/types").GlobalConf} GlobalConf */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
+/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
/** @typedef {import("../shared/types").Processor} Processor */
* @property {number} line The line number
* @property {number} column The column number
* @property {(string|null)} ruleId The rule ID
+ * @property {string} justification The justification of directive
*/
/**
* @typedef {Object} LinterInternalSlots
* @property {ConfigArray|null} lastConfigArray The `ConfigArray` instance that the last `verify()` call used.
* @property {SourceCode|null} lastSourceCode The `SourceCode` instance that the last `verify()` call used.
+ * @property {SuppressedLintMessage[]} lastSuppressedMessages The `SuppressedLintMessage[]` instance that the last `verify()` call produced.
* @property {Map<string, Parser>} parserMap The loaded parsers.
* @property {Rules} ruleMap The loaded rules.
*/
* @param {token} options.commentToken The Comment token
* @param {string} options.value The value after the directive in the comment
* comment specified no specific rules, so it applies to all rules (e.g. `eslint-disable`)
+ * @param {string} options.justification The justification of the directive
* @param {function(string): {create: Function}} options.ruleMapper A map from rule IDs to defined rules
* @returns {Object} Directives and problems from the comment
*/
function createDisableDirectives(options) {
- const { commentToken, type, value, ruleMapper } = options;
+ const { commentToken, type, value, justification, ruleMapper } = options;
const ruleIds = Object.keys(commentParser.parseListConfig(value));
const directiveRules = ruleIds.length ? ruleIds : [null];
const result = {
// push to directives, if the rule is defined(including null, e.g. /*eslint enable*/)
if (ruleId === null || !!ruleMapper(ruleId)) {
- result.directives.push({ parentComment, type, line: commentToken.loc.start.line, column: commentToken.loc.start.column + 1, ruleId });
+ if (type === "disable-next-line") {
+ result.directives.push({
+ parentComment,
+ type,
+ line: commentToken.loc.end.line,
+ column: commentToken.loc.end.column + 1,
+ ruleId,
+ justification
+ });
+ } else {
+ result.directives.push({
+ parentComment,
+ type,
+ line: commentToken.loc.start.line,
+ column: commentToken.loc.start.column + 1,
+ ruleId,
+ justification
+ });
+ }
} else {
result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc }));
}
}
/**
- * 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.
+ * Extract the directive and the justification from a given directive comment and trim them.
+ * @param {string} value The comment text to extract.
+ * @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification.
*/
-function stripDirectiveComment(value) {
- return value.split(/\s-{2,}\s/u)[0].trim();
+function extractDirectiveComment(value) {
+ const match = /\s-{2,}\s/u.exec(value);
+
+ if (!match) {
+ return { directivePart: value.trim(), justificationPart: "" };
+ }
+
+ const directive = value.slice(0, match.index).trim();
+ const justification = value.slice(match.index + match[0].length).trim();
+
+ return { directivePart: directive, justificationPart: justification };
}
/**
* Parses comments in file to extract file-specific config of rules, globals
* and environments and merges them with global config; also code blocks
* where reporting is disabled or enabled and merges them with reporting config.
- * @param {string} filename The file being checked.
* @param {ASTNode} ast The top node of the AST.
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
* @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from.
* @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
* A collection of the directive comments that were found, along with any problems that occurred when parsing
*/
-function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
+function getDirectiveComments(ast, ruleMapper, warnInlineConfig) {
const configuredRules = {};
const enabledGlobals = Object.create(null);
const exportedVariables = {};
});
ast.comments.filter(token => token.type !== "Shebang").forEach(comment => {
- const trimmedCommentText = stripDirectiveComment(comment.value);
- const match = /^(eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(trimmedCommentText);
+ const { directivePart, justificationPart } = extractDirectiveComment(comment.value);
+
+ const match = /^(eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(directivePart);
if (!match) {
return;
return;
}
- if (lineCommentSupported && comment.loc.start.line !== comment.loc.end.line) {
+ if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) {
const message = `${directiveText} comment should not span multiple lines.`;
problems.push(createLintingProblem({
return;
}
- const directiveValue = trimmedCommentText.slice(match.index + directiveText.length);
+ const directiveValue = directivePart.slice(match.index + directiveText.length);
switch (directiveText) {
case "eslint-disable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = directiveText.slice("eslint-".length);
- const options = { commentToken: comment, type: directiveType, value: directiveValue, ruleMapper };
+ const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
const { directives, directiveProblems } = createDisableDirectives(options);
disableDirectives.push(...directives);
if (match[0].endsWith("*/")) {
retv = Object.assign(
retv || {},
- commentParser.parseListConfig(stripDirectiveComment(match[1]))
+ commentParser.parseListConfig(extractDirectiveComment(match[1]).directivePart)
);
}
}
* problem that ESLint identified just like any other.
*/
try {
+ debug("Parsing:", filePath);
const parseResult = (typeof parser.parseForESLint === "function")
? parser.parseForESLint(textToParse, parserOptions)
: { ast: parser.parse(textToParse, parserOptions) };
+
+ debug("Parsing successful:", filePath);
const ast = parseResult.ast;
const parserServices = parseResult.services || {};
const visitorKeys = parseResult.visitorKeys || evk.KEYS;
+
+ debug("Scope analysis:", filePath);
const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys);
+ debug("Scope analysis successful:", filePath);
+
return {
success: true,
)
);
- const ruleListeners = createRuleListeners(rule, ruleContext);
+ const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext);
/**
* Include `ruleId` in error logs
};
}
+ if (typeof ruleListeners === "undefined" || ruleListeners === null) {
+ throw new Error(`The create() function for rule '${ruleId}' did not return an object.`);
+ }
+
// add all the selectors from the rule as listeners
Object.keys(ruleListeners).forEach(selector => {
const ruleListener = timing.enabled
cwd: normalizeCwd(cwd),
lastConfigArray: null,
lastSourceCode: null,
+ lastSuppressedMessages: [],
configType, // TODO: Remove after flat config conversion
parserMap: new Map([["espree", espree]]),
ruleMap: new Rules()
* @param {ConfigData} providedConfig An ESLintConfig instance to configure everything.
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
* @throws {Error} If during rule execution.
- * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
*/
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
const slots = internalSlotsMap.get(this);
const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
- ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
+ ? getDirectiveComments(sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
// augment global scope with declared global variables
configArray.normalizeSync();
}
- return this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true);
+ return this._distinguishSuppressedMessages(this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true));
}
if (typeof config.extractConfig === "function") {
- return this._verifyWithConfigArray(textOrSourceCode, config, options);
+ return this._distinguishSuppressedMessages(this._verifyWithConfigArray(textOrSourceCode, config, options));
}
}
* So we cannot apply multiple processors.
*/
if (options.preprocess || options.postprocess) {
- return this._verifyWithProcessor(textOrSourceCode, config, options);
+ return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, config, options));
}
- return this._verifyWithoutProcessors(textOrSourceCode, config, options);
+ return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, config, options));
}
/**
* @param {FlatConfig} config The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
* @param {FlatConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
- * @returns {LintMessage[]} The found problems.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options, configForRecursive) {
const filename = options.filename || "<input>";
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));
const originalExtname = path.extname(filename);
- const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
+
+ let blocks;
+
+ try {
+ blocks = preprocess(text, filenameToExpose);
+ } catch (ex) {
+
+ // If the message includes a leading line number, strip it:
+ const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
+
+ debug("%s\n%s", message, ex.stack);
+
+ return [
+ {
+ ruleId: null,
+ fatal: true,
+ severity: 2,
+ message,
+ line: ex.lineNumber,
+ column: ex.column
+ }
+ ];
+ }
+
+ const messageLists = blocks.map((block, i) => {
debug("A code block was found: %o", block.filename || "(unnamed)");
// Keep the legacy behavior.
* @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything.
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
* @throws {Error} If during rule execution.
- * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
*/
_verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
const slots = internalSlotsMap.get(this);
...languageOptions.globals
};
+ // double check that there is a parser to avoid mysterious error messages
+ if (!languageOptions.parser) {
+ throw new TypeError(`No parser specified for ${options.filename}`);
+ }
+
// Espree expects this information to be passed in
if (isEspree(languageOptions.parser)) {
const parserOptions = languageOptions.parserOptions;
const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
? getDirectiveComments(
- options.filename,
sourceCode.ast,
ruleId => getRuleFromConfig(ruleId, config),
options.warnInlineConfig
* @param {string|SourceCode} textOrSourceCode The source code.
* @param {ConfigArray} configArray The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
- * @returns {LintMessage[]} The found problems.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithConfigArray(textOrSourceCode, configArray, options) {
debug("With ConfigArray: %s", options.filename);
* @param {VerifyOptions&ProcessorOptions} options The options.
* @param {boolean} [firstCall=false] Indicates if this is being called directly
* from verify(). (TODO: Remove once eslintrc is removed.)
- * @returns {LintMessage[]} The found problems.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithFlatConfigArray(textOrSourceCode, configArray, options, firstCall = false) {
debug("With flat config: %s", options.filename);
// we need a filename to match configs against
- const filename = options.filename || "<input>";
+ const filename = options.filename || "__placeholder__.js";
// Store the config array in order to get plugin envs and rules later.
internalSlotsMap.get(this).lastConfigArray = configArray;
const config = configArray.getConfig(filename);
+ if (!config) {
+ return [
+ {
+ ruleId: null,
+ severity: 1,
+ message: `No matching configuration found for ${filename}.`,
+ line: 0,
+ column: 0
+ }
+ ];
+ }
+
// Verify.
if (config.processor) {
debug("Apply the processor: %o", config.processor);
* @param {ConfigData|ExtractedConfig} config The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
* @param {ConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
- * @returns {LintMessage[]} The found problems.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) {
const filename = options.filename || "<input>";
const physicalFilename = options.physicalFilename || filenameToExpose;
const text = ensureText(textOrSourceCode);
const preprocess = options.preprocess || (rawText => [rawText]);
-
const postprocess = options.postprocess || (messagesList => messagesList.flat());
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));
const originalExtname = path.extname(filename);
- const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
+
+ let blocks;
+
+ try {
+ blocks = preprocess(text, filenameToExpose);
+ } catch (ex) {
+
+ // If the message includes a leading line number, strip it:
+ const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`;
+
+ debug("%s\n%s", message, ex.stack);
+
+ return [
+ {
+ ruleId: null,
+ fatal: true,
+ severity: 2,
+ message,
+ line: ex.lineNumber,
+ column: ex.column
+ }
+ ];
+ }
+
+ const messageLists = blocks.map((block, i) => {
debug("A code block was found: %o", block.filename || "(unnamed)");
// Keep the legacy behavior.
return postprocess(messageLists, filenameToExpose);
}
+ /**
+ * Given a list of reported problems, distinguish problems between normal messages and suppressed messages.
+ * The normal messages will be returned and the suppressed messages will be stored as lastSuppressedMessages.
+ * @param {Problem[]} problems A list of reported problems.
+ * @returns {LintMessage[]} A list of LintMessage.
+ */
+ _distinguishSuppressedMessages(problems) {
+ const messages = [];
+ const suppressedMessages = [];
+ const slots = internalSlotsMap.get(this);
+
+ for (const problem of problems) {
+ if (problem.suppressions) {
+ suppressedMessages.push(problem);
+ } else {
+ messages.push(problem);
+ }
+ }
+
+ slots.lastSuppressedMessages = suppressedMessages;
+
+ return messages;
+ }
+
/**
* Gets the SourceCode object representing the parsed source.
* @returns {SourceCode} The SourceCode object.
return internalSlotsMap.get(this).lastSourceCode;
}
+ /**
+ * Gets the list of SuppressedLintMessage produced in the last running.
+ * @returns {SuppressedLintMessage[]} The list of SuppressedLintMessage
+ */
+ getSuppressedMessages() {
+ return internalSlotsMap.get(this).lastSuppressedMessages;
+ }
+
/**
* Defines a new linting rule.
* @param {string} ruleId A unique rule identifier