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();