]> git.proxmox.com Git - pve-eslint.git/blobdiff - eslint/lib/linter/linter.js
import 8.23.1 source
[pve-eslint.git] / eslint / lib / linter / linter.js
index f897b8ddb8c011e53885d6e9a272cf54b3b3e77a..a29ce9237928e9972317f885c817c289bd30c2df 100644 (file)
@@ -59,6 +59,7 @@ const globals = require("../../conf/globals");
 /** @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 */
@@ -77,6 +78,7 @@ const globals = require("../../conf/globals");
  * @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
  */
 
 /**
@@ -84,6 +86,7 @@ const globals = require("../../conf/globals");
  * @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.
  */
@@ -287,11 +290,12 @@ function createLintingProblem(options) {
  * @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 = {
@@ -305,7 +309,25 @@ function createDisableDirectives(options) {
 
         // 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 }));
         }
@@ -314,26 +336,34 @@ function createDisableDirectives(options) {
 }
 
 /**
- * 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 = {};
@@ -344,8 +374,9 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
     });
 
     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;
@@ -369,7 +400,7 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
             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({
@@ -380,7 +411,7 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
             return;
         }
 
-        const directiveValue = trimmedCommentText.slice(match.index + directiveText.length);
+        const directiveValue = directivePart.slice(match.index + directiveText.length);
 
         switch (directiveText) {
             case "eslint-disable":
@@ -388,7 +419,7 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
             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);
@@ -545,7 +576,7 @@ function findEslintEnv(text) {
         if (match[0].endsWith("*/")) {
             retv = Object.assign(
                 retv || {},
-                commentParser.parseListConfig(stripDirectiveComment(match[1]))
+                commentParser.parseListConfig(extractDirectiveComment(match[1]).directivePart)
             );
         }
     }
@@ -769,14 +800,21 @@ function parse(text, languageOptions, filePath) {
      * 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,
 
@@ -1063,7 +1101,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
             )
         );
 
-        const ruleListeners = createRuleListeners(rule, ruleContext);
+        const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext);
 
         /**
          * Include `ruleId` in error logs
@@ -1081,6 +1119,10 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO
             };
         }
 
+        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
@@ -1220,6 +1262,7 @@ class Linter {
             cwd: normalizeCwd(cwd),
             lastConfigArray: null,
             lastSourceCode: null,
+            lastSuppressedMessages: [],
             configType, // TODO: Remove after flat config conversion
             parserMap: new Map([["espree", espree]]),
             ruleMap: new Rules()
@@ -1243,7 +1286,7 @@ class Linter {
      * @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);
@@ -1332,7 +1375,7 @@ class Linter {
 
         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
@@ -1425,11 +1468,11 @@ class Linter {
                     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));
             }
         }
 
@@ -1443,9 +1486,9 @@ class Linter {
          * 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));
     }
 
     /**
@@ -1454,7 +1497,7 @@ class Linter {
      * @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>";
@@ -1467,7 +1510,31 @@ class Linter {
             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.
@@ -1511,7 +1578,7 @@ class Linter {
      * @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);
@@ -1541,6 +1608,11 @@ class Linter {
             ...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;
@@ -1593,7 +1665,6 @@ class Linter {
         const sourceCode = slots.lastSourceCode;
         const commentDirectives = options.allowInlineConfig
             ? getDirectiveComments(
-                options.filename,
                 sourceCode.ast,
                 ruleId => getRuleFromConfig(ruleId, config),
                 options.warnInlineConfig
@@ -1661,7 +1732,7 @@ class Linter {
      * @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);
@@ -1698,18 +1769,30 @@ class Linter {
      * @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);
@@ -1738,7 +1821,7 @@ class Linter {
      * @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>";
@@ -1746,13 +1829,36 @@ class Linter {
         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.
@@ -1790,6 +1896,30 @@ class Linter {
         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.
@@ -1798,6 +1928,14 @@ class Linter {
         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