evk = require("eslint-visitor-keys"),
espree = require("espree"),
merge = require("lodash.merge"),
- BuiltInEnvironments = require("@eslint/eslintrc/conf/environments"),
pkg = require("../../package.json"),
astUtils = require("../shared/ast-utils"),
- ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"),
- ConfigValidator = require("@eslint/eslintrc/lib/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"),
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");
//------------------------------------------------------------------------------
// 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").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
*/
/**
* @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.
*/
* 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 {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, 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 });
+ result.directives.push({ parentComment, type, line: commentToken.loc.start.line, column: commentToken.loc.start.column + 1, ruleId });
} else {
- result.directiveProblems.push(createLintingProblem({ ruleId, loc }));
+ result.directiveProblems.push(createLintingProblem({ ruleId, loc: commentToken.loc }));
}
}
return result;
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, ruleMapper };
const { directives, directiveProblems } = createDisableDirectives(options);
disableDirectives.push(...directives);
/**
* 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 ((parser[parserSymbol] || parser) === espree) {
+ 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(.+?)\*\//gsu;
+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(stripDirectiveComment(match[1]))
+ );
+ }
}
return retv;
/**
* 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) => merge(parserOptions, env.parserOptions), {});
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;
}
*/
function analyzeScope(ast, parserOptions, visitorKeys) {
const ecmaFeatures = parserOptions.ecmaFeatures || {};
- const ecmaVersion = parserOptions.ecmaVersion || 5;
+ const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION;
return eslintScope.analyze(ast, {
ignoreEval: true,
nodejsScope: ecmaFeatures.globalReturn,
impliedStrict: ecmaFeatures.impliedStrict,
- ecmaVersion,
+ ecmaVersion: typeof ecmaVersion === "number" ? ecmaVersion : 6,
sourceType: parserOptions.sourceType || "script",
childVisitorKeys: visitorKeys || evk.KEYS,
fallback: Traverser.getKeys
* 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) {
}
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);
+ /**
+ * 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;
+ }
+ };
+ }
+
// 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)
);
});
});
}
// 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;
}
/**
* 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.
*/
constructor({ cwd } = {}) {
internalSlotsMap.set(this, {
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
* @param {ConfigData} providedConfig An ESLintConfig instance to configure everything.
* @param {VerifyOptions} [providedOptions] The optional filename of the file being checked.
+ * @throws {Error} If during rule execution.
* @returns {LintMessage[]} The results as an array of messages or an empty array if no messages.
*/
_verifyWithoutProcessors(textOrSourceCode, providedConfig, providedOptions) {
.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 || {};
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),
const text = ensureText(textOrSourceCode);
const preprocess = options.preprocess || (rawText => [rawText]);
- // TODO(stephenwade): Replace this with array.flat() when we drop support for Node v10
- const postprocess = options.postprocess || (array => [].concat(...array));
+ const postprocess = options.postprocess || (messagesList => messagesList.flat());
const filterCodeBlock =
options.filterCodeBlock ||
(blockFilename => blockFilename.endsWith(".js"));