2025-04-17 07:44:37 -04:00

2856 lines
81 KiB
JavaScript

/**
* @fileoverview Main Linter Class
* @author Gyandeep Singh
* @author aladdin-add
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const path = require("node:path"),
eslintScope = require("eslint-scope"),
evk = require("eslint-visitor-keys"),
espree = require("espree"),
merge = require("lodash.merge"),
pkg = require("../../package.json"),
{
Legacy: {
ConfigOps,
ConfigValidator,
environments: BuiltInEnvironments,
},
} = require("@eslint/eslintrc/universal"),
Traverser = require("../shared/traverser"),
{ SourceCode } = require("../languages/js/source-code"),
applyDisableDirectives = require("./apply-disable-directives"),
{ ConfigCommentParser } = require("@eslint/plugin-kit"),
NodeEventGenerator = require("./node-event-generator"),
createReportTranslator = require("./report-translator"),
Rules = require("./rules"),
createEmitter = require("./safe-emitter"),
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 { assertIsRuleSeverity } = require("../config/flat-config-schema");
const {
normalizeSeverityToString,
normalizeSeverityToNumber,
} = require("../shared/severity");
const { deepMergeArrays } = require("../shared/deep-merge-arrays");
const jslang = require("../languages/js");
const {
activeFlags,
inactiveFlags,
getInactivityReasonMessage,
} = require("../shared/flags");
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 { VFile } = require("./vfile");
const { ParserService } = require("../services/parser-service");
const { FileContext } = require("./file-context");
const { ProcessorService } = require("../services/processor-service");
const { containsDifferentProperty } = require("../shared/option-utils");
const STEP_KIND_VISIT = 1;
const STEP_KIND_CALL = 2;
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
/** @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("../types").Rule.RuleModule} Rule */
/** @typedef {import("../shared/types").Times} Times */
/** @typedef {import("@eslint/core").Language} Language */
/** @typedef {import("@eslint/core").RuleSeverity} RuleSeverity */
/** @typedef {import("@eslint/core").RuleConfig} RuleConfig */
/** @typedef {import("../types").Linter.StringSeverity} StringSeverity */
/* 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 Type of directive
* @property {number} line The line number
* @property {number} column The column number
* @property {(string|null)} ruleId The rule ID
* @property {string} justification The justification of directive
*/
/**
* The private data for `Linter` instance.
* @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
* @property {boolean} [allowInlineConfig] Allow/disallow inline comments' ability
* to change config once it is set. Defaults to true if not supplied.
* Useful if you want to validate JS without comments overriding rules.
* @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
* predicate function that selects adopt code blocks.
* @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.
* If provided, this should accept a string of source text, and return an
* array of code blocks to lint.
*/
/**
* @typedef {Object} FixOptions
* @property {boolean | ((message: LintMessage) => boolean)} [fix] Determines
* whether fixes should be applied.
*/
/**
* @typedef {Object} InternalOptions
* @property {string | null} warnInlineConfig The config name what `noInlineConfig` setting came from. If `noInlineConfig` setting didn't exist, this is null. If this is a config name, then the linter warns directive comments.
* @property {StringSeverity} reportUnusedDisableDirectives Severity to report unused disable directives, if not "off" (boolean values were normalized).
* @property {StringSeverity} reportUnusedInlineConfigs Severity to report unused inline configs, if not "off".
*/
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Determines if a given object is Espree.
* @param {Object} parser The parser to check.
* @returns {boolean} True if the parser is Espree or false if not.
*/
function isEspree(parser) {
return !!(parser === espree || parser[parserSymbol] === espree);
}
/**
* Ensures that variables representing built-in properties of the Global Object,
* and any globals declared by special block comments, are present in the global
* scope.
* @param {Scope} globalScope The global scope.
* @param {Object} configGlobals The globals declared in configuration
* @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration
* @returns {void}
*/
function addDeclaredGlobals(
globalScope,
configGlobals,
{ exportedVariables, enabledGlobals },
) {
// Define configured global variables.
for (const id of new Set([
...Object.keys(configGlobals),
...Object.keys(enabledGlobals),
])) {
/*
* `ConfigOps.normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would
* typically be caught when validating a config anyway (validity for inline global comments is checked separately).
*/
const configValue =
configGlobals[id] === void 0
? void 0
: ConfigOps.normalizeConfigGlobal(configGlobals[id]);
const commentValue = enabledGlobals[id] && enabledGlobals[id].value;
const value = commentValue || configValue;
const sourceComments =
enabledGlobals[id] && enabledGlobals[id].comments;
if (value === "off") {
continue;
}
let variable = globalScope.set.get(id);
if (!variable) {
variable = new eslintScope.Variable(id, globalScope);
globalScope.variables.push(variable);
globalScope.set.set(id, variable);
}
variable.eslintImplicitGlobalSetting = configValue;
variable.eslintExplicitGlobal = sourceComments !== void 0;
variable.eslintExplicitGlobalComments = sourceComments;
variable.writeable = value === "writable";
}
// mark all exported variables as such
Object.keys(exportedVariables).forEach(name => {
const variable = globalScope.set.get(name);
if (variable) {
variable.eslintUsed = true;
variable.eslintExported = true;
}
});
/*
* "through" contains all references which definitions cannot be found.
* Since we augment the global scope using configuration, we need to update
* references and remove the ones that were added by configuration.
*/
globalScope.through = globalScope.through.filter(reference => {
const name = reference.identifier.name;
const variable = globalScope.set.get(name);
if (variable) {
/*
* Links the variable and the reference.
* And this reference is removed from `Scope#through`.
*/
reference.resolved = variable;
variable.references.push(reference);
return false;
}
return true;
});
}
/**
* creates a missing-rule message.
* @param {string} ruleId the ruleId to create
* @returns {string} created error message
* @private
*/
function createMissingRuleMessage(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.`;
}
/**
* Updates a given location based on the language offsets. This allows us to
* change 0-based locations to 1-based locations. We always want ESLint
* reporting lines and columns starting from 1.
* @param {Object} location The location to update.
* @param {number} location.line The starting line number.
* @param {number} location.column The starting column number.
* @param {number} [location.endLine] The ending line number.
* @param {number} [location.endColumn] The ending column number.
* @param {Language} language The language to use to adjust the location information.
* @returns {Object} The updated location.
*/
function updateLocationInformation(
{ line, column, endLine, endColumn },
language,
) {
const columnOffset = language.columnStart === 1 ? 0 : 1;
const lineOffset = language.lineStart === 1 ? 0 : 1;
// calculate separately to account for undefined
const finalEndLine = endLine === void 0 ? endLine : endLine + lineOffset;
const finalEndColumn =
endColumn === void 0 ? endColumn : endColumn + columnOffset;
return {
line: line + lineOffset,
column: column + columnOffset,
endLine: finalEndLine,
endColumn: finalEndColumn,
};
}
/**
* creates a linting problem
* @param {Object} options to create linting error
* @param {string} [options.ruleId] the ruleId to report
* @param {Object} [options.loc] the loc to report
* @param {string} [options.message] the error message to report
* @param {RuleSeverity} [options.severity] the error message to report
* @param {Language} [options.language] the language to use to adjust the location information
* @returns {LintMessage} created problem, returns a missing-rule problem if only provided ruleId.
* @private
*/
function createLintingProblem(options) {
const {
ruleId = null,
loc = DEFAULT_ERROR_LOC,
message = createMissingRuleMessage(options.ruleId),
severity = 2,
// fallback for eslintrc mode
language = {
columnStart: 0,
lineStart: 1,
},
} = options;
return {
ruleId,
message,
...updateLocationInformation(
{
line: loc.start.line,
column: loc.start.column,
endLine: loc.end.line,
endColumn: loc.end.column,
},
language,
),
severity,
nodeType: null,
};
}
/**
* Wraps the value in an Array if it isn't already one.
* @template T
* @param {T|T[]} value Value to be wrapped.
* @returns {Array} The value as an array.
*/
function asArray(value) {
return Array.isArray(value) ? value : [value];
}
/**
* Pushes a problem to inlineConfigProblems if ruleOptions are redundant.
* @param {ConfigData} config Provided config.
* @param {Object} loc A line/column location
* @param {Array} problems Problems that may be added to.
* @param {string} ruleId The rule ID.
* @param {Array} ruleOptions The rule options, merged with the config's.
* @param {Array} ruleOptionsInline The rule options from the comment.
* @param {"error"|"warn"} severity The severity to report.
* @returns {void}
*/
function addProblemIfSameSeverityAndOptions(
config,
loc,
problems,
ruleId,
ruleOptions,
ruleOptionsInline,
severity,
) {
const existingConfigRaw = config.rules?.[ruleId];
const existingConfig = existingConfigRaw
? asArray(existingConfigRaw)
: ["off"];
const existingSeverity = normalizeSeverityToString(existingConfig[0]);
const inlineSeverity = normalizeSeverityToString(ruleOptions[0]);
const sameSeverity = existingSeverity === inlineSeverity;
if (!sameSeverity) {
return;
}
const alreadyConfigured = existingConfigRaw
? `is already configured to '${existingSeverity}'`
: "is not enabled so can't be turned off";
let message;
if (
(existingConfig.length === 1 && ruleOptions.length === 1) ||
existingSeverity === "off"
) {
message = `Unused inline config ('${ruleId}' ${alreadyConfigured}).`;
} else if (
!containsDifferentProperty(
ruleOptions.slice(1),
existingConfig.slice(1),
)
) {
message =
ruleOptionsInline.length === 1
? `Unused inline config ('${ruleId}' ${alreadyConfigured}).`
: `Unused inline config ('${ruleId}' ${alreadyConfigured} with the same options).`;
}
if (message) {
problems.push(
createLintingProblem({
ruleId: null,
message,
loc,
language: config.language,
severity: normalizeSeverityToNumber(severity),
}),
);
}
}
/**
* 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 {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 {ASTNode|token} options.node The Comment node/token.
* @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules
* @param {Language} language The language to use to adjust the location information.
* @param {SourceCode} sourceCode The SourceCode object to get comments from.
* @returns {Object} Directives and problems from the comment
*/
function createDisableDirectives(
{ type, value, justification, node },
ruleMapper,
language,
sourceCode,
) {
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, value, ruleIds };
for (const ruleId of directiveRules) {
const loc = sourceCode.getLoc(node);
// push to directives, if the rule is defined(including null, e.g. /*eslint enable*/)
if (ruleId === null || !!ruleMapper(ruleId)) {
if (type === "disable-next-line") {
const { line, column } = updateLocationInformation(
loc.end,
language,
);
result.directives.push({
parentDirective,
type,
line,
column,
ruleId,
justification,
});
} else {
const { line, column } = updateLocationInformation(
loc.start,
language,
);
result.directives.push({
parentDirective,
type,
line,
column,
ruleId,
justification,
});
}
} else {
result.directiveProblems.push(
createLintingProblem({ ruleId, loc, language }),
);
}
}
return result;
}
/**
* Parses comments in file to extract file-specific config of rules, globals
* and environments and merges them with global config; also code blocks
* where reporting is disabled or enabled and merges them with reporting config.
* @param {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,
config,
) {
const configuredRules = {};
const enabledGlobals = Object.create(null);
const exportedVariables = {};
const problems = [];
const disableDirectives = [];
const validator = new ConfigValidator({
builtInRules: Rules,
});
sourceCode
.getInlineConfigNodes()
.filter(token => token.type !== "Shebang")
.forEach(comment => {
const directive = commentParser.parseDirective(comment.value);
if (!directive) {
return;
}
const {
label,
value,
justification: justificationPart,
} = directive;
const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(
label,
);
if (comment.type === "Line" && !lineCommentSupported) {
return;
}
const loc = sourceCode.getLoc(comment);
if (warnInlineConfig) {
const kind =
comment.type === "Block" ? `/*${label}*/` : `//${label}`;
problems.push(
createLintingProblem({
ruleId: null,
message: `'${kind}' has no effect because you have 'noInlineConfig' setting in ${warnInlineConfig}.`,
loc,
severity: 1,
}),
);
return;
}
if (
label === "eslint-disable-line" &&
loc.start.line !== loc.end.line
) {
const message = `${label} comment should not span multiple lines.`;
problems.push(
createLintingProblem({
ruleId: null,
message,
loc,
}),
);
return;
}
switch (label) {
case "eslint-disable":
case "eslint-enable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = label.slice("eslint-".length);
const { directives, directiveProblems } =
createDisableDirectives(
{
type: directiveType,
value,
justification: justificationPart,
node: comment,
},
ruleMapper,
jslang,
sourceCode,
);
disableDirectives.push(...directives);
problems.push(...directiveProblems);
break;
}
case "exported":
Object.assign(
exportedVariables,
commentParser.parseListConfig(value),
);
break;
case "globals":
case "global":
for (const [id, idSetting] of Object.entries(
commentParser.parseStringConfig(value),
)) {
let normalizedValue;
try {
normalizedValue =
ConfigOps.normalizeConfigGlobal(idSetting);
} catch (err) {
problems.push(
createLintingProblem({
ruleId: null,
loc,
message: err.message,
}),
);
continue;
}
if (enabledGlobals[id]) {
enabledGlobals[id].comments.push(comment);
enabledGlobals[id].value = normalizedValue;
} else {
enabledGlobals[id] = {
comments: [comment],
value: normalizedValue,
};
}
}
break;
case "eslint": {
const parseResult =
commentParser.parseJSONLikeConfig(value);
if (parseResult.ok) {
Object.keys(parseResult.config).forEach(name => {
const rule = ruleMapper(name);
const ruleValue = parseResult.config[name];
if (!rule) {
problems.push(
createLintingProblem({ ruleId: name, 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,
}),
);
return;
}
let ruleOptions = asArray(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
...asArray(config.rules[name]).slice(1), // options from the provided config
];
}
try {
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,
}),
);
// do not apply the config, if found invalid options.
return;
}
configuredRules[name] = ruleOptions;
});
} else {
const problem = createLintingProblem({
ruleId: null,
loc,
message: parseResult.error.message,
});
problem.fatal = true;
problems.push(problem);
}
break;
}
// no default
}
});
return {
configuredRules,
enabledGlobals,
exportedVariables,
problems,
disableDirectives,
};
}
/**
* Parses comments in file to extract disable directives.
* @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 {Language} language The language to use to adjust the location information
* @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, language) {
const disableDirectives = [];
const problems = [];
if (sourceCode.getDisableDirectives) {
const { directives: directivesSources, problems: directivesProblems } =
sourceCode.getDisableDirectives();
problems.push(
...directivesProblems.map(directiveProblem =>
createLintingProblem({
...directiveProblem,
language,
}),
),
);
directivesSources.forEach(directive => {
const { directives, directiveProblems } = createDisableDirectives(
directive,
ruleMapper,
language,
sourceCode,
);
disableDirectives.push(...directives);
problems.push(...directiveProblems);
});
}
return {
problems,
disableDirectives,
};
}
/**
* Normalize 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(parser, ecmaVersion) {
if (isEspree(parser)) {
if (ecmaVersion === "latest") {
return espree.latestEcmaVersion;
}
}
/*
* Calculate ECMAScript edition number from official year version starting with
* ES2015, which corresponds with ES6 (or a difference of 2009).
*/
return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion;
}
/**
* Normalize ECMAScript version from the initial config into languageOptions (year)
* format.
* @param {any} [ecmaVersion] ECMAScript version from the initial config
* @returns {number} normalized ECMAScript version
*/
function normalizeEcmaVersionForLanguageOptions(ecmaVersion) {
switch (ecmaVersion) {
case 3:
return 3;
// void 0 = no ecmaVersion specified so use the default
case 5:
case void 0:
return 5;
default:
if (typeof ecmaVersion === "number") {
return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009;
}
}
/*
* 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 LATEST_ECMA_VERSION;
}
const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu;
/**
* Checks whether or not there is a comment which has "eslint-env *" in a given text.
* @param {string} text A source code text to check.
* @returns {Object|null} A result of parseListConfig() with "eslint-env *" comment.
*/
function findEslintEnv(text) {
let match, retv;
eslintEnvPattern.lastIndex = 0;
while ((match = eslintEnvPattern.exec(text)) !== null) {
if (match[0].endsWith("*/")) {
retv = Object.assign(
retv || {},
commentParser.parseListConfig(
commentParser.parseDirective(match[0].slice(2, -2)).value,
),
);
}
}
return retv;
}
/**
* Convert "/path/to/<text>" to "<text>".
* `CLIEngine#executeOnText()` method gives "/path/to/<text>" if the filename
* was omitted because `configArray.extractConfig()` requires an absolute path.
* But the linter should pass `<text>` to `RuleContext#filename` in that
* case.
* Also, code blocks can have their virtual filename. If the parent filename was
* `<text>`, the virtual filename is `<text>/0_foo.js` or something like (i.e.,
* it's not an absolute path).
* @param {string} filename The filename to normalize.
* @returns {string} The normalized filename.
*/
function normalizeFilename(filename) {
const parts = filename.split(path.sep);
const index = parts.lastIndexOf("<text>");
return index === -1 ? filename : parts.slice(index).join(path.sep);
}
/**
* Normalizes the possible options for `linter.verify` and `linter.verifyAndFix` to a
* consistent shape.
* @param {VerifyOptions} providedOptions Options
* @param {ConfigData} config Config.
* @returns {Required<VerifyOptions> & InternalOptions} Normalized options
*/
function normalizeVerifyOptions(providedOptions, config) {
const linterOptions = config.linterOptions || config;
// .noInlineConfig for eslintrc, .linterOptions.noInlineConfig for flat
const disableInlineConfig = linterOptions.noInlineConfig === true;
const ignoreInlineConfig = providedOptions.allowInlineConfig === false;
const configNameOfNoInlineConfig = config.configNameOfNoInlineConfig
? ` (${config.configNameOfNoInlineConfig})`
: "";
let reportUnusedDisableDirectives =
providedOptions.reportUnusedDisableDirectives;
if (typeof reportUnusedDisableDirectives === "boolean") {
reportUnusedDisableDirectives = reportUnusedDisableDirectives
? "error"
: "off";
}
if (typeof reportUnusedDisableDirectives !== "string") {
if (typeof linterOptions.reportUnusedDisableDirectives === "boolean") {
reportUnusedDisableDirectives =
linterOptions.reportUnusedDisableDirectives ? "warn" : "off";
} else {
reportUnusedDisableDirectives =
linterOptions.reportUnusedDisableDirectives === void 0
? "off"
: normalizeSeverityToString(
linterOptions.reportUnusedDisableDirectives,
);
}
}
const reportUnusedInlineConfigs =
linterOptions.reportUnusedInlineConfigs === void 0
? "off"
: normalizeSeverityToString(
linterOptions.reportUnusedInlineConfigs,
);
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,
reportUnusedInlineConfigs,
disableFixes: Boolean(providedOptions.disableFixes),
stats: providedOptions.stats,
ruleFilter,
};
}
/**
* Combines the provided parserOptions with the options from environments
* @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(parser, providedOptions, enabledEnvironments) {
const parserOptionsFromEnv = enabledEnvironments
.filter(env => env.parserOptions)
.reduce(
(parserOptions, env) => merge(parserOptions, env.parserOptions),
{},
);
const mergedParserOptions = merge(
parserOptionsFromEnv,
providedOptions || {},
);
const isModule = mergedParserOptions.sourceType === "module";
if (isModule) {
/*
* can't have global return inside of modules
* TODO: espree validate parserOptions.globalReturn when sourceType is setting to module.(@aladdin-add)
*/
mergedParserOptions.ecmaFeatures = Object.assign(
{},
mergedParserOptions.ecmaFeatures,
{ globalReturn: false },
);
}
mergedParserOptions.ecmaVersion = normalizeEcmaVersion(
parser,
mergedParserOptions.ecmaVersion,
);
return mergedParserOptions;
}
/**
* Converts parserOptions to languageOptions for backwards compatibility with eslintrc.
* @param {ConfigData} config Config object.
* @param {Object} config.globals Global variable definitions.
* @param {Parser} config.parser The parser to use.
* @param {ParserOptions} config.parserOptions The parserOptions to use.
* @returns {LanguageOptions} The languageOptions equivalent.
*/
function createLanguageOptions({
globals: configuredGlobals,
parser,
parserOptions,
}) {
const { ecmaVersion, sourceType } = parserOptions;
return {
globals: configuredGlobals,
ecmaVersion: normalizeEcmaVersionForLanguageOptions(ecmaVersion),
sourceType,
parser,
parserOptions,
};
}
/**
* Combines the provided globals object with the globals from environments
* @param {Record<string, GlobalConf>} providedGlobals The 'globals' key in a config
* @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,
);
}
/**
* 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 {RuleConfig} ruleConfig rule configuration
* @param {Object|undefined} defaultOptions rule.meta.defaultOptions
* @returns {Array} of rule options, empty Array if none
*/
function getRuleOptions(ruleConfig, defaultOptions) {
if (Array.isArray(ruleConfig)) {
return deepMergeArrays(defaultOptions, ruleConfig.slice(1));
}
return defaultOptions ?? [];
}
/**
* Analyze scope of the given AST.
* @param {ASTNode} ast The `Program` node to analyze.
* @param {LanguageOptions} languageOptions The parser options.
* @param {Record<string, string[]>} visitorKeys The visitor keys.
* @returns {ScopeManager} The analysis result.
*/
function analyzeScope(ast, languageOptions, visitorKeys) {
const parserOptions = languageOptions.parserOptions;
const ecmaFeatures = parserOptions.ecmaFeatures || {};
const ecmaVersion = languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
return eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict,
ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
sourceType: languageOptions.sourceType || "script",
childVisitorKeys: visitorKeys || evk.KEYS,
fallback: Traverser.getKeys,
});
}
/**
* Runs a rule, and gets its listeners
* @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;
}
}
/**
* 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
* @param {string | undefined} parserName The name of the parser in the config
* @param {Language} language The language object used for parsing.
* @param {LanguageOptions} languageOptions The options for parsing the code.
* @param {Object} settings The settings that were enabled in the config
* @param {string} filename The reported filename of the code
* @param {boolean} applyDefaultOptions If true, apply rules' meta.defaultOptions in computing their config options.
* @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,
language,
languageOptions,
settings,
filename,
applyDefaultOptions,
disableFixes,
cwd,
physicalFilename,
ruleFilter,
stats,
slots,
) {
const emitter = createEmitter();
// 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 = new FileContext({
cwd,
filename,
physicalFilename: physicalFilename || filename,
sourceCode,
parserOptions: {
...languageOptions.parserOptions,
},
parserPath: parserName,
languageOptions,
settings,
});
const lintingProblems = [];
Object.keys(configuredRules).forEach(ruleId => {
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
// not load disabled rules
if (severity === 0) {
return;
}
if (ruleFilter && !ruleFilter({ ruleId, severity })) {
return;
}
const rule = ruleMapper(ruleId);
if (!rule) {
lintingProblems.push(createLintingProblem({ ruleId, language }));
return;
}
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(Object.create(sharedTraversalContext), {
id: ruleId,
options: getRuleOptions(
configuredRules[ruleId],
applyDefaultOptions ? rule.meta?.defaultOptions : void 0,
),
report(...args) {
/*
* Create a report translator lazily.
* In a vast majority of cases, any given rule reports zero errors on a given
* piece of code. Creating a translator lazily avoids the performance cost of
* creating a new translator function for each rule that usually doesn't get
* called.
*
* Using lazy report translators improves end-to-end performance by about 3%
* with Node 8.4.0.
*/
if (reportTranslator === null) {
reportTranslator = createReportTranslator({
ruleId,
severity,
sourceCode,
messageIds,
disableFixes,
language,
});
}
const problem = reportTranslator(...args);
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);
},
}),
);
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 {
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;
}
};
}
if (typeof ruleListeners === "undefined" || ruleListeners === null) {
throw new Error(
`The create() function for rule '${ruleId}' did not return an object.`,
);
}
// add all the selectors from the rule as listeners
Object.keys(ruleListeners).forEach(selector => {
const ruleListener =
timing.enabled || stats
? timing.time(ruleId, ruleListeners[selector], stats)
: ruleListeners[selector];
emitter.on(selector, addRuleErrorHandler(ruleListener));
});
});
const eventGenerator = new NodeEventGenerator(emitter, {
visitorKeys: sourceCode.visitorKeys ?? language.visitorKeys,
fallback: Traverser.getKeys,
matchClass: language.matchesSelectorClass ?? (() => false),
nodeTypeKey: language.nodeTypeKey,
});
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;
}
case STEP_KIND_CALL: {
emitter.emit(step.target, ...step.args);
break;
}
default:
throw new Error(
`Invalid traversal step found: "${step.type}".`,
);
}
}
return lintingProblems;
}
/**
* Ensure the source code to be a string.
* @param {string|SourceCode} textOrSourceCode The text or source code object.
* @returns {string} The source code text.
*/
function ensureText(textOrSourceCode) {
if (typeof textOrSourceCode === "object") {
const { hasBOM, text } = textOrSourceCode;
const bom = hasBOM ? "\uFEFF" : "";
return bom + text;
}
return String(textOrSourceCode);
}
/**
* Get an environment.
* @param {LinterInternalSlots} slots The internal slots of Linter.
* @param {string} envId The environment ID to get.
* @returns {Environment|null} The environment.
*/
function getEnv(slots, envId) {
return (
(slots.lastConfigArray &&
slots.lastConfigArray.pluginEnvironments.get(envId)) ||
BuiltInEnvironments.get(envId) ||
null
);
}
/**
* Get a rule.
* @param {LinterInternalSlots} slots The internal slots of Linter.
* @param {string} ruleId The rule ID to get.
* @returns {Rule|null} The rule.
*/
function getRule(slots, ruleId) {
return (
(slots.lastConfigArray &&
slots.lastConfigArray.pluginRules.get(ruleId)) ||
slots.ruleMap.get(ruleId)
);
}
/**
* Normalize the value of the cwd
* @param {string | undefined} cwd raw value of the cwd, path to a directory that should be considered as the current working directory, can be undefined.
* @returns {string | undefined} normalized cwd
*/
function normalizeCwd(cwd) {
if (cwd) {
return cwd;
}
if (typeof process === "object") {
return process.cwd();
}
// It's more explicit to assign the undefined
// eslint-disable-next-line no-undefined -- Consistently returning a value
return undefined;
}
/**
* The map to store private data.
* @type {WeakMap<Linter, LinterInternalSlots>}
*/
const internalSlotsMap = new WeakMap();
/**
* Throws an error when the given linter is in flat config mode.
* @param {Linter} linter The linter to check.
* @returns {void}
* @throws {Error} If the linter is in flat config mode.
*/
function assertEslintrcConfig(linter) {
const { configType } = internalSlotsMap.get(linter);
if (configType === "flat") {
throw new Error(
"This method cannot be used with flat config. Add your entries directly into the config array.",
);
}
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Object that is responsible for verifying JavaScript text
* @name Linter
*/
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 {Array<string>} [config.flags] the feature flags to enable.
* @param {"flat"|"eslintrc"} [config.configType="flat"] the type of config used.
*/
constructor({ cwd, configType = "flat", flags = [] } = {}) {
const processedFlags = [];
flags.forEach(flag => {
if (inactiveFlags.has(flag)) {
const inactiveFlagData = inactiveFlags.get(flag);
const inactivityReason =
getInactivityReasonMessage(inactiveFlagData);
if (typeof inactiveFlagData.replacedBy === "undefined") {
throw new Error(
`The flag '${flag}' is inactive: ${inactivityReason}`,
);
}
// if there's a replacement, enable it instead of original
if (typeof inactiveFlagData.replacedBy === "string") {
processedFlags.push(inactiveFlagData.replacedBy);
}
globalThis.process?.emitWarning?.(
`The flag '${flag}' is inactive: ${inactivityReason}`,
`ESLintInactiveFlag_${flag}`,
);
return;
}
if (!activeFlags.has(flag)) {
throw new Error(`Unknown flag '${flag}'.`);
}
processedFlags.push(flag);
});
internalSlotsMap.set(this, {
cwd: normalizeCwd(cwd),
flags: processedFlags,
lastConfigArray: null,
lastSourceCode: null,
lastSuppressedMessages: [],
configType, // TODO: Remove after flat config conversion
parserMap: new Map([["espree", espree]]),
ruleMap: new Rules(),
});
this.version = pkg.version;
}
/**
* Getter for package version.
* @static
* @returns {string} The version from package.json.
*/
static get version() {
return pkg.version;
}
/**
* Indicates if the given feature flag is enabled for this instance.
* @param {string} flag The feature flag to check.
* @returns {boolean} `true` if the feature flag is enabled, `false` if not.
*/
hasFlag(flag) {
return internalSlotsMap.get(this).flags.includes(flag);
}
/**
* Lint using eslintrc and without processors.
* @param {VFile} file The file to lint.
* @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|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
*/
#eslintrcVerifyWithoutProcessors(file, providedConfig, providedOptions) {
const slots = internalSlotsMap.get(this);
const config = providedConfig || {};
const options = normalizeVerifyOptions(providedOptions, config);
// Resolve parser.
let parserName = DEFAULT_PARSER_NAME;
let parser = espree;
if (typeof config.parser === "object" && config.parser !== null) {
parserName = config.parser.filePath;
parser = config.parser.definition;
} else if (typeof config.parser === "string") {
if (!slots.parserMap.has(config.parser)) {
return [
{
ruleId: null,
fatal: true,
severity: 2,
message: `Configured parser '${config.parser}' was not found.`,
line: 0,
column: 0,
nodeType: null,
},
];
}
parserName = config.parser;
parser = slots.parserMap.get(config.parser);
}
// search and apply "eslint-env *".
const envInFile =
options.allowInlineConfig && !options.warnInlineConfig
? findEslintEnv(file.body)
: {};
const resolvedEnvConfig = Object.assign(
{ builtin: true },
config.env,
envInFile,
);
const enabledEnvs = Object.keys(resolvedEnvConfig)
.filter(envName => resolvedEnvConfig[envName])
.map(envName => getEnv(slots, envName))
.filter(env => env);
const parserOptions = resolveParserOptions(
parser,
config.parserOptions || {},
enabledEnvs,
);
const configuredGlobals = resolveGlobals(
config.globals || {},
enabledEnvs,
);
const settings = config.settings || {};
const languageOptions = createLanguageOptions({
globals: config.globals,
parser,
parserOptions,
});
if (!slots.lastSourceCode) {
let t;
if (options.stats) {
t = startTime();
}
const parserService = new ParserService();
const parseResult = parserService.parseSync(file, {
language: jslang,
languageOptions,
});
if (options.stats) {
const time = endTime(t);
const timeOpts = { type: "parse" };
storeTime(time, timeOpts, slots);
}
if (!parseResult.ok) {
return parseResult.errors;
}
slots.lastSourceCode = parseResult.sourceCode;
} else {
/*
* If the given source code object as the first argument does not have scopeManager, analyze the scope.
* This is for backward compatibility (SourceCode is frozen so it cannot rebind).
*/
if (!slots.lastSourceCode.scopeManager) {
slots.lastSourceCode = new SourceCode({
text: slots.lastSourceCode.text,
ast: slots.lastSourceCode.ast,
hasBOM: slots.lastSourceCode.hasBOM,
parserServices: slots.lastSourceCode.parserServices,
visitorKeys: slots.lastSourceCode.visitorKeys,
scopeManager: analyzeScope(
slots.lastSourceCode.ast,
languageOptions,
),
});
}
}
const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
? getDirectiveComments(
sourceCode,
ruleId => getRule(slots, ruleId),
options.warnInlineConfig,
config,
)
: {
configuredRules: {},
enabledGlobals: {},
exportedVariables: {},
problems: [],
disableDirectives: [],
};
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,
configuredRules,
ruleId => getRule(slots, ruleId),
parserName,
jslang,
languageOptions,
settings,
options.filename,
true,
options.disableFixes,
slots.cwd,
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);
if (err.currentNode) {
const { line } = sourceCode.getLoc(err.currentNode).start;
debug("Line:", line);
err.message += `:${line}`;
}
debug("Parser Options:", parserOptions);
debug("Parser Path:", parserName);
debug("Settings:", settings);
if (err.ruleId) {
err.message += `\nRule: "${err.ruleId}"`;
}
throw err;
}
return applyDisableDirectives({
language: jslang,
sourceCode,
directives: commentDirectives.disableDirectives,
disableFixes: options.disableFixes,
problems: lintingProblems
.concat(commentDirectives.problems)
.sort(
(problemA, problemB) =>
problemA.line - problemB.line ||
problemA.column - problemB.column,
),
reportUnusedDisableDirectives:
options.reportUnusedDisableDirectives,
});
}
/**
* Same as linter.verify, except without support for processors.
* @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|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
*/
_verifyWithoutProcessors(
textOrSourceCode,
providedConfig,
providedOptions,
) {
const slots = internalSlotsMap.get(this);
const filename = normalizeFilename(
providedOptions.filename || "<input>",
);
let text;
// evaluate arguments
if (typeof textOrSourceCode === "string") {
slots.lastSourceCode = null;
text = textOrSourceCode;
} else {
slots.lastSourceCode = textOrSourceCode;
text = textOrSourceCode.text;
}
const file = new VFile(filename, text, {
physicalPath: providedOptions.physicalFilename,
});
return this.#eslintrcVerifyWithoutProcessors(
file,
providedConfig,
providedOptions,
);
}
/**
* Verifies the text against the rules specified by the second argument.
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
* @param {ConfigData|ConfigArray} config An ESLintConfig instance to configure everything.
* @param {(string|(VerifyOptions&ProcessorOptions))} [filenameOrOptions] The optional filename of the file being checked.
* If this is not set, the filename will default to '<input>' in the rule context. If
* an object, then it has "filename", "allowInlineConfig", and some properties.
* @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
*/
verify(textOrSourceCode, config, filenameOrOptions) {
debug("Verify");
const { configType, cwd } = internalSlotsMap.get(this);
const options =
typeof filenameOrOptions === "string"
? { filename: filenameOrOptions }
: filenameOrOptions || {};
const configToUse = config ?? {};
if (configType !== "eslintrc") {
/*
* 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;
if (
!Array.isArray(configToUse) ||
typeof configToUse.getConfig !== "function"
) {
configArray = new FlatConfigArray(configToUse, {
basePath: cwd,
});
configArray.normalizeSync();
}
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,
configToUse,
options,
),
);
}
return this._distinguishSuppressedMessages(
this._verifyWithoutProcessors(
textOrSourceCode,
configToUse,
options,
),
);
}
/**
* Verify with a processor.
* @param {string|SourceCode} textOrSourceCode The source code.
* @param {FlatConfig} config The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
* @param {FlatConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithFlatConfigArrayAndProcessor(
textOrSourceCode,
config,
options,
configForRecursive,
) {
const slots = internalSlotsMap.get(this);
const filename = options.filename || "<input>";
const filenameToExpose = normalizeFilename(filename);
const physicalFilename = options.physicalFilename || filenameToExpose;
const text = ensureText(textOrSourceCode);
const file = new VFile(filenameToExpose, text, {
physicalPath: physicalFilename,
});
const preprocess = options.preprocess || (rawText => [rawText]);
const postprocess =
options.postprocess || (messagesList => messagesList.flat());
const processorService = new ProcessorService();
const preprocessResult = processorService.preprocessSync(file, {
processor: {
preprocess,
postprocess,
},
});
if (!preprocessResult.ok) {
return preprocessResult.errors;
}
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));
const originalExtname = path.extname(filename);
const { files } = preprocessResult;
const messageLists = files.map(block => {
debug("A code block was found: %o", block.path || "(unnamed)");
// Keep the legacy behavior.
if (typeof block === "string") {
return this._verifyWithFlatConfigArrayAndWithoutProcessors(
block,
config,
options,
);
}
// Skip this block if filtered.
if (!filterCodeBlock(block.path, block.body)) {
debug("This code block was skipped.");
return [];
}
// Resolve configuration again if the file content or extension was changed.
if (
configForRecursive &&
(text !== block.rawBody ||
path.extname(block.path) !== originalExtname)
) {
debug(
"Resolving configuration again because the file content or extension was changed.",
);
return this._verifyWithFlatConfigArray(
block.rawBody,
configForRecursive,
{
...options,
filename: block.path,
physicalFilename: block.physicalPath,
},
);
}
slots.lastSourceCode = null;
// Does lint.
return this.#flatVerifyWithoutProcessors(block, config, {
...options,
filename: block.path,
physicalFilename: block.physicalPath,
});
});
return processorService.postprocessSync(file, messageLists, {
processor: {
preprocess,
postprocess,
},
});
}
/**
* Verify using flat config and without any processors.
* @param {VFile} file The file to lint.
* @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything.
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
* @throws {Error} If during rule execution.
* @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
*/
#flatVerifyWithoutProcessors(file, providedConfig, providedOptions) {
const slots = internalSlotsMap.get(this);
const config = providedConfig || {};
const { settings = {}, languageOptions } = config;
const options = normalizeVerifyOptions(providedOptions, config);
if (!slots.lastSourceCode) {
let t;
if (options.stats) {
t = startTime();
}
const parserService = new ParserService();
const parseResult = parserService.parseSync(file, config);
if (options.stats) {
const time = endTime(t);
storeTime(time, { type: "parse" }, slots);
}
if (!parseResult.ok) {
return parseResult.errors;
}
slots.lastSourceCode = parseResult.sourceCode;
} else {
/*
* If the given source code object as the first argument does not have scopeManager, analyze the scope.
* This is for backward compatibility (SourceCode is frozen so it cannot rebind).
*
* We check explicitly for `null` to ensure that this is a JS-flavored language.
* For non-JS languages we don't want to do this.
*
* TODO: Remove this check when we stop exporting the `SourceCode` object.
*/
if (slots.lastSourceCode.scopeManager === null) {
slots.lastSourceCode = new SourceCode({
text: slots.lastSourceCode.text,
ast: slots.lastSourceCode.ast,
hasBOM: slots.lastSourceCode.hasBOM,
parserServices: slots.lastSourceCode.parserServices,
visitorKeys: slots.lastSourceCode.visitorKeys,
scopeManager: analyzeScope(
slots.lastSourceCode.ast,
languageOptions,
),
});
}
}
const sourceCode = slots.lastSourceCode;
/*
* Make adjustments based on the language options. For JavaScript,
* this is primarily about adding variables into the global scope
* to account for ecmaVersion and configured globals.
*/
sourceCode.applyLanguageOptions?.(languageOptions);
const mergedInlineConfig = {
rules: {},
};
const inlineConfigProblems = [];
/*
* Inline config can be either enabled or disabled. If disabled, it's possible
* to detect the inline config and emit a warning (though this is not required).
* So we first check to see if inline config is allowed at all, and if so, we
* need to check if it's a warning or not.
*/
if (options.allowInlineConfig) {
// if inline config should warn then add the warnings
if (options.warnInlineConfig) {
if (sourceCode.getInlineConfigNodes) {
sourceCode.getInlineConfigNodes().forEach(node => {
const loc = sourceCode.getLoc(node);
const range = sourceCode.getRange(node);
inlineConfigProblems.push(
createLintingProblem({
ruleId: null,
message: `'${sourceCode.text.slice(range[0], range[1])}' has no effect because you have 'noInlineConfig' setting in ${options.warnInlineConfig}.`,
loc,
severity: 1,
language: config.language,
}),
);
});
}
} else {
const inlineConfigResult = sourceCode.applyInlineConfig?.();
if (inlineConfigResult) {
inlineConfigProblems.push(
...inlineConfigResult.problems
.map(problem =>
createLintingProblem({
...problem,
language: config.language,
}),
)
.map(problem => {
problem.fatal = true;
return problem;
}),
);
// next we need to verify information about the specified rules
const ruleValidator = new RuleValidator();
for (const {
config: inlineConfig,
loc,
} of inlineConfigResult.configs) {
Object.keys(inlineConfig.rules).forEach(ruleId => {
const rule = getRuleFromConfig(ruleId, config);
const ruleValue = inlineConfig.rules[ruleId];
if (!rule) {
inlineConfigProblems.push(
createLintingProblem({
ruleId,
loc,
language: config.language,
}),
);
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,
language: config.language,
}),
);
return;
}
try {
const ruleOptionsInline = asArray(ruleValue);
let ruleOptions = ruleOptionsInline;
assertIsRuleSeverity(ruleId, ruleOptions[0]);
/*
* 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;
}
} else {
/**
* Since we know the user provided options, apply defaults on top of them
*/
const slicedOptions = ruleOptions.slice(1);
const mergedOptions = deepMergeArrays(
rule.meta?.defaultOptions,
slicedOptions,
);
if (mergedOptions.length) {
ruleOptions = [
ruleOptions[0],
...mergedOptions,
];
}
}
if (
options.reportUnusedInlineConfigs !== "off"
) {
addProblemIfSameSeverityAndOptions(
config,
loc,
inlineConfigProblems,
ruleId,
ruleOptions,
ruleOptionsInline,
options.reportUnusedInlineConfigs,
);
}
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();
if (err.messageTemplate) {
baseMessage += ` You passed "${ruleValue}".`;
}
inlineConfigProblems.push(
createLintingProblem({
ruleId,
message: `Inline configuration for rule "${ruleId}" is invalid:\n\t${baseMessage}\n`,
loc,
language: config.language,
}),
);
}
});
}
}
}
}
const commentDirectives =
options.allowInlineConfig && !options.warnInlineConfig
? getDirectiveCommentsForFlatConfig(
sourceCode,
ruleId => getRuleFromConfig(ruleId, config),
config.language,
)
: { problems: [], disableDirectives: [] };
const configuredRules = Object.assign(
{},
config.rules,
mergedInlineConfig.rules,
);
let lintingProblems;
sourceCode.finalize?.();
try {
lintingProblems = runRules(
sourceCode,
configuredRules,
ruleId => getRuleFromConfig(ruleId, config),
void 0,
config.language,
languageOptions,
settings,
options.filename,
false,
options.disableFixes,
slots.cwd,
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);
if (err.currentNode) {
const { line } = sourceCode.getLoc(err.currentNode).start;
debug("Line:", line);
err.message += `:${line}`;
}
debug("Parser Options:", languageOptions.parserOptions);
// debug("Parser Path:", parserName);
debug("Settings:", settings);
if (err.ruleId) {
err.message += `\nRule: "${err.ruleId}"`;
}
throw err;
}
return applyDisableDirectives({
language: config.language,
sourceCode,
directives: commentDirectives.disableDirectives,
disableFixes: options.disableFixes,
problems: lintingProblems
.concat(commentDirectives.problems)
.concat(inlineConfigProblems)
.sort(
(problemA, problemB) =>
problemA.line - problemB.line ||
problemA.column - problemB.column,
),
reportUnusedDisableDirectives:
options.reportUnusedDisableDirectives,
ruleFilter: options.ruleFilter,
configuredRules,
});
}
/**
* Same as linter.verify, except without support for processors.
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
* @param {FlatConfig} providedConfig An ESLintConfig instance to configure everything.
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
* @throws {Error} If during rule execution.
* @returns {(LintMessage|SuppressedLintMessage)[]} The results as an array of messages or an empty array if no messages.
*/
_verifyWithFlatConfigArrayAndWithoutProcessors(
textOrSourceCode,
providedConfig,
providedOptions,
) {
const slots = internalSlotsMap.get(this);
const filename = normalizeFilename(
providedOptions.filename || "<input>",
);
let text;
// evaluate arguments
if (typeof textOrSourceCode === "string") {
slots.lastSourceCode = null;
text = textOrSourceCode;
} else {
slots.lastSourceCode = textOrSourceCode;
text = textOrSourceCode.text;
}
const file = new VFile(filename, text, {
physicalPath: providedOptions.physicalFilename,
});
return this.#flatVerifyWithoutProcessors(
file,
providedConfig,
providedOptions,
);
}
/**
* Verify a given code with `ConfigArray`.
* @param {string|SourceCode} textOrSourceCode The source code.
* @param {ConfigArray} configArray The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithConfigArray(textOrSourceCode, configArray, options) {
debug("With ConfigArray: %s", options.filename);
// Store the config array in order to get plugin envs and rules later.
internalSlotsMap.get(this).lastConfigArray = configArray;
// Extract the final config for this file.
const config = configArray.extractConfig(options.filename);
const processor =
config.processor &&
configArray.pluginProcessors.get(config.processor);
// Verify.
if (processor) {
debug("Apply the processor: %o", config.processor);
const { preprocess, postprocess, supportsAutofix } = processor;
const disableFixes = options.disableFixes || !supportsAutofix;
return this._verifyWithProcessor(
textOrSourceCode,
config,
{ ...options, disableFixes, postprocess, preprocess },
configArray,
);
}
return this._verifyWithoutProcessors(textOrSourceCode, config, options);
}
/**
* Verify a given code with a flat config.
* @param {string|SourceCode} textOrSourceCode The source code.
* @param {FlatConfigArray} configArray The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
* @param {boolean} [firstCall=false] Indicates if this is being called directly
* from verify(). (TODO: Remove once eslintrc is removed.)
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithFlatConfigArray(
textOrSourceCode,
configArray,
options,
firstCall = false,
) {
debug("With flat config: %s", options.filename);
// we need a filename to match configs against
const filename = options.filename || "__placeholder__.js";
// Store the config array in order to get plugin envs and rules later.
internalSlotsMap.get(this).lastConfigArray = configArray;
const config = configArray.getConfig(filename);
if (!config) {
return [
{
ruleId: null,
severity: 1,
message: `No matching configuration found for ${filename}.`,
line: 0,
column: 0,
nodeType: null,
},
];
}
// Verify.
if (config.processor) {
debug("Apply the processor: %o", config.processor);
const { preprocess, postprocess, supportsAutofix } =
config.processor;
const disableFixes = options.disableFixes || !supportsAutofix;
return this._verifyWithFlatConfigArrayAndProcessor(
textOrSourceCode,
config,
{ ...options, filename, disableFixes, postprocess, preprocess },
configArray,
);
}
// check for options-based processing
if (firstCall && (options.preprocess || options.postprocess)) {
return this._verifyWithFlatConfigArrayAndProcessor(
textOrSourceCode,
config,
options,
);
}
return this._verifyWithFlatConfigArrayAndWithoutProcessors(
textOrSourceCode,
config,
options,
);
}
/**
* Verify with a processor.
* @param {string|SourceCode} textOrSourceCode The source code.
* @param {ConfigData|ExtractedConfig} config The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
* @param {ConfigArray} [configForRecursive] The `ConfigArray` object to apply multiple processors recursively.
* @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithProcessor(
textOrSourceCode,
config,
options,
configForRecursive,
) {
const slots = internalSlotsMap.get(this);
const filename = options.filename || "<input>";
const filenameToExpose = normalizeFilename(filename);
const physicalFilename = options.physicalFilename || filenameToExpose;
const text = ensureText(textOrSourceCode);
const file = new VFile(filenameToExpose, text, {
physicalPath: physicalFilename,
});
const preprocess = options.preprocess || (rawText => [rawText]);
const postprocess =
options.postprocess || (messagesList => messagesList.flat());
const processorService = new ProcessorService();
const preprocessResult = processorService.preprocessSync(file, {
processor: {
preprocess,
postprocess,
},
});
if (!preprocessResult.ok) {
return preprocessResult.errors;
}
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilePath => blockFilePath.endsWith(".js"));
const originalExtname = path.extname(filename);
const { files } = preprocessResult;
const messageLists = files.map(block => {
debug("A code block was found: %o", block.path ?? "(unnamed)");
// Keep the legacy behavior.
if (typeof block === "string") {
return this._verifyWithoutProcessors(block, config, options);
}
// Skip this block if filtered.
if (!filterCodeBlock(block.path, block.body)) {
debug("This code block was skipped.");
return [];
}
// Resolve configuration again if the file content or extension was changed.
if (
configForRecursive &&
(text !== block.rawBody ||
path.extname(block.path) !== originalExtname)
) {
debug(
"Resolving configuration again because the file content or extension was changed.",
);
return this._verifyWithConfigArray(
block.rawBody,
configForRecursive,
{
...options,
filename: block.path,
physicalFilename: block.physicalPath,
},
);
}
slots.lastSourceCode = null;
// Does lint.
return this.#eslintrcVerifyWithoutProcessors(block, config, {
...options,
filename: block.path,
physicalFilename: block.physicalPath,
});
});
return processorService.postprocessSync(file, messageLists, {
processor: {
preprocess,
postprocess,
},
});
}
/**
* Given a list of reported problems, distinguish problems between normal messages and suppressed messages.
* The normal messages will be returned and the suppressed messages will be stored as lastSuppressedMessages.
* @param {Array<LintMessage|SuppressedLintMessage>} problems A list of reported problems.
* @returns {LintMessage[]} A list of LintMessage.
*/
_distinguishSuppressedMessages(problems) {
const messages = [];
const suppressedMessages = [];
const slots = internalSlotsMap.get(this);
for (const problem of problems) {
if (problem.suppressions) {
suppressedMessages.push(problem);
} else {
messages.push(problem);
}
}
slots.lastSuppressedMessages = suppressedMessages;
return messages;
}
/**
* Gets the SourceCode object representing the parsed source.
* @returns {SourceCode} The SourceCode object.
*/
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 {Rule} rule A rule object
* @returns {void}
*/
defineRule(ruleId, rule) {
assertEslintrcConfig(this);
internalSlotsMap.get(this).ruleMap.define(ruleId, rule);
}
/**
* Defines many new linting rules.
* @param {Record<string, Rule>} rulesToDefine map from unique rule identifier to rule
* @returns {void}
*/
defineRules(rulesToDefine) {
assertEslintrcConfig(this);
Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
this.defineRule(ruleId, rulesToDefine[ruleId]);
});
}
/**
* Gets an object with all loaded rules.
* @returns {Map<string, Rule>} All loaded rules
*/
getRules() {
assertEslintrcConfig(this);
const { lastConfigArray, ruleMap } = internalSlotsMap.get(this);
return new Map(
(function* () {
yield* ruleMap;
if (lastConfigArray) {
yield* lastConfigArray.pluginRules;
}
})(),
);
}
/**
* Define a new parser module
* @param {string} parserId Name of the parser
* @param {Parser} parserModule The parser object
* @returns {void}
*/
defineParser(parserId, parserModule) {
assertEslintrcConfig(this);
internalSlotsMap.get(this).parserMap.set(parserId, parserModule);
}
/**
* Performs multiple autofix passes over the text until as many fixes as possible
* have been applied.
* @param {string} text The source text to apply fixes to.
* @param {ConfigData|ConfigArray|FlatConfigArray} config The ESLint config object to use.
* @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,
fixedResult,
fixed = false,
passNumber = 0,
currentText = text,
secondPreviousText,
previousText;
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) {
break;
}
// keep track if any fixes were ever applied - important for return value
fixed = fixed || fixedResult.fixed;
// update to use the fixed output instead of the original text
secondPreviousText = previousText;
previousText = currentText;
currentText = fixedResult.output;
if (stats) {
tTotal = endTime(tTotal);
const passIndex = slots.times.passes.length - 1;
slots.times.passes[passIndex].total = tTotal;
}
// Stop if we've made a circular fix
if (
passNumber > 1 &&
currentText.length === secondPreviousText.length &&
currentText === secondPreviousText
) {
debug(
`Circular fixes detected after pass ${passNumber}. Exiting fix loop.`,
);
globalThis?.process?.emitWarning?.(
`Circular fixes detected while fixing ${options?.filename ?? "text"}. It is likely that you have conflicting rules in your configuration.`,
"ESLintCircularFixesWarning",
);
break;
}
} 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;
return fixedResult;
}
}
module.exports = {
Linter,
/**
* Get the internal slots of a given Linter instance for tests.
* @param {Linter} instance The Linter instance to get.
* @returns {LinterInternalSlots} The internal slots.
*/
getLinterInternalSlots(instance) {
return internalSlotsMap.get(instance);
},
};