You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2406 lines
90 KiB
2406 lines
90 KiB
/** |
|
* @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 } = require("../shared/severity"); |
|
const jslang = require("../languages/js"); |
|
const { activeFlags, inactiveFlags } = 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 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("../shared/types").Rule} 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 */ |
|
|
|
|
|
/* 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 {"off" | "warn" | "error"} reportUnusedDisableDirectives (boolean values were normalized) |
|
*/ |
|
|
|
//------------------------------------------------------------------------------ |
|
// 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 |
|
}; |
|
} |
|
|
|
/** |
|
* 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 = 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, 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); |
|
} |
|
} |
|
|
|
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), |
|
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 |
|
* @returns {Array} of rule options, empty Array if none |
|
*/ |
|
function getRuleOptions(ruleConfig) { |
|
if (Array.isArray(ruleConfig)) { |
|
return ruleConfig.slice(1); |
|
} |
|
return []; |
|
|
|
} |
|
|
|
/** |
|
* 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} 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, 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]), |
|
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 = [] } = {}) { |
|
|
|
flags.forEach(flag => { |
|
if (inactiveFlags.has(flag)) { |
|
throw new Error(`The flag '${flag}' is inactive: ${inactiveFlags.get(flag)}`); |
|
} |
|
|
|
if (!activeFlags.has(flag)) { |
|
throw new Error(`Unknown flag '${flag}'.`); |
|
} |
|
}); |
|
|
|
internalSlotsMap.set(this, { |
|
cwd: normalizeCwd(cwd), |
|
flags, |
|
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, |
|
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 options = normalizeVerifyOptions(providedOptions, config); |
|
const languageOptions = config.languageOptions; |
|
|
|
if (config.language === jslang) { |
|
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( |
|
languageOptions.ecmaVersion |
|
); |
|
|
|
// Espree expects this information to be passed in |
|
if (isEspree(languageOptions.parser)) { |
|
const parserOptions = languageOptions.parserOptions; |
|
|
|
if (languageOptions.sourceType) { |
|
|
|
parserOptions.sourceType = languageOptions.sourceType; |
|
|
|
if ( |
|
parserOptions.sourceType === "module" && |
|
parserOptions.ecmaFeatures && |
|
parserOptions.ecmaFeatures.globalReturn |
|
) { |
|
parserOptions.ecmaFeatures.globalReturn = false; |
|
} |
|
} |
|
} |
|
} |
|
|
|
const settings = config.settings || {}; |
|
|
|
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 { |
|
|
|
let ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; |
|
|
|
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; |
|
} |
|
} |
|
|
|
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, |
|
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; |
|
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 |
|
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; |
|
|
|
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); |
|
} |
|
};
|
|
|