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;