]> git.proxmox.com Git - pve-eslint.git/blobdiff - eslint/lib/linter/linter.js
import 8.3.0 source
[pve-eslint.git] / eslint / lib / linter / linter.js
index e94b507b5dd30ab522fa6630cc435b29c55ef453..4e07a25751e114102711d94e30d5ac1367ac8a56 100644 (file)
@@ -16,11 +16,15 @@ const
     evk = require("eslint-visitor-keys"),
     espree = require("espree"),
     merge = require("lodash.merge"),
-    BuiltInEnvironments = require("@eslint/eslintrc/conf/environments"),
     pkg = require("../../package.json"),
     astUtils = require("../shared/ast-utils"),
-    ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"),
-    ConfigValidator = require("@eslint/eslintrc/lib/shared/config-validator"),
+    {
+        Legacy: {
+            ConfigOps,
+            ConfigValidator,
+            environments: BuiltInEnvironments
+        }
+    } = require("@eslint/eslintrc/universal"),
     Traverser = require("../shared/traverser"),
     { SourceCode } = require("../source-code"),
     CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"),
@@ -37,15 +41,17 @@ const
 const debug = require("debug")("eslint:linter");
 const MAX_AUTOFIX_PASSES = 10;
 const DEFAULT_PARSER_NAME = "espree";
+const DEFAULT_ECMA_VERSION = 5;
 const commentParser = new ConfigCommentParser();
 const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } };
+const parserSymbol = Symbol.for("eslint.RuleTester.parser");
 
 //------------------------------------------------------------------------------
 // Typedefs
 //------------------------------------------------------------------------------
 
-/** @typedef {InstanceType<import("../cli-engine/config-array")["ConfigArray"]>} ConfigArray */
-/** @typedef {InstanceType<import("../cli-engine/config-array")["ExtractedConfig"]>} ExtractedConfig */
+/** @typedef {InstanceType<import("../cli-engine/config-array").ConfigArray>} ConfigArray */
+/** @typedef {InstanceType<import("../cli-engine/config-array").ExtractedConfig>} ExtractedConfig */
 /** @typedef {import("../shared/types").ConfigData} ConfigData */
 /** @typedef {import("../shared/types").Environment} Environment */
 /** @typedef {import("../shared/types").GlobalConf} GlobalConf */
@@ -54,17 +60,19 @@ const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, colum
 /** @typedef {import("../shared/types").Processor} Processor */
 /** @typedef {import("../shared/types").Rule} Rule */
 
+/* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
 /**
  * @template T
  * @typedef {{ [P in keyof T]-?: T[P] }} Required
  */
+/* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */
 
 /**
  * @typedef {Object} DisableDirective
- * @property {("disable"|"enable"|"disable-line"|"disable-next-line")} type
- * @property {number} line
- * @property {number} column
- * @property {(string|null)} ruleId
+ * @property {("disable"|"enable"|"disable-line"|"disable-next-line")} type Type of directive
+ * @property {number} line The line number
+ * @property {number} column The column number
+ * @property {(string|null)} ruleId The rule ID
  */
 
 /**
@@ -92,12 +100,12 @@ const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, colum
  * @typedef {Object} ProcessorOptions
  * @property {(filename:string, text:string) => boolean} [filterCodeBlock] the
  *      predicate function that selects adopt code blocks.
- * @property {Processor["postprocess"]} [postprocess] postprocessor for report
+ * @property {Processor.postprocess} [postprocess] postprocessor for report
  *      messages. If provided, this should accept an array of the message lists
  *      for each code block returned from the preprocessor, apply a mapping to
  *      the messages as appropriate, and return a one-dimensional array of
  *      messages.
- * @property {Processor["preprocess"]} [preprocess] preprocessor for source text.
+ * @property {Processor.preprocess} [preprocess] preprocessor for source text.
  *      If provided, this should accept a string of source text, and return an
  *      array of code blocks to lint.
  */
@@ -240,14 +248,14 @@ function createLintingProblem(options) {
  * Creates a collection of disable directives from a comment
  * @param {Object} options to create disable directives
  * @param {("disable"|"enable"|"disable-line"|"disable-next-line")} options.type The type of directive comment
- * @param {{line: number, column: number}} options.loc The 0-based location of the comment token
+ * @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 {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 { type, loc, value, ruleMapper } = options;
+    const { commentToken, type, value, ruleMapper } = options;
     const ruleIds = Object.keys(commentParser.parseListConfig(value));
     const directiveRules = ruleIds.length ? ruleIds : [null];
     const result = {
@@ -255,13 +263,15 @@ function createDisableDirectives(options) {
         directiveProblems: [] // problems in directives
     };
 
+    const parentComment = { commentToken, ruleIds };
+
     for (const ruleId of directiveRules) {
 
         // push to directives, if the rule is defined(including null, e.g. /*eslint enable*/)
         if (ruleId === null || ruleMapper(ruleId) !== null) {
-            result.directives.push({ type, line: loc.start.line, column: loc.start.column + 1, ruleId });
+            result.directives.push({ parentComment, type, line: commentToken.loc.start.line, column: commentToken.loc.start.column + 1, ruleId });
         } else {
-            result.directiveProblems.push(createLintingProblem({ ruleId, loc }));
+            result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc }));
         }
     }
     return result;
@@ -342,7 +352,7 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
             case "eslint-disable-next-line":
             case "eslint-disable-line": {
                 const directiveType = directiveText.slice("eslint-".length);
-                const options = { type: directiveType, loc: comment.loc, value: directiveValue, ruleMapper };
+                const options = { commentToken: comment, type: directiveType, value: directiveValue, ruleMapper };
                 const { directives, directiveProblems } = createDisableDirectives(options);
 
                 disableDirectives.push(...directives);
@@ -432,10 +442,16 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
 
 /**
  * Normalize ECMAScript version from the initial config
- * @param  {number} ecmaVersion ECMAScript version from the initial config
+ * @param {Parser} parser The parser which uses this options.
+ * @param {number} ecmaVersion ECMAScript version from the initial config
  * @returns {number} normalized ECMAScript version
  */
-function normalizeEcmaVersion(ecmaVersion) {
+function normalizeEcmaVersion(parser, ecmaVersion) {
+    if ((parser[parserSymbol] || parser) === espree) {
+        if (ecmaVersion === "latest") {
+            return espree.latestEcmaVersion;
+        }
+    }
 
     /*
      * Calculate ECMAScript edition number from official year version starting with
@@ -444,7 +460,7 @@ function normalizeEcmaVersion(ecmaVersion) {
     return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion;
 }
 
-const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gsu;
+const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu;
 
 /**
  * Checks whether or not there is a comment which has "eslint-env *" in a given text.
@@ -457,10 +473,12 @@ function findEslintEnv(text) {
     eslintEnvPattern.lastIndex = 0;
 
     while ((match = eslintEnvPattern.exec(text)) !== null) {
-        retv = Object.assign(
-            retv || {},
-            commentParser.parseListConfig(stripDirectiveComment(match[1]))
-        );
+        if (match[0].endsWith("*/")) {
+            retv = Object.assign(
+                retv || {},
+                commentParser.parseListConfig(stripDirectiveComment(match[1]))
+            );
+        }
     }
 
     return retv;
@@ -521,12 +539,13 @@ function normalizeVerifyOptions(providedOptions, config) {
 
 /**
  * Combines the provided parserOptions with the options from environments
- * @param {string} parserName The parser name which uses this options.
+ * @param {Parser} parser The parser which uses this options.
  * @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config
  * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
  * @returns {ParserOptions} Resulting parser options after merge
  */
-function resolveParserOptions(parserName, providedOptions, enabledEnvironments) {
+function resolveParserOptions(parser, providedOptions, enabledEnvironments) {
+
     const parserOptionsFromEnv = enabledEnvironments
         .filter(env => env.parserOptions)
         .reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {});
@@ -542,12 +561,7 @@ function resolveParserOptions(parserName, providedOptions, enabledEnvironments)
         mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false });
     }
 
-    /*
-     * TODO: @aladdin-add
-     * 1. for a 3rd-party parser, do not normalize parserOptions
-     * 2. for espree, no need to do this (espree will do it)
-     */
-    mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion);
+    mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion);
 
     return mergedParserOptions;
 }
@@ -606,13 +620,13 @@ function getRuleOptions(ruleConfig) {
  */
 function analyzeScope(ast, parserOptions, visitorKeys) {
     const ecmaFeatures = parserOptions.ecmaFeatures || {};
-    const ecmaVersion = parserOptions.ecmaVersion || 5;
+    const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
 
     return eslintScope.analyze(ast, {
         ignoreEval: true,
         nodejsScope: ecmaFeatures.globalReturn,
         impliedStrict: ecmaFeatures.impliedStrict,
-        ecmaVersion,
+        ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
         sourceType: parserOptions.sourceType || "script",
         childVisitorKeys: visitorKeys || evk.KEYS,
         fallback: Traverser.getKeys
@@ -754,6 +768,7 @@ function markVariableAsUsed(scopeManager, currentNode, parserOptions, name) {
  * Runs a rule, and gets its listeners
  * @param {Rule} rule A normalized rule with a `create` method
  * @param {Context} ruleContext The context that should be passed to the rule
+ * @throws {any} Any error during the rule's `create`
  * @returns {Object} A map of selector listeners provided by the rule
  */
 function createRuleListeners(rule, ruleContext) {
@@ -921,8 +936,16 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
                         }
                         const problem = reportTranslator(...args);
 
-                        if (problem.fix && rule.meta && !rule.meta.fixable) {
-                            throw new Error("Fixable rules should export a `meta.fixable` property.");
+                        if (problem.fix && !(rule.meta && rule.meta.fixable)) {
+                            throw new Error("Fixable rules must set the `meta.fixable` property to \"code\" or \"whitespace\".");
+                        }
+                        if (problem.suggestions && !(rule.meta && rule.meta.hasSuggestions === true)) {
+                            if (rule.meta && rule.meta.docs && typeof rule.meta.docs.suggestion !== "undefined") {
+
+                                // Encourage migration from the former property name.
+                                throw new Error("Rules with suggestions must set the `meta.hasSuggestions` property to `true`. `meta.docs.suggestion` is ignored by ESLint.");
+                            }
+                            throw new Error("Rules with suggestions must set the `meta.hasSuggestions` property to `true`.");
                         }
                         lintingProblems.push(problem);
                     }
@@ -932,13 +955,31 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser
 
         const ruleListeners = createRuleListeners(rule, ruleContext);
 
+        /**
+         * Include `ruleId` in error logs
+         * @param {Function} ruleListener A rule method that listens for a node.
+         * @returns {Function} ruleListener wrapped in error handler
+         */
+        function addRuleErrorHandler(ruleListener) {
+            return function ruleErrorHandler(...listenerArgs) {
+                try {
+                    return ruleListener(...listenerArgs);
+                } catch (e) {
+                    e.ruleId = ruleId;
+                    throw e;
+                }
+            };
+        }
+
         // add all the selectors from the rule as listeners
         Object.keys(ruleListeners).forEach(selector => {
+            const ruleListener = timing.enabled
+                ? timing.time(ruleId, ruleListeners[selector])
+                : ruleListeners[selector];
+
             emitter.on(
                 selector,
-                timing.enabled
-                    ? timing.time(ruleId, ruleListeners[selector])
-                    : ruleListeners[selector]
+                addRuleErrorHandler(ruleListener)
             );
         });
     });
@@ -1023,7 +1064,7 @@ function normalizeCwd(cwd) {
     }
 
     // It's more explicit to assign the undefined
-    // eslint-disable-next-line no-undefined
+    // eslint-disable-next-line no-undefined -- Consistently returning a value
     return undefined;
 }
 
@@ -1046,7 +1087,7 @@ class Linter {
     /**
      * Initialize the Linter.
      * @param {Object} [config] the config object
-     * @param {string} [config.cwd]  path to a directory that should be considered as the current working directory, can be undefined.
+     * @param {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
      */
     constructor({ cwd } = {}) {
         internalSlotsMap.set(this, {
@@ -1074,6 +1115,7 @@ class Linter {
      * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
      * @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.
      */
     _verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
@@ -1123,7 +1165,7 @@ class Linter {
             .map(envName => getEnv(slots, envName))
             .filter(env => env);
 
-        const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs);
+        const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs);
         const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
         const settings = config.settings || {};
 
@@ -1199,11 +1241,17 @@ class Linter {
             debug("Parser Options:", parserOptions);
             debug("Parser Path:", parserName);
             debug("Settings:", settings);
+
+            if (err.ruleId) {
+                err.message += `\nRule: "${err.ruleId}"`;
+            }
+
             throw err;
         }
 
         return applyDisableDirectives({
             directives: commentDirectives.disableDirectives,
+            disableFixes: options.disableFixes,
             problems: lintingProblems
                 .concat(commentDirectives.problems)
                 .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
@@ -1291,8 +1339,7 @@ class Linter {
         const text = ensureText(textOrSourceCode);
         const preprocess = options.preprocess || (rawText => [rawText]);
 
-        // TODO(stephenwade): Replace this with array.flat() when we drop support for Node v10
-        const postprocess = options.postprocess || (array => [].concat(...array));
+        const postprocess = options.postprocess || (messagesList => messagesList.flat());
         const filterCodeBlock =
             options.filterCodeBlock ||
             (blockFilename => blockFilename.endsWith(".js"));