|
|
|
|
|
|
|
|
|
|
|
|
|
"use strict"; |
|
|
|
|
|
|
|
|
|
|
|
const |
|
path = require("path"), |
|
eslintScope = require("eslint-scope"), |
|
evk = require("eslint-visitor-keys"), |
|
espree = require("espree"), |
|
merge = require("lodash.merge"), |
|
pkg = require("../../package.json"), |
|
astUtils = require("../shared/ast-utils"), |
|
{ |
|
directivesPattern |
|
} = require("../shared/directives"), |
|
{ |
|
Legacy: { |
|
ConfigOps, |
|
ConfigValidator, |
|
environments: BuiltInEnvironments |
|
} |
|
} = require("@eslint/eslintrc/universal"), |
|
Traverser = require("../shared/traverser"), |
|
{ SourceCode } = require("../source-code"), |
|
CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), |
|
applyDisableDirectives = require("./apply-disable-directives"), |
|
ConfigCommentParser = require("./config-comment-parser"), |
|
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 { RuleValidator } = require("../config/rule-validator"); |
|
const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema"); |
|
const { normalizeSeverityToString } = require("../shared/severity"); |
|
const debug = require("debug")("eslint:linter"); |
|
const MAX_AUTOFIX_PASSES = 10; |
|
const DEFAULT_PARSER_NAME = "espree"; |
|
const DEFAULT_ECMA_VERSION = 5; |
|
const commentParser = new ConfigCommentParser(); |
|
const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; |
|
const parserSymbol = Symbol.for("eslint.RuleTester.parser"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isEspree(parser) { |
|
return !!(parser === espree || parser[parserSymbol] === espree); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function addDeclaredGlobals(globalScope, configGlobals, { exportedVariables, enabledGlobals }) { |
|
|
|
|
|
for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(enabledGlobals)])) { |
|
|
|
|
|
|
|
|
|
|
|
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"); |
|
} |
|
|
|
|
|
Object.keys(exportedVariables).forEach(name => { |
|
const variable = globalScope.set.get(name); |
|
|
|
if (variable) { |
|
variable.eslintUsed = true; |
|
variable.eslintExported = true; |
|
} |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
globalScope.through = globalScope.through.filter(reference => { |
|
const name = reference.identifier.name; |
|
const variable = globalScope.set.get(name); |
|
|
|
if (variable) { |
|
|
|
|
|
|
|
|
|
|
|
reference.resolved = variable; |
|
variable.references.push(reference); |
|
|
|
return false; |
|
} |
|
|
|
return true; |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createMissingRuleMessage(ruleId) { |
|
return Object.prototype.hasOwnProperty.call(ruleReplacements.rules, ruleId) |
|
? `Rule '${ruleId}' was removed and replaced by: ${ruleReplacements.rules[ruleId].join(", ")}` |
|
: `Definition for rule '${ruleId}' was not found.`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createLintingProblem(options) { |
|
const { |
|
ruleId = null, |
|
loc = DEFAULT_ERROR_LOC, |
|
message = createMissingRuleMessage(options.ruleId), |
|
severity = 2 |
|
} = options; |
|
|
|
return { |
|
ruleId, |
|
message, |
|
line: loc.start.line, |
|
column: loc.start.column + 1, |
|
endLine: loc.end.line, |
|
endColumn: loc.end.column + 1, |
|
severity, |
|
nodeType: null |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createDisableDirectives(options) { |
|
const { commentToken, type, value, justification, ruleMapper } = options; |
|
const ruleIds = Object.keys(commentParser.parseListConfig(value)); |
|
const directiveRules = ruleIds.length ? ruleIds : [null]; |
|
const result = { |
|
directives: [], |
|
directiveProblems: [] |
|
}; |
|
|
|
const parentComment = { commentToken, ruleIds }; |
|
|
|
for (const ruleId of directiveRules) { |
|
|
|
|
|
if (ruleId === null || !!ruleMapper(ruleId)) { |
|
if (type === "disable-next-line") { |
|
result.directives.push({ |
|
parentComment, |
|
type, |
|
line: commentToken.loc.end.line, |
|
column: commentToken.loc.end.column + 1, |
|
ruleId, |
|
justification |
|
}); |
|
} else { |
|
result.directives.push({ |
|
parentComment, |
|
type, |
|
line: commentToken.loc.start.line, |
|
column: commentToken.loc.start.column + 1, |
|
ruleId, |
|
justification |
|
}); |
|
} |
|
} else { |
|
result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc })); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { |
|
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 { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value); |
|
|
|
const match = directivesPattern.exec(directivePart); |
|
|
|
if (!match) { |
|
return; |
|
} |
|
const directiveText = match[1]; |
|
const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText); |
|
|
|
if (comment.type === "Line" && !lineCommentSupported) { |
|
return; |
|
} |
|
|
|
if (warnInlineConfig) { |
|
const kind = comment.type === "Block" ? `/*${directiveText}*/` : `//${directiveText}`; |
|
|
|
problems.push(createLintingProblem({ |
|
ruleId: null, |
|
message: `'${kind}' has no effect because you have 'noInlineConfig' setting in ${warnInlineConfig}.`, |
|
loc: comment.loc, |
|
severity: 1 |
|
})); |
|
return; |
|
} |
|
|
|
if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) { |
|
const message = `${directiveText} comment should not span multiple lines.`; |
|
|
|
problems.push(createLintingProblem({ |
|
ruleId: null, |
|
message, |
|
loc: comment.loc |
|
})); |
|
return; |
|
} |
|
|
|
const directiveValue = directivePart.slice(match.index + directiveText.length); |
|
|
|
switch (directiveText) { |
|
case "eslint-disable": |
|
case "eslint-enable": |
|
case "eslint-disable-next-line": |
|
case "eslint-disable-line": { |
|
const directiveType = directiveText.slice("eslint-".length); |
|
const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper }; |
|
const { directives, directiveProblems } = createDisableDirectives(options); |
|
|
|
disableDirectives.push(...directives); |
|
problems.push(...directiveProblems); |
|
break; |
|
} |
|
|
|
case "exported": |
|
Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment)); |
|
break; |
|
|
|
case "globals": |
|
case "global": |
|
for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) { |
|
let normalizedValue; |
|
|
|
try { |
|
normalizedValue = ConfigOps.normalizeConfigGlobal(value); |
|
} catch (err) { |
|
problems.push(createLintingProblem({ |
|
ruleId: null, |
|
loc: comment.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.parseJsonConfig(directiveValue, comment.loc); |
|
|
|
if (parseResult.success) { |
|
Object.keys(parseResult.config).forEach(name => { |
|
const rule = ruleMapper(name); |
|
const ruleValue = parseResult.config[name]; |
|
|
|
if (!rule) { |
|
problems.push(createLintingProblem({ ruleId: name, loc: comment.loc })); |
|
return; |
|
} |
|
|
|
try { |
|
validator.validateRuleOptions(rule, name, ruleValue); |
|
} catch (err) { |
|
problems.push(createLintingProblem({ |
|
ruleId: name, |
|
message: err.message, |
|
loc: comment.loc |
|
})); |
|
|
|
|
|
return; |
|
} |
|
|
|
configuredRules[name] = ruleValue; |
|
}); |
|
} else { |
|
problems.push(parseResult.error); |
|
} |
|
|
|
break; |
|
} |
|
|
|
|
|
} |
|
}); |
|
|
|
return { |
|
configuredRules, |
|
enabledGlobals, |
|
exportedVariables, |
|
problems, |
|
disableDirectives |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getDirectiveCommentsForFlatConfig(sourceCode, ruleMapper) { |
|
const problems = []; |
|
const disableDirectives = []; |
|
|
|
sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => { |
|
const { directivePart, justificationPart } = commentParser.extractDirectiveComment(comment.value); |
|
|
|
const match = directivesPattern.exec(directivePart); |
|
|
|
if (!match) { |
|
return; |
|
} |
|
const directiveText = match[1]; |
|
const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText); |
|
|
|
if (comment.type === "Line" && !lineCommentSupported) { |
|
return; |
|
} |
|
|
|
if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) { |
|
const message = `${directiveText} comment should not span multiple lines.`; |
|
|
|
problems.push(createLintingProblem({ |
|
ruleId: null, |
|
message, |
|
loc: comment.loc |
|
})); |
|
return; |
|
} |
|
|
|
const directiveValue = directivePart.slice(match.index + directiveText.length); |
|
|
|
switch (directiveText) { |
|
case "eslint-disable": |
|
case "eslint-enable": |
|
case "eslint-disable-next-line": |
|
case "eslint-disable-line": { |
|
const directiveType = directiveText.slice("eslint-".length); |
|
const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper }; |
|
const { directives, directiveProblems } = createDisableDirectives(options); |
|
|
|
disableDirectives.push(...directives); |
|
problems.push(...directiveProblems); |
|
break; |
|
} |
|
|
|
|
|
} |
|
}); |
|
|
|
return { |
|
problems, |
|
disableDirectives |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeEcmaVersion(parser, ecmaVersion) { |
|
|
|
if (isEspree(parser)) { |
|
if (ecmaVersion === "latest") { |
|
return espree.latestEcmaVersion; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeEcmaVersionForLanguageOptions(ecmaVersion) { |
|
|
|
switch (ecmaVersion) { |
|
case 3: |
|
return 3; |
|
|
|
|
|
case 5: |
|
case void 0: |
|
return 5; |
|
|
|
default: |
|
if (typeof ecmaVersion === "number") { |
|
return ecmaVersion >= 2015 ? ecmaVersion : ecmaVersion + 2009; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return espree.latestEcmaVersion + 2009; |
|
} |
|
|
|
const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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.extractDirectiveComment(match[1]).directivePart) |
|
); |
|
} |
|
} |
|
|
|
return retv; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeFilename(filename) { |
|
const parts = filename.split(path.sep); |
|
const index = parts.lastIndexOf("<text>"); |
|
|
|
return index === -1 ? filename : parts.slice(index).join(path.sep); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeVerifyOptions(providedOptions, config) { |
|
|
|
const linterOptions = config.linterOptions || config; |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
return { |
|
filename: normalizeFilename(providedOptions.filename || "<input>"), |
|
allowInlineConfig: !ignoreInlineConfig, |
|
warnInlineConfig: disableInlineConfig && !ignoreInlineConfig |
|
? `your config${configNameOfNoInlineConfig}` |
|
: null, |
|
reportUnusedDisableDirectives, |
|
disableFixes: Boolean(providedOptions.disableFixes) |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
|
|
|
|
mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false }); |
|
} |
|
|
|
mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion); |
|
|
|
return mergedParserOptions; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createLanguageOptions({ globals: configuredGlobals, parser, parserOptions }) { |
|
|
|
const { |
|
ecmaVersion, |
|
sourceType |
|
} = parserOptions; |
|
|
|
return { |
|
globals: configuredGlobals, |
|
ecmaVersion: normalizeEcmaVersionForLanguageOptions(ecmaVersion), |
|
sourceType, |
|
parser, |
|
parserOptions |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resolveGlobals(providedGlobals, enabledEnvironments) { |
|
return Object.assign( |
|
{}, |
|
...enabledEnvironments.filter(env => env.globals).map(env => env.globals), |
|
providedGlobals |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function stripUnicodeBOM(text) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (text.charCodeAt(0) === 0xFEFF) { |
|
return text.slice(1); |
|
} |
|
return text; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function getRuleOptions(ruleConfig) { |
|
if (Array.isArray(ruleConfig)) { |
|
return ruleConfig.slice(1); |
|
} |
|
return []; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function parse(text, languageOptions, filePath) { |
|
const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`); |
|
const { ecmaVersion, sourceType, parser } = languageOptions; |
|
const parserOptions = Object.assign( |
|
{ ecmaVersion, sourceType }, |
|
languageOptions.parserOptions, |
|
{ |
|
loc: true, |
|
range: true, |
|
raw: true, |
|
tokens: true, |
|
comment: true, |
|
eslintVisitorKeys: true, |
|
eslintScopeManager: true, |
|
filePath |
|
} |
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
debug("Parsing:", filePath); |
|
const parseResult = (typeof parser.parseForESLint === "function") |
|
? parser.parseForESLint(textToParse, parserOptions) |
|
: { ast: parser.parse(textToParse, parserOptions) }; |
|
|
|
debug("Parsing successful:", filePath); |
|
const ast = parseResult.ast; |
|
const parserServices = parseResult.services || {}; |
|
const visitorKeys = parseResult.visitorKeys || evk.KEYS; |
|
|
|
debug("Scope analysis:", filePath); |
|
const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys); |
|
|
|
debug("Scope analysis successful:", filePath); |
|
|
|
return { |
|
success: true, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sourceCode: new SourceCode({ |
|
text, |
|
ast, |
|
parserServices, |
|
scopeManager, |
|
visitorKeys |
|
}) |
|
}; |
|
} catch (ex) { |
|
|
|
|
|
const message = `Parsing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`; |
|
|
|
debug("%s\n%s", message, ex.stack); |
|
|
|
return { |
|
success: false, |
|
error: { |
|
ruleId: null, |
|
fatal: true, |
|
severity: 2, |
|
message, |
|
line: ex.lineNumber, |
|
column: ex.column, |
|
nodeType: null |
|
} |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createRuleListeners(rule, ruleContext) { |
|
try { |
|
return rule.create(ruleContext); |
|
} catch (ex) { |
|
ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`; |
|
throw ex; |
|
} |
|
} |
|
|
|
|
|
const DEPRECATED_SOURCECODE_PASSTHROUGHS = { |
|
getSource: "getText", |
|
getSourceLines: "getLines", |
|
getAllComments: "getAllComments", |
|
getNodeByRangeIndex: "getNodeByRangeIndex", |
|
getComments: "getComments", |
|
getCommentsBefore: "getCommentsBefore", |
|
getCommentsAfter: "getCommentsAfter", |
|
getCommentsInside: "getCommentsInside", |
|
getJSDocComment: "getJSDocComment", |
|
getFirstToken: "getFirstToken", |
|
getFirstTokens: "getFirstTokens", |
|
getLastToken: "getLastToken", |
|
getLastTokens: "getLastTokens", |
|
getTokenAfter: "getTokenAfter", |
|
getTokenBefore: "getTokenBefore", |
|
getTokenByRangeStart: "getTokenByRangeStart", |
|
getTokens: "getTokens", |
|
getTokensAfter: "getTokensAfter", |
|
getTokensBefore: "getTokensBefore", |
|
getTokensBetween: "getTokensBetween" |
|
}; |
|
|
|
|
|
const BASE_TRAVERSAL_CONTEXT = Object.freeze( |
|
Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce( |
|
(contextInfo, methodName) => |
|
Object.assign(contextInfo, { |
|
[methodName](...args) { |
|
return this.sourceCode[DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]](...args); |
|
} |
|
}), |
|
{} |
|
) |
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) { |
|
const emitter = createEmitter(); |
|
const nodeQueue = []; |
|
let currentNode = sourceCode.ast; |
|
|
|
Traverser.traverse(sourceCode.ast, { |
|
enter(node, parent) { |
|
node.parent = parent; |
|
nodeQueue.push({ isEntering: true, node }); |
|
}, |
|
leave(node) { |
|
nodeQueue.push({ isEntering: false, node }); |
|
}, |
|
visitorKeys: sourceCode.visitorKeys |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
const sharedTraversalContext = Object.freeze( |
|
Object.assign( |
|
Object.create(BASE_TRAVERSAL_CONTEXT), |
|
{ |
|
getAncestors: () => sourceCode.getAncestors(currentNode), |
|
getDeclaredVariables: node => sourceCode.getDeclaredVariables(node), |
|
getCwd: () => cwd, |
|
cwd, |
|
getFilename: () => filename, |
|
filename, |
|
getPhysicalFilename: () => physicalFilename || filename, |
|
physicalFilename: physicalFilename || filename, |
|
getScope: () => sourceCode.getScope(currentNode), |
|
getSourceCode: () => sourceCode, |
|
sourceCode, |
|
markVariableAsUsed: name => sourceCode.markVariableAsUsed(name, currentNode), |
|
parserOptions: { |
|
...languageOptions.parserOptions |
|
}, |
|
parserPath: parserName, |
|
languageOptions, |
|
parserServices: sourceCode.parserServices, |
|
settings |
|
} |
|
) |
|
); |
|
|
|
const lintingProblems = []; |
|
|
|
Object.keys(configuredRules).forEach(ruleId => { |
|
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]); |
|
|
|
|
|
if (severity === 0) { |
|
return; |
|
} |
|
|
|
const rule = ruleMapper(ruleId); |
|
|
|
if (!rule) { |
|
lintingProblems.push(createLintingProblem({ ruleId })); |
|
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) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (reportTranslator === null) { |
|
reportTranslator = createReportTranslator({ |
|
ruleId, |
|
severity, |
|
sourceCode, |
|
messageIds, |
|
disableFixes |
|
}); |
|
} |
|
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") { |
|
|
|
|
|
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 ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext); |
|
|
|
|
|
|
|
|
|
|
|
|
|
function addRuleErrorHandler(ruleListener) { |
|
return function ruleErrorHandler(...listenerArgs) { |
|
try { |
|
return ruleListener(...listenerArgs); |
|
} 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.`); |
|
} |
|
|
|
|
|
Object.keys(ruleListeners).forEach(selector => { |
|
const ruleListener = timing.enabled |
|
? timing.time(ruleId, ruleListeners[selector]) |
|
: ruleListeners[selector]; |
|
|
|
emitter.on( |
|
selector, |
|
addRuleErrorHandler(ruleListener) |
|
); |
|
}); |
|
}); |
|
|
|
|
|
const eventGenerator = nodeQueue[0].node.type === "Program" |
|
? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys })) |
|
: new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }); |
|
|
|
nodeQueue.forEach(traversalInfo => { |
|
currentNode = traversalInfo.node; |
|
|
|
try { |
|
if (traversalInfo.isEntering) { |
|
eventGenerator.enterNode(currentNode); |
|
} else { |
|
eventGenerator.leaveNode(currentNode); |
|
} |
|
} catch (err) { |
|
err.currentNode = currentNode; |
|
throw err; |
|
} |
|
}); |
|
|
|
return lintingProblems; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function ensureText(textOrSourceCode) { |
|
if (typeof textOrSourceCode === "object") { |
|
const { hasBOM, text } = textOrSourceCode; |
|
const bom = hasBOM ? "\uFEFF" : ""; |
|
|
|
return bom + text; |
|
} |
|
|
|
return String(textOrSourceCode); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getEnv(slots, envId) { |
|
return ( |
|
(slots.lastConfigArray && slots.lastConfigArray.pluginEnvironments.get(envId)) || |
|
BuiltInEnvironments.get(envId) || |
|
null |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getRule(slots, ruleId) { |
|
return ( |
|
(slots.lastConfigArray && slots.lastConfigArray.pluginRules.get(ruleId)) || |
|
slots.ruleMap.get(ruleId) |
|
); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeCwd(cwd) { |
|
if (cwd) { |
|
return cwd; |
|
} |
|
if (typeof process === "object") { |
|
return process.cwd(); |
|
} |
|
|
|
|
|
|
|
return undefined; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const internalSlotsMap = new WeakMap(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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."); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Linter { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor({ cwd, configType } = {}) { |
|
internalSlotsMap.set(this, { |
|
cwd: normalizeCwd(cwd), |
|
lastConfigArray: null, |
|
lastSourceCode: null, |
|
lastSuppressedMessages: [], |
|
configType, |
|
parserMap: new Map([["espree", espree]]), |
|
ruleMap: new Rules() |
|
}); |
|
|
|
this.version = pkg.version; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
static get version() { |
|
return pkg.version; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) { |
|
const slots = internalSlotsMap.get(this); |
|
const config = providedConfig || {}; |
|
const options = normalizeVerifyOptions(providedOptions, config); |
|
let text; |
|
|
|
|
|
if (typeof textOrSourceCode === "string") { |
|
slots.lastSourceCode = null; |
|
text = textOrSourceCode; |
|
} else { |
|
slots.lastSourceCode = textOrSourceCode; |
|
text = textOrSourceCode.text; |
|
} |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
const envInFile = options.allowInlineConfig && !options.warnInlineConfig |
|
? findEslintEnv(text) |
|
: {}; |
|
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) { |
|
const parseResult = parse( |
|
text, |
|
languageOptions, |
|
options.filename |
|
); |
|
|
|
if (!parseResult.success) { |
|
return [parseResult.error]; |
|
} |
|
|
|
slots.lastSourceCode = parseResult.sourceCode; |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
if (!slots.lastSourceCode.scopeManager) { |
|
slots.lastSourceCode = new SourceCode({ |
|
text: slots.lastSourceCode.text, |
|
ast: slots.lastSourceCode.ast, |
|
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) |
|
: { 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, |
|
languageOptions, |
|
settings, |
|
options.filename, |
|
options.disableFixes, |
|
slots.cwd, |
|
providedOptions.physicalFilename |
|
); |
|
} catch (err) { |
|
err.message += `\nOccurred while linting ${options.filename}`; |
|
debug("An error occurred while traversing"); |
|
debug("Filename:", options.filename); |
|
if (err.currentNode) { |
|
const { line } = err.currentNode.loc.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({ |
|
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 |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
verify(textOrSourceCode, config, filenameOrOptions) { |
|
debug("Verify"); |
|
|
|
const { configType, cwd } = internalSlotsMap.get(this); |
|
|
|
const options = typeof filenameOrOptions === "string" |
|
? { filename: filenameOrOptions } |
|
: filenameOrOptions || {}; |
|
|
|
if (config) { |
|
if (configType === "flat") { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let configArray = config; |
|
|
|
if (!Array.isArray(config) || typeof config.getConfig !== "function") { |
|
configArray = new FlatConfigArray(config, { basePath: cwd }); |
|
configArray.normalizeSync(); |
|
} |
|
|
|
return this._distinguishSuppressedMessages(this._verifyWithFlatConfigArray(textOrSourceCode, configArray, options, true)); |
|
} |
|
|
|
if (typeof config.extractConfig === "function") { |
|
return this._distinguishSuppressedMessages(this._verifyWithConfigArray(textOrSourceCode, config, options)); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (options.preprocess || options.postprocess) { |
|
return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, config, options)); |
|
} |
|
return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, config, options)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options, configForRecursive) { |
|
const filename = options.filename || "<input>"; |
|
const filenameToExpose = normalizeFilename(filename); |
|
const physicalFilename = options.physicalFilename || filenameToExpose; |
|
const text = ensureText(textOrSourceCode); |
|
const preprocess = options.preprocess || (rawText => [rawText]); |
|
const postprocess = options.postprocess || (messagesList => messagesList.flat()); |
|
const filterCodeBlock = |
|
options.filterCodeBlock || |
|
(blockFilename => blockFilename.endsWith(".js")); |
|
const originalExtname = path.extname(filename); |
|
|
|
let blocks; |
|
|
|
try { |
|
blocks = preprocess(text, filenameToExpose); |
|
} catch (ex) { |
|
|
|
|
|
const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`; |
|
|
|
debug("%s\n%s", message, ex.stack); |
|
|
|
return [ |
|
{ |
|
ruleId: null, |
|
fatal: true, |
|
severity: 2, |
|
message, |
|
line: ex.lineNumber, |
|
column: ex.column, |
|
nodeType: null |
|
} |
|
]; |
|
} |
|
|
|
const messageLists = blocks.map((block, i) => { |
|
debug("A code block was found: %o", block.filename || "(unnamed)"); |
|
|
|
|
|
if (typeof block === "string") { |
|
return this._verifyWithFlatConfigArrayAndWithoutProcessors(block, config, options); |
|
} |
|
|
|
const blockText = block.text; |
|
const blockName = path.join(filename, `${i}_${block.filename}`); |
|
|
|
|
|
if (!filterCodeBlock(blockName, blockText)) { |
|
debug("This code block was skipped."); |
|
return []; |
|
} |
|
|
|
|
|
if (configForRecursive && (text !== blockText || path.extname(blockName) !== originalExtname)) { |
|
debug("Resolving configuration again because the file content or extension was changed."); |
|
return this._verifyWithFlatConfigArray( |
|
blockText, |
|
configForRecursive, |
|
{ ...options, filename: blockName, physicalFilename } |
|
); |
|
} |
|
|
|
|
|
return this._verifyWithFlatConfigArrayAndWithoutProcessors( |
|
blockText, |
|
config, |
|
{ ...options, filename: blockName, physicalFilename } |
|
); |
|
}); |
|
|
|
return postprocess(messageLists, filenameToExpose); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) { |
|
const slots = internalSlotsMap.get(this); |
|
const config = providedConfig || {}; |
|
const options = normalizeVerifyOptions(providedOptions, config); |
|
let text; |
|
|
|
|
|
if (typeof textOrSourceCode === "string") { |
|
slots.lastSourceCode = null; |
|
text = textOrSourceCode; |
|
} else { |
|
slots.lastSourceCode = textOrSourceCode; |
|
text = textOrSourceCode.text; |
|
} |
|
|
|
const languageOptions = config.languageOptions; |
|
|
|
languageOptions.ecmaVersion = normalizeEcmaVersionForLanguageOptions( |
|
languageOptions.ecmaVersion |
|
); |
|
|
|
|
|
if (!languageOptions.parser) { |
|
throw new TypeError(`No parser specified for ${options.filename}`); |
|
} |
|
|
|
|
|
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) { |
|
const parseResult = parse( |
|
text, |
|
languageOptions, |
|
options.filename |
|
); |
|
|
|
if (!parseResult.success) { |
|
return [parseResult.error]; |
|
} |
|
|
|
slots.lastSourceCode = parseResult.sourceCode; |
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
if (!slots.lastSourceCode.scopeManager) { |
|
slots.lastSourceCode = new SourceCode({ |
|
text: slots.lastSourceCode.text, |
|
ast: slots.lastSourceCode.ast, |
|
parserServices: slots.lastSourceCode.parserServices, |
|
visitorKeys: slots.lastSourceCode.visitorKeys, |
|
scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions) |
|
}); |
|
} |
|
} |
|
|
|
const sourceCode = slots.lastSourceCode; |
|
|
|
|
|
|
|
|
|
|
|
|
|
sourceCode.applyLanguageOptions(languageOptions); |
|
|
|
const mergedInlineConfig = { |
|
rules: {} |
|
}; |
|
const inlineConfigProblems = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (options.allowInlineConfig) { |
|
|
|
|
|
if (options.warnInlineConfig) { |
|
sourceCode.getInlineConfigNodes().forEach(node => { |
|
inlineConfigProblems.push(createLintingProblem({ |
|
ruleId: null, |
|
message: `'${sourceCode.text.slice(node.range[0], node.range[1])}' has no effect because you have 'noInlineConfig' setting in ${options.warnInlineConfig}.`, |
|
loc: node.loc, |
|
severity: 1 |
|
})); |
|
|
|
}); |
|
} else { |
|
const inlineConfigResult = sourceCode.applyInlineConfig(); |
|
|
|
inlineConfigProblems.push( |
|
...inlineConfigResult.problems |
|
.map(createLintingProblem) |
|
.map(problem => { |
|
problem.fatal = true; |
|
return problem; |
|
}) |
|
); |
|
|
|
|
|
const ruleValidator = new RuleValidator(); |
|
|
|
for (const { config: inlineConfig, node } 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: node.loc })); |
|
return; |
|
} |
|
|
|
try { |
|
|
|
const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; |
|
|
|
assertIsRuleOptions(ruleId, ruleValue); |
|
assertIsRuleSeverity(ruleId, ruleOptions[0]); |
|
|
|
ruleValidator.validate({ |
|
plugins: config.plugins, |
|
rules: { |
|
[ruleId]: ruleOptions |
|
} |
|
}); |
|
mergedInlineConfig.rules[ruleId] = ruleValue; |
|
} catch (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: node.loc |
|
})); |
|
} |
|
}); |
|
} |
|
} |
|
} |
|
|
|
const commentDirectives = options.allowInlineConfig && !options.warnInlineConfig |
|
? getDirectiveCommentsForFlatConfig( |
|
sourceCode, |
|
ruleId => getRuleFromConfig(ruleId, config) |
|
) |
|
: { 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, |
|
languageOptions, |
|
settings, |
|
options.filename, |
|
options.disableFixes, |
|
slots.cwd, |
|
providedOptions.physicalFilename |
|
); |
|
} catch (err) { |
|
err.message += `\nOccurred while linting ${options.filename}`; |
|
debug("An error occurred while traversing"); |
|
debug("Filename:", options.filename); |
|
if (err.currentNode) { |
|
const { line } = err.currentNode.loc.start; |
|
|
|
debug("Line:", line); |
|
err.message += `:${line}`; |
|
} |
|
debug("Parser Options:", languageOptions.parserOptions); |
|
|
|
|
|
debug("Settings:", settings); |
|
|
|
if (err.ruleId) { |
|
err.message += `\nRule: "${err.ruleId}"`; |
|
} |
|
|
|
throw err; |
|
} |
|
|
|
return applyDisableDirectives({ |
|
directives: commentDirectives.disableDirectives, |
|
disableFixes: options.disableFixes, |
|
problems: lintingProblems |
|
.concat(commentDirectives.problems) |
|
.concat(inlineConfigProblems) |
|
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column), |
|
reportUnusedDisableDirectives: options.reportUnusedDisableDirectives |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_verifyWithConfigArray(textOrSourceCode, configArray, options) { |
|
debug("With ConfigArray: %s", options.filename); |
|
|
|
|
|
internalSlotsMap.get(this).lastConfigArray = configArray; |
|
|
|
|
|
const config = configArray.extractConfig(options.filename); |
|
const processor = |
|
config.processor && |
|
configArray.pluginProcessors.get(config.processor); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_verifyWithFlatConfigArray(textOrSourceCode, configArray, options, firstCall = false) { |
|
debug("With flat config: %s", options.filename); |
|
|
|
|
|
const filename = options.filename || "__placeholder__.js"; |
|
|
|
|
|
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 |
|
} |
|
]; |
|
} |
|
|
|
|
|
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 |
|
); |
|
} |
|
|
|
|
|
if (firstCall && (options.preprocess || options.postprocess)) { |
|
return this._verifyWithFlatConfigArrayAndProcessor(textOrSourceCode, config, options); |
|
} |
|
|
|
return this._verifyWithFlatConfigArrayAndWithoutProcessors(textOrSourceCode, config, options); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) { |
|
const filename = options.filename || "<input>"; |
|
const filenameToExpose = normalizeFilename(filename); |
|
const physicalFilename = options.physicalFilename || filenameToExpose; |
|
const text = ensureText(textOrSourceCode); |
|
const preprocess = options.preprocess || (rawText => [rawText]); |
|
const postprocess = options.postprocess || (messagesList => messagesList.flat()); |
|
const filterCodeBlock = |
|
options.filterCodeBlock || |
|
(blockFilename => blockFilename.endsWith(".js")); |
|
const originalExtname = path.extname(filename); |
|
|
|
let blocks; |
|
|
|
try { |
|
blocks = preprocess(text, filenameToExpose); |
|
} catch (ex) { |
|
|
|
|
|
const message = `Preprocessing error: ${ex.message.replace(/^line \d+:/iu, "").trim()}`; |
|
|
|
debug("%s\n%s", message, ex.stack); |
|
|
|
return [ |
|
{ |
|
ruleId: null, |
|
fatal: true, |
|
severity: 2, |
|
message, |
|
line: ex.lineNumber, |
|
column: ex.column, |
|
nodeType: null |
|
} |
|
]; |
|
} |
|
|
|
const messageLists = blocks.map((block, i) => { |
|
debug("A code block was found: %o", block.filename || "(unnamed)"); |
|
|
|
|
|
if (typeof block === "string") { |
|
return this._verifyWithoutProcessors(block, config, options); |
|
} |
|
|
|
const blockText = block.text; |
|
const blockName = path.join(filename, `${i}_${block.filename}`); |
|
|
|
|
|
if (!filterCodeBlock(blockName, blockText)) { |
|
debug("This code block was skipped."); |
|
return []; |
|
} |
|
|
|
|
|
if (configForRecursive && (text !== blockText || path.extname(blockName) !== originalExtname)) { |
|
debug("Resolving configuration again because the file content or extension was changed."); |
|
return this._verifyWithConfigArray( |
|
blockText, |
|
configForRecursive, |
|
{ ...options, filename: blockName, physicalFilename } |
|
); |
|
} |
|
|
|
|
|
return this._verifyWithoutProcessors( |
|
blockText, |
|
config, |
|
{ ...options, filename: blockName, physicalFilename } |
|
); |
|
}); |
|
|
|
return postprocess(messageLists, filenameToExpose); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getSourceCode() { |
|
return internalSlotsMap.get(this).lastSourceCode; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getSuppressedMessages() { |
|
return internalSlotsMap.get(this).lastSuppressedMessages; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defineRule(ruleId, ruleModule) { |
|
assertEslintrcConfig(this); |
|
internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
defineRules(rulesToDefine) { |
|
assertEslintrcConfig(this); |
|
Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => { |
|
this.defineRule(ruleId, rulesToDefine[ruleId]); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
getRules() { |
|
assertEslintrcConfig(this); |
|
const { lastConfigArray, ruleMap } = internalSlotsMap.get(this); |
|
|
|
return new Map(function *() { |
|
yield* ruleMap; |
|
|
|
if (lastConfigArray) { |
|
yield* lastConfigArray.pluginRules; |
|
} |
|
}()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defineParser(parserId, parserModule) { |
|
assertEslintrcConfig(this); |
|
internalSlotsMap.get(this).parserMap.set(parserId, parserModule); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
do { |
|
passNumber++; |
|
|
|
debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`); |
|
messages = this.verify(currentText, config, options); |
|
|
|
debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`); |
|
fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix); |
|
|
|
|
|
|
|
|
|
|
|
if (messages.length === 1 && messages[0].fatal) { |
|
break; |
|
} |
|
|
|
|
|
fixed = fixed || fixedResult.fixed; |
|
|
|
|
|
currentText = fixedResult.output; |
|
|
|
} while ( |
|
fixedResult.fixed && |
|
passNumber < MAX_AUTOFIX_PASSES |
|
); |
|
|
|
|
|
|
|
|
|
|
|
if (fixedResult.fixed) { |
|
fixedResult.messages = this.verify(currentText, config, options); |
|
} |
|
|
|
|
|
fixedResult.fixed = fixed; |
|
fixedResult.output = currentText; |
|
|
|
return fixedResult; |
|
} |
|
} |
|
|
|
module.exports = { |
|
Linter, |
|
|
|
|
|
|
|
|
|
|
|
|
|
getLinterInternalSlots(instance) { |
|
return internalSlotsMap.get(instance); |
|
} |
|
}; |
|
|