node_modules/eslint/lib/linter/linter.js in immosquare-cleaner-0.1.32 vs node_modules/eslint/lib/linter/linter.js in immosquare-cleaner-0.1.38

- old
+ new

@@ -9,11 +9,11 @@ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const - path = require("path"), + path = require("node:path"), eslintScope = require("eslint-scope"), evk = require("eslint-visitor-keys"), espree = require("espree"), merge = require("lodash.merge"), pkg = require("../../package.json"), @@ -28,11 +28,10 @@ environments: BuiltInEnvironments } } = require("@eslint/eslintrc/universal"), Traverser = require("../shared/traverser"), { SourceCode } = require("../source-code"), - CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), applyDisableDirectives = require("./apply-disable-directives"), ConfigCommentParser = require("./config-comment-parser"), NodeEventGenerator = require("./node-event-generator"), createReportTranslator = require("./report-translator"), Rules = require("./rules"), @@ -40,36 +39,39 @@ SourceCodeFixer = require("./source-code-fixer"), timing = require("./timing"), ruleReplacements = require("../../conf/replacements.json"); const { getRuleFromConfig } = require("../config/flat-config-helpers"); const { FlatConfigArray } = require("../config/flat-config-array"); +const { startTime, endTime } = require("../shared/stats"); const { RuleValidator } = require("../config/rule-validator"); -const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema"); +const { assertIsRuleSeverity } = require("../config/flat-config-schema"); const { normalizeSeverityToString } = require("../shared/severity"); 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"); +const { LATEST_ECMA_VERSION } = require("../../conf/ecma-version"); +const STEP_KIND_VISIT = 1; +const STEP_KIND_CALL = 2; //------------------------------------------------------------------------------ // Typedefs //------------------------------------------------------------------------------ -/** @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 */ /** @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 */ /** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {import("../shared/types").Times} Times */ /* 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 @@ -90,10 +92,11 @@ * @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 {Times} times The times spent on applying a rule to a file (see `stats` option). * @property {Rules} ruleMap The loaded rules. */ /** * @typedef {Object} VerifyOptions @@ -103,10 +106,11 @@ * @property {boolean} [disableFixes] if `true` then the linter doesn't make `fix` * properties into the lint result. * @property {string} [filename] the filename of the source code. * @property {boolean | "off" | "warn" | "error"} [reportUnusedDisableDirectives] Adds reported errors for * unused `eslint-disable` directives. + * @property {Function} [ruleFilter] A predicate function that determines whether a given rule should run. */ /** * @typedef {Object} ProcessorOptions * @property {(filename:string, text:string) => boolean} [filterCodeBlock] the @@ -228,11 +232,11 @@ * @param {string} ruleId the ruleId to create * @returns {string} created error message * @private */ function createMissingRuleMessage(ruleId) { - return Object.prototype.hasOwnProperty.call(ruleReplacements.rules, ruleId) + return Object.hasOwn(ruleReplacements.rules, ruleId) ? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements.rules[ruleId].join(", ")}` : `Definition for rule '${ruleId}' was not found.`; } /** @@ -267,53 +271,51 @@ /** * 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 {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 + * @param {ASTNode|token} options.node The Comment node/token. + * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules * @returns {Object} Directives and problems from the comment */ -function createDisableDirectives(options) { - const { commentToken, type, value, justification, ruleMapper } = options; +function createDisableDirectives({ type, value, justification, node }, ruleMapper) { const ruleIds = Object.keys(commentParser.parseListConfig(value)); const directiveRules = ruleIds.length ? ruleIds : [null]; const result = { directives: [], // valid disable directives directiveProblems: [] // problems in directives }; + const parentDirective = { node, ruleIds }; - 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)) { if (type === "disable-next-line") { result.directives.push({ - parentComment, + parentDirective, type, - line: commentToken.loc.end.line, - column: commentToken.loc.end.column + 1, + line: node.loc.end.line, + column: node.loc.end.column + 1, ruleId, justification }); } else { result.directives.push({ - parentComment, + parentDirective, type, - line: commentToken.loc.start.line, - column: commentToken.loc.start.column + 1, + line: node.loc.start.line, + column: node.loc.start.column + 1, ruleId, justification }); } } else { - result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc })); + result.directiveProblems.push(createLintingProblem({ ruleId, loc: node.loc })); } } return result; } @@ -322,14 +324,15 @@ * and environments and merges them with global config; also code blocks * where reporting is disabled or enabled and merges them with reporting config. * @param {SourceCode} sourceCode The SourceCode object to get comments from. * @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. + * @param {ConfigData} config Provided config. * @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: LintMessage[], disableDirectives: DisableDirective[]}} * A collection of the directive comments that were found, along with any problems that occurred when parsing */ -function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { +function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig, config) { const configuredRules = {}; const enabledGlobals = Object.create(null); const exportedVariables = {}; const problems = []; const disableDirectives = []; @@ -381,20 +384,24 @@ case "eslint-disable": case "eslint-enable": case "eslint-disable-next-line": case "eslint-disable-line": { const directiveType = directiveText.slice("eslint-".length); - const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper }; - const { directives, directiveProblems } = createDisableDirectives(options); + const { directives, directiveProblems } = createDisableDirectives({ + type: directiveType, + value: directiveValue, + justification: justificationPart, + node: comment + }, ruleMapper); disableDirectives.push(...directives); problems.push(...directiveProblems); break; } case "exported": - Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment)); + Object.assign(exportedVariables, commentParser.parseListConfig(directiveValue, comment)); break; case "globals": case "global": for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) { @@ -434,24 +441,83 @@ if (!rule) { problems.push(createLintingProblem({ ruleId: name, loc: comment.loc })); return; } + if (Object.hasOwn(configuredRules, name)) { + problems.push(createLintingProblem({ + message: `Rule "${name}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`, + loc: comment.loc + })); + return; + } + + let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; + + /* + * If the rule was already configured, inline rule configuration that + * only has severity should retain options from the config and just override the severity. + * + * Example: + * + * { + * rules: { + * curly: ["error", "multi"] + * } + * } + * + * /* eslint curly: ["warn"] * / + * + * Results in: + * + * curly: ["warn", "multi"] + */ + if ( + + /* + * If inline config for the rule has only severity + */ + ruleOptions.length === 1 && + + /* + * And the rule was already configured + */ + config.rules && Object.hasOwn(config.rules, name) + ) { + + /* + * Then use severity from the inline config and options from the provided config + */ + ruleOptions = [ + ruleOptions[0], // severity from the inline config + ...Array.isArray(config.rules[name]) ? config.rules[name].slice(1) : [] // options from the provided config + ]; + } + try { - validator.validateRuleOptions(rule, name, ruleValue); + validator.validateRuleOptions(rule, name, ruleOptions); } catch (err) { + + /* + * If the rule has invalid `meta.schema`, throw the error because + * this is not an invalid inline configuration but an invalid rule. + */ + if (err.code === "ESLINT_INVALID_RULE_OPTIONS_SCHEMA") { + throw err; + } + problems.push(createLintingProblem({ ruleId: name, message: err.message, loc: comment.loc })); // do not apply the config, if found invalid options. return; } - configuredRules[name] = ruleValue; + configuredRules[name] = ruleOptions; }); } else { problems.push(parseResult.error); } @@ -477,57 +543,25 @@ * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules * @returns {{problems: LintMessage[], disableDirectives: DisableDirective[]}} * A collection of the directive comments that were found, along with any problems that occurred when parsing */ function getDirectiveCommentsForFlatConfig(sourceCode, ruleMapper) { - const problems = []; const disableDirectives = []; + const problems = []; - sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => { - const { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value); + const { + directives: directivesSources, + problems: directivesProblems + } = sourceCode.getDisableDirectives(); - const match = directivesPattern.exec(directivePart); + problems.push(...directivesProblems.map(createLintingProblem)); - if (!match) { - return; - } - const directiveText = match[1]; - const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText); + directivesSources.forEach(directive => { + const { directives, directiveProblems } = createDisableDirectives(directive, ruleMapper); - if (comment.type === "Line" && !lineCommentSupported) { - return; - } - - 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({ - ruleId: null, - message, - loc: comment.loc - })); - return; - } - - const directiveValue = directivePart.slice(match.index + directiveText.length); - - switch (directiveText) { - case "eslint-disable": - case "eslint-enable": - case "eslint-disable-next-line": - case "eslint-disable-line": { - const directiveType = directiveText.slice("eslint-".length); - const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper }; - const { directives, directiveProblems } = createDisableDirectives(options); - - disableDirectives.push(...directives); - problems.push(...directiveProblems); - break; - } - - // no default - } + disableDirectives.push(...directives); + problems.push(...directiveProblems); }); return { problems, disableDirectives @@ -582,11 +616,11 @@ * We default to the latest supported ecmaVersion for everything else. * Remember, this is for languageOptions.ecmaVersion, which sets the version * that is used for a number of processes inside of ESLint. It's normally * safe to assume people want the latest unless otherwise specified. */ - return espree.latestEcmaVersion + 2009; + return LATEST_ECMA_VERSION; } const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu; /** @@ -659,18 +693,26 @@ } else { reportUnusedDisableDirectives = linterOptions.reportUnusedDisableDirectives === void 0 ? "off" : normalizeSeverityToString(linterOptions.reportUnusedDisableDirectives); } } + let ruleFilter = providedOptions.ruleFilter; + + if (typeof ruleFilter !== "function") { + ruleFilter = () => true; + } + return { filename: normalizeFilename(providedOptions.filename || "<input>"), allowInlineConfig: !ignoreInlineConfig, warnInlineConfig: disableInlineConfig && !ignoreInlineConfig ? `your config${configNameOfNoInlineConfig}` : null, reportUnusedDisableDirectives, - disableFixes: Boolean(providedOptions.disableFixes) + disableFixes: Boolean(providedOptions.disableFixes), + stats: providedOptions.stats, + ruleFilter }; } /** * Combines the provided parserOptions with the options from environments @@ -731,11 +773,11 @@ * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments * @returns {Record<string, GlobalConf>} The resolved globals object */ function resolveGlobals(providedGlobals, enabledEnvironments) { return Object.assign( - {}, + Object.create(null), ...enabledEnvironments.filter(env => env.globals).map(env => env.globals), providedGlobals ); } @@ -756,10 +798,40 @@ } return text; } /** + * Store time measurements in map + * @param {number} time Time measurement + * @param {Object} timeOpts Options relating which time was measured + * @param {WeakMap<Linter, LinterInternalSlots>} slots Linter internal slots map + * @returns {void} + */ +function storeTime(time, timeOpts, slots) { + const { type, key } = timeOpts; + + if (!slots.times) { + slots.times = { passes: [{}] }; + } + + const passIndex = slots.fixPasses; + + if (passIndex > slots.times.passes.length - 1) { + slots.times.passes.push({}); + } + + if (key) { + slots.times.passes[passIndex][type] ??= {}; + slots.times.passes[passIndex][type][key] ??= { total: 0 }; + slots.times.passes[passIndex][type][key].total += time; + } else { + slots.times.passes[passIndex][type] ??= { total: 0 }; + slots.times.passes[passIndex][type].total += time; + } +} + +/** * Get the options for a rule (not including severity), if any * @param {Array|number} ruleConfig rule configuration * @returns {Array} of rule options, empty Array if none */ function getRuleOptions(ruleConfig) { @@ -883,61 +955,30 @@ } } /** * Runs a rule, and gets its listeners - * @param {Rule} rule A normalized rule with a `create` method + * @param {Rule} rule A rule object * @param {Context} ruleContext The context that should be passed to the rule + * @throws {TypeError} If `rule` is not an object with a `create` method * @throws {any} Any error during the rule's `create` * @returns {Object} A map of selector listeners provided by the rule */ function createRuleListeners(rule, ruleContext) { + + if (!rule || typeof rule !== "object" || typeof rule.create !== "function") { + throw new TypeError(`Error while loading rule '${ruleContext.id}': Rule must be an object with a \`create\` method`); + } + try { return rule.create(ruleContext); } catch (ex) { ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`; throw ex; } } -// methods that exist on SourceCode object -const DEPRECATED_SOURCECODE_PASSTHROUGHS = { - getSource: "getText", - getSourceLines: "getLines", - getAllComments: "getAllComments", - getNodeByRangeIndex: "getNodeByRangeIndex", - getComments: "getComments", - getCommentsBefore: "getCommentsBefore", - getCommentsAfter: "getCommentsAfter", - getCommentsInside: "getCommentsInside", - getJSDocComment: "getJSDocComment", - getFirstToken: "getFirstToken", - getFirstTokens: "getFirstTokens", - getLastToken: "getLastToken", - getLastTokens: "getLastTokens", - getTokenAfter: "getTokenAfter", - getTokenBefore: "getTokenBefore", - getTokenByRangeStart: "getTokenByRangeStart", - getTokens: "getTokens", - getTokensAfter: "getTokensAfter", - getTokensBefore: "getTokensBefore", - getTokensBetween: "getTokensBetween" -}; - - -const BASE_TRAVERSAL_CONTEXT = Object.freeze( - Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce( - (contextInfo, methodName) => - Object.assign(contextInfo, { - [methodName](...args) { - return this.sourceCode[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]](...args); - } - }), - {} - ) -); - /** * Runs the given rules on the given SourceCode object * @param {SourceCode} sourceCode A SourceCode object for the given text * @param {Object} configuredRules The rules configuration * @param {function(string): Rule} ruleMapper A mapper function from rule names to rules @@ -946,58 +987,45 @@ * @param {Object} settings The settings that were enabled in the config * @param {string} filename The reported filename of the code * @param {boolean} disableFixes If true, it doesn't make `fix` properties. * @param {string | undefined} cwd cwd of the cli * @param {string} physicalFilename The full path of the file on disk without any code block information + * @param {Function} ruleFilter A predicate function to filter which rules should be executed. + * @param {boolean} stats If true, stats are collected appended to the result + * @param {WeakMap<Linter, LinterInternalSlots>} slots InternalSlotsMap of linter * @returns {LintMessage[]} An array of reported problems + * @throws {Error} If traversal into a node fails. */ -function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) { +function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter, + stats, slots) { const emitter = createEmitter(); - const nodeQueue = []; - let currentNode = sourceCode.ast; - Traverser.traverse(sourceCode.ast, { - enter(node, parent) { - node.parent = parent; - nodeQueue.push({ isEntering: true, node }); - }, - leave(node) { - nodeQueue.push({ isEntering: false, node }); - }, - visitorKeys: sourceCode.visitorKeys - }); + // must happen first to assign all node.parent properties + const eventQueue = sourceCode.traverse(); /* * Create a frozen object with the ruleContext properties and methods that are shared by all rules. * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the * properties once for each rule. */ const sharedTraversalContext = Object.freeze( - Object.assign( - Object.create(BASE_TRAVERSAL_CONTEXT), - { - getAncestors: () => sourceCode.getAncestors(currentNode), - getDeclaredVariables: node => sourceCode.getDeclaredVariables(node), - getCwd: () => cwd, - cwd, - getFilename: () => filename, - filename, - getPhysicalFilename: () => physicalFilename || filename, - physicalFilename: physicalFilename || filename, - getScope: () => sourceCode.getScope(currentNode), - getSourceCode: () => sourceCode, - sourceCode, - markVariableAsUsed: name => sourceCode.markVariableAsUsed(name, currentNode), - parserOptions: { - ...languageOptions.parserOptions - }, - parserPath: parserName, - languageOptions, - parserServices: sourceCode.parserServices, - settings - } - ) + { + getCwd: () => cwd, + cwd, + getFilename: () => filename, + filename, + getPhysicalFilename: () => physicalFilename || filename, + physicalFilename: physicalFilename || filename, + getSourceCode: () => sourceCode, + sourceCode, + parserOptions: { + ...languageOptions.parserOptions + }, + parserPath: parserName, + languageOptions, + settings + } ); const lintingProblems = []; Object.keys(configuredRules).forEach(ruleId => { @@ -1006,10 +1034,14 @@ // not load disabled rules if (severity === 0) { return; } + if (ruleFilter && !ruleFilter({ ruleId, severity })) { + return; + } + const rule = ruleMapper(ruleId); if (!rule) { lintingProblems.push(createLintingProblem({ ruleId })); return; @@ -1061,21 +1093,36 @@ } } ) ); - const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext); + const ruleListenersReturn = (timing.enabled || stats) + ? timing.time(ruleId, createRuleListeners, stats)(rule, ruleContext) : createRuleListeners(rule, ruleContext); + const ruleListeners = stats ? ruleListenersReturn.result : ruleListenersReturn; + + if (stats) { + storeTime(ruleListenersReturn.tdiff, { type: "rules", key: ruleId }, slots); + } + /** * 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); + const ruleListenerReturn = ruleListener(...listenerArgs); + + const ruleListenerResult = stats ? ruleListenerReturn.result : ruleListenerReturn; + + if (stats) { + storeTime(ruleListenerReturn.tdiff, { type: "rules", key: ruleId }, slots); + } + + return ruleListenerResult; } catch (e) { e.ruleId = ruleId; throw e; } }; @@ -1085,41 +1132,49 @@ 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 - ? timing.time(ruleId, ruleListeners[selector]) - : ruleListeners[selector]; + const ruleListener = (timing.enabled || stats) + ? timing.time(ruleId, ruleListeners[selector], stats) : ruleListeners[selector]; emitter.on( selector, addRuleErrorHandler(ruleListener) ); }); }); - // only run code path analyzer if the top level node is "Program", skip otherwise - const eventGenerator = nodeQueue[0].node.type === "Program" - ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys })) - : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }); + const eventGenerator = new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }); - nodeQueue.forEach(traversalInfo => { - currentNode = traversalInfo.node; + for (const step of eventQueue) { + switch (step.kind) { + case STEP_KIND_VISIT: { + try { + if (step.phase === 1) { + eventGenerator.enterNode(step.target); + } else { + eventGenerator.leaveNode(step.target); + } + } catch (err) { + err.currentNode = step.target; + throw err; + } + break; + } - try { - if (traversalInfo.isEntering) { - eventGenerator.enterNode(currentNode); - } else { - eventGenerator.leaveNode(currentNode); + case STEP_KIND_CALL: { + emitter.emit(step.target, ...step.args); + break; } - } catch (err) { - err.currentNode = currentNode; - throw err; + + default: + throw new Error(`Invalid traversal step found: "${step.type}".`); } - }); + } + return lintingProblems; } /** * Ensure the source code to be a string. @@ -1200,11 +1255,10 @@ if (configType === "flat") { throw new Error("This method cannot be used with flat config. Add your entries directly into the config array."); } } - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ /** @@ -1215,13 +1269,13 @@ /** * 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 {"flat"|"eslintrc"} [config.configType="eslintrc"] the type of config used. + * @param {"flat"|"eslintrc"} [config.configType="flat"] the type of config used. */ - constructor({ cwd, configType } = {}) { + constructor({ cwd, configType = "flat" } = {}) { internalSlotsMap.set(this, { cwd: normalizeCwd(cwd), lastConfigArray: null, lastSourceCode: null, lastSuppressedMessages: [], @@ -1306,16 +1360,29 @@ parser, parserOptions }); if (!slots.lastSourceCode) { + let t; + + if (options.stats) { + t = startTime(); + } + const parseResult = parse( text, languageOptions, options.filename ); + if (options.stats) { + const time = endTime(t); + const timeOpts = { type: "parse" }; + + storeTime(time, timeOpts, slots); + } + if (!parseResult.success) { return [parseResult.error]; } slots.lastSourceCode = parseResult.sourceCode; @@ -1336,21 +1403,22 @@ } } const sourceCode = slots.lastSourceCode; const commentDirectives = options.allowInlineConfig - ? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig) + ? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig, config) : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; // augment global scope with declared global variables addDeclaredGlobals( sourceCode.scopeManager.scopes[0], configuredGlobals, { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals } ); const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules); + let lintingProblems; try { lintingProblems = runRules( sourceCode, @@ -1360,11 +1428,14 @@ languageOptions, settings, options.filename, options.disableFixes, slots.cwd, - providedOptions.physicalFilename + providedOptions.physicalFilename, + null, + options.stats, + slots ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; debug("An error occurred while traversing"); debug("Filename:", options.filename); @@ -1411,48 +1482,48 @@ const options = typeof filenameOrOptions === "string" ? { filename: filenameOrOptions } : filenameOrOptions || {}; - if (config) { - if (configType === "flat") { + const configToUse = config ?? {}; - /* - * Because of how Webpack packages up the files, we can't - * compare directly to `FlatConfigArray` using `instanceof` - * because it's not the same `FlatConfigArray` as in the tests. - * So, we work around it by assuming an array is, in fact, a - * `FlatConfigArray` if it has a `getConfig()` method. - */ - let configArray = config; + if (configType !== "eslintrc") { - if (!Array.isArray(config) || typeof config.getConfig !== "function") { - configArray = new FlatConfigArray(config, { basePath: cwd }); - configArray.normalizeSync(); - } + /* + * Because of how Webpack packages up the files, we can't + * compare directly to `FlatConfigArray` using `instanceof` + * because it's not the same `FlatConfigArray` as in the tests. + * So, we work around it by assuming an array is, in fact, a + * `FlatConfigArray` if it has a `getConfig()` method. + */ + let configArray = configToUse; - return this._distinguishSuppressedMessages(this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true)); + if (!Array.isArray(configToUse) || typeof configToUse.getConfig !== "function") { + configArray = new FlatConfigArray(configToUse, { basePath: cwd }); + configArray.normalizeSync(); } - if (typeof config.extractConfig === "function") { - return this._distinguishSuppressedMessages(this._verifyWithConfigArray(textOrSourceCode, config, options)); - } + return this._distinguishSuppressedMessages(this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true)); } + if (typeof configToUse.extractConfig === "function") { + return this._distinguishSuppressedMessages(this._verifyWithConfigArray(textOrSourceCode, configToUse, options)); + } + /* * If we get to here, it means `config` is just an object rather * than a config array so we can go right into linting. */ /* * `Linter` doesn't support `overrides` property in configuration. * So we cannot apply multiple processors. */ if (options.preprocess || options.postprocess) { - return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, config, options)); + return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, configToUse, options)); } - return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, config, options)); + return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, configToUse, options)); } /** * Verify with a processor. * @param {string|SourceCode} textOrSourceCode The source code. @@ -1588,16 +1659,28 @@ } const settings = config.settings || {}; if (!slots.lastSourceCode) { + let t; + + if (options.stats) { + t = startTime(); + } + const parseResult = parse( text, languageOptions, options.filename ); + if (options.stats) { + const time = endTime(t); + + storeTime(time, { type: "parse" }, slots); + } + if (!parseResult.success) { return [parseResult.error]; } slots.lastSourceCode = parseResult.sourceCode; @@ -1675,26 +1758,92 @@ if (!rule) { inlineConfigProblems.push(createLintingProblem({ ruleId, loc: node.loc })); return; } + if (Object.hasOwn(mergedInlineConfig.rules, ruleId)) { + inlineConfigProblems.push(createLintingProblem({ + message: `Rule "${ruleId}" is already configured by another configuration comment in the preceding code. This configuration is ignored.`, + loc: node.loc + })); + return; + } + try { - const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; + let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; - assertIsRuleOptions(ruleId, ruleValue); assertIsRuleSeverity(ruleId, ruleOptions[0]); - ruleValidator.validate({ - plugins: config.plugins, - rules: { - [ruleId]: ruleOptions + /* + * If the rule was already configured, inline rule configuration that + * only has severity should retain options from the config and just override the severity. + * + * Example: + * + * { + * rules: { + * curly: ["error", "multi"] + * } + * } + * + * /* eslint curly: ["warn"] * / + * + * Results in: + * + * curly: ["warn", "multi"] + */ + + let shouldValidateOptions = true; + + if ( + + /* + * If inline config for the rule has only severity + */ + ruleOptions.length === 1 && + + /* + * And the rule was already configured + */ + config.rules && Object.hasOwn(config.rules, ruleId) + ) { + + /* + * Then use severity from the inline config and options from the provided config + */ + ruleOptions = [ + ruleOptions[0], // severity from the inline config + ...config.rules[ruleId].slice(1) // options from the provided config + ]; + + // if the rule was enabled, the options have already been validated + if (config.rules[ruleId][0] > 0) { + shouldValidateOptions = false; } - }); - mergedInlineConfig.rules[ruleId] = ruleValue; + } + + if (shouldValidateOptions) { + ruleValidator.validate({ + plugins: config.plugins, + rules: { + [ruleId]: ruleOptions + } + }); + } + + mergedInlineConfig.rules[ruleId] = ruleOptions; } catch (err) { + /* + * If the rule has invalid `meta.schema`, throw the error because + * this is not an invalid inline configuration but an invalid rule. + */ + if (err.code === "ESLINT_INVALID_RULE_OPTIONS_SCHEMA") { + throw err; + } + let baseMessage = err.message.slice( err.message.startsWith("Key \"rules\":") ? err.message.indexOf(":", 12) + 1 : err.message.indexOf(":") + 1 ).trim(); @@ -1720,10 +1869,11 @@ ruleId => getRuleFromConfig(ruleId, config) ) : { problems: [], disableDirectives: [] }; const configuredRules = Object.assign({}, config.rules, mergedInlineConfig.rules); + let lintingProblems; sourceCode.finalize(); try { @@ -1735,11 +1885,14 @@ languageOptions, settings, options.filename, options.disableFixes, slots.cwd, - providedOptions.physicalFilename + providedOptions.physicalFilename, + options.ruleFilter, + options.stats, + slots ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; debug("An error occurred while traversing"); debug("Filename:", options.filename); @@ -1766,11 +1919,13 @@ disableFixes: options.disableFixes, problems: lintingProblems .concat(commentDirectives.problems) .concat(inlineConfigProblems) .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column), - reportUnusedDisableDirectives: options.reportUnusedDisableDirectives + reportUnusedDisableDirectives: options.reportUnusedDisableDirectives, + ruleFilter: options.ruleFilter, + configuredRules }); } /** * Verify a given code with `ConfigArray`. @@ -1974,31 +2129,47 @@ getSourceCode() { return internalSlotsMap.get(this).lastSourceCode; } /** + * Gets the times spent on (parsing, fixing, linting) a file. + * @returns {LintTimes} The times. + */ + getTimes() { + return internalSlotsMap.get(this).times ?? { passes: [] }; + } + + /** + * Gets the number of autofix passes that were made in the last run. + * @returns {number} The number of autofix passes. + */ + getFixPassCount() { + return internalSlotsMap.get(this).fixPasses ?? 0; + } + + /** * 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 - * @param {Function | Rule} ruleModule Function from context to object mapping AST node types to event handlers + * @param {Rule} rule A rule object * @returns {void} */ - defineRule(ruleId, ruleModule) { + defineRule(ruleId, rule) { assertEslintrcConfig(this); - internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule); + internalSlotsMap.get(this).ruleMap.define(ruleId, rule); } /** * Defines many new linting rules. - * @param {Record<string, Function | Rule>} rulesToDefine map from unique rule identifier to rule + * @param {Record<string, Rule>} rulesToDefine map from unique rule identifier to rule * @returns {void} */ defineRules(rulesToDefine) { assertEslintrcConfig(this); Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => { @@ -2042,36 +2213,68 @@ * @param {VerifyOptions&ProcessorOptions&FixOptions} options The ESLint options object to use. * @returns {{fixed:boolean,messages:LintMessage[],output:string}} The result of the fix operation as returned from the * SourceCodeFixer. */ verifyAndFix(text, config, options) { - let messages = [], + let messages, fixedResult, fixed = false, passNumber = 0, currentText = text; const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`; const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true; + const stats = options?.stats; /** * This loop continues until one of the following is true: * * 1. No more fixes have been applied. * 2. Ten passes have been made. * * That means anytime a fix is successfully applied, there will be another pass. * Essentially, guaranteeing a minimum of two passes. */ + const slots = internalSlotsMap.get(this); + + // Remove lint times from the last run. + if (stats) { + delete slots.times; + slots.fixPasses = 0; + } + do { passNumber++; + let tTotal; + if (stats) { + tTotal = startTime(); + } + debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`); messages = this.verify(currentText, config, options); debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`); + let t; + + if (stats) { + t = startTime(); + } + fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix); + if (stats) { + + if (fixedResult.fixed) { + const time = endTime(t); + + storeTime(time, { type: "fix" }, slots); + slots.fixPasses++; + } else { + storeTime(0, { type: "fix" }, slots); + } + } + /* * stop if there are any syntax errors. * 'fixedResult.output' is a empty string. */ if (messages.length === 1 && messages[0].fatal) { @@ -2082,20 +2285,38 @@ fixed = fixed || fixedResult.fixed; // update to use the fixed output instead of the original text currentText = fixedResult.output; + if (stats) { + tTotal = endTime(tTotal); + const passIndex = slots.times.passes.length - 1; + + slots.times.passes[passIndex].total = tTotal; + } + } while ( fixedResult.fixed && passNumber < MAX_AUTOFIX_PASSES ); /* * If the last result had fixes, we need to lint again to be sure we have * the most up-to-date information. */ if (fixedResult.fixed) { + let tTotal; + + if (stats) { + tTotal = startTime(); + } + fixedResult.messages = this.verify(currentText, config, options); + + if (stats) { + storeTime(0, { type: "fix" }, slots); + slots.times.passes.at(-1).total = endTime(tTotal); + } } // ensure the last result properly reflects if fixes were done fixedResult.fixed = fixed; fixedResult.output = currentText;