diff --git a/conf/environments.js b/conf/environments.js
index 44f2e0dee56..13020ebb525 100644
--- a/conf/environments.js
+++ b/conf/environments.js
@@ -15,7 +15,9 @@ const globals = require("globals");
//------------------------------------------------------------------------------
module.exports = {
- builtin: globals.es5,
+ builtin: {
+ globals: globals.es5
+ },
browser: {
/*
diff --git a/lib/linter.js b/lib/linter.js
index 889448ab027..c97dd6ef8eb 100755
--- a/lib/linter.js
+++ b/lib/linter.js
@@ -178,31 +178,12 @@ function parseListConfig(string) {
* and any globals declared by special block comments, are present in the global
* scope.
* @param {Scope} globalScope The global scope.
- * @param {Object} config The existing configuration data.
+ * @param {Object} configGlobals The globals declared in configuration
* @param {{exportedVariables: Object, enabledGlobals: Object}} commentDirectives Directives from comment configuration
- * @param {Environments} envContext Env context
* @returns {void}
*/
-function addDeclaredGlobals(globalScope, config, commentDirectives, envContext) {
- const declaredGlobals = {},
- explicitGlobals = {},
- builtin = envContext.get("builtin");
-
- Object.assign(declaredGlobals, builtin);
-
- Object.keys(config.env).filter(name => config.env[name]).forEach(name => {
- const env = envContext.get(name),
- environmentGlobals = env && env.globals;
-
- if (environmentGlobals) {
- Object.assign(declaredGlobals, environmentGlobals);
- }
- });
-
- Object.assign(declaredGlobals, config.globals);
- Object.assign(explicitGlobals, commentDirectives.enabledGlobals);
-
- Object.keys(declaredGlobals).forEach(name => {
+function addDeclaredGlobals(globalScope, configGlobals, commentDirectives) {
+ Object.keys(configGlobals).forEach(name => {
let variable = globalScope.set.get(name);
if (!variable) {
@@ -211,20 +192,20 @@ function addDeclaredGlobals(globalScope, config, commentDirectives, envContext)
globalScope.variables.push(variable);
globalScope.set.set(name, variable);
}
- variable.writeable = declaredGlobals[name];
+ variable.writeable = configGlobals[name];
});
- Object.keys(explicitGlobals).forEach(name => {
+ Object.keys(commentDirectives.enabledGlobals).forEach(name => {
let variable = globalScope.set.get(name);
if (!variable) {
variable = new eslintScope.Variable(name, globalScope);
variable.eslintExplicitGlobal = true;
- variable.eslintExplicitGlobalComment = explicitGlobals[name].comment;
+ variable.eslintExplicitGlobalComment = commentDirectives.enabledGlobals[name].comment;
globalScope.variables.push(variable);
globalScope.set.set(name, variable);
}
- variable.writeable = explicitGlobals[name].value;
+ variable.writeable = commentDirectives.enabledGlobals[name].value;
});
// mark all exported variables as such
@@ -395,59 +376,6 @@ function normalizeEcmaVersion(ecmaVersion, isModule) {
return ecmaVersion;
}
-/**
- * Populates a config file with the default values, and merges any parserOptions stored in an env into
- * the parserOptions of the populated config.
- * @param {Object} config Initial config
- * @param {Environments} envContext Env context
- * @returns {Object} Processed config
- */
-function populateConfig(config, envContext) {
- if (typeof config.rules === "object") {
- Object.keys(config.rules).forEach(k => {
- const rule = config.rules[k];
-
- if (rule === null) {
- throw new Error(`Invalid config for rule '${k}'.`);
- }
- });
- }
-
- // merge in environment parserOptions
- const envNamesEnabledInConfig = typeof config.env === "object"
- ? Object.keys(config.env).filter(envName => config.env[envName])
- : [];
-
- const parserOptionsFromEnv = envNamesEnabledInConfig
- .reduce((parserOptions, envName) => {
- const env = envContext.get(envName);
-
- return env && env.parserOptions
- ? ConfigOps.merge(parserOptions, env.parserOptions)
- : parserOptions;
- }, {});
-
- const preparedConfig = {
- rules: config.rules || {},
- parser: config.parser || DEFAULT_PARSER_NAME,
- globals: config.globals || {},
- env: config.env || {},
- settings: config.settings || {},
- parserOptions: ConfigOps.merge(parserOptionsFromEnv, config.parserOptions || {})
- };
- const isModule = preparedConfig.parserOptions.sourceType === "module";
-
- if (isModule) {
-
- // can't have global return inside of modules
- preparedConfig.parserOptions.ecmaFeatures = Object.assign({}, preparedConfig.parserOptions.ecmaFeatures, { globalReturn: false });
- }
-
- preparedConfig.parserOptions.ecmaVersion = normalizeEcmaVersion(preparedConfig.parserOptions.ecmaVersion, isModule);
-
- return preparedConfig;
-}
-
const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//g;
/**
@@ -467,6 +395,64 @@ function findEslintEnv(text) {
return retv;
}
+/**
+ * Normalizes the possible options for `linter.verify` and `linter.verifyAndFix` to a
+ * consistent shape.
+ * @param {(string|{reportUnusedDisableDirectives: boolean, filename: string, allowInlineConfig: boolean})} providedOptions Options
+ * @returns {{reportUnusedDisableDirectives: boolean, filename: string, allowInlineConfig: boolean}} Normalized options
+ */
+function normalizeVerifyOptions(providedOptions) {
+ const isObjectOptions = typeof providedOptions === "object";
+ const providedFilename = isObjectOptions ? providedOptions.filename : providedOptions;
+
+ return {
+ filename: typeof providedFilename === "string" ? providedFilename : "",
+ allowInlineConfig: !isObjectOptions || providedOptions.allowInlineConfig !== false,
+ reportUnusedDisableDirectives: isObjectOptions && !!providedOptions.reportUnusedDisableDirectives
+ };
+}
+
+/**
+ * Combines the provided parserOptions with the options from environments
+ * @param {Object} providedOptions The provided 'parserOptions' key in a config
+ * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments
+ * @returns {Object} Resulting parser options after merge
+ */
+function resolveParserOptions(providedOptions, enabledEnvironments) {
+ const parserOptionsFromEnv = enabledEnvironments
+ .filter(env => env.parserOptions)
+ .reduce((parserOptions, env) => ConfigOps.merge(parserOptions, env.parserOptions), {});
+
+ const mergedParserOptions = ConfigOps.merge(parserOptionsFromEnv, providedOptions || {});
+
+ const isModule = mergedParserOptions.sourceType === "module";
+
+ if (isModule) {
+
+ // can't have global return inside of modules
+ mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false });
+ }
+
+ mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion, isModule);
+
+ return mergedParserOptions;
+}
+
+/**
+ * Combines the provided globals object with the globals from environments
+ * @param {Object} providedGlobals The 'globals' key in a config
+ * @param {Environments[]} enabledEnvironments The environments enabled in configuration and with inline comments
+ * @returns {Object} The resolved globals object
+ */
+function resolveGlobals(providedGlobals, enabledEnvironments) {
+ return Object.assign.apply(
+ null,
+ [{}]
+ .concat(enabledEnvironments.filter(env => env.globals).map(env => env.globals))
+ .concat(providedGlobals)
+ );
+}
+
/**
* Strips Unicode BOM from a given text.
*
@@ -664,6 +650,21 @@ function markVariableAsUsed(scopeManager, currentNode, parserOptions, name) {
return false;
}
+/**
+ * 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
+ * @returns {Object} A map of selector listeners provided by the rule
+ */
+function createRuleListeners(rule, ruleContext) {
+ try {
+ return rule.create(ruleContext);
+ } catch (ex) {
+ ex.message = `Error while loading rule '${ruleContext.id}': ${ex.message}`;
+ throw ex;
+ }
+}
+
// methods that exist on SourceCode object
const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
getSource: "getText",
@@ -702,6 +703,155 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze(
)
);
+/**
+ * Runs the given rules on the given SourceCode object
+ * @param {SourceCode} sourceCode A SourceCode object for the given text
+ * @param {Object} configuredRules The rules configuration
+ * @param {function(string): Rule} ruleMapper A mapper function from rule names to rules
+ * @param {Object} parserOptions The options that were passed to the parser
+ * @param {string} parserName The name of the parser in the config
+ * @param {Object} settings The settings that were enabled in the config
+ * @param {string} filename The reported filename of the code
+ * @returns {Problem[]} An array of reported problems
+ */
+function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename) {
+ const emitter = createEmitter();
+ const traverser = new Traverser();
+
+ /*
+ * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
+ * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
+ * properties once for each rule.
+ */
+ const sharedTraversalContext = Object.freeze(
+ Object.assign(
+ Object.create(BASE_TRAVERSAL_CONTEXT),
+ {
+ getAncestors: () => traverser.parents(),
+ getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
+ getFilename: () => filename,
+ getScope: () => getScope(sourceCode.scopeManager, traverser.current(), parserOptions.ecmaVersion),
+ getSourceCode: () => sourceCode,
+ markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, traverser.current(), parserOptions, name),
+ parserOptions,
+ parserPath: parserName,
+ parserServices: sourceCode.parserServices,
+ settings,
+
+ /**
+ * This is used to avoid breaking rules that used to monkeypatch the `Linter#report` method
+ * by using the `_linter` property on rule contexts.
+ *
+ * This should be removed in a major release after we create a better way to
+ * lint for unused disable comments.
+ * https://github.com/eslint/eslint/issues/9193
+ */
+ _linter: {
+ report() {},
+ on: emitter.on
+ }
+ }
+ )
+ );
+
+
+ const lintingProblems = [];
+
+ Object.keys(configuredRules).forEach(ruleId => {
+ const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
+
+ if (severity === 0) {
+ return;
+ }
+
+ const rule = ruleMapper(ruleId);
+ 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() {
+
+ /*
+ * Create a report translator lazily.
+ * In a vast majority of cases, any given rule reports zero errors on a given
+ * piece of code. Creating a translator lazily avoids the performance cost of
+ * creating a new translator function for each rule that usually doesn't get
+ * called.
+ *
+ * Using lazy report translators improves end-to-end performance by about 3%
+ * with Node 8.4.0.
+ */
+ if (reportTranslator === null) {
+ reportTranslator = createReportTranslator({ ruleId, severity, sourceCode, messageIds });
+ }
+ const problem = reportTranslator.apply(null, arguments);
+
+ if (problem.fix && rule.meta && !rule.meta.fixable) {
+ throw new Error("Fixable rules should export a `meta.fixable` property.");
+ }
+ lintingProblems.push(problem);
+
+ /*
+ * This is used to avoid breaking rules that used monkeypatch Linter, and relied on
+ * `linter.report` getting called with report info every time a rule reports a problem.
+ * To continue to support this, make sure that `context._linter.report` is called every
+ * time a problem is reported by a rule, even though `context._linter` is no longer a
+ * `Linter` instance.
+ *
+ * This should be removed in a major release after we create a better way to
+ * lint for unused disable comments.
+ * https://github.com/eslint/eslint/issues/9193
+ */
+ sharedTraversalContext._linter.report( // eslint-disable-line no-underscore-dangle
+ problem.ruleId,
+ problem.severity,
+ { loc: { start: { line: problem.line, column: problem.column - 1 } } },
+ problem.message
+ );
+ }
+ }
+ )
+ );
+
+ const ruleListeners = createRuleListeners(rule, ruleContext);
+
+ // add all the selectors from the rule as listeners
+ Object.keys(ruleListeners).forEach(selector => {
+ emitter.on(
+ selector,
+ timing.enabled
+ ? timing.time(ruleId, ruleListeners[selector])
+ : ruleListeners[selector]
+ );
+ });
+ });
+
+ const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
+
+ /*
+ * Each node has a type property. Whenever a particular type of
+ * node is found, an event is fired. This allows any listeners to
+ * automatically be informed that this type of node has been found
+ * and react accordingly.
+ */
+ traverser.traverse(sourceCode.ast, {
+ enter(node, parent) {
+ node.parent = parent;
+ eventGenerator.enterNode(node);
+ },
+ leave(node) {
+ eventGenerator.leaveNode(node);
+ },
+ visitorKeys: sourceCode.visitorKeys
+ });
+
+ return lintingProblems;
+}
+
const lastSourceCodes = new WeakMap();
//------------------------------------------------------------------------------
@@ -737,7 +887,7 @@ module.exports = class Linter {
/**
* Same as linter.verify, except without support for processors.
* @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object.
- * @param {ESLintConfig} config An ESLintConfig instance to configure everything.
+ * @param {ESLintConfig} providedConfig An ESLintConfig instance to configure everything.
* @param {(string|Object)} [filenameOrOptions] The optional filename of the file being checked.
* If this is not set, the filename will default to '' in the rule context. If
* an object, then it has "filename", "saveState", and "allowInlineConfig" properties.
@@ -747,22 +897,12 @@ module.exports = class Linter {
* eslint-disable directives
* @returns {Object[]} The results as an array of messages or null if no messages.
*/
- _verifyWithoutProcessors(textOrSourceCode, config, filenameOrOptions) {
- let text,
- allowInlineConfig,
- providedFilename,
- reportUnusedDisableDirectives;
+ _verifyWithoutProcessors(textOrSourceCode, providedConfig, filenameOrOptions) {
+ const config = providedConfig || {};
+ const options = normalizeVerifyOptions(filenameOrOptions);
+ let text;
// evaluate arguments
- if (typeof filenameOrOptions === "object") {
- providedFilename = filenameOrOptions.filename;
- allowInlineConfig = filenameOrOptions.allowInlineConfig !== false;
- reportUnusedDisableDirectives = filenameOrOptions.reportUnusedDisableDirectives;
- } else {
- providedFilename = filenameOrOptions;
- allowInlineConfig = true;
- }
-
if (typeof textOrSourceCode === "string") {
lastSourceCodes.set(this, null);
text = textOrSourceCode;
@@ -771,23 +911,18 @@ module.exports = class Linter {
text = textOrSourceCode.text;
}
- const filename = typeof providedFilename === "string" ? providedFilename : "";
-
// search and apply "eslint-env *".
const envInFile = findEslintEnv(text);
+ const resolvedEnvConfig = Object.assign({ builtin: true }, config.env, envInFile);
+ const enabledEnvs = Object.keys(resolvedEnvConfig)
+ .filter(envName => resolvedEnvConfig[envName])
+ .map(envName => this.environments.get(envName))
+ .filter(env => env);
- config = Object.assign({}, config);
-
- if (envInFile) {
- if (config.env) {
- config.env = Object.assign({}, config.env, envInFile);
- } else {
- config.env = envInFile;
- }
- }
-
- // process initial config to make it safe to extend
- config = populateConfig(config, this.environments);
+ const parserOptions = resolveParserOptions(config.parserOptions || {}, enabledEnvs);
+ const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs);
+ const parserName = config.parser || DEFAULT_PARSER_NAME;
+ const settings = config.settings || {};
if (!lastSourceCodes.get(this)) {
@@ -800,7 +935,7 @@ module.exports = class Linter {
let parser;
try {
- parser = this._parsers.get(config.parser) || require(config.parser);
+ parser = this._parsers.get(parserName) || require(parserName);
} catch (ex) {
return [{
ruleId: null,
@@ -814,9 +949,9 @@ module.exports = class Linter {
}
const parseResult = parse(
text,
- config.parserOptions,
+ parserOptions,
parser,
- filename
+ options.filename
);
if (!parseResult.success) {
@@ -838,171 +973,41 @@ module.exports = class Linter {
ast: lastSourceCode.ast,
parserServices: lastSourceCode.parserServices,
visitorKeys: lastSourceCode.visitorKeys,
- scopeManager: analyzeScope(lastSourceCode.ast, config.parserOptions)
+ scopeManager: analyzeScope(lastSourceCode.ast, parserOptions)
}));
}
}
const sourceCode = lastSourceCodes.get(this);
- const commentDirectives = allowInlineConfig
- ? getDirectiveComments(filename, sourceCode.ast, ruleId => this.rules.get(ruleId))
+ const commentDirectives = options.allowInlineConfig
+ ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => this.rules.get(ruleId))
: { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] };
- const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
-
// augment global scope with declared global variables
addDeclaredGlobals(
sourceCode.scopeManager.scopes[0],
- config,
- { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals },
- this.environments
+ configuredGlobals,
+ { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals }
);
- const emitter = createEmitter();
- const traverser = new Traverser();
+ const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules);
- /*
- * Create a frozen object with the ruleContext properties and methods that are shared by all rules.
- * All rule contexts will inherit from this object. This avoids the performance penalty of copying all the
- * properties once for each rule.
- */
- const sharedTraversalContext = Object.freeze(
- Object.assign(
- Object.create(BASE_TRAVERSAL_CONTEXT),
- {
- getAncestors: () => traverser.parents(),
- getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager),
- getFilename: () => filename,
- getScope: () => getScope(sourceCode.scopeManager, traverser.current(), config.parserOptions.ecmaVersion),
- getSourceCode: () => sourceCode,
- markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, traverser.current(), config.parserOptions, name),
- parserOptions: config.parserOptions,
- parserPath: config.parser,
- parserServices: sourceCode.parserServices,
- settings: config.settings,
-
- /**
- * This is used to avoid breaking rules that used to monkeypatch the `Linter#report` method
- * by using the `_linter` property on rule contexts.
- *
- * This should be removed in a major release after we create a better way to
- * lint for unused disable comments.
- * https://github.com/eslint/eslint/issues/9193
- */
- _linter: {
- report() {},
- on: emitter.on
- }
- }
- )
+ const lintingProblems = runRules(
+ sourceCode,
+ configuredRules,
+ ruleId => this.rules.get(ruleId),
+ parserOptions,
+ parserName,
+ settings,
+ options.filename
);
- const lintingProblems = [];
-
- // enable appropriate rules
- Object.keys(configuredRules).forEach(ruleId => {
- const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);
-
- if (severity === 0) {
- return;
- }
-
- const rule = this.rules.get(ruleId);
- const messageIds = rule && rule.meta && rule.meta.messages;
- let reportTranslator = null;
- const ruleContext = Object.freeze(
- Object.assign(
- Object.create(sharedTraversalContext),
- {
- id: ruleId,
- options: getRuleOptions(configuredRules[ruleId]),
- report() {
-
- /*
- * Create a report translator lazily.
- * In a vast majority of cases, any given rule reports zero errors on a given
- * piece of code. Creating a translator lazily avoids the performance cost of
- * creating a new translator function for each rule that usually doesn't get
- * called.
- *
- * Using lazy report translators improves end-to-end performance by about 3%
- * with Node 8.4.0.
- */
- if (reportTranslator === null) {
- reportTranslator = createReportTranslator({ ruleId, severity, sourceCode, messageIds });
- }
- const problem = reportTranslator.apply(null, arguments);
-
- if (problem.fix && rule.meta && !rule.meta.fixable) {
- throw new Error("Fixable rules should export a `meta.fixable` property.");
- }
- lintingProblems.push(problem);
-
- /*
- * This is used to avoid breaking rules that used monkeypatch Linter, and relied on
- * `linter.report` getting called with report info every time a rule reports a problem.
- * To continue to support this, make sure that `context._linter.report` is called every
- * time a problem is reported by a rule, even though `context._linter` is no longer a
- * `Linter` instance.
- *
- * This should be removed in a major release after we create a better way to
- * lint for unused disable comments.
- * https://github.com/eslint/eslint/issues/9193
- */
- sharedTraversalContext._linter.report( // eslint-disable-line no-underscore-dangle
- problem.ruleId,
- problem.severity,
- { loc: { start: { line: problem.line, column: problem.column - 1 } } },
- problem.message
- );
- }
- }
- )
- );
-
- try {
- const ruleListeners = rule.create(ruleContext);
-
- // add all the selectors from the rule as listeners
- Object.keys(ruleListeners).forEach(selector => {
- emitter.on(
- selector,
- timing.enabled
- ? timing.time(ruleId, ruleListeners[selector])
- : ruleListeners[selector]
- );
- });
- } catch (ex) {
- ex.message = `Error while loading rule '${ruleId}': ${ex.message}`;
- throw ex;
- }
- });
-
- const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));
-
- /*
- * Each node has a type property. Whenever a particular type of
- * node is found, an event is fired. This allows any listeners to
- * automatically be informed that this type of node has been found
- * and react accordingly.
- */
- traverser.traverse(sourceCode.ast, {
- enter(node, parent) {
- node.parent = parent;
- eventGenerator.enterNode(node);
- },
- leave(node) {
- eventGenerator.leaveNode(node);
- },
- visitorKeys: sourceCode.visitorKeys
- });
-
return applyDisableDirectives({
directives: commentDirectives.disableDirectives,
problems: lintingProblems
.concat(commentDirectives.problems)
.sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column),
- reportUnusedDisableDirectives
+ reportUnusedDisableDirectives: options.reportUnusedDisableDirectives
});
}
diff --git a/tests/lib/linter.js b/tests/lib/linter.js
index 5f42ffd9c9c..7c3ac678afb 100644
--- a/tests/lib/linter.js
+++ b/tests/lib/linter.js
@@ -2790,16 +2790,6 @@ describe("Linter", () => {
});
});
- describe("when using invalid rule config", () => {
- const code = TEST_CODE;
-
- it("should throw an error", () => {
- assert.throws(() => {
- linter.verify(code, { rules: { foobar: null } });
- }, /Invalid config for rule 'foobar'\./);
- });
- });
-
describe("when calling getRules", () => {
it("should return all loaded rules", () => {
const rules = linter.getRules();