eslintScope = require("eslint-scope"),
evk = require("eslint-visitor-keys"),
espree = require("espree"),
- lodash = require("lodash"),
- BuiltInEnvironments = require("../../conf/environments"),
+ merge = require("lodash.merge"),
pkg = require("../../package.json"),
astUtils = require("../shared/ast-utils"),
- ConfigOps = require("../shared/config-ops"),
- validator = require("../shared/config-validator"),
+ {
+ 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"),
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 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 globals = require("../../conf/globals");
//------------------------------------------------------------------------------
// Typedefs
//------------------------------------------------------------------------------
-/** @typedef {InstanceType<import("../cli-engine/config-array")["ConfigArray"]>} ConfigArray */
-/** @typedef {InstanceType<import("../cli-engine/config-array")["ExtractedConfig"]>} ExtractedConfig */
+/** @typedef {InstanceType<import("../cli-engine/config-array").ConfigArray>} ConfigArray */
+/** @typedef {InstanceType<import("../cli-engine/config-array").ExtractedConfig>} ExtractedConfig */
/** @typedef {import("../shared/types").ConfigData} ConfigData */
/** @typedef {import("../shared/types").Environment} Environment */
/** @typedef {import("../shared/types").GlobalConf} GlobalConf */
/** @typedef {import("../shared/types").LintMessage} LintMessage */
+/** @typedef {import("../shared/types").SuppressedLintMessage} SuppressedLintMessage */
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
+/** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */
/** @typedef {import("../shared/types").Processor} Processor */
/** @typedef {import("../shared/types").Rule} Rule */
+/* 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
- * @property {number} line
- * @property {number} column
- * @property {(string|null)} ruleId
+ * @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
*/
/**
* @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 {Rules} ruleMap The loaded rules.
*/
* @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
+ * @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.
+ * @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.
*/
// 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);
+}
+
+/**
+ * Retrieves globals for the given ecmaVersion.
+ * @param {number} ecmaVersion The version to retrieve globals for.
+ * @returns {Object} The globals for the given ecmaVersion.
+ */
+function getGlobalsForEcmaVersion(ecmaVersion) {
+
+ switch (ecmaVersion) {
+ case 3:
+ return globals.es3;
+
+ case 5:
+ return globals.es5;
+
+ default:
+ if (ecmaVersion < 2015) {
+ return globals[`es${ecmaVersion + 2009}`];
+ }
+
+ return globals[`es${ecmaVersion}`];
+ }
+}
+
/**
* Ensures that variables representing built-in properties of the Global Object,
* and any globals declared by special block comments, are present in the global
* 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 {{line: number, column: number}} options.loc The 0-based location of the comment token
+ * @param {token} options.commentToken The Comment token
* @param {string} options.value The value after the directive in the comment
* comment specified no specific rules, so it applies to all rules (e.g. `eslint-disable`)
+ * @param {string} options.justification The justification of the directive
* @param {function(string): {create: Function}} options.ruleMapper A map from rule IDs to defined rules
* @returns {Object} Directives and problems from the comment
*/
function createDisableDirectives(options) {
- const { type, loc, value, ruleMapper } = options;
+ const { commentToken, type, value, justification, ruleMapper } = options;
const ruleIds = Object.keys(commentParser.parseListConfig(value));
const directiveRules = ruleIds.length ? ruleIds : [null];
const result = {
directiveProblems: [] // problems in directives
};
+ const parentComment = { commentToken, ruleIds };
+
for (const ruleId of directiveRules) {
// push to directives, if the rule is defined(including null, e.g. /*eslint enable*/)
- if (ruleId === null || ruleMapper(ruleId) !== null) {
- result.directives.push({ type, line: loc.start.line, column: loc.start.column + 1, ruleId });
+ 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 }));
+ result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc }));
}
}
return result;
}
/**
- * Remove the ignored part from a given directive comment and trim it.
- * @param {string} value The comment text to strip.
- * @returns {string} The stripped text.
+ * Extract the directive and the justification from a given directive comment and trim them.
+ * @param {string} value The comment text to extract.
+ * @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification.
*/
-function stripDirectiveComment(value) {
- return value.split(/\s-{2,}\s/u)[0].trim();
+function extractDirectiveComment(value) {
+ const match = /\s-{2,}\s/u.exec(value);
+
+ if (!match) {
+ return { directivePart: value.trim(), justificationPart: "" };
+ }
+
+ const directive = value.slice(0, match.index).trim();
+ const justification = value.slice(match.index + match[0].length).trim();
+
+ return { directivePart: directive, justificationPart: justification };
}
/**
* 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 {string} filename The file being checked.
* @param {ASTNode} ast The top node of the AST.
* @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.
* @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: Problem[], disableDirectives: DisableDirective[]}}
* A collection of the directive comments that were found, along with any problems that occurred when parsing
*/
-function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) {
+function getDirectiveComments(ast, ruleMapper, warnInlineConfig) {
const configuredRules = {};
const enabledGlobals = Object.create(null);
const exportedVariables = {};
const problems = [];
const disableDirectives = [];
+ const validator = new ConfigValidator({
+ builtInRules: Rules
+ });
ast.comments.filter(token => token.type !== "Shebang").forEach(comment => {
- const trimmedCommentText = stripDirectiveComment(comment.value);
- const match = /^(eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(trimmedCommentText);
+ const { directivePart, justificationPart } = extractDirectiveComment(comment.value);
+
+ const match = /^(eslint(?:-env|-enable|-disable(?:(?:-next)?-line)?)?|exported|globals?)(?:\s|$)/u.exec(directivePart);
if (!match) {
return;
return;
}
- if (lineCommentSupported && comment.loc.start.line !== comment.loc.end.line) {
+ 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({
return;
}
- const directiveValue = trimmedCommentText.slice(match.index + directiveText.length);
+ const directiveValue = directivePart.slice(match.index + directiveText.length);
switch (directiveText) {
case "eslint-disable":
case "eslint-disable-next-line":
case "eslint-disable-line": {
const directiveType = directiveText.slice("eslint-".length);
- const options = { type: directiveType, loc: comment.loc, value: directiveValue, ruleMapper };
+ const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper };
const { directives, directiveProblems } = createDisableDirectives(options);
disableDirectives.push(...directives);
const rule = ruleMapper(name);
const ruleValue = parseResult.config[name];
- if (rule === null) {
+ if (!rule) {
problems.push(createLintingProblem({ ruleId: name, loc: comment.loc }));
return;
}
/**
* Normalize ECMAScript version from the initial config
- * @param {number} ecmaVersion 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(ecmaVersion) {
+function normalizeEcmaVersion(parser, ecmaVersion) {
+
+ if (isEspree(parser)) {
+ if (ecmaVersion === "latest") {
+ return espree.latestEcmaVersion;
+ }
+ }
/*
* Calculate ECMAScript edition number from official year version starting with
return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion;
}
-const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gu;
+/**
+ * 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 espree.latestEcmaVersion + 2009;
+}
+
+const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)(?:\*\/|$)/gsu;
/**
* Checks whether or not there is a comment which has "eslint-env *" in a given text.
eslintEnvPattern.lastIndex = 0;
while ((match = eslintEnvPattern.exec(text)) !== null) {
- retv = Object.assign(
- retv || {},
- commentParser.parseListConfig(stripDirectiveComment(match[1]))
- );
+ if (match[0].endsWith("*/")) {
+ retv = Object.assign(
+ retv || {},
+ commentParser.parseListConfig(extractDirectiveComment(match[1]).directivePart)
+ );
+ }
}
return retv;
* @returns {Required<VerifyOptions> & InternalOptions} Normalized options
*/
function normalizeVerifyOptions(providedOptions, config) {
- const disableInlineConfig = config.noInlineConfig === true;
+
+ 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})`
reportUnusedDisableDirectives = reportUnusedDisableDirectives ? "error" : "off";
}
if (typeof reportUnusedDisableDirectives !== "string") {
- reportUnusedDisableDirectives = config.reportUnusedDisableDirectives ? "warn" : "off";
+ reportUnusedDisableDirectives =
+ linterOptions.reportUnusedDisableDirectives
+ ? "warn" : "off";
}
return {
/**
* Combines the provided parserOptions with the options from environments
- * @param {string} parserName The parser name which uses this options.
+ * @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(parserName, providedOptions, enabledEnvironments) {
+function resolveParserOptions(parser, providedOptions, enabledEnvironments) {
+
const parserOptionsFromEnv = enabledEnvironments
.filter(env => env.parserOptions)
- .reduce((parserOptions, env) => lodash.merge(parserOptions, env.parserOptions), {});
- const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {});
+ .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 });
}
- /*
- * TODO: @aladdin-add
- * 1. for a 3rd-party parser, do not normalize parserOptions
- * 2. for espree, no need to do this (espree will do it)
- */
- mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion);
+ 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
/**
* Analyze scope of the given AST.
* @param {ASTNode} ast The `Program` node to analyze.
- * @param {ParserOptions} parserOptions The parser options.
+ * @param {LanguageOptions} languageOptions The parser options.
* @param {Record<string, string[]>} visitorKeys The visitor keys.
* @returns {ScopeManager} The analysis result.
*/
-function analyzeScope(ast, parserOptions, visitorKeys) {
+function analyzeScope(ast, languageOptions, visitorKeys) {
+ const parserOptions = languageOptions.parserOptions;
const ecmaFeatures = parserOptions.ecmaFeatures || {};
- const ecmaVersion = parserOptions.ecmaVersion || 5;
+ const ecmaVersion = languageOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
return eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict,
- ecmaVersion,
- sourceType: parserOptions.sourceType || "script",
+ ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
+ sourceType: languageOptions.sourceType || "script",
childVisitorKeys: visitorKeys || evk.KEYS,
fallback: Traverser.getKeys
});
* optimization of functions, so it's best to keep the try-catch as isolated
* as possible
* @param {string} text The text to parse.
- * @param {Parser} parser The parser to parse.
- * @param {ParserOptions} providedParserOptions Options to pass to the parser
+ * @param {LanguageOptions} languageOptions Options to pass to the parser
* @param {string} filePath The path to the file being parsed.
* @returns {{success: false, error: Problem}|{success: true, sourceCode: SourceCode}}
* An object containing the AST and parser services if parsing was successful, or the error if parsing failed
* @private
*/
-function parse(text, parser, providedParserOptions, filePath) {
+function parse(text, languageOptions, filePath) {
const textToParse = stripUnicodeBOM(text).replace(astUtils.shebangPattern, (match, captured) => `//${captured}`);
- const parserOptions = Object.assign({}, providedParserOptions, {
- loc: true,
- range: true,
- raw: true,
- tokens: true,
- comment: true,
- eslintVisitorKeys: true,
- eslintScopeManager: true,
- filePath
- });
+ 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
+ }
+ );
/*
* Check for parsing errors first. If there's a parsing error, nothing
* problem that ESLint identified just like any other.
*/
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;
- const scopeManager = parseResult.scopeManager || analyzeScope(ast, parserOptions, visitorKeys);
+
+ debug("Scope analysis:", filePath);
+ const scopeManager = parseResult.scopeManager || analyzeScope(ast, languageOptions, visitorKeys);
+
+ debug("Scope analysis successful:", filePath);
return {
success: true,
* Marks a variable as used in the current scope
* @param {ScopeManager} scopeManager The scope manager for this AST. The scope may be mutated by this function.
* @param {ASTNode} currentNode The node currently being traversed
- * @param {Object} parserOptions The options used to parse this text
+ * @param {LanguageOptions} languageOptions The options used to parse this text
* @param {string} name The name of the variable that should be marked as used.
* @returns {boolean} True if the variable was found and marked as used, false if not.
*/
-function markVariableAsUsed(scopeManager, currentNode, parserOptions, name) {
- const hasGlobalReturn = parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn;
- const specialScope = hasGlobalReturn || parserOptions.sourceType === "module";
+function markVariableAsUsed(scopeManager, currentNode, languageOptions, name) {
+ const parserOptions = languageOptions.parserOptions;
+ const sourceType = languageOptions.sourceType;
+ const hasGlobalReturn =
+ (parserOptions.ecmaFeatures && parserOptions.ecmaFeatures.globalReturn) ||
+ sourceType === "commonjs";
+ const specialScope = hasGlobalReturn || sourceType === "module";
const currentScope = getScope(scopeManager, currentNode);
// Special Node.js scope means we need to start one level deeper
* Runs a rule, and gets its listeners
* @param {Rule} rule A normalized rule with a `create` method
* @param {Context} ruleContext The context that should be passed to the rule
+ * @throws {any} Any error during the rule's `create`
* @returns {Object} A map of selector listeners provided by the rule
*/
function createRuleListeners(rule, ruleContext) {
* @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 {Object} parserOptions The options that were passed to the parser
- * @param {string} parserName The name of the parser in the config
+ * @param {string | undefined} parserName The name of the parser in the config
+ * @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
* @returns {Problem[]} An array of reported problems
*/
-function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) {
+function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename) {
const emitter = createEmitter();
const nodeQueue = [];
let currentNode = sourceCode.ast;
getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
getCwd: () => cwd,
getFilename: () => filename,
+ getPhysicalFilename: () => physicalFilename || filename,
getScope: () => getScope(sourceCode.scopeManager, currentNode),
getSourceCode: () => sourceCode,
- markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name),
- parserOptions,
+ markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, languageOptions, name),
+ parserOptions: {
+ ...languageOptions.parserOptions
+ },
parserPath: parserName,
+ languageOptions,
parserServices: sourceCode.parserServices,
settings
}
)
);
-
const lintingProblems = [];
Object.keys(configuredRules).forEach(ruleId => {
const rule = ruleMapper(ruleId);
- if (rule === null) {
+ if (!rule) {
lintingProblems.push(createLintingProblem({ ruleId }));
return;
}
}
const problem = reportTranslator(...args);
- if (problem.fix && rule.meta && !rule.meta.fixable) {
- throw new Error("Fixable rules should export a `meta.fixable` property.");
+ 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 ruleListeners = createRuleListeners(rule, ruleContext);
+ const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext);
+
+ /**
+ * Include `ruleId` in error logs
+ * @param {Function} ruleListener A rule method that listens for a node.
+ * @returns {Function} ruleListener wrapped in error handler
+ */
+ function addRuleErrorHandler(ruleListener) {
+ return function ruleErrorHandler(...listenerArgs) {
+ try {
+ return ruleListener(...listenerArgs);
+ } 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
+ ? timing.time(ruleId, ruleListeners[selector])
+ : ruleListeners[selector];
+
emitter.on(
selector,
- timing.enabled
- ? timing.time(ruleId, ruleListeners[selector])
- : ruleListeners[selector]
+ addRuleErrorHandler(ruleListener)
);
});
});
- const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
+ // only run code path analyzer if the top level node is "Program", skip otherwise
+ const eventGenerator = nodeQueue[0].node.type === "Program"
+ ? new CodePathAnalyzer(new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys }))
+ : new NodeEventGenerator(emitter, { visitorKeys: sourceCode.visitorKeys, fallback: Traverser.getKeys });
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
}
// It's more explicit to assign the undefined
- // eslint-disable-next-line no-undefined
+ // eslint-disable-next-line no-undefined -- Consistently returning a value
return undefined;
}
*/
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 eslint
+ * @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 {string} [config.cwd] path to a directory that should be considered as the current working directory, can be undefined.
+ * @param {"flat"|"eslintrc"} [config.configType="eslintrc"] the type of config used.
*/
- constructor({ cwd } = {}) {
+ constructor({ cwd, configType } = {}) {
internalSlotsMap.set(this, {
cwd: normalizeCwd(cwd),
lastConfigArray: null,
lastSourceCode: null,
+ lastSuppressedMessages: [],
+ configType, // TODO: Remove after flat config conversion
parserMap: new Map([["espree", espree]]),
ruleMap: new Rules()
});
* @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.
- * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
+ * @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);
.map(envName => getEnv(slots, envName))
.filter(env => env);
- const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs);
+ 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,
- parser,
- parserOptions,
+ languageOptions,
options.filename
);
ast: slots.lastSourceCode.ast,
parserServices: slots.lastSourceCode.parserServices,
visitorKeys: slots.lastSourceCode.visitorKeys,
- scopeManager: analyzeScope(slots.lastSourceCode.ast, parserOptions)
+ scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions)
});
}
}
const sourceCode = slots.lastSourceCode;
const commentDirectives = options.allowInlineConfig
- ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
+ ? getDirectiveComments(sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig)
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
// augment global scope with declared global variables
sourceCode,
configuredRules,
ruleId => getRule(slots, ruleId),
- parserOptions,
parserName,
+ languageOptions,
settings,
options.filename,
options.disableFixes,
- slots.cwd
+ slots.cwd,
+ providedOptions.physicalFilename
);
} catch (err) {
err.message += `\nOccurred while linting ${options.filename}`;
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),
*/
verify(textOrSourceCode, config, filenameOrOptions) {
debug("Verify");
+
+ const { configType } = internalSlotsMap.get(this);
+
const options = typeof filenameOrOptions === "string"
? { filename: filenameOrOptions }
: filenameOrOptions || {};
- // CLIEngine passes a `ConfigArray` object.
- if (config && typeof config.extractConfig === "function") {
- return this._verifyWithConfigArray(textOrSourceCode, config, options);
+ if (config) {
+ if (configType === "flat") {
+
+ /*
+ * Because of how Webpack packages up the files, we can't
+ * compare directly to `FlatConfigArray` using `instanceof`
+ * because it's not the same `FlatConfigArray` as in the tests.
+ * So, we work around it by assuming an array is, in fact, a
+ * `FlatConfigArray` if it has a `getConfig()` method.
+ */
+ let configArray = config;
+
+ if (!Array.isArray(config) || typeof config.getConfig !== "function") {
+ configArray = new FlatConfigArray(config);
+ 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 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._verifyWithProcessor(textOrSourceCode, config, options);
+ return this._distinguishSuppressedMessages(this._verifyWithProcessor(textOrSourceCode, config, options));
}
- return this._verifyWithoutProcessors(textOrSourceCode, config, options);
+ return this._distinguishSuppressedMessages(this._verifyWithoutProcessors(textOrSourceCode, config, 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 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) {
+
+ // If the message includes a leading line number, strip it:
+ 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
+ }
+ ];
+ }
+
+ const messageLists = blocks.map((block, i) => {
+ debug("A code block was found: %o", block.filename || "(unnamed)");
+
+ // Keep the legacy behavior.
+ if (typeof block === "string") {
+ return this._verifyWithFlatConfigArrayAndWithoutProcessors(block, config, options);
+ }
+
+ const blockText = block.text;
+ const blockName = path.join(filename, `${i}_${block.filename}`);
+
+ // Skip this block if filtered.
+ if (!filterCodeBlock(blockName, blockText)) {
+ debug("This code block was skipped.");
+ return [];
+ }
+
+ // Resolve configuration again if the file content or extension was changed.
+ 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 }
+ );
+ }
+
+ // Does lint.
+ return this._verifyWithFlatConfigArrayAndWithoutProcessors(
+ blockText,
+ config,
+ { ...options, filename: blockName, physicalFilename }
+ );
+ });
+
+ return postprocess(messageLists, filenameToExpose);
+ }
+
+ /**
+ * 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 config = providedConfig || {};
+ const options = normalizeVerifyOptions(providedOptions, config);
+ let text;
+
+ // evaluate arguments
+ 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
+ );
+
+ // add configured globals and language globals
+ const configuredGlobals = {
+ ...(getGlobalsForEcmaVersion(languageOptions.ecmaVersion)),
+ ...(languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0),
+ ...languageOptions.globals
+ };
+
+ // double check that there is a parser to avoid mysterious error messages
+ if (!languageOptions.parser) {
+ throw new TypeError(`No parser specified for ${options.filename}`);
+ }
+
+ // 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) {
+ const parseResult = parse(
+ text,
+ languageOptions,
+ options.filename
+ );
+
+ if (!parseResult.success) {
+ return [parseResult.error];
+ }
+
+ 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,
+ parserServices: slots.lastSourceCode.parserServices,
+ visitorKeys: slots.lastSourceCode.visitorKeys,
+ scopeManager: analyzeScope(slots.lastSourceCode.ast, languageOptions)
+ });
+ }
+ }
+
+ const sourceCode = slots.lastSourceCode;
+ const commentDirectives = options.allowInlineConfig
+ ? getDirectiveComments(
+ sourceCode.ast,
+ ruleId => getRuleFromConfig(ruleId, config),
+ options.warnInlineConfig
+ )
+ : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
+
+ // augment global scope with declared global variables
+ addDeclaredGlobals(
+ sourceCode.scopeManager.scopes[0],
+ configuredGlobals,
+ { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
+ );
+
+ const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
+
+ let lintingProblems;
+
+ try {
+ lintingProblems = runRules(
+ sourceCode,
+ 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("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
+ });
}
/**
* @param {string|SourceCode} textOrSourceCode The source code.
* @param {ConfigArray} configArray The config array.
* @param {VerifyOptions&ProcessorOptions} options The options.
- * @returns {LintMessage[]} The found problems.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_verifyWithConfigArray(textOrSourceCode, configArray, options) {
debug("With ConfigArray: %s", options.filename);
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
+ }
+ ];
+ }
+
+ // 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[]} The found problems.
+ * @returns {(LintMessage|SuppressedLintMessage)[]} The found problems.
*/
_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 || lodash.flatten;
+ const postprocess = options.postprocess || (messagesList => messagesList.flat());
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));
const originalExtname = path.extname(filename);
- const messageLists = preprocess(text, filenameToExpose).map((block, i) => {
+
+ let blocks;
+
+ try {
+ blocks = preprocess(text, filenameToExpose);
+ } catch (ex) {
+
+ // If the message includes a leading line number, strip it:
+ 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
+ }
+ ];
+ }
+
+ const messageLists = blocks.map((block, i) => {
debug("A code block was found: %o", block.filename || "(unnamed)");
// Keep the legacy behavior.
return [];
}
- // Resolve configuration again if the file extension was changed.
- if (configForRecursive && path.extname(blockName) !== originalExtname) {
- debug("Resolving configuration again because the file extension was changed.");
+ // Resolve configuration again if the file content or extension was changed.
+ 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 }
+ { ...options, filename: blockName, physicalFilename }
);
}
return this._verifyWithoutProcessors(
blockText,
config,
- { ...options, filename: blockName }
+ { ...options, filename: blockName, physicalFilename }
);
});
return postprocess(messageLists, filenameToExpose);
}
+ /**
+ * 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 {Problem[]} 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.
return internalSlotsMap.get(this).lastSourceCode;
}
+ /**
+ * 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
* @returns {void}
*/
defineRule(ruleId, ruleModule) {
+ assertEslintrcConfig(this);
internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule);
}
* @returns {void}
*/
defineRules(rulesToDefine) {
+ assertEslintrcConfig(this);
Object.getOwnPropertyNames(rulesToDefine).forEach(ruleId => {
this.defineRule(ruleId, rulesToDefine[ruleId]);
});
* @returns {Map<string, Rule>} All loaded rules
*/
getRules() {
+ assertEslintrcConfig(this);
const { lastConfigArray, ruleMap } = internalSlotsMap.get(this);
return new Map(function *() {
* @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} config The ESLint config object to use.
+ * @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.