From ea25be21ee4350674a33000472215095cfa1a4ec Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Tue, 9 Apr 2019 18:33:50 +0900 Subject: [PATCH 01/49] Breaking: fix config loading (fixes #11510, fixes #11559, fixes #11586) --- Makefile.js | 4 +- conf/environments.js | 5 +- conf/eslint-all.js | 14 +- conf/eslint-recommended.js | 17 +- lib/api.js | 4 +- lib/built-in-rules-index.js | 5 +- lib/cli-engine.js | 821 ++++--- .../cascading-config-array-factory.js | 393 +++ lib/cli-engine/config-array-factory.js | 896 +++++++ lib/cli-engine/config-array/config-array.js | 445 ++++ .../config-array/config-dependency.js | 104 + .../config-array/extracted-config.js | 98 + lib/cli-engine/config-array/index.js | 18 + .../config-array/override-tester.js | 177 ++ lib/cli-engine/file-enumerator.js | 459 ++++ lib/cli.js | 2 +- lib/config.js | 388 --- lib/config/autoconfig.js | 2 +- lib/config/config-cache.js | 130 - lib/config/config-file.js | 464 +--- lib/config/config-initializer.js | 38 +- lib/config/config-ops.js | 271 -- lib/config/config-rule.js | 9 +- lib/config/config-validator.js | 78 +- lib/config/environments.js | 84 - lib/config/plugins.js | 175 -- lib/linter.js | 336 ++- lib/load-rules.js | 4 +- lib/rules.js | 33 +- lib/testers/rule-tester.js | 12 +- lib/util/file-finder.js | 144 -- lib/util/glob-utils.js | 285 --- lib/util/glob.js | 63 - lib/util/ignored-paths.js | 70 +- lib/util/lint-result-cache.js | 36 +- lib/util/naming.js | 16 +- lib/util/path-utils.js | 72 - lib/util/relative-module-resolver.js | 31 +- lib/util/report-translator.js | 4 +- lib/util/source-code-utils.js | 53 +- lib/util/types.js | 126 + messages/file-not-found.txt | 2 +- messages/no-config-found.txt | 2 +- messages/plugin-missing.txt | 2 +- package.json | 6 +- tests/bench/bench.js | 2 +- .../config-file/extends-chain-2/parser.js | 5 + .../config-file/js/node_modules/foo/index.js | 2 +- .../plugins-with-prefix-and-namespace.json | 2 +- ...plugins-without-prefix-with-namespace.json | 2 +- tests/lib/cli-engine.js | 335 ++- tests/lib/cli-engine/_utils.js | 507 ++++ .../cascading-config-array-factory.js | 1224 +++++++++ tests/lib/cli-engine/config-array-factory.js | 2185 +++++++++++++++++ .../cli-engine/config-array/config-array.js | 724 ++++++ .../config-array/config-dependency.js | 92 + .../config-array/extracted-config.js | 139 ++ .../config-array/override-tester.js | 254 ++ tests/lib/cli-engine/file-enumerator.js | 454 ++++ tests/lib/cli.js | 30 +- .../code-path-analysis/code-path-analyzer.js | 2 +- tests/lib/code-path-analysis/code-path.js | 2 +- tests/lib/config.js | 1373 ----------- tests/lib/config/config-file.js | 768 +----- tests/lib/config/config-initializer.js | 30 +- tests/lib/config/config-ops.js | 678 ----- tests/lib/config/config-rule.js | 4 +- tests/lib/config/config-validator.js | 102 +- tests/lib/config/environments.js | 69 - tests/lib/config/plugins.js | 238 -- tests/lib/linter.js | 15 +- tests/lib/load-rules.js | 2 +- tests/lib/rules.js | 7 +- tests/lib/util/ast-utils.js | 2 +- tests/lib/util/file-finder.js | 177 -- tests/lib/util/glob-utils.js | 414 ---- tests/lib/util/ignored-paths.js | 150 +- tests/lib/util/lint-result-cache.js | 47 +- tests/lib/util/npm-utils.js | 67 +- tests/lib/util/path-utils.js | 93 - tests/lib/util/source-code-utils.js | 14 +- tests/lib/util/source-code.js | 2 +- 82 files changed, 9695 insertions(+), 6916 deletions(-) create mode 100644 lib/cli-engine/cascading-config-array-factory.js create mode 100644 lib/cli-engine/config-array-factory.js create mode 100644 lib/cli-engine/config-array/config-array.js create mode 100644 lib/cli-engine/config-array/config-dependency.js create mode 100644 lib/cli-engine/config-array/extracted-config.js create mode 100644 lib/cli-engine/config-array/index.js create mode 100644 lib/cli-engine/config-array/override-tester.js create mode 100644 lib/cli-engine/file-enumerator.js delete mode 100644 lib/config.js delete mode 100644 lib/config/config-cache.js delete mode 100644 lib/config/environments.js delete mode 100644 lib/config/plugins.js delete mode 100644 lib/util/file-finder.js delete mode 100644 lib/util/glob-utils.js delete mode 100644 lib/util/glob.js delete mode 100644 lib/util/path-utils.js create mode 100644 lib/util/types.js create mode 100644 tests/lib/cli-engine/_utils.js create mode 100644 tests/lib/cli-engine/cascading-config-array-factory.js create mode 100644 tests/lib/cli-engine/config-array-factory.js create mode 100644 tests/lib/cli-engine/config-array/config-array.js create mode 100644 tests/lib/cli-engine/config-array/config-dependency.js create mode 100644 tests/lib/cli-engine/config-array/extracted-config.js create mode 100644 tests/lib/cli-engine/config-array/override-tester.js create mode 100644 tests/lib/cli-engine/file-enumerator.js delete mode 100644 tests/lib/config.js delete mode 100644 tests/lib/config/environments.js delete mode 100644 tests/lib/config/plugins.js delete mode 100644 tests/lib/util/file-finder.js delete mode 100644 tests/lib/util/glob-utils.js delete mode 100644 tests/lib/util/path-utils.js diff --git a/Makefile.js b/Makefile.js index 08e10256418..91e93e4012c 100644 --- a/Makefile.js +++ b/Makefile.js @@ -26,7 +26,7 @@ const lodash = require("lodash"), ejs = require("ejs"), loadPerf = require("load-perf"), yaml = require("js-yaml"), - CLIEngine = require("./lib/cli-engine"); + { CLIEngine } = require("./lib/cli-engine"); const { cat, cd, cp, echo, exec, exit, find, ls, mkdir, pwd, rm, test } = require("shelljs"); @@ -871,7 +871,7 @@ target.checkRuleFiles = function() { // check parity between rules index file and rules directory const builtInRulesIndexPath = "./lib/built-in-rules-index"; const ruleIdsInIndex = require(builtInRulesIndexPath); - const ruleEntryFromIndexIsMissing = !(basename in ruleIdsInIndex); + const ruleEntryFromIndexIsMissing = !ruleIdsInIndex.has(basename); if (ruleEntryFromIndexIsMissing) { console.error(`Missing rule from index (${builtInRulesIndexPath}.js): ${basename}. If you just added a ` + diff --git a/conf/environments.js b/conf/environments.js index 1c2b12eed31..f404d0e1435 100644 --- a/conf/environments.js +++ b/conf/environments.js @@ -14,7 +14,8 @@ const globals = require("globals"); // Public Interface //------------------------------------------------------------------------------ -module.exports = { +/** @type {Map} */ +module.exports = new Map(Object.entries({ builtin: { globals: globals.es5 }, @@ -106,4 +107,4 @@ module.exports = { greasemonkey: { globals: globals.greasemonkey } -}; +})); diff --git a/conf/eslint-all.js b/conf/eslint-all.js index 3850fcea3ab..9df71073dbe 100644 --- a/conf/eslint-all.js +++ b/conf/eslint-all.js @@ -15,15 +15,17 @@ const builtInRules = require("../lib/built-in-rules-index"); // Helpers //------------------------------------------------------------------------------ -const enabledRules = Object.keys(builtInRules).reduce((result, ruleId) => { - if (!builtInRules[ruleId].meta.deprecated) { - result[ruleId] = "error"; +const allRules = {}; + +for (const [ruleId, rule] of builtInRules) { + if (!rule.meta.deprecated) { + allRules[ruleId] = "error"; } - return result; -}, {}); +} //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ -module.exports = { rules: enabledRules }; +/** @type {import("../lib/util/types").ConfigData} */ +module.exports = { rules: allRules }; diff --git a/conf/eslint-recommended.js b/conf/eslint-recommended.js index d6368000fc9..9886d19d0f8 100644 --- a/conf/eslint-recommended.js +++ b/conf/eslint-recommended.js @@ -7,12 +7,13 @@ "use strict"; const builtInRules = require("../lib/built-in-rules-index"); +const recommendedRules = {}; -module.exports = { - rules: Object.assign( - {}, - ...Object.keys(builtInRules) - .filter(ruleId => builtInRules[ruleId].meta.docs.recommended) - .map(ruleId => ({ [ruleId]: "error" })) - ) -}; +for (const [ruleId, rule] of builtInRules) { + if (rule.meta.docs.recommended) { + recommendedRules[ruleId] = "error"; + } +} + +/** @type {import("../lib/util/types").ConfigData} */ +module.exports = { rules: recommendedRules }; diff --git a/lib/api.js b/lib/api.js index 91dae3c7cbb..b521e5c8d25 100644 --- a/lib/api.js +++ b/lib/api.js @@ -5,11 +5,11 @@ "use strict"; -const Linter = require("./linter"); +const { Linter } = require("./linter"); module.exports = { Linter, - CLIEngine: require("./cli-engine"), + CLIEngine: require("./cli-engine").CLIEngine, RuleTester: require("./testers/rule-tester"), SourceCode: require("./util/source-code") }; diff --git a/lib/built-in-rules-index.js b/lib/built-in-rules-index.js index d75fbbc698d..6895c8c3d4d 100644 --- a/lib/built-in-rules-index.js +++ b/lib/built-in-rules-index.js @@ -8,7 +8,8 @@ /* eslint sort-keys: ["error", "asc"] */ -module.exports = { +/** @type {Map} */ +module.exports = new Map(Object.entries({ "accessor-pairs": require("./rules/accessor-pairs"), "array-bracket-newline": require("./rules/array-bracket-newline"), "array-bracket-spacing": require("./rules/array-bracket-spacing"), @@ -275,4 +276,4 @@ module.exports = { "wrap-regex": require("./rules/wrap-regex"), "yield-star-spacing": require("./rules/yield-star-spacing"), yoda: require("./rules/yoda") -}; +})); diff --git a/lib/cli-engine.js b/lib/cli-engine.js index 1f4020338f2..65bbe96b220 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -15,22 +15,21 @@ // Requirements //------------------------------------------------------------------------------ -const fs = require("fs"), - path = require("path"), - defaultOptions = require("../conf/default-cli-options"), - Linter = require("./linter"), - lodash = require("lodash"), - IgnoredPaths = require("./util/ignored-paths"), - Config = require("./config"), - ConfigOps = require("./config/config-ops"), - LintResultCache = require("./util/lint-result-cache"), - globUtils = require("./util/glob-utils"), - validator = require("./config/config-validator"), - hash = require("./util/hash"), - relativeModuleResolver = require("./util/relative-module-resolver"), - naming = require("./util/naming"), - pkg = require("../package.json"), - loadRules = require("./load-rules"); +const fs = require("fs"); +const path = require("path"); +const defaultOptions = require("../conf/default-cli-options"); +const pkg = require("../package.json"); +const { CascadingConfigArrayFactory } = require("./cli-engine/cascading-config-array-factory"); +const { getUsedExtractedConfigs } = require("./cli-engine/config-array"); +const { FileEnumerator } = require("./cli-engine/file-enumerator"); +const ConfigOps = require("./config/config-ops"); +const hash = require("./util/hash"); +const { IgnoredPaths } = require("./util/ignored-paths"); +const LintResultCache = require("./util/lint-result-cache"); +const naming = require("./util/naming"); +const ModuleResolver = require("./util/relative-module-resolver"); +const builtInRules = require("./built-in-rules-index"); +const { Linter } = require("./linter"); const debug = require("debug")("eslint:cli-engine"); const validFixTypes = new Set(["problem", "suggestion", "layout"]); @@ -39,11 +38,21 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); // Typedefs //------------------------------------------------------------------------------ +// For VSCode IntelliSense +/** @typedef {import("./util/types").ConfigData} ConfigData */ +/** @typedef {import("./util/types").LintMessage} LintMessage */ +/** @typedef {import("./util/types").ParserOptions} ParserOptions */ +/** @typedef {import("./util/types").Plugin} Plugin */ +/** @typedef {import("./util/types").RuleConf} RuleConf */ +/** @typedef {import("./util/types").Rule} Rule */ +/** @typedef {ReturnType} ConfigArray */ +/** @typedef {ReturnType} ExtractedConfig */ + /** * The options to configure a CLI engine with. * @typedef {Object} CLIEngineOptions * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. - * @property {Object} baseConfig Base config object, extended by all configs used with this CLIEngine instance + * @property {ConfigData} baseConfig Base config object, extended by all configs used with this CLIEngine instance * @property {boolean} cache Enable result caching. * @property {string} cacheLocation The cache file to use instead of .eslintcache. * @property {string} configFile The configuration file to use. @@ -58,17 +67,12 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @property {string} ignorePattern A glob pattern of files to ignore. * @property {boolean} useEslintrc False disables looking for .eslintrc * @property {string} parser The name of the parser to use. - * @property {Object} parserOptions An object of parserOption settings to use. + * @property {ParserOptions} parserOptions An object of parserOption settings to use. * @property {string[]} plugins An array of plugins to load. - * @property {Object} rules An object of rules to use. + * @property {Record} rules An object of rules to use. * @property {string[]} rulePaths An array of directories to load custom rules from. * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives - */ - -/** - * A linting warning or error. - * @typedef {Object} LintMessage - * @property {string} message The message to display to the user. + * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. */ /** @@ -84,10 +88,45 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @property {string=} [output] The source code of the file that was linted, with as many fixes applied as possible. */ +/** + * Information of deprecated rules. + * @typedef {Object} DeprecatedRuleInfo + * @property {string} ruleId The rule ID. + * @property {string[]} replacedBy The rule IDs that replace this deprecated rule. + */ + +/** + * Linting results. + * @typedef {Object} LintReport + * @property {LintResult[]} results All of the result. + * @property {number} errorCount Number of errors for the result. + * @property {number} warningCount Number of warnings for the result. + * @property {number} fixableErrorCount Number of fixable errors for the result. + * @property {number} fixableWarningCount Number of fixable warnings for the result. + * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. + */ + +/** + * Private data for CLIEngine. + * @typedef {Object} CLIEngineInternalSlots + * @property {Map} additionalPluginPool The map for additional plugins. + * @property {string} cacheFilePath The path to the cache of lint results. + * @property {CascadingConfigArrayFactory} configArrayFactory The factory of configs. + * @property {FileEnumerator} fileEnumerator The file enumerator. + * @property {IgnoredPaths} ignoredPaths The ignored paths. + * @property {ConfigArray[]} lastConfigArrays The list of config arrays that the last `executeOnFiles` or `executeOnText` used. + * @property {LintResultCache|null} lintResultCache The cache of lint results. + * @property {Linter} linter The linter instance which has loaded rules. + * @property {CLIEngineOptions} options The normalized options of this instance. + */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +/** @type {WeakMap} */ +const internalSlotsMap = new WeakMap(); + /** * Determines if each fix type in an array is supported by ESLint and throws * an error if not. @@ -105,7 +144,7 @@ function validateFixTypes(fixTypes) { /** * It will calculate the error and warning count for collection of messages per file - * @param {Object[]} messages - Collection of messages + * @param {LintMessage[]} messages - Collection of messages * @returns {Object} Contains the stats * @private */ @@ -133,7 +172,7 @@ function calculateStatsPerFile(messages) { /** * It will calculate the error and warning count for collection of results from all files - * @param {Object[]} results - Collection of messages from all the files + * @param {LintResult[]} results - Collection of messages from all the files * @returns {Object} Contains the stats * @private */ @@ -154,105 +193,62 @@ function calculateStatsPerRun(results) { /** * Processes an source code using ESLint. - * @param {string} text The source code to check. - * @param {Object} configHelper The configuration options for ESLint. - * @param {string} filename An optional string representing the texts filename. - * @param {boolean|Function} fix Indicates if fixes should be processed. - * @param {boolean} allowInlineConfig Allow/ignore comments that change config. - * @param {boolean} reportUnusedDisableDirectives Allow/ignore comments that change config. - * @param {Linter} linter Linter context - * @returns {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it. + * @param {string} text The source code to verify. + * @param {string} filePath The path to the file of `text`. + * @param {ConfigArray} config The config. + * @param {boolean} fix If `true` then it does fix. + * @param {boolean} allowInlineConfig If `true` then it uses directive comments. + * @param {boolean} reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments. + * @param {Linter} linter The linter instance to verify. + * @returns {LintResult} The result of linting. * @private */ -function processText(text, configHelper, filename, fix, allowInlineConfig, reportUnusedDisableDirectives, linter) { - let filePath, - fileExtension, - processor; - - if (filename) { - filePath = path.resolve(filename); - fileExtension = path.extname(filename); - } - - const effectiveFilename = filename || ""; - - debug(`Linting ${effectiveFilename}`); - const config = configHelper.getConfig(filePath); - - if (config.plugins) { - configHelper.plugins.loadAll(config.plugins); - } - - if (config.parser) { - if (!path.isAbsolute(config.parser)) { - throw new Error(`Expected parser to be an absolute path but found ${config.parser}. This is a bug.`); - } - linter.defineParser(config.parser, require(config.parser)); - } - - const loadedPlugins = configHelper.plugins.getAll(); - - for (const plugin in loadedPlugins) { - if (loadedPlugins[plugin].processors && Object.keys(loadedPlugins[plugin].processors).indexOf(fileExtension) >= 0) { - processor = loadedPlugins[plugin].processors[fileExtension]; - break; +function verifyText( + text, + filePath, + config, + fix, + allowInlineConfig, + reportUnusedDisableDirectives, + linter +) { + debug(`Lint ${filePath}`); + + // Verify. + const { fixed, messages, output } = linter.verifyAndFix( + text, + config, + { + allowInlineConfig, + filename: filePath, + fix, + reportUnusedDisableDirectives } - } + ); - const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix); - const fixedResult = linter.verifyAndFix(text, config, { - filename: effectiveFilename, - allowInlineConfig, - reportUnusedDisableDirectives, - fix: !!autofixingEnabled && fix, - preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)), - postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename)) - }); - const stats = calculateStatsPerFile(fixedResult.messages); + const basename = path.basename(filePath, path.extname(filePath)); + const resultFilePath = basename.startsWith("<") && basename.endsWith(">") + ? basename + : filePath; + // Tweak and return. const result = { - filePath: effectiveFilename, - messages: fixedResult.messages, - errorCount: stats.errorCount, - warningCount: stats.warningCount, - fixableErrorCount: stats.fixableErrorCount, - fixableWarningCount: stats.fixableWarningCount + filePath: resultFilePath, + messages, + ...calculateStatsPerFile(messages) }; - if (fixedResult.fixed) { - result.output = fixedResult.output; + if (fixed) { + result.output = output; } - - if (result.errorCount + result.warningCount > 0 && typeof result.output === "undefined") { + if ( + result.errorCount + result.warningCount > 0 && + typeof result.output === "undefined" + ) { result.source = text; } - return { result, config }; -} - -/** - * Processes an individual file using ESLint. Files used here are known to - * exist, so no need to check that here. - * @param {string} filename The filename of the file being checked. - * @param {Object} configHelper The configuration options for ESLint. - * @param {Object} options The CLIEngine options object. - * @param {Linter} linter Linter context - * @returns {{rules: LintResult, config: Object}} The results for linting on this text and the fully-resolved config for it. - * @private - */ -function processFile(filename, configHelper, options, linter) { - - const text = fs.readFileSync(path.resolve(filename), "utf8"); - - return processText( - text, - configHelper, - filename, - options.fix, - options.allowInlineConfig, - options.reportUnusedDisableDirectives, - linter - ); + return result; } /** @@ -295,36 +291,70 @@ function createIgnoreResult(filePath, baseDir) { } /** - * Produces rule warnings (i.e. deprecation) from configured rules - * @param {(Array|Set)} usedRules - Rules configured - * @param {Map} loadedRules - Map of loaded rules - * @returns {Array} Contains rule warnings - * @private + * Get a rule. + * @param {string} ruleId The rule ID to get. + * @param {ConfigArray[]} configArrays The config arrays that have plugin rules. + * @returns {Rule|null} The rule or null. + */ +function getRule(ruleId, configArrays) { + for (const configArray of configArrays) { + const rule = configArray.pluginRules.get(ruleId); + + if (rule) { + return rule; + } + } + return builtInRules.get(ruleId) || null; +} + +/** + * Collect used deprecated rules. + * @param {ConfigArray[]} usedConfigArrays The config arrays which were used. + * @param {Map} ruleMap The rule definitions which were used (built-ins). + * @returns {IterableIterator} Used deprecated rules. */ -function createRuleDeprecationWarnings(usedRules, loadedRules) { - const usedDeprecatedRules = []; +function *iterateRuleDeprecationWarnings(usedConfigArrays) { + const processedRuleIds = new Set(); - usedRules.forEach(name => { - const loadedRule = loadedRules.get(name); + // Flatten used configs. + /** @type {ExtractedConfig[]} */ + const configs = [].concat( + ...usedConfigArrays.map(getUsedExtractedConfigs) + ); - if (loadedRule && loadedRule.meta && loadedRule.meta.deprecated) { - const deprecatedRule = { ruleId: name }; - const replacedBy = lodash.get(loadedRule, "meta.replacedBy", []); + // Traverse rule configs. + for (const config of configs) { + for (const [ruleId, ruleConfig] of Object.entries(config.rules)) { - if (replacedBy.every(newRule => lodash.isString(newRule))) { - deprecatedRule.replacedBy = replacedBy; + // Skip if it was processed. + if (processedRuleIds.has(ruleId)) { + continue; } + processedRuleIds.add(ruleId); - usedDeprecatedRules.push(deprecatedRule); - } - }); + // Skip if it's not used. + if (!ConfigOps.getRuleSeverity(ruleConfig)) { + continue; + } + const rule = getRule(ruleId, usedConfigArrays); + + // Skip if it's not deprecated. + if (!(rule && rule.meta && rule.meta.deprecated)) { + continue; + } - return usedDeprecatedRules; + // This rule was used and deprecated. + yield { + ruleId, + replacedBy: rule.meta.replacedBy || [] + }; + } + } } /** * Checks if the given message is an error message. - * @param {Object} message The message to check. + * @param {LintMessage} message The message to check. * @returns {boolean} Whether or not the message is an error message. * @private */ @@ -406,6 +436,72 @@ function getCacheFile(cacheFile, cwd) { return resolvedCacheFile; } +/** + * Convert a string array to a boolean map. + * @param {string[]|null} keys The keys to assign true. + * @param {boolean} defaultValue The default value for each property. + * @param {string} displayName The property name which is used in error message. + * @returns {Record} The boolean map. + */ +function toBooleanMap(keys, defaultValue, displayName) { + if (keys && !Array.isArray(keys)) { + throw new Error(`${displayName} must be an array.`); + } + if (keys && keys.length > 0) { + return keys.reduce((map, def) => { + const [key, value] = def.split(":"); + + if (key !== "__proto__") { + map[key] = value === void 0 + ? defaultValue + : value === "true"; + } + + return map; + }, {}); + } + return void 0; +} + +/** + * Create a config data from CLI options. + * @param {CLIEngineOptions} options The options + * @returns {ConfigData|null} The created config data. + */ +function createConfigDataFromOptions(options) { + const { parser, parserOptions, plugins, rules } = options; + const env = toBooleanMap(options.envs, true, "envs"); + const globals = toBooleanMap(options.globals, false, "globals"); + + if ( + env === void 0 && + globals === void 0 && + parser === void 0 && + parserOptions === void 0 && + plugins === void 0 && + rules === void 0 + ) { + return null; + } + return { env, globals, parser, parserOptions, plugins, rules }; +} + +/** + * Checks whether a directory exists at the given location + * @param {string} resolvedPath A path from the CWD + * @returns {boolean} `true` if a directory exists + */ +function directoryExists(resolvedPath) { + try { + return fs.statSync(resolvedPath).isDirectory(); + } catch (error) { + if (error && error.code === "ENOENT") { + return false; + } + throw error; + } +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -418,7 +514,6 @@ class CLIEngine { * @constructor */ constructor(providedOptions) { - const options = Object.assign( Object.create(null), defaultOptions, @@ -426,108 +521,86 @@ class CLIEngine { providedOptions ); - /* - * if an --ignore-path option is provided, ensure that the ignore - * file exists and is not a directory - */ - if (options.ignore && options.ignorePath) { - try { - if (!fs.statSync(options.ignorePath).isFile()) { - throw new Error(`${options.ignorePath} is not a file`); - } - } catch (e) { - e.message = `Error: Could not load file ${options.ignorePath}\nError: ${e.message}`; - throw e; - } - } - - /** - * Stored options for this instance - * @type {Object} - */ - this.options = options; - this.linter = new Linter(); - - // load in additional rules - if (this.options.rulePaths) { - const cwd = this.options.cwd; - - this.options.rulePaths.forEach(rulesdir => { - debug(`Loading rules from ${rulesdir}`); - this.linter.defineRules(loadRules(rulesdir, cwd)); - }); - } - - if (this.options.rules && Object.keys(this.options.rules).length) { - const loadedRules = this.linter.getRules(); - - // Ajv validator with default schema will mutate original object, so we must clone it recursively. - this.options.rules = lodash.cloneDeep(this.options.rules); - - Object.keys(this.options.rules).forEach(name => { - validator.validateRuleOptions(loadedRules.get(name), name, this.options.rules[name], "CLI"); - }); + if (options.fix === void 0) { + options.fix = false; } - this.config = new Config( - { - cwd: this.options.cwd, - baseConfig: this.options.baseConfig, - rules: this.options.rules, - ignore: this.options.ignore, - ignorePath: this.options.ignorePath, - parser: this.options.parser, - parserOptions: this.options.parserOptions, - useEslintrc: this.options.useEslintrc, - envs: this.options.envs, - globals: this.options.globals, - configFile: this.options.configFile, - plugins: this.options.plugins - }, - this.linter + const additionalPluginPool = new Map(); + const cacheFilePath = getCacheFile( + options.cacheLocation || options.cacheFile, + options.cwd ); - - if (this.options.cache) { - const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd); - - /** - * Cache used to avoid operating on files that haven't changed since the - * last successful execution. - * @type {Object} - */ - this._lintResultCache = new LintResultCache(cacheFile, this.config); - } + const configArrayFactory = new CascadingConfigArrayFactory({ + additionalPluginPool, + baseConfig: options.baseConfig || null, + cliConfig: createConfigDataFromOptions(options), + cwd: options.cwd, + rulePaths: options.rulePaths, + specificConfigPath: options.configFile, + useEslintrc: options.useEslintrc + }); + const ignoredPaths = new IgnoredPaths(options); + const fileEnumerator = new FileEnumerator({ + configArrayFactory, + cwd: options.cwd, + extensions: options.extensions, + globInputPaths: options.globInputPaths, + ignore: options.ignore, + ignoredPaths + }); + const lintResultCache = + options.cache ? new LintResultCache(cacheFilePath) : null; + const linter = new Linter(); + + /** @type {ConfigArray[]} */ + const lastConfigArrays = []; + + // Store private data. + internalSlotsMap.set(this, { + additionalPluginPool, + cacheFilePath, + configArrayFactory, + fileEnumerator, + ignoredPaths, + lastConfigArrays, + lintResultCache, + linter, + options + }); // setup special filter for fixes - if (this.options.fix && this.options.fixTypes && this.options.fixTypes.length > 0) { - - debug(`Using fix types ${this.options.fixTypes}`); + if (options.fix && options.fixTypes && options.fixTypes.length > 0) { + debug(`Using fix types ${options.fixTypes}`); // throw an error if any invalid fix types are found - validateFixTypes(this.options.fixTypes); + validateFixTypes(options.fixTypes); // convert to Set for faster lookup - const fixTypes = new Set(this.options.fixTypes); + const fixTypes = new Set(options.fixTypes); // save original value of options.fix in case it's a function - const originalFix = (typeof this.options.fix === "function") - ? this.options.fix : () => this.options.fix; - - // create a cache of rules (but don't populate until needed) - this._rulesCache = null; + const originalFix = (typeof options.fix === "function") + ? options.fix : () => true; - this.options.fix = lintResult => { - const rule = this._rulesCache.get(lintResult.ruleId); - const matches = rule.meta && fixTypes.has(rule.meta.type); + options.fix = message => { + const rule = message.ruleId && getRule(message.ruleId, lastConfigArrays); + const matches = rule && rule.meta && fixTypes.has(rule.meta.type); - return matches && originalFix(lintResult); + return matches && originalFix(message); }; } - } getRules() { - return this.linter.getRules(); + const { lastConfigArrays } = internalSlotsMap.get(this); + + return new Map(function *() { + yield* builtInRules; + + for (const configArray of lastConfigArrays) { + yield* configArray.pluginRules; + } + }()); } /** @@ -542,16 +615,14 @@ class CLIEngine { const filteredMessages = result.messages.filter(isErrorMessage); if (filteredMessages.length > 0) { - filtered.push( - { - ...result, - messages: filteredMessages, - errorCount: filteredMessages.length, - warningCount: 0, - fixableErrorCount: result.fixableErrorCount, - fixableWarningCount: 0 - } - ); + filtered.push({ + ...result, + messages: filteredMessages, + errorCount: filteredMessages.length, + warningCount: 0, + fixableErrorCount: result.fixableErrorCount, + fixableWarningCount: 0 + }); } }); @@ -560,7 +631,7 @@ class CLIEngine { /** * Outputs fixes from the given results to files. - * @param {Object} report The report object created by CLIEngine. + * @param {LintReport} report The report object created by CLIEngine. * @returns {void} */ static outputFixes(report) { @@ -573,11 +644,17 @@ class CLIEngine { /** * Add a plugin by passing its configuration * @param {string} name Name of the plugin. - * @param {Object} pluginobject Plugin configuration object. + * @param {Plugin} pluginObject Plugin configuration object. * @returns {void} */ - addPlugin(name, pluginobject) { - this.config.plugins.define(name, pluginobject); + addPlugin(name, pluginObject) { + const { + additionalPluginPool, + configArrayFactory + } = internalSlotsMap.get(this); + + additionalPluginPool.set(name, pluginObject); + configArrayFactory.clearCache(); } /** @@ -587,92 +664,139 @@ class CLIEngine { * @returns {string[]} The equivalent glob patterns. */ resolveFileGlobPatterns(patterns) { - return globUtils.resolveFileGlobPatterns(patterns.filter(Boolean), this.options); + const { options } = internalSlotsMap.get(this); + + if (options.globInputPaths === false) { + return patterns.filter(Boolean); + } + + const extensions = options.extensions.map(ext => ext.replace(/^\./u, "")); + const dirSuffix = extensions.length === 1 + ? `/**/*.${extensions[0]}` + : `/**/*.{${extensions.join(",")}}`; + + return patterns.filter(Boolean).map(pathname => { + const resolvedPath = path.resolve(options.cwd, pathname); + const newPath = directoryExists(resolvedPath) + ? pathname.replace(/[/\\]$/u, "") + dirSuffix + : pathname; + + return path.normalize(newPath).replace(/\\/gu, "/"); + }); } /** * Executes the current configuration on an array of file and directory names. * @param {string[]} patterns An array of file and directory names. - * @returns {Object} The results for all files that were linted. + * @returns {LintReport} The results for all files that were linted. */ executeOnFiles(patterns) { - const options = this.options, - lintResultCache = this._lintResultCache, - configHelper = this.config; - const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd); + const { + cacheFilePath, + fileEnumerator, + lastConfigArrays, + lintResultCache, + linter, + options: { + allowInlineConfig, + cache, + cwd, + fix, + reportUnusedDisableDirectives + } + } = internalSlotsMap.get(this); + const results = []; + const startTime = Date.now(); - if (!options.cache && fs.existsSync(cacheFile)) { - fs.unlinkSync(cacheFile); + // Clear the last used config arrays. + lastConfigArrays.length = 0; + + // Delete cache file; should this do here? + if (!cache) { + try { + fs.unlinkSync(cacheFilePath); + } catch (error) { + if (!error || error.code !== "ENOENT") { + throw error; + } + } } - const startTime = Date.now(); - const fileList = globUtils.listFilesToProcess(patterns, options); - const allUsedRules = new Set(); - const results = fileList.map(fileInfo => { - if (fileInfo.ignored) { - return createIgnoreResult(fileInfo.filename, options.cwd); + // Iterate source code files. + for (const { config, filePath, ignored } of fileEnumerator.iterateFiles(patterns)) { + if (ignored) { + results.push(createIgnoreResult(filePath, cwd)); + continue; + } + + /* + * Store used configs for: + * - this method uses to collect used deprecated rules. + * - `getRules()` method uses to collect all loaded rules. + * - `--fix-type` option uses to get the loaded rule's meta data. + */ + if (!lastConfigArrays.includes(config)) { + lastConfigArrays.push(config); } - if (options.cache) { - const cachedLintResults = lintResultCache.getCachedLintResults(fileInfo.filename); + // Skip if there is cached result. + if (lintResultCache) { + const cachedResult = + lintResultCache.getCachedLintResults(filePath, config); - if (cachedLintResults) { - const resultHadMessages = cachedLintResults.messages && cachedLintResults.messages.length; + if (cachedResult) { + const hadMessages = + cachedResult.messages && + cachedResult.messages.length > 0; - if (resultHadMessages && options.fix) { - debug(`Reprocessing cached file to allow autofix: ${fileInfo.filename}`); + if (hadMessages && fix) { + debug(`Reprocessing cached file to allow autofix: ${filePath}`); } else { - debug(`Skipping file since it hasn't changed: ${fileInfo.filename}`); - - return cachedLintResults; + debug(`Skipping file since it hasn't changed: ${filePath}`); + results.push(cachedResult); + continue; } } } - // if there's a cache, populate it - if ("_rulesCache" in this) { - this._rulesCache = this.getRules(); - } - - debug(`Processing ${fileInfo.filename}`); - - const { result, config } = processFile(fileInfo.filename, configHelper, options, this.linter); - - Object.keys(config.rules) - .filter(ruleId => ConfigOps.getRuleSeverity(config.rules[ruleId])) - .forEach(ruleId => allUsedRules.add(ruleId)); - - return result; - }); + // Do lint. + const result = verifyText( + fs.readFileSync(filePath, "utf8"), + filePath, + config, + fix, + allowInlineConfig, + reportUnusedDisableDirectives, + linter + ); - if (options.cache) { - results.forEach(result => { + results.push(result); - /* - * Store the lint result in the LintResultCache. - * NOTE: The LintResultCache will remove the file source and any - * other properties that are difficult to serialize, and will - * hydrate those properties back in on future lint runs. - */ - lintResultCache.setCachedLintResults(result.filePath, result); - }); + /* + * Store the lint result in the LintResultCache. + * NOTE: The LintResultCache will remove the file source and any + * other properties that are difficult to serialize, and will + * hydrate those properties back in on future lint runs. + */ + if (lintResultCache) { + lintResultCache.setCachedLintResults(filePath, config, result); + } + } - // persist the cache to disk + // Persist the cache to disk. + if (lintResultCache) { lintResultCache.reconcile(); } - const stats = calculateStatsPerRun(results); - - const usedDeprecatedRules = createRuleDeprecationWarnings(allUsedRules, this.getRules()); + // Collect used deprecated rules. + const usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(lastConfigArrays) + ); debug(`Linting complete in: ${Date.now() - startTime}ms`); - return { results, - errorCount: stats.errorCount, - warningCount: stats.warningCount, - fixableErrorCount: stats.fixableErrorCount, - fixableWarningCount: stats.fixableWarningCount, + ...calculateStatsPerRun(results), usedDeprecatedRules }; } @@ -682,59 +806,65 @@ class CLIEngine { * @param {string} text A string of JavaScript code to lint. * @param {string} filename An optional string representing the texts filename. * @param {boolean} warnIgnored Always warn when a file is ignored - * @returns {Object} The results for the linting. + * @returns {LintReport} The results for the linting. */ executeOnText(text, filename, warnIgnored) { + const { + configArrayFactory, + ignoredPaths, + lastConfigArrays, + linter, + options: { + allowInlineConfig, + cwd, + fix, + reportUnusedDisableDirectives + } + } = internalSlotsMap.get(this); + const results = []; + const startTime = Date.now(); + const resolvedFilename = path.resolve(cwd, filename || ".js"); - const results = [], - options = this.options, - configHelper = this.config, - ignoredPaths = new IgnoredPaths(options); - - // resolve filename based on options.cwd (for reporting, ignoredPaths also resolves) - - const resolvedFilename = filename && !path.isAbsolute(filename) - ? path.resolve(options.cwd, filename) - : filename; - let usedDeprecatedRules; + // Clear the last used config arrays. + lastConfigArrays.length = 0; - if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) { + if (filename && ignoredPaths.contains(resolvedFilename)) { if (warnIgnored) { - results.push(createIgnoreResult(resolvedFilename, options.cwd)); + results.push(createIgnoreResult(resolvedFilename, cwd)); } - usedDeprecatedRules = []; } else { + const config = + configArrayFactory.getConfigArrayForFile(resolvedFilename); + + /* + * Store used configs for: + * - this method uses to collect used deprecated rules. + * - `getRules()` method uses to collect all loaded rules. + * - `--fix-type` option uses to get the loaded rule's meta data. + */ + lastConfigArrays.push(config); - // if there's a cache, populate it - if ("_rulesCache" in this) { - this._rulesCache = this.getRules(); - } - - const { result, config } = processText( + // Do lint. + results.push(verifyText( text, - configHelper, resolvedFilename, - options.fix, - options.allowInlineConfig, - options.reportUnusedDisableDirectives, - this.linter - ); - - results.push(result); - usedDeprecatedRules = createRuleDeprecationWarnings( - Object.keys(config.rules).filter(rule => ConfigOps.getRuleSeverity(config.rules[rule])), - this.getRules() - ); + config, + fix, + allowInlineConfig, + reportUnusedDisableDirectives, + linter + )); } - const stats = calculateStatsPerRun(results); + // Collect used deprecated rules. + const usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(lastConfigArrays) + ); + debug(`Linting complete in: ${Date.now() - startTime}ms`); return { results, - errorCount: stats.errorCount, - warningCount: stats.warningCount, - fixableErrorCount: stats.fixableErrorCount, - fixableWarningCount: stats.fixableWarningCount, + ...calculateStatsPerRun(results), usedDeprecatedRules }; } @@ -744,12 +874,16 @@ class CLIEngine { * This is the same logic used by the ESLint CLI executable to determine * configuration for each file it processes. * @param {string} filePath The path of the file to retrieve a config object for. - * @returns {Object} A configuration object for the file. + * @returns {ConfigData} A configuration object for the file. */ - getConfigForFile(filePath) { - const configHelper = this.config; - - return configHelper.getConfig(filePath); + getConfigForFile(filePath = "a.js") { + const { configArrayFactory, options } = internalSlotsMap.get(this); + const absolutePath = path.resolve(options.cwd, filePath); + + return configArrayFactory + .getConfigArrayForFile(absolutePath) + .extractConfig(absolutePath) + .toCompatibleObjectAsConfigFileContent(); } /** @@ -758,10 +892,9 @@ class CLIEngine { * @returns {boolean} Whether or not the given path is ignored. */ isPathIgnored(filePath) { - const resolvedPath = path.resolve(this.options.cwd, filePath); - const ignoredPaths = new IgnoredPaths(this.options); + const { ignoredPaths } = internalSlotsMap.get(this); - return ignoredPaths.contains(resolvedPath); + return ignoredPaths.contains(filePath); } /** @@ -782,7 +915,8 @@ class CLIEngine { // replace \ with / for Windows compatibility const normalizedFormatName = resolvedFormatName.replace(/\\/gu, "/"); - const cwd = this.options ? this.options.cwd : process.cwd(); + const slots = internalSlotsMap.get(this); + const cwd = slots ? slots.options.cwd : process.cwd(); const namespace = naming.getNamespaceFromTerm(normalizedFormatName); let formatterPath; @@ -794,7 +928,7 @@ class CLIEngine { try { const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); - formatterPath = relativeModuleResolver(npmFormat, path.join(cwd, "__placeholder__.js")); + formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js")); } catch (e) { formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName); } @@ -816,4 +950,15 @@ class CLIEngine { CLIEngine.version = pkg.version; CLIEngine.getFormatter = CLIEngine.prototype.getFormatter; -module.exports = CLIEngine; +module.exports = { + CLIEngine, + + /** + * Get the internal slots of a given CLIEngine instance for tests. + * @param {CLIEngine} instance The CLIEngine instance to get. + * @returns {CLIEngineInternalSlots} The internal slots. + */ + getCLIEngineInternalSlots(instance) { + return internalSlotsMap.get(instance); + } +}; diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js new file mode 100644 index 00000000000..033a8170ff3 --- /dev/null +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -0,0 +1,393 @@ +/** + * @fileoverview `CascadingConfigArrayFactory` class. + * + * `CascadingConfigArrayFactory` class has a responsibility: + * + * 1. Handles cascading of config files. + * + * It provies two methods: + * + * - `getConfigArrayForFile(filePath)` + * Get the corresponded configuration of a given file. This method doesn't + * throw even if the given file didn't exist. + * - `clearCache()` + * Clear the internal cache. You have to call this method when + * `additionalPluginPool` was updated if `baseConfig` or `cliConfig` depends + * on the additional plugins. (`CLIEngine#addPlugin()` method calls this.) + * + * @author Toru Nagashima + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const os = require("os"); +const path = require("path"); +const { validateConfigArray } = require("../config/config-validator"); +const { ConfigArrayFactory } = require("./config-array-factory"); +const { ConfigDependency } = require("./config-array"); +const loadRules = require("../load-rules"); +const debug = require("debug")("eslint:cascading-config-array-factory"); + +// debug.enabled = true; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +// Define types for VSCode IntelliSense. +/** @typedef {import("../util/types").ConfigData} ConfigData */ +/** @typedef {import("../util/types").Parser} Parser */ +/** @typedef {import("../util/types").Plugin} Plugin */ +/** @typedef {ReturnType} ConfigArray */ + +/** + * @typedef {Object} CascadingConfigArrayFactoryOptions + * @property {Map} [additionalPluginPool] The map for additional plugins. + * @property {ConfigData} [baseConfig] The config by `baseConfig` option. + * @property {ConfigData} [cliConfig] The config by CLI options. This is prior to regular config files. + * @property {string} [cwd] The base directory to start lookup. + * @property {string[]} [rulePaths] The value of `--rulesdir` option. + * @property {string} [specificConfigPath] The value of `--config` option. + * @property {boolean} [useEslintrc] if `false` then it doesn't load config files. + */ + +/** + * @typedef {Object} CascadingConfigArrayFactoryInternalSlots + * @property {ConfigArray} baseConfigArray The config array of `baseConfig` option. + * @property {ConfigData} baseConfigData The config data of `baseConfig` option. This is used to reset `baseConfigArray`. + * @property {ConfigArray} cliConfigArray The config array of CLI options. + * @property {ConfigData} cliConfigData The config data of CLI options. This is used to reset `cliConfigArray`. + * @property {ConfigArrayFactory} configArrayFactory The factory for config arrays. + * @property {Map} configCache The cache from directory paths to config arrays. + * @property {string} cwd The base directory to start lookup. + * @property {WeakMap} finalizeCache The cache from config arrays to finalized config arrays. + * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`. + * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`. + * @property {boolean} useEslintrc if `false` then it doesn't load config files. + */ + +/** @type {WeakMap} */ +const internalSlotsMap = new WeakMap(); + +/** + * Create the config array from `baseConfig` and `rulePaths`. + * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots. + * @returns {ConfigArray} The config array of the base configs. + */ +function createBaseConfigArray({ + configArrayFactory, + baseConfigData, + rulePaths, + cwd +}) { + const baseConfigArray = configArrayFactory.create( + baseConfigData, + { name: "BaseConfig" } + ); + + if (rulePaths && rulePaths.length > 0) { + + /* + * Load rules `--rulesdir` option as a pseudo plugin. + * Use a pseudo plugin to define rules of `--rulesdir`, so we can + * validate the rule's options with only information in the config + * array. + */ + baseConfigArray.push({ + name: "--rulesdir", + filePath: "", + plugins: { + "": new ConfigDependency({ + definition: { + rules: rulePaths.reduce( + (map, rulesPath) => Object.assign( + map, + loadRules(rulesPath, cwd) + ), + {} + ) + }, + filePath: "", + id: "", + importerName: "--rulesdir", + importerPath: "" + }) + } + }); + } + + return baseConfigArray; +} + +/** + * Create the config array from CLI options. + * @param {CascadingConfigArrayFactoryInternalSlots} slots The slots. + * @returns {ConfigArray} The config array of the base configs. + */ +function createCLIConfigArray({ + cliConfigData, + configArrayFactory, + specificConfigPath +}) { + const cliConfigArray = configArrayFactory.create( + cliConfigData, + { name: "CLIOptions" } + ); + + if (specificConfigPath) { + cliConfigArray.unshift( + ...configArrayFactory.loadFile( + specificConfigPath, + { name: "--config" } + ) + ); + } + + return cliConfigArray; +} + +/** + * The error type when there are files matched by a glob, but all of them have been ignored. + */ +class ConfigurationNotFoundError extends Error { + + /** + * @param {string} directoryPath - The directory path. + */ + constructor(directoryPath) { + super(`No ESLint configuration found on ${directoryPath}.`); + this.messageTemplate = "no-config-found"; + this.messageData = { directoryPath }; + } +} + +/** + * This class provides the functionality that enumerates every file which is + * matched by given glob patterns and that configuration. + */ +class CascadingConfigArrayFactory { + + /** + * Initialize this enumerator. + * @param {CascadingConfigArrayFactoryOptions} options The options. + */ + constructor({ + additionalPluginPool = new Map(), + baseConfig: baseConfigData = null, + cliConfig: cliConfigData = null, + cwd = process.cwd(), + rulePaths = [], + specificConfigPath = null, + useEslintrc = true + } = {}) { + const configArrayFactory = new ConfigArrayFactory({ + additionalPluginPool, + cwd + }); + + internalSlotsMap.set(this, { + baseConfigArray: createBaseConfigArray({ + baseConfigData, + configArrayFactory, + cwd, + rulePaths + }), + baseConfigData, + cliConfigArray: createCLIConfigArray({ + cliConfigData, + configArrayFactory, + specificConfigPath + }), + cliConfigData, + configArrayFactory, + configCache: new Map(), + cwd, + finalizeCache: new WeakMap(), + rulePaths, + specificConfigPath, + useEslintrc + }); + } + + /** + * The path to the current working directory. + * This is used by tests. + * @type {string} + */ + get cwd() { + const { cwd } = internalSlotsMap.get(this); + + return cwd; + } + + /** + * Get the config array of a given file. + * @param {string} [filePath] The file path to a file. + * @returns {ConfigArray} The config array of the file. + */ + getConfigArrayForFile(filePath = "a.js") { + const { cwd } = internalSlotsMap.get(this); + const directoryPath = path.dirname(path.resolve(cwd, filePath)); + + debug(`Load config files for ${directoryPath}.`); + + return this._finalizeConfigArray( + this._loadConfigInAncestors(directoryPath), + directoryPath + ); + } + + /** + * Clear config cache. + * @returns {void} + */ + clearCache() { + const slots = internalSlotsMap.get(this); + + slots.baseConfigArray = createBaseConfigArray(slots); + slots.cliConfigArray = createCLIConfigArray(slots); + slots.configCache.clear(); + } + + /** + * Load and normalize config files from the ancestor directories. + * @param {string} directoryPath The path to a leaf directory. + * @returns {ConfigArray} The loaded config. + * @private + */ + _loadConfigInAncestors(directoryPath) { + const { + baseConfigArray, + configArrayFactory, + configCache, + cwd, + useEslintrc + } = internalSlotsMap.get(this); + + if (!useEslintrc) { + return baseConfigArray; + } + + let configArray = configCache.get(directoryPath); + + // Hit cache. + if (configArray) { + debug(`Cache hit: ${directoryPath}.`); + return configArray; + } + debug(`No cache found: ${directoryPath}.`); + + const homePath = os.homedir(); + + // Consider this is root. + if (directoryPath === homePath && cwd !== homePath) { + debug("Stop traversing because of considered root."); + configCache.set(directoryPath, baseConfigArray); + return baseConfigArray; + } + + // Load the config on this directory. + try { + configArray = configArrayFactory.loadOnDirectory(directoryPath); + } catch (error) { + /* istanbul ignore next */ + if (error.code === "EACCES") { + debug("Stop traversing because of 'EACCES' error."); + configCache.set(directoryPath, baseConfigArray); + return baseConfigArray; + } + throw error; + } + + if (configArray.length > 0 && configArray.root) { + debug("Stop traversing because of 'root:true'."); + configCache.set(directoryPath, configArray); + return configArray; + } + + // Load from the ancestors and merge it. + const parentPath = path.dirname(directoryPath); + const parentConfigArray = parentPath && parentPath !== directoryPath + ? this._loadConfigInAncestors(parentPath) + : baseConfigArray; + + if (configArray.length > 0) { + configArray.unshift(...parentConfigArray); + } else { + configArray = parentConfigArray; + } + + // Cache and return. + configCache.set(directoryPath, configArray); + return configArray; + } + + /** + * Finalize a given config array. + * Concatinate `--config` and other CLI options. + * @param {ConfigArray} configArray The parent config array. + * @param {string} directoryPath The path to the leaf directory to find config files. + * @returns {ConfigArray} The loaded config. + * @private + */ + _finalizeConfigArray(configArray, directoryPath) { + const { + cliConfigArray, + configArrayFactory, + finalizeCache, + useEslintrc + } = internalSlotsMap.get(this); + + let finalConfigArray = finalizeCache.get(configArray); + + if (!finalConfigArray) { + finalConfigArray = configArray; + + // Load the personal config if there are no regular config files. + if ( + useEslintrc && + configArray.every(c => !c.filePath) && + cliConfigArray.every(c => !c.filePath) // `--config` option can be a file. + ) { + debug("Loading the config file of the home directory."); + + finalConfigArray = configArrayFactory.loadOnDirectory( + os.homedir(), + { name: "PersonalConfig", parent: finalConfigArray } + ); + } + + // Apply CLI options. + if (cliConfigArray.length > 0) { + finalConfigArray = finalConfigArray.concat(cliConfigArray); + } + + // Validate rule settings and environments. + validateConfigArray(finalConfigArray); + + // Cache it. + finalizeCache.set(configArray, finalConfigArray); + + debug( + "Configuration was determined: %o on %s", + finalConfigArray, + directoryPath + ); + } + + if (useEslintrc && finalConfigArray.length === 0) { + throw new ConfigurationNotFoundError(directoryPath); + } + + return finalConfigArray; + } +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { CascadingConfigArrayFactory }; diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js new file mode 100644 index 00000000000..e6bd4c55bcc --- /dev/null +++ b/lib/cli-engine/config-array-factory.js @@ -0,0 +1,896 @@ +/** + * @fileoverview The factory of `ConfigArray` objects. + * + * This class provides methods to create `ConfigArray` instance. + * + * - `create(configData, options)` + * Create a `ConfigArray` instance from a config data. This is to handle CLI + * options except `--config`. + * - `loadFile(filePath, options)` + * Create a `ConfigArray` instance from a config file. This is to handle + * `--config` option. If the file was not found, throws the following error: + * - If the filename was `*.js`, a `MODULE_NOT_FOUND` error. + * - If the filename was `package.json`, an IO error or an + * `ESLINT_CONFIG_FIELD_NOT_FOUND` error. + * - Otherwise, an IO error such as `ENOENT`. + * - `loadOnDirectory(directoryPath, options)` + * Create a `ConfigArray` instance from a config file which is on a given + * directory. This tries to load `.eslintrc.*` or `package.json`. If not + * found, returns an empty `ConfigArray`. + * + * `ConfigArrayFactory` class has the responsibility that loads configuration + * files, including loading `extends`, `parser`, and `plugins`. The created + * `ConfigArray` instance has the loaded `extends`, `parser`, and `plugins`. + * + * But this class doesn't handle cascading. `CascadingConfigArrayFactory` class + * handles cascading and hierarchy. + * + * @author Toru Nagashima + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const fs = require("fs"); +const path = require("path"); +const importFresh = require("import-fresh"); +const stripComments = require("strip-json-comments"); +const { validateConfigSchema } = require("../config/config-validator"); +const { ConfigArray, ConfigDependency, OverrideTester } = require("./config-array"); +const ModuleResolver = require("../util/relative-module-resolver"); +const naming = require("../util/naming"); +const debug = require("debug")("eslint:config-array-factory"); + +// debug.enabled = true; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const eslintRecommendedPath = path.resolve(__dirname, "../../conf/eslint-recommended.js"); +const eslintAllPath = path.resolve(__dirname, "../../conf/eslint-all.js"); +const configFilenames = [ + ".eslintrc.js", + ".eslintrc.yaml", + ".eslintrc.yml", + ".eslintrc.json", + ".eslintrc", + "package.json" +]; + +// Define types for VSCode IntelliSense. +/** @typedef {import("../util/types").ConfigData} ConfigData */ +/** @typedef {import("../util/types").OverrideConfigData} OverrideConfigData */ +/** @typedef {import("../util/types").Parser} Parser */ +/** @typedef {import("../util/types").Plugin} Plugin */ +/** @typedef {import("./config-array/config-dependency").DependentParser} DependentParser */ +/** @typedef {import("./config-array/config-dependency").DependentPlugin} DependentPlugin */ +/** @typedef {ConfigArray[0]} ConfigArrayElement */ + +/** + * @typedef {Object} ConfigArrayFactoryOptions + * @property {Map} [additionalPluginPool] The map for additional plugins. + * @property {string} [cwd] The path to the current working directory. + */ + +/** + * @typedef {Object} ConfigArrayFactoryInternalSlots + * @property {Map} additionalPluginPool The map for additional plugins. + * @property {string} cwd The path to the current working directory. + */ + +/** @type {WeakMap} */ +const internalSlotsMap = new WeakMap(); + +/** + * Check if a given string is a file path. + * @param {string} nameOrPath A module name or file path. + * @returns {boolean} `true` if the `nameOrPath` is a file path. + */ +function isFilePath(nameOrPath) { + return ( + /^\.{1,2}[/\\]/u.test(nameOrPath) || + path.isAbsolute(nameOrPath) + ); +} + +/** + * Convenience wrapper for synchronously reading file contents. + * @param {string} filePath The filename to read. + * @returns {string} The file contents, with the BOM removed. + * @private + */ +function readFile(filePath) { + return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/u, ""); +} + +/** + * Loads a YAML configuration from a file. + * @param {string} filePath The filename to load. + * @returns {ConfigData} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadYAMLConfigFile(filePath) { + debug(`Loading YAML config file: ${filePath}`); + + // lazy load YAML to improve performance when not used + const yaml = require("js-yaml"); + + try { + + // empty YAML file can be null, so always use + return yaml.safeLoad(readFile(filePath)) || {}; + } catch (e) { + debug(`Error reading YAML file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a JSON configuration from a file. + * @param {string} filePath The filename to load. + * @returns {ConfigData} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadJSONConfigFile(filePath) { + debug(`Loading JSON config file: ${filePath}`); + + try { + return JSON.parse(stripComments(readFile(filePath))); + } catch (e) { + debug(`Error reading JSON file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + e.messageTemplate = "failed-to-read-json"; + e.messageData = { + path: filePath, + message: e.message + }; + throw e; + } +} + +/** + * Loads a legacy (.eslintrc) configuration from a file. + * @param {string} filePath The filename to load. + * @returns {ConfigData} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadLegacyConfigFile(filePath) { + debug(`Loading legacy config file: ${filePath}`); + + // lazy load YAML to improve performance when not used + const yaml = require("js-yaml"); + + try { + return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {}; + } catch (e) { + debug("Error reading YAML file: %s\n%o", filePath, e); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a JavaScript configuration from a file. + * @param {string} filePath The filename to load. + * @returns {ConfigData} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadJSConfigFile(filePath) { + debug(`Loading JS config file: ${filePath}`); + try { + return importFresh(filePath); + } catch (e) { + debug(`Error reading JavaScript file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Loads a configuration from a package.json file. + * @param {string} filePath The filename to load. + * @returns {ConfigData} The configuration object from the file. + * @throws {Error} If the file cannot be read. + * @private + */ +function loadPackageJSONConfigFile(filePath) { + debug(`Loading package.json config file: ${filePath}`); + try { + const packageData = loadJSONConfigFile(filePath); + + if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) { + throw Object.assign( + new Error("package.json file doesn't have 'eslintConfig' field."), + { code: "ESLINT_CONFIG_FIELD_NOT_FOUND" } + ); + } + + return packageData.eslintConfig; + } catch (e) { + debug(`Error reading package.json file: ${filePath}`); + e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * Creates an error to notify about a missing config to extend from. + * @param {string} configName The name of the missing config. + * @returns {Error} The error object to throw + * @private + */ +function configMissingError(configName) { + return Object.assign( + new Error(`Failed to load config "${configName}" to extend from.`), + { + messageTemplate: "extend-config-missing", + messageData: { configName } + } + ); +} + +/** + * Loads a configuration file regardless of the source. Inspects the file path + * to determine the correctly way to load the config file. + * @param {string} filePath The path to the configuration. + * @returns {ConfigData|null} The configuration information. + * @private + */ +function loadConfigFile(filePath) { + switch (path.extname(filePath)) { + case ".js": + return loadJSConfigFile(filePath); + + case ".json": + if (path.basename(filePath) === "package.json") { + return loadPackageJSONConfigFile(filePath); + } + return loadJSONConfigFile(filePath); + + case ".yaml": + case ".yml": + return loadYAMLConfigFile(filePath); + + default: + return loadLegacyConfigFile(filePath); + } +} + +/** + * Write debug log. + * @param {string} request The requested module name. + * @param {string} relativeTo The file path to resolve the request relative to. + * @param {string} filePath The resolved file path. + * @returns {void} + */ +function writeDebugLogForLoading(request, relativeTo, filePath) { + /* istanbul ignore next */ + if (debug.enabled) { + let nameAndVersion = null; + + try { + const packageJsonPath = ModuleResolver.resolve( + `${request}/package.json`, + relativeTo + ); + const { version = "unknown" } = require(packageJsonPath); + + nameAndVersion = `${request}@${version}`; + } catch (error) { + debug("package.json was not found:", error.message); + nameAndVersion = request; + } + + debug("Loaded: %s (%s)", nameAndVersion, filePath); + } +} + +/** + * Concatenate two config data. + * @param {IterableIterator|null} elements The config elements. + * @param {ConfigArray|null} parentConfigArray The parent config array. + * @returns {ConfigArray} The concatenated config array. + */ +function createConfigArray(elements, parentConfigArray) { + if (!elements) { + return parentConfigArray || new ConfigArray(); + } + const configArray = new ConfigArray(...elements); + + if (parentConfigArray && !configArray.root) { + configArray.unshift(...parentConfigArray); + } + return configArray; +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +/** + * The factory of `ConfigArray` objects. + */ +class ConfigArrayFactory { + + /** + * Initialize this instance. + * @param {ConfigArrayFactoryOptions} [options] The map for additional plugins. + */ + constructor({ + additionalPluginPool = new Map(), + cwd = process.cwd() + } = {}) { + internalSlotsMap.set(this, { additionalPluginPool, cwd }); + } + + /** + * Create `ConfigArray` instance from a config data. + * @param {ConfigData|null} configData The config data to create. + * @param {Object} [options] The options. + * @param {string} [options.filePath] The path to this config data. + * @param {string} [options.name] The config name. + * @param {ConfigArray} [options.parent] The parent config array. + * @returns {ConfigArray} Loaded config. + */ + create(configData, { filePath, name, parent } = {}) { + return createConfigArray( + configData + ? this._normalizeConfigData(configData, filePath, name) + : null, + parent + ); + } + + /** + * Load a config file. + * @param {string} filePath The path to a config file. + * @param {Object} [options] The options. + * @param {string} [options.name] The config name. + * @param {ConfigArray} [options.parent] The parent config array. + * @returns {ConfigArray} Loaded config. + */ + loadFile(filePath, { name, parent } = {}) { + const { cwd } = internalSlotsMap.get(this); + const absolutePath = path.resolve(cwd, filePath); + + return createConfigArray( + this._loadConfigData(absolutePath, name), + parent + ); + } + + /** + * Load the config file on a given directory if exists. + * @param {string} directoryPath The path to a directory. + * @param {Object} [options] The options. + * @param {string} [options.name] The config name. + * @param {ConfigArray} [options.parent] The parent config array. + * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. + */ + loadOnDirectory(directoryPath, { name, parent } = {}) { + const { cwd } = internalSlotsMap.get(this); + const absolutePath = path.resolve(cwd, directoryPath); + + return createConfigArray( + this._loadConfigDataOnDirectory(absolutePath, name), + parent + ); + } + + /** + * Load a given config file. + * @param {string} filePath The path to a config file. + * @param {string} name The config name. + * @returns {IterableIterator} Loaded config. + * @private + */ + _loadConfigData(filePath, name) { + return this._normalizeConfigData( + loadConfigFile(filePath), + filePath, + name + ); + } + + /** + * Load the config file on a given directory if exists. + * @param {string} directoryPath The path to a directory. + * @param {string} name The config name. + * @returns {IterableIterator | null} Loaded config. `null` if any config doesn't exist. + * @private + */ + _loadConfigDataOnDirectory(directoryPath, name) { + for (const filename of configFilenames) { + const filePath = path.join(directoryPath, filename); + const originalEnabled = debug.enabled; + let configData; + + // Make silent temporary because of too verbose. + debug.enabled = false; + try { + configData = loadConfigFile(filePath); + } catch (error) { + if ( + error.code !== "ENOENT" && + error.code !== "MODULE_NOT_FOUND" && + error.code !== "ESLINT_CONFIG_FIELD_NOT_FOUND" + ) { + throw error; + } + } finally { + debug.enabled = originalEnabled; + } + + if (configData) { + debug(`Config file found: ${filePath}`); + return this._normalizeConfigData(configData, filePath, name); + } + } + + debug(`Config file not found on ${directoryPath}`); + return null; + } + + /** + * Normalize a given config to an array. + * @param {ConfigData} configData The config data to normalize. + * @param {string|undefined} providedFilePath The file path of this config. + * @param {string|undefined} providedName The name of this config. + * @returns {IterableIterator} The normalized config. + * @private + */ + _normalizeConfigData(configData, providedFilePath, providedName) { + const { cwd } = internalSlotsMap.get(this); + const filePath = providedFilePath + ? path.resolve(cwd, providedFilePath) + : ""; + const name = providedName || (filePath && path.relative(cwd, filePath)); + + validateConfigSchema(configData, name || filePath); + + return this._normalizeObjectConfigData(configData, filePath, name); + } + + /** + * Normalize a given config to an array. + * @param {ConfigData|OverrideConfigData} configData The config data to normalize. + * @param {string} filePath The file path of this config. + * @param {string} name The name of this config. + * @returns {IterableIterator} The normalized config. + * @private + */ + *_normalizeObjectConfigData(configData, filePath, name) { + const { cwd } = internalSlotsMap.get(this); + const { files, excludedFiles, ...configBody } = configData; + const basePath = filePath ? path.dirname(filePath) : cwd; + const criteria = OverrideTester.create(files, excludedFiles, basePath); + const elements = + this._normalizeObjectConfigDataBody(configBody, filePath, name); + + // Apply the criteria to every element. + for (const element of elements) { + element.criteria = OverrideTester.and(criteria, element.criteria); + + /* + * Adopt the base path of the entry file (the outermost base path). + * Also, ensure the elements which came from `overrides` settings + * don't have `root` property even if it came from `extends` in + * `overrides`. + */ + if (element.criteria) { + element.criteria.basePath = basePath; + element.root = void 0; + } + + yield element; + } + } + + /** + * Normalize a given config to an array. + * @param {ConfigData} configData The config data to normalize. + * @param {string} filePath The file path of this config. + * @param {string} name The name of this config. + * @returns {IterableIterator} The normalized config. + * @private + */ + *_normalizeObjectConfigDataBody( + { + env, + extends: extend, + globals, + parser: parserName, + parserOptions, + plugins: pluginList, + processor, // processor is only for file extension processors. + root, + rules, + settings, + overrides: overrideList = [] + }, + filePath, + name + ) { + const extendList = Array.isArray(extend) ? extend : [extend]; + + // Flatten `extends`. + for (const extendName of extendList.filter(Boolean)) { + yield* this._loadExtends(extendName, filePath, name); + } + + // Load parser & plugins. + const parser = + parserName && this._loadParser(parserName, filePath, name); + const plugins = + pluginList && this._loadPlugins(pluginList, filePath, name); + + // Yield pseudo config data for file extension processors. + if (plugins) { + yield* this._takeFileExtensionProcessors(plugins, filePath, name); + } + + // Yield the config data except `extends` and `overrides`. + yield { + + // Debug information. + name, + filePath, + + // Config data. + criteria: null, + env, + globals, + parser, + parserOptions, + plugins, + processor, + root, + rules, + settings + }; + + // Flatten `overries`. + for (let i = 0; i < overrideList.length; ++i) { + yield* this._normalizeObjectConfigData( + overrideList[i], + filePath, + `${name}#overrides[${i}]` + ); + } + } + + /** + * Load configs of an element in `extends`. + * @param {string} extendName The name of a base config. + * @param {string} importerPath The file path which has the `extends` property. + * @param {string} importerName The name of the config which has the `extends` property. + * @returns {IterableIterator} The normalized config. + * @private + */ + _loadExtends(extendName, importerPath, importerName) { + debug("Loading {extends:%j} relative to %s", extendName, importerPath); + try { + if (extendName.startsWith("eslint:")) { + return this._loadExtendedBuiltInConfig( + extendName, + importerName + ); + } + if (extendName.startsWith("plugin:")) { + return this._loadExtendedPluginConfig( + extendName, + importerPath, + importerName + ); + } + return this._loadExtendedShareableConfig( + extendName, + importerPath, + importerName + ); + } catch (error) { + error.message += `\nReferenced from: ${importerPath || importerName}`; + throw error; + } + } + + /** + * Load configs of an element in `extends`. + * @param {string} extendName The name of a base config. + * @param {string} importerName The name of the config which has the `extends` property. + * @returns {IterableIterator} The normalized config. + * @private + */ + _loadExtendedBuiltInConfig(extendName, importerName) { + const name = `${importerName} » ${extendName}`; + + if (extendName === "eslint:recommended") { + return this._loadConfigData(eslintRecommendedPath, name); + } + if (extendName === "eslint:all") { + return this._loadConfigData(eslintAllPath, name); + } + + throw configMissingError(extendName); + } + + /** + * Load configs of an element in `extends`. + * @param {string} extendName The name of a base config. + * @param {string} importerPath The file path which has the `extends` property. + * @param {string} importerName The name of the config which has the `extends` property. + * @returns {IterableIterator} The normalized config. + * @private + */ + _loadExtendedPluginConfig(extendName, importerPath, importerName) { + const slashIndex = extendName.lastIndexOf("/"); + const pluginName = extendName.slice(7, slashIndex); + const configName = extendName.slice(slashIndex + 1); + + if (isFilePath(pluginName)) { + throw new Error("'extends' cannot use a file path for plugins."); + } + + const plugin = this._loadPlugin(pluginName, importerPath, importerName); + const configData = + plugin.definition && + plugin.definition.configs && + plugin.definition.configs[configName]; + + if (configData) { + return this._normalizeConfigData( + configData, + plugin.filePath, + `${importerName} » plugin:${plugin.id}/${configName}` + ); + } + + throw plugin.error || configMissingError(extendName); + } + + /** + * Load configs of an element in `extends`. + * @param {string} extendName The name of a base config. + * @param {string} importerPath The file path which has the `extends` property. + * @param {string} importerName The name of the config which has the `extends` property. + * @returns {IterableIterator} The normalized config. + * @private + */ + _loadExtendedShareableConfig(extendName, importerPath, importerName) { + const { cwd } = internalSlotsMap.get(this); + const relativeTo = importerPath || path.join(cwd, ".eslintrc"); + let request; + + if (isFilePath(extendName)) { + request = extendName; + } else if (extendName.startsWith(".")) { + request = `./${extendName}`; // For backward compatibility. A ton of tests depended on this behavior. + } else { + request = naming.normalizePackageName( + extendName, + "eslint-config" + ); + } + + try { + const filePath = ModuleResolver.resolve(request, relativeTo); + + writeDebugLogForLoading(request, relativeTo, filePath); + + return this._loadConfigData(filePath, `${importerName} » ${request}`); + } catch (error) { + /* istanbul ignore next */ + if (!error || error.code !== "MODULE_NOT_FOUND") { + throw error; + } + } + + throw configMissingError(extendName); + } + + /** + * Load given plugins. + * @param {string[]} names The plugin names to load. + * @param {string} importerPath The path to a config file that imports it. This is just a debug info. + * @param {string} importerName The name of a config file that imports it. This is just a debug info. + * @returns {Record} The loaded parser. + * @private + */ + _loadPlugins(names, importerPath, importerName) { + return names.reduce((map, name) => { + if (isFilePath(name)) { + throw new Error("Plugins array cannot includes file paths."); + } + const plugin = this._loadPlugin(name, importerPath, importerName); + + map[plugin.id] = plugin; + + return map; + }, {}); + } + + /** + * Load a given parser. + * @param {string} nameOrPath The package name or the path to a parser file. + * @param {string} importerPath The path to a config file that imports it. + * @param {string} importerName The name of a config file that imports it. This is just a debug info. + * @returns {DependentParser} The loaded parser. + */ + _loadParser(nameOrPath, importerPath, importerName) { + debug("Loading parser %j from %s", nameOrPath, importerPath); + + const { cwd } = internalSlotsMap.get(this); + const relativeTo = importerPath || path.join(cwd, ".eslintrc"); + + try { + const filePath = ModuleResolver.resolve(nameOrPath, relativeTo); + + writeDebugLogForLoading(nameOrPath, relativeTo, filePath); + + return new ConfigDependency({ + definition: require(filePath), + filePath, + id: nameOrPath, + importerName, + importerPath + }); + } catch (error) { + + // If the parser name is "espree", load the espree of ESLint. + if (nameOrPath === "espree") { + debug("Fallback espree."); + return new ConfigDependency({ + definition: require("espree"), + filePath: require.resolve("espree"), + id: nameOrPath, + importerName, + importerPath + }); + } + + debug("Failed to load parser '%s' declared in '%s'.", nameOrPath, importerName); + error.message = `Failed to load parser '${nameOrPath}' declared in '${importerName}': ${error.message}`; + + return new ConfigDependency({ + error, + id: nameOrPath, + importerName, + importerPath + }); + } + } + + /** + * Load a given plugin. + * @param {string} nameOrPath The plugin name to load. + * @param {string} importerPath The path to a config file that imports it. This is just a debug info. + * @param {string} importerName The name of a config file that imports it. This is just a debug info. + * @returns {DependentPlugin} The loaded plugin. + * @private + */ + _loadPlugin(nameOrPath, importerPath, importerName) { + debug("Loading plugin %j from %s", nameOrPath, importerPath); + + const { additionalPluginPool, cwd } = internalSlotsMap.get(this); + let request, id; + + if (isFilePath(nameOrPath)) { + request = id = nameOrPath; + } else { + request = naming.normalizePackageName(nameOrPath, "eslint-plugin"); + id = naming.getShorthandName(request, "eslint-plugin"); + + if (nameOrPath.match(/\s+/u)) { + const error = Object.assign( + new Error(`Whitespace found in plugin name '${nameOrPath}'`), + { + messageTemplate: "whitespace-found", + messageData: { pluginName: request } + } + ); + + return new ConfigDependency({ + error, + id, + importerName, + importerPath + }); + } + + // Check for additional pool. + const plugin = + additionalPluginPool.get(request) || + additionalPluginPool.get(id); + + if (plugin) { + return new ConfigDependency({ + definition: plugin, + filePath: importerPath, + id, + importerName, + importerPath + }); + } + } + + try { + + // Resolve the plugin file relative to the project root. + const relativeTo = path.join(cwd, ".eslintrc"); + const filePath = ModuleResolver.resolve(request, relativeTo); + + writeDebugLogForLoading(request, relativeTo, filePath); + + return new ConfigDependency({ + definition: require(filePath), + filePath, + id, + importerName, + importerPath + }); + } catch (error) { + debug("Failed to load plugin '%s' declared in '%s'.", nameOrPath, importerName); + + if (error && error.code === "MODULE_NOT_FOUND" && error.message.includes(request)) { + error.messageTemplate = "plugin-missing"; + error.messageData = { + pluginName: request, + pluginRootPath: cwd, + importerName + }; + } + error.message = `Failed to load plugin '${nameOrPath}' declared in '${importerName}': ${error.message}`; + + return new ConfigDependency({ + error, + id, + importerName, + importerPath + }); + } + } + + /** + * Take file expression processors as config array elements. + * @param {Record} plugins The plugin definitions. + * @param {string} filePath The file path of this config. + * @param {string} name The name of this config. + * @returns {IterableIterator} The config array elements of file expression processors. + * @private + */ + *_takeFileExtensionProcessors(plugins, filePath, name) { + for (const pluginId of Object.keys(plugins)) { + const processors = + plugins[pluginId] && + plugins[pluginId].definition && + plugins[pluginId].definition.processors; + + if (!processors) { + continue; + } + + for (const processorId of Object.keys(processors)) { + if (processorId.startsWith(".")) { + yield* this._normalizeObjectConfigData( + { + files: [`*${processorId}`], + processor: `${pluginId}/${processorId}` + }, + filePath, + `${name}#processors["${pluginId}/${processorId}"]` + ); + } + } + } + } +} + +module.exports = { ConfigArrayFactory }; diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js new file mode 100644 index 00000000000..e74ee176af7 --- /dev/null +++ b/lib/cli-engine/config-array/config-array.js @@ -0,0 +1,445 @@ +/** + * @fileoverview `ConfigArray` class. + * + * `ConfigArray` class expresses the full of a configuration. It has the entry + * config file, base config files that were extended, loaded parsers, and loaded + * plugins. + * + * `ConfigArray` class provies four properties and one method. + * + * - `pluginEnvironments` + * - `pluginProcessors` + * - `pluginRules` + * The `Map` objects that contain the members of all plugins that this + * config array contains. Those map objects don't have mutation methods. + * Those keys are the member ID such as `pluginId/memberName`. + * - `root` + * If `true` then this configuration has `root:true` property. + * - `extractConfig(filePath)` + * Extract the final configuration for a given file. This means merging + * every config array element which that `criteria` property matched. The + * `filePath` argument must be an absolute path. + * + * `ConfigArrayFactory` provides the loading logic of config files. + * + * @author Toru Nagashima + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const { ExtractedConfig } = require("./extracted-config"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +// Define types for VSCode IntelliSense. +/** @typedef {import("../../util/types").Environment} Environment */ +/** @typedef {import("../../util/types").GlobalConf} GlobalConf */ +/** @typedef {import("../../util/types").RuleConf} RuleConf */ +/** @typedef {import("../../util/types").Rule} Rule */ +/** @typedef {import("../../util/types").Plugin} Plugin */ +/** @typedef {import("../../util/types").Processor} Processor */ +/** @typedef {import("./config-dependency").DependentParser} DependentParser */ +/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */ +/** @typedef {import("./override-tester")["OverrideTester"]} OverrideTester */ + +/** + * @typedef {Object} ConfigArrayElement + * @property {string} name The name of this config element. + * @property {string} filePath The path to the source file of this config element. + * @property {InstanceType|null} criteria The tester for the `files` and `excludedFiles` of this config element. + * @property {Record|undefined} env The environment settings. + * @property {Record|undefined} globals The global variable settings. + * @property {DependentParser|undefined} parser The parser loader. + * @property {Object|undefined} parserOptions The parser options. + * @property {Record|undefined} plugins The plugin loaders. + * @property {string|undefined} processor The processor name to refer plugin's processor. + * @property {boolean|undefined} root The flag to express root. + * @property {Record|undefined} rules The rule settings + * @property {Object|undefined} settings The shared settings. + */ + +/** + * @typedef {Object} ConfigArrayInternalSlots + * @property {Map} cache The cache to extract configs. + * @property {ReadonlyMap|null} envMap The map from environment ID to environment definition. + * @property {ReadonlyMap|null} processorMap The map from processor ID to environment definition. + * @property {ReadonlyMap|null} ruleMap The map from rule ID to rule definition. + */ + +/** @type {WeakMap} */ +const internalSlotsMap = new class extends WeakMap { + get(key) { + let value = super.get(key); + + if (!value) { + value = { + cache: new Map(), + envMap: null, + processorMap: null, + ruleMap: null + }; + super.set(key, value); + } + + return value; + } +}(); + +/** + * Get the indices which are matched to a given file. + * @param {ConfigArrayElement[]} elements The elements. + * @param {string} filePath The path to a target file. + * @returns {number[]} The indices. + */ +function getMatchedIndices(elements, filePath) { + const indices = []; + + for (let i = elements.length - 1; i >= 0; --i) { + const element = elements[i]; + + if (!element.criteria || element.criteria.test(filePath)) { + indices.push(i); + } + } + + return indices; +} + +/** + * Check if a value is a non-null object. + * @param {any} x The value to check. + * @returns {boolean} `true` if the value is a non-null object. + */ +function isNonNullObject(x) { + return typeof x === "object" && x !== null; +} + +/** + * Merge two objects. + * + * Assign every property values of `y` to `x` if `x` doesn't have the property. + * If `x`'s property value is an object, it does recursive. + * + * @param {Object} target The destination to merge + * @param {Object|undefined} source The source to merge. + * @returns {void} + */ +function mergeWithoutOverwrite(target, source) { + if (!isNonNullObject(source)) { + return; + } + + for (const key of Object.keys(source)) { + if (isNonNullObject(target[key])) { + mergeWithoutOverwrite(target[key], source[key]); + } else if (target[key] === void 0) { + if (isNonNullObject(source[key])) { + target[key] = Array.isArray(source[key]) ? [] : {}; + mergeWithoutOverwrite(target[key], source[key]); + } else if (source[key] !== void 0) { + target[key] = source[key]; + } + } + } +} + +/** + * Merge plugins. + * `target`'s definition is prior to `source`'s. + * + * @param {Record} target The destination to merge + * @param {Record|undefined} source The source to merge. + * @returns {void} + */ +function mergePlugins(target, source) { + if (!isNonNullObject(source)) { + return; + } + + for (const key of Object.keys(source)) { + const targetValue = target[key]; + const sourceValue = source[key]; + + // Adopt the plugin which was found at first. + if (targetValue === void 0) { + if (sourceValue.error) { + throw sourceValue.error; + } + target[key] = sourceValue; + } + } +} + +/** + * Merge rules. + * `target`'s definition is prior to `source`'s. + * + * @param {Record} target The destination to merge + * @param {Record|undefined} source The source to merge. + * @returns {void} + */ +function mergeRules(target, source) { + if (!isNonNullObject(source)) { + return; + } + + for (const key of Object.keys(source)) { + const targetDef = target[key]; + const sourceDef = source[key]; + + // Adopt the rule config which was found at first. + if (targetDef === void 0) { + if (Array.isArray(sourceDef)) { + target[key] = [...sourceDef]; + } else { + target[key] = [sourceDef]; + } + + /* + * If the first found rule config is severity only and the current rule + * config has options, merge the severity and the options. + */ + } else if ( + targetDef.length === 1 && + Array.isArray(sourceDef) && + sourceDef.length >= 2 + ) { + targetDef.push(...sourceDef.slice(1)); + } + } +} + +/** + * Create the extracted config. + * @param {ConfigArray} instance The config elements. + * @param {number[]} indices The indices to use. + * @returns {ExtractedConfig} The extracted config. + */ +function createConfig(instance, indices) { + const slots = internalSlotsMap.get(instance); + const config = new ExtractedConfig(); + + // Merge elements. + for (const index of indices) { + const element = instance[index]; + + // Adopt the parser which was found at first. + if (!config.parser && element.parser) { + if (element.parser.error) { + throw element.parser.error; + } + config.parser = element.parser; + } + + // Adopt the processor which was found at first. + if (!config.processor && element.processor) { + config.processor = element.processor; + } + + // Merge others. + mergeWithoutOverwrite(config.env, element.env); + mergeWithoutOverwrite(config.globals, element.globals); + mergeWithoutOverwrite(config.parserOptions, element.parserOptions); + mergeWithoutOverwrite(config.settings, element.settings); + mergePlugins(config.plugins, element.plugins, slots); + mergeRules(config.rules, element.rules); + } + + return config; +} + +/** + * Collect definitions. + * @template T, U + * @param {string} pluginId The plugin ID for prefix. + * @param {Record} defs The definitions to collect. + * @param {Map} map The map to output. + * @param {function(T): U} [normalize] The normalize function for each value. + * @returns {void} + */ +function collect(pluginId, defs, map, normalize) { + if (defs) { + const prefix = pluginId && `${pluginId}/`; + + for (const [key, value] of Object.entries(defs)) { + map.set( + `${prefix}${key}`, + normalize ? normalize(value) : value + ); + } + } +} + +/** + * Normalize a rule definition. + * @param {Function|Rule} rule The rule definition to normalize. + * @returns {Rule} The normalized rule definition. + */ +function normalizePluginRule(rule) { + return typeof rule === "function" ? { create: rule } : rule; +} + +/** + * Delete the mutation methods from a given map. + * @param {Map} map The map object to delete. + * @returns {void} + */ +function deleteMutationMethods(map) { + Object.defineProperties(map, { + clear: { configurable: true, value: void 0 }, + delete: { configurable: true, value: void 0 }, + set: { configurable: true, value: void 0 } + }); +} + +/** + * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array. + * @param {ConfigArrayElement[]} elements The config elements. + * @param {ConfigArrayInternalSlots} slots The internal slots. + * @returns {void} + */ +function initPluginMemberMaps(elements, slots) { + const processed = new Set(); + + slots.envMap = new Map(); + slots.processorMap = new Map(); + slots.ruleMap = new Map(); + + for (const element of elements) { + if (!element.plugins) { + continue; + } + + for (const [pluginId, value] of Object.entries(element.plugins)) { + const plugin = value.definition; + + if (!plugin || processed.has(pluginId)) { + continue; + } + processed.add(pluginId); + + collect(pluginId, plugin.environments, slots.envMap); + collect(pluginId, plugin.processors, slots.processorMap); + collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule); + } + } + + deleteMutationMethods(slots.envMap); + deleteMutationMethods(slots.processorMap); + deleteMutationMethods(slots.ruleMap); +} + +/** + * Create `envMap`, `processorMap`, `ruleMap` with the plugins in the config array. + * @param {ConfigArray} instance The config elements. + * @returns {ConfigArrayInternalSlots} The extracted config. + */ +function ensurePluginMemberMaps(instance) { + const slots = internalSlotsMap.get(instance); + + if (!slots.ruleMap) { + initPluginMemberMaps(instance, slots); + } + + return slots; +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +/** + * The Config Array. + * + * `ConfigArray` instance contains all settings, parsers, and plugins. + * You need to call `ConfigArray#extractConfig(filePath)` method in order to + * extract, merge and get only the config data which is related to an arbitrary + * file. + * + * @extends {Array} + */ +class ConfigArray extends Array { + + /** + * Get the plugin environments. + * The returned map cannot be mutated. + * @type {ReadonlyMap} The plugin environments. + */ + get pluginEnvironments() { + return ensurePluginMemberMaps(this).envMap; + } + + /** + * Get the plugin processors. + * The returned map cannot be mutated. + * @type {ReadonlyMap} The plugin processors. + */ + get pluginProcessors() { + return ensurePluginMemberMaps(this).processorMap; + } + + /** + * Get the plugin rules. + * The returned map cannot be mutated. + * @returns {ReadonlyMap} The plugin rules. + */ + get pluginRules() { + return ensurePluginMemberMaps(this).ruleMap; + } + + /** + * Check if this config has `root` flag. + * @type {boolean} + */ + get root() { + for (let i = this.length - 1; i >= 0; --i) { + const root = this[i].root; + + if (typeof root === "boolean") { + return root; + } + } + return false; + } + + /** + * Extract the config data which is related to a given file. + * @param {string} filePath The absolute path to the target file. + * @returns {ExtractedConfig} The extracted config data. + */ + extractConfig(filePath) { + const { cache } = internalSlotsMap.get(this); + const indices = getMatchedIndices(this, filePath); + const cacheKey = indices.join(","); + + if (!cache.has(cacheKey)) { + cache.set(cacheKey, createConfig(this, indices)); + } + + return cache.get(cacheKey); + } +} + +const exportObject = { + ConfigArray, + + /** + * Get the used extracted configs. + * CLIEngine will use this method to collect used deprecated rules. + * @param {ConfigArray} instance The config array object to get. + * @returns {ExtractedConfig[]} The used extracted configs. + * @private + */ + getUsedExtractedConfigs(instance) { + const { cache } = internalSlotsMap.get(instance); + + return Array.from(cache.values()); + } +}; + +module.exports = exportObject; diff --git a/lib/cli-engine/config-array/config-dependency.js b/lib/cli-engine/config-array/config-dependency.js new file mode 100644 index 00000000000..08a359649ad --- /dev/null +++ b/lib/cli-engine/config-array/config-dependency.js @@ -0,0 +1,104 @@ +/** + * @fileoverview `ConfigDependency` class. + * + * `ConfigDependency` class expresses a loaded parser or plugin. + * + * If the parser or plugin was loaded successfully, it has `definition` property + * and `filePath` property. Otherwise, it has `error` property. + * + * When `JSON.stringify()` converted a `ConfigDependency` object to a JSON, it + * omits `definition` property. + * + * `ConfigArrayFactory` creates `ConfigDependency` objects when it loads parsers + * or plugins. + * + * @author Toru Nagashima + */ +"use strict"; + +const util = require("util"); + +/** + * The class is to store parsers or plugins. + * This class hides the loaded object from `JSON.stringify()` and `console.log`. + * @template T + */ +class ConfigDependency { + + /** + * Initialize this instance. + * @param {Object} data The dependency data. + * @param {T} [data.definition] The dependency if the loading succeeded. + * @param {Error} [data.error] The error object if the loading failed. + * @param {string} [data.filePath] The actual path to the dependency if the loading succeeded. + * @param {string} data.id The ID of this dependency. + * @param {string} data.importerName The name of the config file which loads this dependency. + * @param {string} data.importerPath The path to the config file which loads this dependency. + */ + constructor({ + definition = null, + error = null, + filePath = null, + id, + importerName, + importerPath + }) { + + /** + * The loaded dependency if the loading succeeded. + * @type {T|null} + */ + this.definition = definition; + + /** + * The error object if the loading failed. + * @type {Error|null} + */ + this.error = error; + + /** + * The loaded dependency if the loading succeeded. + * @type {string|null} + */ + this.filePath = filePath; + + /** + * The ID of this dependency. + * @type {string} + */ + this.id = id; + + /** + * The name of the config file which loads this dependency. + * @type {string} + */ + this.importerName = importerName; + + /** + * The path to the config file which loads this dependency. + * @type {string} + */ + this.importerPath = importerPath; + } + + /** + * @returns {Object} a JSON compatible object. + */ + toJSON() { + const { + definition: _ignore, // eslint-disable-line no-unused-vars + ...obj + } = this; + + return obj; + } + + [util.inspect.custom]() { + return this.toJSON(); + } +} + +/** @typedef {ConfigDependency} DependentParser */ +/** @typedef {ConfigDependency} DependentPlugin */ + +module.exports = { ConfigDependency }; diff --git a/lib/cli-engine/config-array/extracted-config.js b/lib/cli-engine/config-array/extracted-config.js new file mode 100644 index 00000000000..930488e8f83 --- /dev/null +++ b/lib/cli-engine/config-array/extracted-config.js @@ -0,0 +1,98 @@ +/** + * @fileoverview `ExtractedConfig` class. + * + * `ExtractedConfig` class expresses a final configuration for a specific file. + * + * It provides one method. + * + * - `toCompatibleObjectAsConfigFileContent()` + * Convert this configuration to the compatible object as the content of + * config files. It converts the loaded parser and plugins to strings. + * `CLIEngine#getConfigForFile(filePath)` method uses this method. + * + * `ConfigArray#extractConfig(filePath)` creates a `ExtractedConfig` instance. + * + * @author Toru Nagashima + */ +"use strict"; + +// For VSCode intellisense +/** @typedef {import("../../util/types").ConfigData} ConfigData */ +/** @typedef {import("../../util/types").GlobalConf} GlobalConf */ +/** @typedef {import("../../util/types").SeverityConf} SeverityConf */ +/** @typedef {import("./config-dependency").DependentParser} DependentParser */ +/** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */ + +/** + * The class for extracted config data. + */ +class ExtractedConfig { + constructor() { + + /** + * Environments. + * @type {Record} + */ + this.env = {}; + + /** + * Global variables. + * @type {Record} + */ + this.globals = {}; + + /** + * Parser definition. + * @type {DependentParser|null} + */ + this.parser = null; + + /** + * Options for the parser. + * @type {Object} + */ + this.parserOptions = {}; + + /** + * Plugin definitions. + * @type {Record} + */ + this.plugins = {}; + + /** + * Processor ID. + * @type {string|null} + */ + this.processor = null; + + /** + * Rule settings. + * @type {Record} + */ + this.rules = {}; + + /** + * Shared settings. + * @type {Object} + */ + this.settings = {}; + } + + /** + * Convert this config to the compatible object as a config file content. + * @returns {ConfigData} The converted object. + */ + toCompatibleObjectAsConfigFileContent() { + const { + processor: _ignore, // eslint-disable-line no-unused-vars + ...config + } = this; + + config.parser = config.parser && config.parser.filePath; + config.plugins = Object.keys(config.plugins).filter(Boolean).reverse(); + + return config; + } +} + +module.exports = { ExtractedConfig }; diff --git a/lib/cli-engine/config-array/index.js b/lib/cli-engine/config-array/index.js new file mode 100644 index 00000000000..de8831906fd --- /dev/null +++ b/lib/cli-engine/config-array/index.js @@ -0,0 +1,18 @@ +/** + * @fileoverview `ConfigArray` class. + * @author Toru Nagashima + */ +"use strict"; + +const { ConfigArray, getUsedExtractedConfigs } = require("./config-array"); +const { ConfigDependency } = require("./config-dependency"); +const { ExtractedConfig } = require("./extracted-config"); +const { OverrideTester } = require("./override-tester"); + +module.exports = { + ConfigArray, + ConfigDependency, + ExtractedConfig, + OverrideTester, + getUsedExtractedConfigs +}; diff --git a/lib/cli-engine/config-array/override-tester.js b/lib/cli-engine/config-array/override-tester.js new file mode 100644 index 00000000000..2618013f6c8 --- /dev/null +++ b/lib/cli-engine/config-array/override-tester.js @@ -0,0 +1,177 @@ +/** + * @fileoverview `OverrideTester` class. + * + * `OverrideTester` class handles `files` property and `excludedFiles` property + * of `overrides` config. + * + * It provides one method. + * + * - `test(filePath)` + * Test if a file path matches the pair of `files` property and + * `excludedFiles` property. The `filePath` argument must be an absolute + * path. + * + * `ConfigArrayFactory` creates `OverrideTester` objects when it processes + * `overrides` properties. + * + * @author Toru Nagashima + */ +"use strict"; + +const path = require("path"); +const util = require("util"); +const { Minimatch } = require("minimatch"); +const minimatchOpts = { dot: true, matchBase: true }; + +/** + * @typedef {Object} Pattern + * @property {InstanceType[] | null} includes The positive matchers. + * @property {InstanceType[] | null} excludes The negative matchers. + */ + +/** + * Normalize a given pattern to an array. + * @param {string|string[]|undefined} patterns A glob pattern or an array of glob patterns. + * @returns {string[]|null} Normalized patterns. + * @private + */ +function normalizePatterns(patterns) { + if (Array.isArray(patterns)) { + return patterns.filter(Boolean); + } + if (typeof patterns === "string" && patterns) { + return [patterns]; + } + return []; +} + +/** + * Create the matchers of given patterns. + * @param {string[]} patterns The patterns. + * @returns {InstanceType[] | null} The matchers. + */ +function toMatcher(patterns) { + if (patterns.length === 0) { + return null; + } + return patterns.map(pattern => new Minimatch(pattern, minimatchOpts)); +} + +/** + * Convert a given matcher to string. + * @param {Pattern} matchers The matchers. + * @returns {string} The string expression of the matcher. + */ +function patternToJson({ includes, excludes }) { + return { + includes: includes && includes.map(m => m.pattern), + excludes: excludes && excludes.map(m => m.pattern) + }; +} + +/** + * The class to test given paths are matched by the patterns. + */ +class OverrideTester { + + /** + * Create a tester with given criteria. + * If there are no criteria, returns `null`. + * @param {string|string[]} files The glob patterns for included files. + * @param {string|string[]} excludedFiles The glob patterns for excluded files. + * @param {string} basePath The path to the base directory to test paths. + * @returns {OverrideTester|null} The created instance or `null`. + */ + static create(files, excludedFiles, basePath) { + const includePatterns = normalizePatterns(files); + const excludePatterns = normalizePatterns(excludedFiles); + const allPatterns = includePatterns.concat(excludePatterns); + + if (allPatterns.length === 0) { + return null; + } + + // Rejects absolute paths or relative paths to parents. + for (const pattern of allPatterns) { + if (path.isAbsolute(pattern) || pattern.includes("..")) { + throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); + } + } + + const includes = toMatcher(includePatterns); + const excludes = toMatcher(excludePatterns); + + return new OverrideTester([{ includes, excludes }], basePath); + } + + /** + * Combine two testers by logical and. + * If either of testers was `null`, returns another. + * @param {OverrideTester|null} a A tester. + * @param {OverrideTester|null} b Another tester. + * @returns {OverrideTester|null} Combined tester. + */ + static and(a, b) { + if (!b) { + return a; + } + if (!a) { + return b; + } + + return new OverrideTester(a.patterns.concat(b.patterns), a.basePath); + } + + /** + * Initialize this instance. + * @param {Pattern[]} patterns The matchers. + * @param {string} basePath The base path. + */ + constructor(patterns, basePath) { + + /** @type {Pattern[]} */ + this.patterns = patterns; + + /** @type {string} */ + this.basePath = basePath; + } + + /** + * Test if a given path is matched or not. + * @param {string} filePath The absolute path to the target file. + * @returns {boolean} `true` if the path was matched. + */ + test(filePath) { + if (typeof filePath !== "string" || !path.isAbsolute(filePath)) { + throw new Error(`'filePath' should be an absolute path, but got ${filePath}.`); + } + const relativePath = path.relative(this.basePath, filePath); + + return this.patterns.every(({ includes, excludes }) => ( + (!includes || includes.some(m => m.match(relativePath))) && + (!excludes || !excludes.some(m => m.match(relativePath))) + )); + } + + /** + * @returns {Object} a JSON compatible object. + */ + toJSON() { + if (this.patterns.length === 1) { + return { + ...patternToJson(this.patterns[0]), + basePath: this.basePath + }; + } + return { + AND: this.patterns.map(patternToJson), + basePath: this.basePath + }; + } + + [util.inspect.custom]() { + return this.toJSON(); + } +} + +module.exports = { OverrideTester }; diff --git a/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js new file mode 100644 index 00000000000..9061f9df554 --- /dev/null +++ b/lib/cli-engine/file-enumerator.js @@ -0,0 +1,459 @@ +/** + * @fileoverview `FileEnumerator` class. + * + * `FileEnumerator` class has two responsibilities: + * + * 1. Find target files by processing glob patterns. + * 2. Tie each target file and appropriate configuration. + * + * It provies a method: + * + * - `iterateFiles(patterns)` + * Iterate files which are matched by given patterns together with the + * corresponded configuration. This is for `CLIEngine#executeOnFiles()`. + * While iterating files, it loads the configuration file of each directory + * before iterate files on the directory, so we can use the configuration + * files to determine target files. + * + * @example + * const enumerator = new FileEnumerator(); + * const linter = new Linter(); + * + * for (const { config, filePath } of enumerator.iterateFiles(["*.js"])) { + * const code = fs.readFileSync(filePath, "utf8"); + * const messages = linter.verify(code, config, filePath); + * + * console.log(messages); + * } + * + * @author Toru Nagashima + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const fs = require("fs"); +const path = require("path"); +const getGlobParent = require("glob-parent"); +const isGlob = require("is-glob"); +const { escapeRegExp } = require("lodash"); +const { Minimatch } = require("minimatch"); +const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory"); +const { IgnoredPaths } = require("../util/ignored-paths"); +const debug = require("debug")("eslint:file-enumerator"); + +// debug.enabled = true; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const minimatchOpts = { dot: true, matchBase: true }; +const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u; +const NONE = 0; +const IGNORED_SILENTLY = 1; +const IGNORED = 2; + +// For VSCode intellisense +/** @typedef {ReturnType} ConfigArray */ + +/** + * @typedef {Object} FileEnumeratorOptions + * @property {CascadingConfigArrayFactory} [configArrayFactory] The factory for config arrays. + * @property {string} [cwd] The base directory to start lookup. + * @property {string[]} [extensions] The extensions to match files for directory patterns. + * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. + * @property {boolean} [ignore] The flag to check ignored files. + * @property {IgnoredPaths} [ignoredPaths] The ignored paths. + * @property {string[]} [rulePaths] The value of `--rulesdir` option. + */ + +/** + * @typedef {Object} FileAndConfig + * @property {string} filePath The path to a target file. + * @property {ConfigArray} config The config entries of that file. + * @property {boolean} ignored If `true` then this file should be ignored and warned because it was directly specified. + */ + +/** + * @typedef {Object} FileEntry + * @property {string} filePath The path to a target file. + * @property {ConfigArray} config The config entries of that file. + * @property {NONE|IGNORED_SILENTLY|IGNORED} flag The flag. + * - `NONE` means the file is a target file. + * - `IGNORED_SILENTLY` means the file should be ignored silently. + * - `IGNORED` means the file should be ignored and warned because it was directly specified. + */ + +/** + * @typedef {Object} FileEnumeratorInternalSlots + * @property {CascadingConfigArrayFactory} configArrayFactory The factory for config arrays. + * @property {string} cwd The base directory to start lookup. + * @property {RegExp} extRegExp The RegExp to test if a string ends with specific file extensions. + * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. + * @property {boolean} ignoreFlag The flag to check ignored files. + * @property {IgnoredPaths} ignoredPathsWithDotfiles The ignored paths but don't include dot files. + * @property {IgnoredPaths} ignoredPaths The ignored paths. + */ + +/** @type {WeakMap} */ +const internalSlotsMap = new WeakMap(); + +/** + * Check if a string is a glob pattern or not. + * @param {string} pattern A glob pattern. + * @returns {boolean} `true` if the string is a glob pattern. + */ +function isGlobPattern(pattern) { + return isGlob(path.sep === "\\" ? pattern.replace(/\\/gu, "/") : pattern); +} + +/** + * Get stats of a given path. + * @param {string} filePath The path to target file. + * @returns {fs.Stats|null} The stats. + * @private + */ +function statSafeSync(filePath) { + try { + return fs.statSync(filePath); + } catch (error) { + /* istanbul ignore next */ + if (error.code !== "ENOENT") { + throw error; + } + return null; + } +} + +/** + * Get filenames in a given path to a directory. + * @param {string} directoryPath The path to target directory. + * @returns {string[]} The filenames. + * @private + */ +function readdirSafeSync(directoryPath) { + try { + return fs.readdirSync(directoryPath); + } catch (error) { + /* istanbul ignore next */ + if (error.code !== "ENOENT") { + throw error; + } + return []; + } +} + +/** + * The error type when no files match a glob. + */ +class NoFilesFoundError extends Error { + + /** + * @param {string} pattern - The glob pattern which was not found. + * @param {boolean} globDisabled - If `true` then the pattern was a glob pattern, but glob was disabled. + */ + constructor(pattern, globDisabled) { + super(`No files matching '${pattern}' were found${globDisabled ? " (glob was disabled)" : ""}.`); + this.messageTemplate = "file-not-found"; + this.messageData = { pattern, globDisabled }; + } +} + +/** + * The error type when there are files matched by a glob, but all of them have been ignored. + */ +class AllFilesIgnoredError extends Error { + + /** + * @param {string} pattern - The glob pattern which was not found. + */ + constructor(pattern) { + super(`All files matched by '${pattern}' are ignored.`); + this.messageTemplate = "all-files-ignored"; + this.messageData = { pattern }; + } +} + +/** + * This class provides the functionality that enumerates every file which is + * matched by given glob patterns and that configuration. + */ +class FileEnumerator { + + /** + * Initialize this enumerator. + * @param {FileEnumeratorOptions} options The options. + */ + constructor({ + cwd = process.cwd(), + configArrayFactory = new CascadingConfigArrayFactory({ cwd }), + extensions = [".js"], + globInputPaths = true, + ignore = true, + ignoredPaths = new IgnoredPaths({ cwd, ignore }) + } = {}) { + internalSlotsMap.set(this, { + configArrayFactory, + cwd, + extRegExp: new RegExp( + `.\\.(?:${extensions + .map(ext => escapeRegExp( + ext.startsWith(".") + ? ext.slice(1) + : ext + )) + .join("|") + })$`, + "u" + ), + globInputPaths, + ignoreFlag: ignore, + ignoredPaths, + ignoredPathsWithDotfiles: new IgnoredPaths({ + ...ignoredPaths.options, + dotfiles: true + }) + }); + } + + /** + * Iterate files which are matched by given glob patterns. + * @param {string|string[]} patternOrPatterns The glob patterns to iterate files. + * @returns {IterableIterator} The found files. + */ + *iterateFiles(patternOrPatterns) { + const { globInputPaths } = internalSlotsMap.get(this); + const patterns = Array.isArray(patternOrPatterns) + ? patternOrPatterns + : [patternOrPatterns]; + + debug("Start to iterate files: %o", patterns); + + // The set of paths to remove duplicate. + const set = new Set(); + + for (const pattern of patterns) { + let foundRegardlessOfIgnored = false; + let found = false; + + // Skip empty string. + if (!pattern) { + continue; + } + + // Iterate files of this pttern. + for (const { config, filePath, flag } of this._iterateFiles(pattern)) { + foundRegardlessOfIgnored = true; + if (flag === IGNORED_SILENTLY) { + continue; + } + found = true; + + // Remove duplicate paths while yielding paths. + if (!set.has(filePath)) { + set.add(filePath); + yield { + config, + filePath, + ignored: flag === IGNORED + }; + } + } + + // Raise an error if any files were not found. + if (!foundRegardlessOfIgnored) { + throw new NoFilesFoundError( + pattern, + !globInputPaths && isGlob(pattern) + ); + } + if (!found) { + throw new AllFilesIgnoredError(pattern); + } + } + + debug(`Complete iterating files: ${JSON.stringify(patterns)}`); + } + + /** + * Iterate files which are matched by a given glob pattern. + * @param {string} pattern The glob pattern to iterate files. + * @returns {IterableIterator} The found files. + */ + _iterateFiles(pattern) { + const { cwd, globInputPaths } = internalSlotsMap.get(this); + const absolutePath = path.resolve(cwd, pattern); + + if (globInputPaths && isGlobPattern(pattern)) { + return this._iterateFilesWithGlob( + absolutePath, + dotfilesPattern.test(pattern) + ); + } + + const stat = statSafeSync(absolutePath); + + if (stat && stat.isDirectory()) { + return this._iterateFilesWithDirectory( + absolutePath, + dotfilesPattern.test(pattern) + ); + } + + if (stat && stat.isFile()) { + return this._iterateFilesWithFile(absolutePath); + } + + return []; + } + + /** + * Iterate a file which is matched by a given path. + * @param {string} filePath The path to the target file. + * @returns {IterableIterator} The found files. + * @private + */ + _iterateFilesWithFile(filePath) { + debug(`File: ${filePath}`); + + const { configArrayFactory } = internalSlotsMap.get(this); + const config = configArrayFactory.getConfigArrayForFile(filePath); + const ignored = this._isIgnoredFile(filePath, { direct: true }); + const flag = ignored ? IGNORED : NONE; + + return [{ config, filePath, flag }]; + } + + /** + * Iterate files in a given path. + * @param {string} directoryPath The path to the target directory. + * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default. + * @returns {IterableIterator} The found files. + * @private + */ + _iterateFilesWithDirectory(directoryPath, dotfiles) { + debug(`Directory: ${directoryPath}`); + + return this._iterateFilesRecursive( + directoryPath, + { dotfiles, recursive: true, selector: null } + ); + } + + /** + * Iterate files which are matched by a given glob pattern. + * @param {string} pattern The glob pattern to iterate files. + * @param {boolean} dotfiles If `true` then it doesn't skip dot files by default. + * @returns {IterableIterator} The found files. + * @private + */ + _iterateFilesWithGlob(pattern, dotfiles) { + debug(`Glob: ${pattern}`); + + const directoryPath = getGlobParent(pattern); + const globPart = pattern.slice(directoryPath.length + 1); + + /* + * recursive if there are `**` or path separators in the glob part. + * Otherwise, patterns such as `src/*.js`, it doesn't need recursive. + */ + const recursive = /\*\*|\/|\\/u.test(globPart); + const selector = new Minimatch(pattern, minimatchOpts); + + debug(`recursive? ${recursive}`); + + return this._iterateFilesRecursive( + directoryPath, + { dotfiles, recursive, selector } + ); + } + + /** + * Iterate files in a given path. + * @param {string} directoryPath The path to the target directory. + * @param {Object} options The options to iterate files. + * @param {boolean} [options.dotfiles] If `true` then it doesn't skip dot files by default. + * @param {boolean} [options.recursive] If `true` then it dives into sub directories. + * @param {InstanceType} [options.selector] The matcher to choose files. + * @returns {IterableIterator} The found files. + * @private + */ + *_iterateFilesRecursive(directoryPath, options) { + if (this._isIgnoredFile(directoryPath, options)) { + return; + } + debug(`Enter the directory: ${directoryPath}`); + const { configArrayFactory, extRegExp } = internalSlotsMap.get(this); + + /** @type {ConfigArray|null} */ + let config = null; + + // Enumerate the files of this directory. + for (const filename of readdirSafeSync(directoryPath)) { + const filePath = path.join(directoryPath, filename); + const stat = statSafeSync(filePath); // TODO: Use `withFileTypes` in the future. + + // Check if the file is matched. + if (stat && stat.isFile()) { + if (!config) { + config = configArrayFactory.getConfigArrayForFile(filePath); + } + const ignored = this._isIgnoredFile(filePath, options); + const flag = ignored ? IGNORED_SILENTLY : NONE; + const matched = options.selector + + // Started with a glob pattern; choose by the pattern. + ? options.selector.match(filePath) + + // Started with a directory path; choose by file extensions. + : extRegExp.test(filePath); + + if (matched) { + debug(`Yield: ${filename}${ignored ? " but ignored" : ""}`); + yield { config, filePath, flag }; + } else { + debug(`Didn't match: ${filename}`); + } + + // Dive into the sub directory. + } else if (options.recursive && stat && stat.isDirectory()) { + yield* this._iterateFilesRecursive(filePath, options); + } + } + + debug(`Leave the directory: ${directoryPath}`); + } + + /** + * Check if a given file should be ignored. + * @param {string} filePath The path to a file to check. + * @param {Object} options Options + * @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default. + * @param {boolean} [options.direct] If `true` then this is a direct specified file. + * @returns {boolean} `true` if the file should be ignored. + * @private + */ + _isIgnoredFile(filePath, { dotfiles = false, direct = false }) { + const { + ignoreFlag, + ignoredPaths, + ignoredPathsWithDotfiles + } = internalSlotsMap.get(this); + const adoptedIgnoredPaths = dotfiles + ? ignoredPathsWithDotfiles + : ignoredPaths; + + return ignoreFlag + ? adoptedIgnoredPaths.contains(filePath) + : (!direct && adoptedIgnoredPaths.contains(filePath, "default")); + } +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { FileEnumerator }; diff --git a/lib/cli.js b/lib/cli.js index 9ce81e55425..06df02550e1 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -18,7 +18,7 @@ const fs = require("fs"), path = require("path"), options = require("./options"), - CLIEngine = require("./cli-engine"), + CLIEngine = require("./cli-engine").CLIEngine, mkdirp = require("mkdirp"), log = require("./util/logging"); diff --git a/lib/config.js b/lib/config.js deleted file mode 100644 index 10e10830c8a..00000000000 --- a/lib/config.js +++ /dev/null @@ -1,388 +0,0 @@ -/** - * @fileoverview Responsible for loading config files - * @author Seth McLaughlin - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const path = require("path"), - os = require("os"), - ConfigOps = require("./config/config-ops"), - ConfigFile = require("./config/config-file"), - ConfigCache = require("./config/config-cache"), - Plugins = require("./config/plugins"), - FileFinder = require("./util/file-finder"), - relativeModuleResolver = require("./util/relative-module-resolver"); - -const debug = require("debug")("eslint:config"); - -//------------------------------------------------------------------------------ -// Constants -//------------------------------------------------------------------------------ - -const PERSONAL_CONFIG_DIR = os.homedir(); -const SUBCONFIG_SEP = ":"; - -// A string which is not a valid config filepath, used to index a `baseConfig` object in the cache. -const BASE_CONFIG_FILEPATH_CACHE_KEY = ""; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Determines if any rules were explicitly passed in as options. - * @param {Object} options The options used to create our configuration. - * @returns {boolean} True if rules were passed in as options, false otherwise. - * @private - */ -function hasRules(options) { - return options.rules && Object.keys(options.rules).length > 0; -} - -//------------------------------------------------------------------------------ -// API -//------------------------------------------------------------------------------ - -/** - * Configuration class - */ -class Config { - - /** - * @param {Object} providedOptions Options to be passed in - * @param {Linter} linterContext Linter instance object - */ - constructor(providedOptions, linterContext) { - const options = providedOptions || {}; - - this.linterContext = linterContext; - this.options = options; - - /* - * `Linter#getRules` is slow, but a rule map is needed for every config file processed. - * Cache the rule map and keep it up to date. - */ - this.ruleMap = new Map(linterContext.getRules()); - this.plugins = new Plugins(linterContext.environments, { - defineRule: (ruleId, rule) => { - linterContext.defineRule(ruleId, rule); - this.ruleMap.set(ruleId, rule); - }, - pluginRootPath: this.options.cwd - }); - this.ignore = options.ignore; - this.ignorePath = options.ignorePath; - this.parser = options.parser; - this.parserOptions = options.parserOptions || {}; - - this.configCache = new ConfigCache(); - this._rootConfigReferencePath = path.join(this.options.cwd, "__placeholder__.js"); - - this.baseConfig = options.baseConfig - ? ConfigFile.loadObject(this, { - config: options.baseConfig, - filePath: path.join(this.options.cwd, "__baseConfig_placeholder_filename__.js"), - configFullName: "(Provided base config)" - }) - : { rules: {} }; - this.configCache.setConfig(BASE_CONFIG_FILEPATH_CACHE_KEY, this.baseConfig); - - this.useEslintrc = (options.useEslintrc !== false); - - this.env = (options.envs || []).reduce((envs, name) => { - envs[name] = true; - return envs; - }, {}); - - /* - * Handle declared globals. - * For global variable foo, handle "foo:false" and "foo:true" to set - * whether global is writable. - * If user declares "foo", convert to "foo:false". - */ - this.globals = (options.globals || []).reduce((globals, def) => { - const parts = def.split(SUBCONFIG_SEP); - - globals[parts[0]] = (parts.length > 1 && parts[1] === "true"); - - return globals; - }, {}); - - this.loadSpecificConfig(options.configFile); - - // Empty values in configs don't merge properly - const cliConfigOptions = { - env: this.env, - rules: this.options.rules, - globals: this.globals, - parserOptions: this.parserOptions, - plugins: this.options.plugins - }; - - this.cliConfig = {}; - Object.keys(cliConfigOptions).forEach(configKey => { - const value = cliConfigOptions[configKey]; - - if (value) { - this.cliConfig[configKey] = value; - } - }); - } - - getPluginRootPath() { - return this.options.cwd; - } - - /** - * Loads the config options from a config specified on the command line, relative to the CWD. - * @param {string} [config] A shareable named config or path to a config file. - * @returns {void} - */ - loadSpecificConfig(config) { - if (config) { - debug(`Using command line config ${config}`); - const resolvedPath = path.resolve(this.options.cwd, config); - - this.specificConfigInfo = { - filePath: resolvedPath, - config: ConfigFile.load( - resolvedPath, - this, - this._rootConfigReferencePath - ) - }; - } - } - - /** - * Gets the personal config object from user's home directory. - * @returns {Object} the personal config object (null if there is no personal config) - * @private - */ - getPersonalConfig() { - if (typeof this.personalConfig === "undefined") { - let config; - const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR); - - if (filename) { - debug("Using personal config"); - config = ConfigFile.load(filename, this, this._rootConfigReferencePath); - } - - this.personalConfig = config ? { config, filePath: filename } : null; - } - - return this.personalConfig; - } - - /** - * Builds a hierarchy of config objects, including the base config, all local configs from the directory tree, - * and a config file specified on the command line, if applicable. - * @param {string} directory a file in whose directory we start looking for a local config - * @returns {{filePath: (string|null), config: Config}[]} The config objects, in ascending order of precedence - * @private - */ - getConfigHierarchy(directory) { - debug(`Constructing config file hierarchy for ${directory}`); - - // Step 1: Always include baseConfig - let configs = [{ filePath: BASE_CONFIG_FILEPATH_CACHE_KEY, config: this.baseConfig }]; - - // Step 2: Add user-specified config from .eslintrc.* and package.json files - if (this.useEslintrc) { - debug("Using .eslintrc and package.json files"); - configs = configs.concat(this.getLocalConfigHierarchy(directory)); - } else { - debug("Not using .eslintrc or package.json files"); - } - - // Step 3: Merge in command line config file - if (this.specificConfigInfo) { - debug("Using command line config file"); - configs.push(this.specificConfigInfo); - } - - return configs; - } - - /** - * Gets a list of config objects extracted from local config files that apply to the current directory, in - * descending order, beginning with the config that is highest in the directory tree. - * @param {string} directory The directory to start looking in for local config files. - * @returns {Object[]} The shallow local config objects, in ascending order of precedence (closest to the current - * directory at the end), or an empty array if there are no local configs. - * @private - */ - getLocalConfigHierarchy(directory) { - const localConfigFiles = this.findLocalConfigFiles(directory), - projectConfigPath = ConfigFile.getFilenameForDirectory(this.options.cwd), - searched = [], - configs = []; - - for (const localConfigFile of localConfigFiles) { - const localConfigDirectory = path.dirname(localConfigFile); - const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory); - - if (localConfigHierarchyCache) { - const localConfigHierarchy = localConfigHierarchyCache.concat(configs); - - this.configCache.setHierarchyLocalConfigs(searched, localConfigHierarchy); - return localConfigHierarchy; - } - - /* - * Don't consider the personal config file in the home directory, - * except if the home directory is the same as the current working directory - */ - if (localConfigDirectory === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) { - continue; - } - - debug(`Loading ${localConfigFile}`); - const localConfig = ConfigFile.load(localConfigFile, this, this._rootConfigReferencePath); - - // Ignore empty config files - if (!localConfig) { - continue; - } - - debug(`Using ${localConfigFile}`); - configs.unshift({ filePath: localConfigFile, config: localConfig }); - searched.push(localConfigDirectory); - - // Stop traversing if a config is found with the root flag set - if (localConfig.root) { - break; - } - } - - if (!configs.length && !this.specificConfigInfo) { - - // Fall back on the personal config from ~/.eslintrc - debug("Using personal config file"); - const personalConfig = this.getPersonalConfig(); - - if (personalConfig) { - configs.unshift(personalConfig); - } else if (!hasRules(this.options) && !this.options.baseConfig) { - - // No config file, no manual configuration, and no rules, so error. - const noConfigError = new Error("No ESLint configuration found."); - - noConfigError.messageTemplate = "no-config-found"; - noConfigError.messageData = { - directory, - filesExamined: localConfigFiles - }; - - throw noConfigError; - } - } - - // Set the caches for the parent directories - this.configCache.setHierarchyLocalConfigs(searched, configs); - - return configs; - } - - /** - * Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of - * entries, each of which in an object specifying a config file path and an array of override indices corresponding - * to entries in the config file's overrides section whose glob patterns match the specified file path; e.g., the - * vector entry { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main - * project .eslintrc file and its first and third override blocks apply to the current file. - * @param {string} sourceFilePath The file path for which to build the hierarchy and config vector. - * @returns {Array} config vector applicable to the specified path - * @private - */ - getConfigVector(sourceFilePath) { - const directory = sourceFilePath ? path.dirname(sourceFilePath) : this.options.cwd; - - return this.getConfigHierarchy(directory).map(({ config, filePath }) => { - const vectorEntry = { - filePath, - matchingOverrides: [] - }; - - if (config.overrides) { - const relativePath = path.relative( - filePath - ? path.dirname(filePath) - : this.options.cwd, - sourceFilePath || directory - ); - - config.overrides.forEach((override, i) => { - if (ConfigOps.pathMatchesGlobs(relativePath, override.files, override.excludedFiles)) { - vectorEntry.matchingOverrides.push(i); - } - }); - } - - return vectorEntry; - }); - } - - /** - * Finds local config files from the specified directory and its parent directories. - * @param {string} directory The directory to start searching from. - * @returns {GeneratorFunction} The paths of local config files found. - */ - findLocalConfigFiles(directory) { - if (!this.localConfigFinder) { - this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd); - } - - return this.localConfigFinder.findAllInDirectoryAndParents(directory); - } - - /** - * Builds the authoritative config object for the specified file path by merging the hierarchy of config objects - * that apply to the current file, including the base config (conf/eslint-recommended), the user's personal config - * from their homedir, all local configs from the directory tree, any specific config file passed on the command - * line, any configuration overrides set directly on the command line, and finally the environment configs - * (conf/environments). - * @param {string} filePath a file in whose directory we start looking for a local config - * @returns {Object} config object - */ - getConfig(filePath) { - const vector = this.getConfigVector(filePath); - let config = this.configCache.getMergedConfig(vector); - - if (config) { - debug("Using config from cache"); - return config; - } - - // Step 1: Merge in the filesystem configurations (base, local, and personal) - config = ConfigOps.getConfigFromVector(vector, this.configCache); - - // Step 2: Merge in command line configurations - config = ConfigOps.merge(config, this.cliConfig); - - if (this.cliConfig.plugins) { - this.plugins.loadAll(this.cliConfig.plugins); - } - - /* - * Step 3: Override parser only if it is passed explicitly through the command line - */ - if (this.parser) { - config = ConfigOps.merge(config, { parser: relativeModuleResolver(this.parser, this._rootConfigReferencePath) }); - } - - // Step 4: Apply environments to the config - config = ConfigOps.applyEnvironments(config, this.linterContext.environments); - - this.configCache.setMergedConfig(vector, config); - - return config; - } -} - -module.exports = Config; diff --git a/lib/config/autoconfig.js b/lib/config/autoconfig.js index f2c707be843..9c38c244951 100644 --- a/lib/config/autoconfig.js +++ b/lib/config/autoconfig.js @@ -10,7 +10,7 @@ //------------------------------------------------------------------------------ const lodash = require("lodash"), - Linter = require("../linter"), + { Linter } = require("../linter"), configRule = require("./config-rule"), ConfigOps = require("./config-ops"), recConfig = require("../../conf/eslint-recommended"); diff --git a/lib/config/config-cache.js b/lib/config/config-cache.js deleted file mode 100644 index 07436a87c8f..00000000000 --- a/lib/config/config-cache.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @fileoverview Responsible for caching config files - * @author Sylvan Mably - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Get a string hash for a config vector - * @param {Array} vector config vector to hash - * @returns {string} hash of the vector values - * @private - */ -function hash(vector) { - return JSON.stringify(vector); -} - -//------------------------------------------------------------------------------ -// API -//------------------------------------------------------------------------------ - -/** - * Configuration caching class - */ -module.exports = class ConfigCache { - - constructor() { - this.configFullNameCache = new Map(); - this.localHierarchyCache = new Map(); - this.mergedVectorCache = new Map(); - this.mergedCache = new Map(); - } - - /** - * Gets a config object from the cache for the specified config file path. - * @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'), - * or the absolute path to a config file. This should uniquely identify a config. - * @returns {Object|null} config object, if found in the cache, otherwise null - * @private - */ - getConfig(configFullName) { - return this.configFullNameCache.get(configFullName); - } - - /** - * Sets a config object in the cache for the specified config file path. - * @param {string} configFullName the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'), - * or the absolute path to a config file. This should uniquely identify a config. - * @param {Object} config the config object to add to the cache - * @returns {void} - * @private - */ - setConfig(configFullName, config) { - this.configFullNameCache.set(configFullName, config); - } - - /** - * Gets a list of hierarchy-local config objects that apply to the specified directory. - * @param {string} directory the path to the directory - * @returns {Object[]|null} a list of config objects, if found in the cache, otherwise null - * @private - */ - getHierarchyLocalConfigs(directory) { - return this.localHierarchyCache.get(directory); - } - - /** - * For each of the supplied parent directories, sets the list of config objects for that directory to the - * appropriate subset of the supplied parent config objects. - * @param {string[]} parentDirectories a list of parent directories to add to the config cache - * @param {Object[]} parentConfigs a list of config objects that apply to the lowest directory in parentDirectories - * @returns {void} - * @private - */ - setHierarchyLocalConfigs(parentDirectories, parentConfigs) { - parentDirectories.forEach((localConfigDirectory, i) => { - const directoryParentConfigs = parentConfigs.slice(0, parentConfigs.length - i); - - this.localHierarchyCache.set(localConfigDirectory, directoryParentConfigs); - }); - } - - /** - * Gets a merged config object corresponding to the supplied vector. - * @param {Array} vector the vector to find a merged config for - * @returns {Object|null} a merged config object, if found in the cache, otherwise null - * @private - */ - getMergedVectorConfig(vector) { - return this.mergedVectorCache.get(hash(vector)); - } - - /** - * Sets a merged config object in the cache for the supplied vector. - * @param {Array} vector the vector to save a merged config for - * @param {Object} config the merged config object to add to the cache - * @returns {void} - * @private - */ - setMergedVectorConfig(vector, config) { - this.mergedVectorCache.set(hash(vector), config); - } - - /** - * Gets a merged config object corresponding to the supplied vector, including configuration options from outside - * the vector. - * @param {Array} vector the vector to find a merged config for - * @returns {Object|null} a merged config object, if found in the cache, otherwise null - * @private - */ - getMergedConfig(vector) { - return this.mergedCache.get(hash(vector)); - } - - /** - * Sets a merged config object in the cache for the supplied vector, including configuration options from outside - * the vector. - * @param {Array} vector the vector to save a merged config for - * @param {Object} config the merged config object to add to the cache - * @returns {void} - * @private - */ - setMergedConfig(vector, config) { - this.mergedCache.set(hash(vector), config); - } -}; diff --git a/lib/config/config-file.js b/lib/config/config-file.js index 218963b27b0..77f14a47299 100644 --- a/lib/config/config-file.js +++ b/lib/config/config-file.js @@ -11,14 +11,7 @@ const fs = require("fs"), path = require("path"), - ConfigOps = require("./config-ops"), - validator = require("./config-validator"), - relativeModuleResolver = require("../util/relative-module-resolver"), - naming = require("../util/naming"), - stripComments = require("strip-json-comments"), - stringify = require("json-stable-stringify-without-jsonify"), - importFresh = require("import-fresh"), - Plugins = require("./plugins"); + stringify = require("json-stable-stringify-without-jsonify"); const debug = require("debug")("eslint:config-file"); @@ -43,204 +36,6 @@ function sortByKey(a, b) { // Private //------------------------------------------------------------------------------ -const CONFIG_FILES = [ - ".eslintrc.js", - ".eslintrc.yaml", - ".eslintrc.yml", - ".eslintrc.json", - ".eslintrc", - "package.json" -]; - -/** - * Convenience wrapper for synchronously reading file contents. - * @param {string} filePath The filename to read. - * @returns {string} The file contents, with the BOM removed. - * @private - */ -function readFile(filePath) { - return fs.readFileSync(filePath, "utf8").replace(/^\ufeff/u, ""); -} - -/** - * Determines if a given string represents a filepath or not using the same - * conventions as require(), meaning that the first character must be nonalphanumeric - * and not the @ sign which is used for scoped packages to be considered a file path. - * @param {string} filePath The string to check. - * @returns {boolean} True if it's a filepath, false if not. - * @private - */ -function isFilePath(filePath) { - return path.isAbsolute(filePath) || !/\w|@/u.test(filePath.charAt(0)); -} - -/** - * Loads a YAML configuration from a file. - * @param {string} filePath The filename to load. - * @returns {Object} The configuration object from the file. - * @throws {Error} If the file cannot be read. - * @private - */ -function loadYAMLConfigFile(filePath) { - debug(`Loading YAML config file: ${filePath}`); - - // lazy load YAML to improve performance when not used - const yaml = require("js-yaml"); - - try { - - // empty YAML file can be null, so always use - return yaml.safeLoad(readFile(filePath)) || {}; - } catch (e) { - debug(`Error reading YAML file: ${filePath}`); - e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; - throw e; - } -} - -/** - * Loads a JSON configuration from a file. - * @param {string} filePath The filename to load. - * @returns {Object} The configuration object from the file. - * @throws {Error} If the file cannot be read. - * @private - */ -function loadJSONConfigFile(filePath) { - debug(`Loading JSON config file: ${filePath}`); - - try { - return JSON.parse(stripComments(readFile(filePath))); - } catch (e) { - debug(`Error reading JSON file: ${filePath}`); - e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; - e.messageTemplate = "failed-to-read-json"; - e.messageData = { - path: filePath, - message: e.message - }; - throw e; - } -} - -/** - * Loads a legacy (.eslintrc) configuration from a file. - * @param {string} filePath The filename to load. - * @returns {Object} The configuration object from the file. - * @throws {Error} If the file cannot be read. - * @private - */ -function loadLegacyConfigFile(filePath) { - debug(`Loading config file: ${filePath}`); - - // lazy load YAML to improve performance when not used - const yaml = require("js-yaml"); - - try { - return yaml.safeLoad(stripComments(readFile(filePath))) || /* istanbul ignore next */ {}; - } catch (e) { - debug(`Error reading YAML file: ${filePath}`); - e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; - throw e; - } -} - -/** - * Loads a JavaScript configuration from a file. - * @param {string} filePath The filename to load. - * @returns {Object} The configuration object from the file. - * @throws {Error} If the file cannot be read. - * @private - */ -function loadJSConfigFile(filePath) { - debug(`Loading JS config file: ${filePath}`); - try { - return importFresh(filePath); - } catch (e) { - debug(`Error reading JavaScript file: ${filePath}`); - e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; - throw e; - } -} - -/** - * Loads a configuration from a package.json file. - * @param {string} filePath The filename to load. - * @returns {Object} The configuration object from the file. - * @throws {Error} If the file cannot be read. - * @private - */ -function loadPackageJSONConfigFile(filePath) { - debug(`Loading package.json config file: ${filePath}`); - try { - return loadJSONConfigFile(filePath).eslintConfig || null; - } catch (e) { - debug(`Error reading package.json file: ${filePath}`); - e.message = `Cannot read config file: ${filePath}\nError: ${e.message}`; - throw e; - } -} - -/** - * Creates an error to notify about a missing config to extend from. - * @param {string} configName The name of the missing config. - * @returns {Error} The error object to throw - * @private - */ -function configMissingError(configName) { - const error = new Error(`Failed to load config "${configName}" to extend from.`); - - error.messageTemplate = "extend-config-missing"; - error.messageData = { - configName - }; - return error; -} - -/** - * Loads a configuration file regardless of the source. Inspects the file path - * to determine the correctly way to load the config file. - * @param {Object} configInfo The path to the configuration. - * @returns {Object} The configuration information. - * @private - */ -function loadConfigFile(configInfo) { - const { filePath } = configInfo; - let config; - - switch (path.extname(filePath)) { - case ".js": - config = loadJSConfigFile(filePath); - if (configInfo.configName) { - config = config.configs[configInfo.configName]; - if (!config) { - throw configMissingError(configInfo.configFullName); - } - } - break; - - case ".json": - if (path.basename(filePath) === "package.json") { - config = loadPackageJSONConfigFile(filePath); - if (config === null) { - return null; - } - } else { - config = loadJSONConfigFile(filePath); - } - break; - - case ".yaml": - case ".yml": - config = loadYAMLConfigFile(filePath); - break; - - default: - config = loadLegacyConfigFile(filePath); - } - - return ConfigOps.merge(ConfigOps.createEmptyConfig(), config); -} - /** * Writes a configuration file in JSON format. * @param {Object} config The configuration object to write. @@ -289,7 +84,7 @@ function writeJSConfigFile(config, filePath) { const stringifiedContent = `module.exports = ${stringify(config, { cmp: sortByKey, space: 4 })};`; try { - const CLIEngine = require("../cli-engine"); + const { CLIEngine } = require("../cli-engine"); const linter = new CLIEngine({ baseConfig: config, fix: true, @@ -340,263 +135,10 @@ function write(config, filePath) { } } -/** - * Resolves a eslint core config path - * @param {string} name The eslint config name. - * @returns {string} The resolved path of the config. - * @private - */ -function getEslintCoreConfigPath(name) { - if (name === "eslint:recommended") { - - /* - * Add an explicit substitution for eslint:recommended to - * conf/eslint-recommended.js. - */ - return path.resolve(__dirname, "../../conf/eslint-recommended.js"); - } - - if (name === "eslint:all") { - - /* - * Add an explicit substitution for eslint:all to conf/eslint-all.js - */ - return path.resolve(__dirname, "../../conf/eslint-all.js"); - } - - throw configMissingError(name); -} - -/** - * Applies values from the "extends" field in a configuration file. - * @param {Object} config The configuration information. - * @param {Config} configContext Plugin context for the config instance - * @param {string} filePath The file path from which the configuration information - * was loaded. - * @returns {Object} A new configuration object with all of the "extends" fields - * loaded and merged. - * @private - */ -function applyExtends(config, configContext, filePath) { - const extendsList = Array.isArray(config.extends) ? config.extends : [config.extends]; - - // Make the last element in an array take the highest precedence - const flattenedConfig = extendsList.reduceRight((previousValue, extendedConfigReference) => { - try { - debug(`Loading ${extendedConfigReference}`); - - // eslint-disable-next-line no-use-before-define - return ConfigOps.merge(load(extendedConfigReference, configContext, filePath), previousValue); - } catch (err) { - - /* - * If the file referenced by `extends` failed to load, add the path - * to the configuration file that referenced it to the error - * message so the user is able to see where it was referenced from, - * then re-throw. - */ - err.message += `\nReferenced from: ${filePath}`; - - if (err.messageTemplate === "plugin-missing") { - err.messageData.configStack.push(filePath); - } - - throw err; - } - - }, Object.assign({}, config)); - - delete flattenedConfig.extends; - return flattenedConfig; -} - -/** - * Resolves a configuration file path into the fully-formed path, whether filename - * or package name. - * @param {string} extendedConfigReference The config to extend, as it appears in a config file - * @param {string} referencedFromPath The path to the config file that contains `extendedConfigReference` - * @param {string} pluginRootPath The absolute path to the directory where plugins should be resolved from - * @returns {Object} An object containing 3 properties: - * - 'filePath' (required) the resolved path that can be used directly to load the configuration. - * - 'configName' the name of the configuration inside the plugin, if this is a plugin config. - * - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'), - * or the absolute path to a config file. This should uniquely identify a config. - * @private - */ -function resolve(extendedConfigReference, referencedFromPath, pluginRootPath) { - if (extendedConfigReference.startsWith("eslint:")) { - return { - filePath: getEslintCoreConfigPath(extendedConfigReference), - configFullName: extendedConfigReference - }; - } - - if (extendedConfigReference.startsWith("plugin:")) { - const configFullName = extendedConfigReference; - const pluginName = extendedConfigReference.slice(7, extendedConfigReference.lastIndexOf("/")); - const configName = extendedConfigReference.slice(extendedConfigReference.lastIndexOf("/") + 1); - - return { - filePath: Plugins.resolve(pluginName, pluginRootPath), - configName, - configFullName - }; - } - - if (isFilePath(extendedConfigReference)) { - const resolvedConfigPath = path.resolve(path.dirname(referencedFromPath), extendedConfigReference); - - return { - filePath: resolvedConfigPath, - configFullName: resolvedConfigPath - }; - } - - const normalizedConfigName = naming.normalizePackageName(extendedConfigReference, "eslint-config"); - - return { - filePath: relativeModuleResolver(normalizedConfigName, referencedFromPath), - configFullName: extendedConfigReference - }; -} - - -/** - * Loads a config object, applying extends/parser/plugins if present. - * @param {Config} configContext Context for the config instance - * @param {Object} options.config a config object to load - * @param {string} options.filePath The path where `extends` and parsers should be resolved from (usually the path where the config was located from) - * @param {string} options.configFullName The full name of the config, for use in error messages - * @returns {Object} the config object with extends applied if present, or the passed config if not - * @private - */ -function loadObject(configContext, { config: configObject, filePath, configFullName }) { - const config = Object.assign({}, configObject); - - // ensure plugins are properly loaded first - if (config.plugins) { - try { - configContext.plugins.loadAll(config.plugins); - } catch (pluginLoadErr) { - if (pluginLoadErr.messageTemplate === "plugin-missing") { - pluginLoadErr.messageData.configStack.push(filePath); - } - throw pluginLoadErr; - } - } - - // include full path of parser if present - if (config.parser) { - try { - config.parser = relativeModuleResolver(config.parser, filePath); - } catch (err) { - if (err.code === "MODULE_NOT_FOUND" && config.parser === "espree") { - config.parser = require.resolve("espree"); - } else { - err.message += `\nFailed to resolve parser '${config.parser}' declared in '${filePath}'.`; - throw err; - } - } - } - - const ruleMap = configContext.ruleMap; - - // validate the configuration before continuing - validator.validate(config, ruleMap.get.bind(ruleMap), configContext.linterContext.environments, configFullName); - - /* - * If an `extends` property is defined, it represents a configuration file to use as - * a "parent". Load the referenced file and merge the configuration recursively. - */ - if (config.extends) { - return applyExtends(config, configContext, filePath); - } - - return config; -} - -/** - * Loads a configuration file from the given file path. - * @param {Object} configInfo The value from calling resolve() on a filename or package name. - * @param {Config} configContext Config context - * @returns {Object} The configuration information. - */ -function loadFromDisk(configInfo, configContext) { - const config = loadConfigFile(configInfo); - - // loadConfigFile will return null for a `package.json` file that does not have an `eslintConfig` property. - if (config) { - return loadObject(configContext, { config, filePath: configInfo.filePath, configFullName: configInfo.configFullName }); - } - - return null; -} - -/** - * Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet - * cached. - * @param {string} extendedConfigReference The config to extend, as it appears in a config file - * @param {Config} configContext Context for the config instance - * @param {string} referencedFromPath The path to the config file that contains `extendedConfigReference` (or where - * the `extendedConfigReference` should be resolved from, if it doesn't appear in a config file) - * @returns {Object} the parsed config object (empty object if there was a parse error) - * @private - */ -function load(extendedConfigReference, configContext, referencedFromPath) { - const resolvedPath = resolve(extendedConfigReference, referencedFromPath, configContext.getPluginRootPath()); - - const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName); - - if (cachedConfig) { - return cachedConfig; - } - - const config = loadFromDisk(resolvedPath, configContext); - - if (config) { - configContext.configCache.setConfig(resolvedPath.configFullName, config); - } - - return config; -} - -/** - * Checks whether the given filename points to a file - * @param {string} filename A path to a file - * @returns {boolean} `true` if a file exists at the given location - */ -function isExistingFile(filename) { - try { - return fs.statSync(filename).isFile(); - } catch (err) { - if (err.code === "ENOENT") { - return false; - } - throw err; - } -} - - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ module.exports = { - load, - loadObject, - resolve, - write, - applyExtends, - CONFIG_FILES, - - /** - * Retrieves the configuration filename for a given directory. It loops over all - * of the valid configuration filenames in order to find the first one that exists. - * @param {string} directory The directory to check for a config file. - * @returns {?string} The filename of the configuration file for the directory - * or null if there is no configuration file in the directory. - */ - getFilenameForDirectory(directory) { - return CONFIG_FILES.map(filename => path.join(directory, filename)).find(isExistingFile) || null; - } + write }; diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js index 6c60905f65f..41d1edc03d9 100644 --- a/lib/config/config-initializer.js +++ b/lib/config/config-initializer.js @@ -18,8 +18,8 @@ const util = require("util"), autoconfig = require("./autoconfig.js"), ConfigFile = require("./config-file"), ConfigOps = require("./config-ops"), - getSourceCodeOfFiles = require("../util/source-code-utils").getSourceCodeOfFiles, - relativeModuleResolver = require("../util/relative-module-resolver"), + { getSourceCodeOfFiles } = require("../util/source-code-utils"), + ModuleResolver = require("../util/relative-module-resolver"), npmUtils = require("../util/npm-utils"), recConfig = require("../../conf/eslint-recommended"), log = require("../util/logging"); @@ -308,33 +308,13 @@ function processAnswers(answers) { return config; } -/** - * process user's style guide of choice and return an appropriate config object. - * @param {string} guide name of the chosen style guide - * @returns {Object} config object - */ -function getConfigForStyleGuide(guide) { - const guides = { - google: { extends: "google" }, - airbnb: { extends: "airbnb" }, - "airbnb-base": { extends: "airbnb-base" }, - standard: { extends: "standard" } - }; - - if (!guides[guide]) { - throw new Error("You referenced an unsupported guide."); - } - - return guides[guide]; -} - /** * Get the version of the local ESLint. * @returns {string|null} The version. If the local ESLint was not found, returns null. */ function getLocalESLintVersion() { try { - const eslintPath = relativeModuleResolver("eslint", path.join(process.cwd(), "__placeholder__.js")); + const eslintPath = ModuleResolver.resolve("eslint", path.join(process.cwd(), "__placeholder__.js")); const eslint = require(eslintPath); return eslint.linter.version || null; @@ -573,7 +553,16 @@ function promptUser() { earlyAnswers.styleguide = "airbnb-base"; } - const config = ConfigOps.merge(processAnswers(earlyAnswers), getConfigForStyleGuide(earlyAnswers.styleguide)); + const config = processAnswers(earlyAnswers); + + if (Array.isArray(config.extends)) { + config.extends.push(earlyAnswers.styleguide); + } else if (config.extends) { + config.extends = [config.extends, earlyAnswers.styleguide]; + } else { + config.extends = earlyAnswers.styleguide; + } + const modules = getModulesList(config); return askInstallModules(modules, earlyAnswers.packageJsonExists) @@ -641,7 +630,6 @@ function promptUser() { //------------------------------------------------------------------------------ const init = { - getConfigForStyleGuide, getModulesList, hasESLintVersionConflict, installModules, diff --git a/lib/config/config-ops.js b/lib/config/config-ops.js index 48a8302d91f..a9dc993fa90 100644 --- a/lib/config/config-ops.js +++ b/lib/config/config-ops.js @@ -5,15 +5,6 @@ */ "use strict"; -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const minimatch = require("minimatch"), - path = require("path"); - -const debug = require("debug")("eslint:config-ops"); - //------------------------------------------------------------------------------ // Private //------------------------------------------------------------------------------ @@ -31,165 +22,6 @@ const RULE_SEVERITY_STRINGS = ["off", "warn", "error"], module.exports = { - /** - * Creates an empty configuration object suitable for merging as a base. - * @returns {Object} A configuration object. - */ - createEmptyConfig() { - return { - globals: {}, - env: {}, - rules: {}, - parserOptions: {} - }; - }, - - /** - * Creates an environment config based on the specified environments. - * @param {Object} env The environment settings. - * @param {Environments} envContext The environment context. - * @returns {Object} A configuration object with the appropriate rules and globals - * set. - */ - createEnvironmentConfig(env, envContext) { - - const envConfig = this.createEmptyConfig(); - - if (env) { - - envConfig.env = env; - - Object.keys(env).filter(name => env[name]).forEach(name => { - const environment = envContext.get(name); - - if (environment) { - debug(`Creating config for environment ${name}`); - if (environment.globals) { - Object.assign(envConfig.globals, environment.globals); - } - - if (environment.parserOptions) { - Object.assign(envConfig.parserOptions, environment.parserOptions); - } - } - }); - } - - return envConfig; - }, - - /** - * Given a config with environment settings, applies the globals and - * ecmaFeatures to the configuration and returns the result. - * @param {Object} config The configuration information. - * @param {Environments} envContent env context. - * @returns {Object} The updated configuration information. - */ - applyEnvironments(config, envContent) { - if (config.env && typeof config.env === "object") { - debug("Apply environment settings to config"); - return this.merge(this.createEnvironmentConfig(config.env, envContent), config); - } - - return config; - }, - - /** - * Merges two config objects. This will not only add missing keys, but will also modify values to match. - * @param {Object} target config object - * @param {Object} src config object. Overrides in this config object will take priority over base. - * @param {boolean} [combine] Whether to combine arrays or not - * @param {boolean} [isRule] Whether its a rule - * @returns {Object} merged config object. - */ - merge: function deepmerge(target, src, combine, isRule) { - - /* - * The MIT License (MIT) - * - * Copyright (c) 2012 Nicholas Fisher - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - - /* - * This code is taken from deepmerge repo - * (https://github.com/KyleAMathews/deepmerge) - * and modified to meet our needs. - */ - const array = Array.isArray(src) || Array.isArray(target); - let dst = array && [] || {}; - - if (array) { - const resolvedTarget = target || []; - - // src could be a string, so check for array - if (isRule && Array.isArray(src) && src.length > 1) { - dst = dst.concat(src); - } else { - dst = dst.concat(resolvedTarget); - } - const resolvedSrc = typeof src === "object" ? src : [src]; - - Object.keys(resolvedSrc).forEach((_, i) => { - const e = resolvedSrc[i]; - - if (typeof dst[i] === "undefined") { - dst[i] = e; - } else if (typeof e === "object") { - if (isRule) { - dst[i] = e; - } else { - dst[i] = deepmerge(resolvedTarget[i], e, combine, isRule); - } - } else { - if (!combine) { - dst[i] = e; - } else { - if (dst.indexOf(e) === -1) { - dst.push(e); - } - } - } - }); - } else { - if (target && typeof target === "object") { - Object.keys(target).forEach(key => { - dst[key] = target[key]; - }); - } - Object.keys(src).forEach(key => { - if (key === "overrides") { - dst[key] = (target[key] || []).concat(src[key] || []); - } else if (Array.isArray(src[key]) || Array.isArray(target[key])) { - dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule); - } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") { - dst[key] = src[key]; - } else { - dst[key] = deepmerge(target[key] || {}, src[key], combine, key === "rules"); - } - }); - } - - return dst; - }, - /** * Normalizes the severity value of a rule's configuration to a number * @param {(number|string|[number, ...*]|[string, ...*])} ruleConfig A rule's configuration value, generally @@ -266,109 +98,6 @@ module.exports = { return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId])); }, - /** - * Merges all configurations in a given config vector. A vector is an array of objects, each containing a config - * file path and a list of subconfig indices that match the current file path. All config data is assumed to be - * cached. - * @param {Array} vector list of config files and their subconfig indices that match the current file path - * @param {Object} configCache the config cache - * @returns {Object} config object - */ - getConfigFromVector(vector, configCache) { - - const cachedConfig = configCache.getMergedVectorConfig(vector); - - if (cachedConfig) { - return cachedConfig; - } - - debug("Using config from partial cache"); - - const subvector = Array.from(vector); - let nearestCacheIndex = subvector.length - 1, - partialCachedConfig; - - while (nearestCacheIndex >= 0) { - partialCachedConfig = configCache.getMergedVectorConfig(subvector); - if (partialCachedConfig) { - break; - } - subvector.pop(); - nearestCacheIndex--; - } - - if (!partialCachedConfig) { - partialCachedConfig = {}; - } - - let finalConfig = partialCachedConfig; - - // Start from entry immediately following nearest cached config (first uncached entry) - for (let i = nearestCacheIndex + 1; i < vector.length; i++) { - finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache); - configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig); - } - - return finalConfig; - }, - - /** - * Merges the config options from a single vector entry into the supplied config. - * @param {Object} config the base config to merge the vector entry's options into - * @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of - * matching override indices - * @param {Object} configCache the config cache - * @returns {Object} merged config object - */ - mergeVectorEntry(config, vectorEntry, configCache) { - const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath)); - let mergedConfig = Object.assign({}, config), - overrides; - - if (vectorEntryConfig.overrides) { - overrides = vectorEntryConfig.overrides.filter( - (override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1 - ); - } else { - overrides = []; - } - - mergedConfig = this.merge(mergedConfig, vectorEntryConfig); - - delete mergedConfig.overrides; - - mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig); - - if (mergedConfig.files) { - delete mergedConfig.files; - } - - return mergedConfig; - }, - - /** - * Checks that the specified file path matches all of the supplied glob patterns. - * @param {string} filePath The file path to test patterns against - * @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path - * @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path - * @returns {boolean} True if all the supplied patterns match the file path, false otherwise - */ - pathMatchesGlobs(filePath, patterns, excludedPatterns) { - const patternList = [].concat(patterns); - const excludedPatternList = [].concat(excludedPatterns || []); - - patternList.concat(excludedPatternList).forEach(pattern => { - if (path.isAbsolute(pattern) || pattern.includes("..")) { - throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); - } - }); - - const opts = { dot: true, matchBase: true }; - - return patternList.some(pattern => minimatch(filePath, pattern, opts)) && - !excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts)); - }, - /** * Normalizes a value for a global in a config * @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in diff --git a/lib/config/config-rule.js b/lib/config/config-rule.js index 29aac0b9a1a..bdeb2706b04 100644 --- a/lib/config/config-rule.js +++ b/lib/config/config-rule.js @@ -9,10 +9,7 @@ // Requirements //------------------------------------------------------------------------------ -const Rules = require("../rules"), - builtInRules = require("../built-in-rules-index"); - -const rules = new Rules(); +const builtInRules = require("../built-in-rules-index"); //------------------------------------------------------------------------------ // Helpers @@ -299,8 +296,8 @@ function generateConfigsFromSchema(schema) { * @returns {rulesConfig} Hash of rule names and arrays of possible configurations */ function createCoreRuleConfigs() { - return Object.keys(builtInRules).reduce((accumulator, id) => { - const rule = rules.get(id); + return Array.from(builtInRules.keys()).reduce((accumulator, id) => { + const rule = builtInRules.get(id); const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema; accumulator[id] = generateConfigsFromSchema(schema); diff --git a/lib/config/config-validator.js b/lib/config/config-validator.js index c6d106e6050..1dda7ab73f4 100644 --- a/lib/config/config-validator.js +++ b/lib/config/config-validator.js @@ -9,10 +9,13 @@ // Requirements //------------------------------------------------------------------------------ -const path = require("path"), - lodash = require("lodash"), - configSchema = require("../../conf/config-schema.js"), +const + path = require("path"), util = require("util"), + lodash = require("lodash"), + configSchema = require("../../conf/config-schema"), + BuiltInEnvironments = require("../../conf/environments"), + BuiltInRules = require("../built-in-rules-index"), ConfigOps = require("./config-ops"); const ajv = require("../util/ajv")(); @@ -141,20 +144,26 @@ function validateRuleOptions(rule, ruleId, options, source = null) { /** * Validates an environment object * @param {Object} environment The environment config object to validate. - * @param {Environments} envContext Env context * @param {string} source The name of the configuration source to report in any errors. + * @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded environments. * @returns {void} */ -function validateEnvironment(environment, envContext, source = null) { +function validateEnvironment( + environment, + source, + getAdditionalEnv = Function.prototype +) { // not having an environment is ok if (!environment) { return; } - Object.keys(environment).forEach(env => { - if (!envContext.get(env)) { - const message = `${source}:\n\tEnvironment key "${env}" is unknown\n`; + Object.keys(environment).forEach(id => { + const env = getAdditionalEnv(id) || BuiltInEnvironments.get(id) || null; + + if (!env) { + const message = `${source}:\n\tEnvironment key "${id}" is unknown\n`; throw new Error(message); } @@ -164,17 +173,23 @@ function validateEnvironment(environment, envContext, source = null) { /** * Validates a rules config object * @param {Object} rulesConfig The rules config object to validate. - * @param {function(string): {create: Function}} ruleMapper A mapper function from strings to loaded rules * @param {string} source The name of the configuration source to report in any errors. + * @param {function(ruleId:string): Object} getAdditionalRule A map from strings to loaded rules * @returns {void} */ -function validateRules(rulesConfig, ruleMapper, source = null) { +function validateRules( + rulesConfig, + source, + getAdditionalRule = Function.prototype +) { if (!rulesConfig) { return; } Object.keys(rulesConfig).forEach(id => { - validateRuleOptions(ruleMapper(id), id, rulesConfig[id], source); + const rule = getAdditionalRule(id) || BuiltInRules.get(id) || null; + + validateRuleOptions(rule, id, rulesConfig[id], source); }); } @@ -265,20 +280,45 @@ function validateConfigSchema(config, source = null) { /** * Validates an entire config object. * @param {Object} config The config object to validate. - * @param {function(string): {create: Function}} ruleMapper A mapper function from rule IDs to defined rules - * @param {Environments} envContext The env context * @param {string} source The name of the configuration source to report in any errors. + * @param {function(ruleId:string): Object} [getAdditionalRule] A map from strings to loaded rules. + * @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded envs. * @returns {void} */ -function validate(config, ruleMapper, envContext, source = null) { +function validate(config, source, getAdditionalRule, getAdditionalEnv) { validateConfigSchema(config, source); - validateRules(config.rules, ruleMapper, source); - validateEnvironment(config.env, envContext, source); + validateRules(config.rules, source, getAdditionalRule); + validateEnvironment(config.env, source, getAdditionalEnv); validateGlobals(config.globals, source); for (const override of config.overrides || []) { - validateRules(override.rules, ruleMapper, source); - validateEnvironment(override.env, envContext, source); + validateRules(override.rules, source, getAdditionalRule); + validateEnvironment(override.env, source, getAdditionalEnv); + validateGlobals(config.globals, source); + } +} + +const validated = new WeakSet(); + +/** + * Validate config array object. + * @param {ConfigArray} configArray The config array to validate. + * @returns {void} + */ +function validateConfigArray(configArray) { + const getPluginEnv = Map.prototype.get.bind(configArray.pluginEnvironments); + const getPluginRule = Map.prototype.get.bind(configArray.pluginRules); + + // Validate. + for (const element of configArray) { + if (validated.has(element)) { + continue; + } + validated.add(element); + + validateEnvironment(element.env, element.name, getPluginEnv); + validateGlobals(element.globals, element.name); + validateRules(element.rules, element.name, getPluginRule); } } @@ -289,5 +329,7 @@ function validate(config, ruleMapper, envContext, source = null) { module.exports = { getRuleOptionsSchema, validate, + validateConfigArray, + validateConfigSchema, validateRuleOptions }; diff --git a/lib/config/environments.js b/lib/config/environments.js deleted file mode 100644 index 1ec9438af5d..00000000000 --- a/lib/config/environments.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @fileoverview Environments manager - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const envs = require("../../conf/environments"); - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -class Environments { - - /** - * create env context - */ - constructor() { - this._environments = new Map(); - - this.load(); - } - - /** - * Loads the default environments. - * @returns {void} - * @private - */ - load() { - Object.keys(envs).forEach(envName => { - this._environments.set(envName, envs[envName]); - }); - } - - /** - * Gets the environment with the given name. - * @param {string} name The name of the environment to retrieve. - * @returns {Object?} The environment object or null if not found. - */ - get(name) { - return this._environments.get(name) || null; - } - - /** - * Gets all the environment present - * @returns {Object} The environment object for each env name - */ - getAll() { - return Array.from(this._environments).reduce((coll, env) => { - coll[env[0]] = env[1]; - return coll; - }, {}); - } - - /** - * Defines an environment. - * @param {string} name The name of the environment. - * @param {Object} env The environment settings. - * @returns {void} - */ - define(name, env) { - this._environments.set(name, env); - } - - /** - * Imports all environments from a plugin. - * @param {Object} plugin The plugin object. - * @param {string} pluginName The name of the plugin. - * @returns {void} - */ - importPlugin(plugin, pluginName) { - if (plugin.environments) { - Object.keys(plugin.environments).forEach(envName => { - this.define(`${pluginName}/${envName}`, plugin.environments[envName]); - }); - } - } -} - -module.exports = Environments; diff --git a/lib/config/plugins.js b/lib/config/plugins.js deleted file mode 100644 index fabcbfeae3a..00000000000 --- a/lib/config/plugins.js +++ /dev/null @@ -1,175 +0,0 @@ -/** - * @fileoverview Plugins manager - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const path = require("path"); -const debug = require("debug")("eslint:plugins"); -const naming = require("../util/naming"); -const relativeModuleResolver = require("../util/relative-module-resolver"); - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -/** - * Plugin class - */ -class Plugins { - - /** - * Creates the plugins context - * @param {Environments} envContext - env context - * @param {function(string, Rule): void} options.defineRule - Callback for when a plugin is defined which introduces rules - * @param {string} options.pluginRootPath The path from which all plugins should be resolved - */ - constructor(envContext, { defineRule, pluginRootPath }) { - this._plugins = Object.create(null); - this._environments = envContext; - this._defineRule = defineRule; - this._pluginRootPath = pluginRootPath; - } - - /** - * Defines a plugin with a given name rather than loading from disk. - * @param {string} pluginName The name of the plugin to load. - * @param {Object} plugin The plugin object. - * @returns {void} - */ - define(pluginName, plugin) { - const longName = naming.normalizePackageName(pluginName, "eslint-plugin"); - const shortName = naming.getShorthandName(longName, "eslint-plugin"); - - // load up environments and rules - this._plugins[shortName] = plugin; - this._environments.importPlugin(plugin, shortName); - - if (plugin.rules) { - Object.keys(plugin.rules).forEach(ruleId => { - const qualifiedRuleId = `${shortName}/${ruleId}`, - rule = plugin.rules[ruleId]; - - this._defineRule(qualifiedRuleId, rule); - }); - } - } - - /** - * Gets a plugin with the given name. - * @param {string} pluginName The name of the plugin to retrieve. - * @returns {Object} The plugin or null if not loaded. - */ - get(pluginName) { - return this._plugins[pluginName] || null; - } - - /** - * Returns all plugins that are loaded. - * @returns {Object} The plugins cache. - */ - getAll() { - return this._plugins; - } - - /** - * Resolves a plugin with the given name - * @param {string} pluginName The name of the plugin to resolve - * @param {string} pluginRootPath The absolute path to the directory where the plugin should be resolved from - * @returns {string} The full path to the plugin module - * @throws {Error} An templated error with debugging information if the plugin cannot be loaded. - */ - static resolve(pluginName, pluginRootPath) { - const longName = naming.normalizePackageName(pluginName, "eslint-plugin"); - const pathToResolveRelativeTo = path.join(pluginRootPath, "__placeholder__.js"); - - try { - return relativeModuleResolver(longName, pathToResolveRelativeTo); - } catch (missingPluginErr) { - - // If the plugin can't be resolved, display the missing plugin error (usually a config or install error) - debug(`Failed to load plugin ${longName} from ${pluginRootPath}.`); - missingPluginErr.message = `Failed to load plugin ${pluginName} from ${pluginRootPath}: ${missingPluginErr.message}`; - missingPluginErr.messageTemplate = "plugin-missing"; - missingPluginErr.messageData = { - pluginName: longName, - pluginRootPath, - configStack: [] - }; - - throw missingPluginErr; - } - } - - /** - * Loads a plugin with the given name. - * @param {string} pluginName The name of the plugin to load. - * @returns {void} - * @throws {Error} If the plugin cannot be loaded. - */ - load(pluginName) { - if (pluginName.match(/\s+/u)) { - const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`); - - whitespaceError.messageTemplate = "whitespace-found"; - whitespaceError.messageData = { pluginName }; - throw whitespaceError; - } - - const longName = naming.normalizePackageName(pluginName, "eslint-plugin"); - const shortName = naming.getShorthandName(longName, "eslint-plugin"); - - if (!this._plugins[shortName]) { - const pluginPath = Plugins.resolve(shortName, this._pluginRootPath); - const plugin = require(pluginPath); - - // This step is costly, so skip if debug is disabled - if (debug.enabled) { - let version = null; - - try { - version = require(relativeModuleResolver(`${longName}/package.json`, this._pluginRootPath)).version; - } catch (e) { - - // Do nothing - } - - const loadedPluginAndVersion = version - ? `${longName}@${version}` - : `${longName}, version unknown`; - - debug(`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${pluginPath})`); - } - - this.define(pluginName, plugin); - } - } - - /** - * Loads all plugins from an array. - * @param {string[]} pluginNames An array of plugins names. - * @returns {void} - * @throws {Error} If a plugin cannot be loaded. - * @throws {Error} If "plugins" in config is not an array - */ - loadAll(pluginNames) { - - // if "plugins" in config is not an array, throw an error so user can fix their config. - if (!Array.isArray(pluginNames)) { - const pluginNotArrayMessage = "ESLint configuration error: \"plugins\" value must be an array"; - - debug(`${pluginNotArrayMessage}: ${JSON.stringify(pluginNames)}`); - - throw new Error(pluginNotArrayMessage); - } - - // load each plugin by name - pluginNames.forEach(this.load, this); - } -} - -module.exports = Plugins; diff --git a/lib/linter.js b/lib/linter.js index bd3d8b23553..085896f0444 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -9,14 +9,15 @@ // Requirements //------------------------------------------------------------------------------ -const eslintScope = require("eslint-scope"), +const path = require("path"), + eslintScope = require("eslint-scope"), evk = require("eslint-visitor-keys"), espree = require("espree"), lodash = require("lodash"), CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), ConfigOps = require("./config/config-ops"), validator = require("./config/config-validator"), - Environments = require("./config/environments"), + BuiltInEnvironments = require("../conf/environments"), applyDisableDirectives = require("./util/apply-disable-directives"), createEmitter = require("./util/safe-emitter"), NodeEventGenerator = require("./util/node-event-generator"), @@ -39,15 +40,13 @@ const commentParser = new ConfigCommentParser(); // Typedefs //------------------------------------------------------------------------------ -/** - * The result of a parsing operation from parseForESLint() - * @typedef {Object} CustomParseResult - * @property {ASTNode} ast The ESTree AST Program node. - * @property {Object} services An object containing additional services related - * to the parser. - * @property {ScopeManager|null} scopeManager The scope manager object of this AST. - * @property {Object|null} visitorKeys The visitor keys to traverse this AST. - */ +/** @typedef {InstanceType} ConfigArray */ +/** @typedef {import("./util/types").ConfigData} ConfigData */ +/** @typedef {import("./util/types").Environment} Environment */ +/** @typedef {import("./util/types").GlobalConf} GlobalConf */ +/** @typedef {import("./util/types").LintMessage} LintMessage */ +/** @typedef {import("./util/types").ParserOptions} ParserOptions */ +/** @typedef {import("./util/types").Rule} Rule */ /** * @typedef {Object} DisableDirective @@ -57,6 +56,15 @@ const commentParser = new ConfigCommentParser(); * @property {(string|null)} ruleId */ +/** + * The private data for `Linter` instance. + * @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 {Map} parserMap The loaded parsers. + * @property {Rules} ruleMap The loaded rules. + */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -336,24 +344,23 @@ function normalizeVerifyOptions(providedOptions) { return { filename: typeof providedFilename === "string" ? providedFilename : "", allowInlineConfig: !isObjectOptions || providedOptions.allowInlineConfig !== false, - reportUnusedDisableDirectives: isObjectOptions && !!providedOptions.reportUnusedDisableDirectives + reportUnusedDisableDirectives: isObjectOptions && !!providedOptions.reportUnusedDisableDirectives, + disableFixes: Boolean(providedOptions && providedOptions.disableFixes) }; } /** * Combines the provided parserOptions with the options from environments * @param {string} parserName The parser name which uses this options. - * @param {Object} providedOptions The provided 'parserOptions' key in a config + * @param {ParserOptions} 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 + * @returns {ParserOptions} Resulting parser options after merge */ function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { const parserOptionsFromEnv = enabledEnvironments .filter(env => env.parserOptions) - .reduce((parserOptions, env) => ConfigOps.merge(parserOptions, env.parserOptions), {}); - - const mergedParserOptions = ConfigOps.merge(parserOptionsFromEnv, providedOptions || {}); - + .reduce((parserOptions, env) => lodash.merge(parserOptions, lodash.cloneDeep(env.parserOptions)), {}); + const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {}); const isModule = mergedParserOptions.sourceType === "module"; if (isModule) { @@ -369,9 +376,9 @@ function resolveParserOptions(parserName, providedOptions, enabledEnvironments) /** * 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 + * @param {Record} providedGlobals The 'globals' key in a config + * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments + * @returns {Record} The resolved globals object */ function resolveGlobals(providedGlobals, enabledEnvironments) { return Object.assign( @@ -416,8 +423,8 @@ function getRuleOptions(ruleConfig) { /** * Analyze scope of the given AST. * @param {ASTNode} ast The `Program` node to analyze. - * @param {Object} parserOptions The parser options. - * @param {Object} visitorKeys The visitor keys. + * @param {ParserOptions} parserOptions The parser options. + * @param {Record} visitorKeys The visitor keys. * @returns {ScopeManager} The analysis result. */ function analyzeScope(ast, parserOptions, visitorKeys) { @@ -440,17 +447,14 @@ function analyzeScope(ast, parserOptions, visitorKeys) { * optimization of functions, so it's best to keep the try-catch as isolated * as possible * @param {string} text The text to parse. - * @param {Object} providedParserOptions Options to pass to the parser - * @param {string} parserName The name of the parser - * @param {Map} parserMap A map from names to loaded parsers + * @param {Parser} parser The parser to parse. + * @param {ParserOptions} providedParserOptions 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, providedParserOptions, parserName, parserMap, filePath) { - - +function parse(text, parser, providedParserOptions, filePath) { const textToParse = stripUnicodeBOM(text).replace(astUtils.SHEBANG_MATCHER, (match, captured) => `//${captured}`); const parserOptions = Object.assign({}, providedParserOptions, { loc: true, @@ -463,22 +467,6 @@ function parse(text, providedParserOptions, parserName, parserMap, filePath) { filePath }); - if (!parserMap.has(parserName)) { - return { - success: false, - error: { - ruleId: null, - fatal: true, - severity: 2, - message: `Configured parser '${parserName}' was not found.`, - line: 0, - column: 0 - } - }; - } - - const parser = parserMap.get(parserName); - /* * Check for parsing errors first. If there's a parsing error, nothing * else can happen. However, a parsing error does not throw an error @@ -659,9 +647,10 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @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 + * @param {boolean} disableFixes If true, it doesn't make `fix` properties. * @returns {Problem[]} An array of reported problems */ -function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename) { +function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes) { const emitter = createEmitter(); const nodeQueue = []; let currentNode = sourceCode.ast; @@ -732,7 +721,13 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser * with Node 8.4.0. */ if (reportTranslator === null) { - reportTranslator = createReportTranslator({ ruleId, severity, sourceCode, messageIds }); + reportTranslator = createReportTranslator({ + ruleId, + severity, + sourceCode, + messageIds, + disableFixes + }); } const problem = reportTranslator(...args); @@ -778,9 +773,40 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser return lintingProblems; } -const lastSourceCodes = new WeakMap(); -const loadedParserMaps = new WeakMap(); -const ruleMaps = new WeakMap(); +/** + * Get an environment. + * @param {LinterInternalSlots} slots The internal slots of Linter. + * @param {string} envId The environment ID to get. + * @returns {Environment|null} The environment. + */ +function getEnv(slots, envId) { + return ( + (slots.lastConfigArray && slots.lastConfigArray.pluginEnvironments.get(envId)) || + BuiltInEnvironments.get(envId) || + null + ); +} + +/** + * Get a rule. + * @param {LinterInternalSlots} slots The internal slots of Linter. + * @param {string} ruleId The rule ID to get. + * @returns {Rule} The rule. + */ +function getRule(slots, ruleId) { + return ( + (slots.lastConfigArray && slots.lastConfigArray.pluginRules.get(ruleId)) || + + // This returns the stub for missing rules if the rule does not exist. + slots.ruleMap.get(ruleId) + ); +} + +/** + * The map to store private data. + * @type {WeakMap} + */ +const internalSlotsMap = new WeakMap(); //------------------------------------------------------------------------------ // Public Interface @@ -790,16 +816,17 @@ const ruleMaps = new WeakMap(); * Object that is responsible for verifying JavaScript text * @name eslint */ -module.exports = class Linter { +class Linter { constructor() { - lastSourceCodes.set(this, null); - loadedParserMaps.set(this, new Map()); - ruleMaps.set(this, new Rules()); - this.version = pkg.version; - this.environments = new Environments(); + internalSlotsMap.set(this, { + lastConfigArray: null, + lastSourceCode: null, + parserMap: new Map([["espree", espree]]), + ruleMap: new Rules() + }); - this.defineParser("espree", espree); + this.version = pkg.version; } /** @@ -811,21 +838,10 @@ module.exports = class Linter { return pkg.version; } - /** - * Configuration object for the `verify` API. A JS representation of the eslintrc files. - * @typedef {Object} ESLintConfig - * @property {Object} rules The rule configuration to verify against. - * @property {string} [parser] Parser to use when generatig the AST. - * @property {Object} [parserOptions] Options for the parsed used. - * @property {Object} [settings] Global settings passed to each rule. - * @property {Object} [env] The environment to verify in. - * @property {Object} [globals] Available globals to the code. - */ - /** * Same as linter.verify, except without support for processors. * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. - * @param {ESLintConfig} providedConfig An ESLintConfig instance to configure everything. + * @param {ConfigData} 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. @@ -833,41 +849,62 @@ module.exports = class Linter { * Useful if you want to validate JS without comments overriding rules. * @param {boolean} [filenameOrOptions.reportUnusedDisableDirectives=false] Adds reported errors for unused * eslint-disable directives - * @returns {Object[]} The results as an array of messages or an empty array if no messages. + * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages. */ _verifyWithoutProcessors(textOrSourceCode, providedConfig, filenameOrOptions) { + const slots = internalSlotsMap.get(this); const config = providedConfig || {}; const options = normalizeVerifyOptions(filenameOrOptions); let text; // evaluate arguments if (typeof textOrSourceCode === "string") { - lastSourceCodes.set(this, null); + slots.lastSourceCode = null; text = textOrSourceCode; } else { - lastSourceCodes.set(this, textOrSourceCode); + slots.lastSourceCode = textOrSourceCode; text = textOrSourceCode.text; } + // Resolve parser. + let parserName = DEFAULT_PARSER_NAME; + let parser = espree; + + if (typeof config.parser === "object" && config.parser !== null) { + parserName = config.parser.filePath; + parser = config.parser.definition; + } else if (typeof config.parser === "string") { + if (!slots.parserMap.has(config.parser)) { + return [{ + ruleId: null, + fatal: true, + severity: 2, + message: `Configured parser '${config.parser}' was not found.`, + line: 0, + column: 0 + }]; + } + parserName = config.parser; + parser = slots.parserMap.get(config.parser); + } + // 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)) + .map(envName => getEnv(slots, envName)) .filter(env => env); - const parserName = config.parser || DEFAULT_PARSER_NAME; const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs); const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs); const settings = config.settings || {}; - if (!lastSourceCodes.get(this)) { + if (!slots.lastSourceCode) { const parseResult = parse( text, + parser, parserOptions, - parserName, - loadedParserMaps.get(this), options.filename ); @@ -875,29 +912,27 @@ module.exports = class Linter { return [parseResult.error]; } - lastSourceCodes.set(this, parseResult.sourceCode); + 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). */ - const lastSourceCode = lastSourceCodes.get(this); - - if (!lastSourceCode.scopeManager) { - lastSourceCodes.set(this, new SourceCode({ - text: lastSourceCode.text, - ast: lastSourceCode.ast, - parserServices: lastSourceCode.parserServices, - visitorKeys: lastSourceCode.visitorKeys, - scopeManager: analyzeScope(lastSourceCode.ast, parserOptions) - })); + 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, parserOptions) + }); } } - const sourceCode = lastSourceCodes.get(this); + const sourceCode = slots.lastSourceCode; const commentDirectives = options.allowInlineConfig - ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => ruleMaps.get(this).get(ruleId)) + ? getDirectiveComments(options.filename, sourceCode.ast, ruleId => getRule(slots, ruleId)) : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; // augment global scope with declared global variables @@ -915,11 +950,12 @@ module.exports = class Linter { lintingProblems = runRules( sourceCode, configuredRules, - ruleId => ruleMaps.get(this).get(ruleId), + ruleId => getRule(slots, ruleId), parserOptions, parserName, settings, - options.filename + options.filename, + options.disableFixes ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; @@ -949,7 +985,7 @@ module.exports = class Linter { /** * Verifies the text against the rules specified by the second argument. * @param {string|SourceCode} textOrSourceCode The text to parse or a SourceCode object. - * @param {ESLintConfig} config An ESLintConfig instance to configure everything. + * @param {ConfigData|ConfigArray} config 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. @@ -957,12 +993,22 @@ module.exports = class Linter { * Useful if you want to validate JS without comments overriding rules. * @param {function(string): string[]} [filenameOrOptions.preprocess] preprocessor for source text. If provided, * this should accept a string of source text, and return an array of code blocks to lint. - * @param {function(Array): Object[]} [filenameOrOptions.postprocess] postprocessor for report messages. If provided, + * @param {function(Array): LintMessage[]} [filenameOrOptions.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 - * @returns {Object[]} The results as an array of messages or an empty array if no messages. + * @returns {LintMessage[]} The results as an array of messages or an empty array if no messages. */ verify(textOrSourceCode, config, filenameOrOptions) { + if (config && typeof config.extractConfig === "function") { + return this._verifyWithConfigArray( + textOrSourceCode, + config, + typeof filenameOrOptions === "string" + ? { filename: filenameOrOptions } + : filenameOrOptions || {} + ); + } + const preprocess = filenameOrOptions && filenameOrOptions.preprocess || (rawText => [rawText]); const postprocess = filenameOrOptions && filenameOrOptions.postprocess || lodash.flatten; @@ -973,27 +1019,84 @@ module.exports = class Linter { ); } + /** + * Verify a given code with `ConfigArray`. + * @param {string} text The source code string. + * @param {ConfigArray} configArray The config array. + * @param {Object} providedOptions The options. + * @returns {LintMessage[]} The found problems. + */ + _verifyWithConfigArray(text, configArray, providedOptions) { + debug("Verify with ConfigArray"); + + // Store the config array in order to get plugin envs and rules later. + internalSlotsMap.get(this).lastConfigArray = configArray; + + /* + * TODO: implement https://github.com/eslint/rfcs/tree/master/designs/2018-processors-improvements here. + */ + + // Extract the final config for this file. + const config = configArray.extractConfig(providedOptions.filename); + + /* + * Convert "/path/to/.js" to "". + * `CLIEngine#executeOnText()` method makes the file path as `.js` + * file that is on the CWD if it was omitted. + * This stripping is for backward compatibility. + */ + const basename = path.basename( + providedOptions.filename, + path.extname(providedOptions.filename) + ); + const filename = basename.startsWith("<") && basename.endsWith(">") + ? basename + : providedOptions.filename; + + // Make options. + const options = { + ...providedOptions, + filename + }; + + // Apply processor. + if (config.processor) { + const processor = configArray.pluginProcessors.get(config.processor); + + options.preprocess = processor.preprocess; + options.postprocess = processor.postprocess; + if (!processor.supportsAutofix) { + + // Use `disableFixes` of https://github.com/eslint/rfcs/tree/master/designs/2018-processors-improvements + options.disableFixes = true; + } + } + + // Verify. + return this.verify(text, config, options); + } + /** * Gets the SourceCode object representing the parsed source. * @returns {SourceCode} The SourceCode object. */ getSourceCode() { - return lastSourceCodes.get(this); + return internalSlotsMap.get(this).lastSourceCode; } /** * Defines a new linting rule. * @param {string} ruleId A unique rule identifier - * @param {Function} ruleModule Function from context to object mapping AST node types to event handlers + * @param {Function | Rule} ruleModule Function from context to object mapping AST node types to event handlers * @returns {void} */ defineRule(ruleId, ruleModule) { - ruleMaps.get(this).define(ruleId, ruleModule); + internalSlotsMap.get(this).ruleMap.define(ruleId, ruleModule); } /** * Defines many new linting rules. - * @param {Object} rulesToDefine map from unique rule identifier to rule + * @param {Record} rulesToDefine map from unique rule identifier to rule * @returns {void} */ defineRules(rulesToDefine) { @@ -1004,27 +1107,35 @@ module.exports = class Linter { /** * Gets an object with all loaded rules. - * @returns {Map} All loaded rules + * @returns {Map} All loaded rules */ getRules() { - return ruleMaps.get(this).getAllLoadedRules(); + const { lastConfigArray, ruleMap } = internalSlotsMap.get(this); + + return new Map(function *() { + yield* ruleMap; + + if (lastConfigArray) { + yield* lastConfigArray.pluginRules; + } + }()); } /** * Define a new parser module - * @param {any} parserId Name of the parser - * @param {any} parserModule The parser object + * @param {string} parserId Name of the parser + * @param {Parser} parserModule The parser object * @returns {void} */ defineParser(parserId, parserModule) { - loadedParserMaps.get(this).set(parserId, parserModule); + 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 {Object} config The ESLint config object to use. + * @param {ConfigData|ConfigArray} config The ESLint config object to use. * @param {Object} options The ESLint options object to use. * @param {string} options.filename The filename from which the text was read. * @param {boolean} options.allowInlineConfig Flag indicating if inline comments @@ -1035,7 +1146,7 @@ module.exports = class Linter { * @param {Function} options.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 - * @returns {Object} The result of the fix operation as returned from the + * @returns {{fixed:boolean,messages:LintMessage[],output:string}} The result of the fix operation as returned from the * SourceCodeFixer. */ verifyAndFix(text, config, options) { @@ -1098,4 +1209,17 @@ module.exports = class Linter { return fixedResult; } +} + +module.exports = { + Linter, + + /** + * Get the internal slots of a given Linter instance for tests. + * @param {Linter} instance The Linter instance to get. + * @returns {LinterInternalSlots} The internal slots. + */ + getLinterInternalSlots(instance) { + return internalSlotsMap.get(instance); + } }; diff --git a/lib/load-rules.js b/lib/load-rules.js index a7383624651..81bab63fab6 100644 --- a/lib/load-rules.js +++ b/lib/load-rules.js @@ -22,7 +22,7 @@ const rulesDirCache = {}; * Load all rule modules from specified directory. * @param {string} relativeRulesDir Path to rules directory, may be relative. * @param {string} cwd Current working directory - * @returns {Object} Loaded rule modules by rule ids (file names). + * @returns {Object} Loaded rule modules. */ module.exports = function(relativeRulesDir, cwd) { const rulesDir = path.resolve(cwd, relativeRulesDir); @@ -38,7 +38,7 @@ module.exports = function(relativeRulesDir, cwd) { if (path.extname(file) !== ".js") { return; } - rules[file.slice(0, -3)] = path.join(rulesDir, file); + rules[file.slice(0, -3)] = require(path.join(rulesDir, file)); }); rulesDirCache[rulesDir] = rules; diff --git a/lib/rules.js b/lib/rules.js index d0f90951416..ab15fa06362 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -59,9 +59,6 @@ function normalizeRule(rule) { class Rules { constructor() { this._rules = Object.create(null); - Object.keys(builtInRules).forEach(ruleId => { - this.define(ruleId, builtInRules[ruleId]); - }); } /** @@ -81,29 +78,25 @@ class Rules { * A rule. This is normalized to always have the new-style shape with a `create` method. */ get(ruleId) { - if (!Object.prototype.hasOwnProperty.call(this._rules, ruleId)) { - return createMissingRule(ruleId); - } if (typeof this._rules[ruleId] === "string") { - return normalizeRule(require(this._rules[ruleId])); + this.define(ruleId, require(this._rules[ruleId])); + } + if (this._rules[ruleId]) { + return this._rules[ruleId]; + } + if (builtInRules.has(ruleId)) { + return builtInRules.get(ruleId); } - return this._rules[ruleId]; + return createMissingRule(ruleId); } - /** - * Get an object with all currently loaded rules - * @returns {Map} All loaded rules - */ - getAllLoadedRules() { - const allRules = new Map(); - - Object.keys(this._rules).forEach(name => { - const rule = this.get(name); + *[Symbol.iterator]() { + yield* builtInRules.entries(); - allRules.set(name, rule); - }); - return allRules; + for (const ruleId of Object.keys(this._rules)) { + yield [ruleId, this.get(ruleId)]; + } } } diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js index 2cad541cea9..166e33cfd02 100644 --- a/lib/testers/rule-tester.js +++ b/lib/testers/rule-tester.js @@ -40,13 +40,13 @@ // Requirements //------------------------------------------------------------------------------ -const lodash = require("lodash"), +const assert = require("assert"), path = require("path"), util = require("util"), - validator = require("../config/config-validator"), - Linter = require("../linter"), - Environments = require("../config/environments"), + lodash = require("lodash"), + { getRuleOptionsSchema, validate } = require("../config/config-validator"), + { Linter } = require("../linter"), SourceCodeFixer = require("../util/source-code-fixer"), interpolate = require("../util/interpolate"); @@ -351,7 +351,7 @@ class RuleTester { config.rules[ruleName] = 1; } - const schema = validator.getRuleOptionsSchema(rule); + const schema = getRuleOptionsSchema(rule); /* * Setup AST getters. @@ -398,7 +398,7 @@ class RuleTester { } } - validator.validate(config, ruleMap.get.bind(ruleMap), new Environments(), "rule-tester"); + validate(config, "rule-tester", ruleMap.get.bind(ruleMap)); return { messages: linter.verify(code, config, filename, true), diff --git a/lib/util/file-finder.js b/lib/util/file-finder.js deleted file mode 100644 index e273e4d46c7..00000000000 --- a/lib/util/file-finder.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * @fileoverview Util class to find config files. - * @author Aliaksei Shytkin - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const fs = require("fs"), - path = require("path"); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Get the entries for a directory. Including a try-catch may be detrimental to - * function performance, so move it out here a separate function. - * @param {string} directory The directory to search in. - * @returns {string[]} The entries in the directory or an empty array on error. - * @private - */ -function getDirectoryEntries(directory) { - try { - - return fs.readdirSync(directory); - } catch (ex) { - return []; - } -} - -/** - * Create a hash of filenames from a directory listing - * @param {string[]} entries Array of directory entries. - * @param {string} directory Path to a current directory. - * @param {string[]} supportedConfigs List of support filenames. - * @returns {Object} Hashmap of filenames - */ -function normalizeDirectoryEntries(entries, directory, supportedConfigs) { - const fileHash = {}; - - entries.forEach(entry => { - if (supportedConfigs.indexOf(entry) >= 0) { - const resolvedEntry = path.resolve(directory, entry); - - if (fs.statSync(resolvedEntry).isFile()) { - fileHash[entry] = resolvedEntry; - } - } - }); - return fileHash; -} - -//------------------------------------------------------------------------------ -// API -//------------------------------------------------------------------------------ - -/** - * FileFinder class - */ -class FileFinder { - - /** - * @param {string[]} files The basename(s) of the file(s) to find. - * @param {stirng} cwd Current working directory - */ - constructor(files, cwd) { - this.fileNames = Array.isArray(files) ? files : [files]; - this.cwd = cwd || process.cwd(); - this.cache = {}; - } - - /** - * Find all instances of files with the specified file names, in directory and - * parent directories. Cache the results. - * Does not check if a matching directory entry is a file. - * Searches for all the file names in this.fileNames. - * Is currently used by lib/config.js to find .eslintrc and package.json files. - * @param {string} relativeDirectory The directory to start the search from. - * @returns {GeneratorFunction} to iterate the file paths found - */ - *findAllInDirectoryAndParents(relativeDirectory) { - const cache = this.cache; - - const initialDirectory = relativeDirectory - ? path.resolve(this.cwd, relativeDirectory) - : this.cwd; - - if (Object.prototype.hasOwnProperty.call(cache, initialDirectory)) { - yield* cache[initialDirectory]; - return; // to avoid doing the normal loop afterwards - } - - const dirs = []; - const fileNames = this.fileNames; - let searched = 0; - let directory = initialDirectory; - - do { - dirs[searched++] = directory; - cache[directory] = []; - - const filesMap = normalizeDirectoryEntries(getDirectoryEntries(directory), directory, fileNames); - - if (Object.keys(filesMap).length) { - for (let k = 0; k < fileNames.length; k++) { - - if (filesMap[fileNames[k]]) { - const filePath = filesMap[fileNames[k]]; - - // Add the file path to the cache of each directory searched. - for (let j = 0; j < searched; j++) { - cache[dirs[j]].push(filePath); - } - yield filePath; - break; - } - } - } - - const child = directory; - - // Assign parent directory to directory. - directory = path.dirname(directory); - - if (directory === child) { - return; - } - - } while (!Object.prototype.hasOwnProperty.call(cache, directory)); - - // Add what has been cached previously to the cache of each directory searched. - for (let i = 0; i < searched; i++) { - cache[dirs[i]].push(...cache[directory]); - } - - yield* cache[dirs[0]]; - } -} - -module.exports = FileFinder; diff --git a/lib/util/glob-utils.js b/lib/util/glob-utils.js deleted file mode 100644 index 33cb8e7c885..00000000000 --- a/lib/util/glob-utils.js +++ /dev/null @@ -1,285 +0,0 @@ -/** - * @fileoverview Utilities for working with globs and the filesystem. - * @author Ian VanSchooten - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const lodash = require("lodash"), - fs = require("fs"), - path = require("path"), - GlobSync = require("./glob"), - - pathUtils = require("./path-utils"), - IgnoredPaths = require("./ignored-paths"); - -const debug = require("debug")("eslint:glob-utils"); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Checks whether a directory exists at the given location - * @param {string} resolvedPath A path from the CWD - * @returns {boolean} `true` if a directory exists - */ -function directoryExists(resolvedPath) { - return fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory(); -} - -/** - * Checks if a provided path is a directory and returns a glob string matching - * all files under that directory if so, the path itself otherwise. - * - * Reason for this is that `glob` needs `/**` to collect all the files under a - * directory where as our previous implementation without `glob` simply walked - * a directory that is passed. So this is to maintain backwards compatibility. - * - * Also makes sure all path separators are POSIX style for `glob` compatibility. - * - * @param {Object} [options] An options object - * @param {string[]} [options.extensions=[".js"]] An array of accepted extensions - * @param {string} [options.cwd=process.cwd()] The cwd to use to resolve relative pathnames - * @returns {Function} A function that takes a pathname and returns a glob that - * matches all files with the provided extensions if - * pathname is a directory. - */ -function processPath(options) { - const cwd = (options && options.cwd) || process.cwd(); - let extensions = (options && options.extensions) || [".js"]; - - extensions = extensions.map(ext => ext.replace(/^\./u, "")); - - let suffix = "/**"; - - if (extensions.length === 1) { - suffix += `/*.${extensions[0]}`; - } else { - suffix += `/*.{${extensions.join(",")}}`; - } - - /** - * A function that converts a directory name to a glob pattern - * - * @param {string} pathname The directory path to be modified - * @returns {string} The glob path or the file path itself - * @private - */ - return function(pathname) { - if (pathname === "") { - return ""; - } - - let newPath = pathname; - const resolvedPath = path.resolve(cwd, pathname); - - if (directoryExists(resolvedPath)) { - newPath = pathname.replace(/[/\\]$/u, "") + suffix; - } - - return pathUtils.convertPathToPosix(newPath); - }; -} - -/** - * The error type when no files match a glob. - */ -class NoFilesFoundError extends Error { - - /** - * @param {string} pattern - The glob pattern which was not found. - */ - constructor(pattern) { - super(`No files matching '${pattern}' were found.`); - - this.messageTemplate = "file-not-found"; - this.messageData = { pattern }; - } - -} - -/** - * The error type when there are files matched by a glob, but all of them have been ignored. - */ -class AllFilesIgnoredError extends Error { - - /** - * @param {string} pattern - The glob pattern which was not found. - */ - constructor(pattern) { - super(`All files matched by '${pattern}' are ignored.`); - this.messageTemplate = "all-files-ignored"; - this.messageData = { pattern }; - } -} - -const NORMAL_LINT = {}; -const SILENTLY_IGNORE = {}; -const IGNORE_AND_WARN = {}; - -/** - * Tests whether a file should be linted or ignored - * @param {string} filename The file to be processed - * @param {{ignore: (boolean|null)}} options If `ignore` is false, updates the behavior to - * not process custom ignore paths, and lint files specified by direct path even if they - * match the default ignore path - * @param {boolean} isDirectPath True if the file was provided as a direct path - * (as opposed to being resolved from a glob) - * @param {IgnoredPaths} ignoredPaths An instance of IgnoredPaths to check whether a given - * file is ignored. - * @returns {(NORMAL_LINT|SILENTLY_IGNORE|IGNORE_AND_WARN)} A directive for how the - * file should be processed (either linted normally, or silently ignored, or ignored - * with a warning that it is being ignored) - */ -function testFileAgainstIgnorePatterns(filename, options, isDirectPath, ignoredPaths) { - const shouldProcessCustomIgnores = options.ignore !== false; - const shouldLintIgnoredDirectPaths = options.ignore === false; - const fileMatchesIgnorePatterns = ignoredPaths.contains(filename, "default") || - (shouldProcessCustomIgnores && ignoredPaths.contains(filename, "custom")); - - if (fileMatchesIgnorePatterns && isDirectPath && !shouldLintIgnoredDirectPaths) { - return IGNORE_AND_WARN; - } - - if (!fileMatchesIgnorePatterns || (isDirectPath && shouldLintIgnoredDirectPaths)) { - return NORMAL_LINT; - } - - return SILENTLY_IGNORE; -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -/** - * Resolves any directory patterns into glob-based patterns for easier handling. - * @param {string[]} patterns File patterns (such as passed on the command line). - * @param {Object} options An options object. - * @param {string} [options.globInputPaths] False disables glob resolution. - * @returns {string[]} The equivalent glob patterns and filepath strings. - */ -function resolveFileGlobPatterns(patterns, options) { - if (options.globInputPaths === false) { - return patterns; - } - - const processPathExtensions = processPath(options); - - return patterns.map(processPathExtensions); -} - -const dotfilesPattern = /(?:(?:^\.)|(?:[/\\]\.))[^/\\.].*/u; - -/** - * Build a list of absolute filesnames on which ESLint will act. - * Ignored files are excluded from the results, as are duplicates. - * - * @param {string[]} globPatterns Glob patterns. - * @param {Object} [providedOptions] An options object. - * @param {string} [providedOptions.cwd] CWD (considered for relative filenames) - * @param {boolean} [providedOptions.ignore] False disables use of .eslintignore. - * @param {string} [providedOptions.ignorePath] The ignore file to use instead of .eslintignore. - * @param {string} [providedOptions.ignorePattern] A pattern of files to ignore. - * @param {string} [providedOptions.globInputPaths] False disables glob resolution. - * @returns {string[]} Resolved absolute filenames. - */ -function listFilesToProcess(globPatterns, providedOptions) { - const options = providedOptions || { ignore: true }; - const cwd = options.cwd || process.cwd(); - - const getIgnorePaths = lodash.memoize( - optionsObj => - new IgnoredPaths(optionsObj) - ); - - /* - * The test "should use default options if none are provided" (source-code-utils.js) checks that 'module.exports.resolveFileGlobPatterns' was called. - * So it cannot use the local function "resolveFileGlobPatterns". - */ - const resolvedGlobPatterns = module.exports.resolveFileGlobPatterns(globPatterns, options); - - debug("Creating list of files to process."); - const resolvedPathsByGlobPattern = resolvedGlobPatterns.map(pattern => { - if (pattern === "") { - return [{ - filename: "", - behavior: SILENTLY_IGNORE - }]; - } - - const file = path.resolve(cwd, pattern); - - if (options.globInputPaths === false || (fs.existsSync(file) && fs.statSync(file).isFile())) { - const ignoredPaths = getIgnorePaths(options); - const fullPath = options.globInputPaths === false ? file : fs.realpathSync(file); - - return [{ - filename: fullPath, - behavior: testFileAgainstIgnorePatterns(fullPath, options, true, ignoredPaths) - }]; - } - - // regex to find .hidden or /.hidden patterns, but not ./relative or ../relative - const globIncludesDotfiles = dotfilesPattern.test(pattern); - let newOptions = options; - - if (!options.dotfiles) { - newOptions = Object.assign({}, options, { dotfiles: globIncludesDotfiles }); - } - - const ignoredPaths = getIgnorePaths(newOptions); - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const globOptions = { - nodir: true, - dot: true, - cwd - }; - - return new GlobSync(pattern, globOptions, shouldIgnore).found.map(globMatch => { - const relativePath = path.resolve(cwd, globMatch); - - return { - filename: relativePath, - behavior: testFileAgainstIgnorePatterns(relativePath, options, false, ignoredPaths) - }; - }); - }); - - const allPathDescriptors = resolvedPathsByGlobPattern.reduce((pathsForAllGlobs, pathsForCurrentGlob, index) => { - if (pathsForCurrentGlob.every(pathDescriptor => pathDescriptor.behavior === SILENTLY_IGNORE && pathDescriptor.filename !== "")) { - throw new (pathsForCurrentGlob.length ? AllFilesIgnoredError : NoFilesFoundError)(globPatterns[index]); - } - - pathsForCurrentGlob.forEach(pathDescriptor => { - switch (pathDescriptor.behavior) { - case NORMAL_LINT: - pathsForAllGlobs.push({ filename: pathDescriptor.filename, ignored: false }); - break; - case IGNORE_AND_WARN: - pathsForAllGlobs.push({ filename: pathDescriptor.filename, ignored: true }); - break; - case SILENTLY_IGNORE: - - // do nothing - break; - - default: - throw new Error(`Unexpected file behavior for ${pathDescriptor.filename}`); - } - }); - - return pathsForAllGlobs; - }, []); - - return lodash.uniqBy(allPathDescriptors, pathDescriptor => pathDescriptor.filename); -} - -module.exports = { - resolveFileGlobPatterns, - listFilesToProcess -}; diff --git a/lib/util/glob.js b/lib/util/glob.js deleted file mode 100644 index f352dae7a4d..00000000000 --- a/lib/util/glob.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @fileoverview An inherited `glob.GlobSync` to support .gitignore patterns. - * @author Kael Zhang - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const Sync = require("glob").GlobSync, - util = require("util"); - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -const IGNORE = Symbol("ignore"); - -/** - * Subclass of `glob.GlobSync` - * @param {string} pattern Pattern to be matched. - * @param {Object} options `options` for `glob` - * @param {function()} shouldIgnore Method to check whether a directory should be ignored. - * @constructor - */ -function GlobSync(pattern, options, shouldIgnore) { - - /** - * We don't put this thing to argument `options` to avoid - * further problems, such as `options` validation. - * - * Use `Symbol` as much as possible to avoid confliction. - */ - this[IGNORE] = shouldIgnore; - - Sync.call(this, pattern, options); -} - -util.inherits(GlobSync, Sync); - -/* eslint no-underscore-dangle: ["error", { "allow": ["_readdir", "_mark"] }] */ - -GlobSync.prototype._readdir = function(abs, inGlobStar) { - - /** - * `options.nodir` makes `options.mark` as `true`. - * Mark `abs` first - * to make sure `"node_modules"` will be ignored immediately with ignore pattern `"node_modules/"`. - * - * There is a built-in cache about marked `File.Stat` in `glob`, so that we could not worry about the extra invocation of `this._mark()` - */ - const marked = this._mark(abs); - - if (this[IGNORE](marked)) { - return null; - } - - return Sync.prototype._readdir.call(this, abs, inGlobStar); -}; - - -module.exports = GlobSync; diff --git a/lib/util/ignored-paths.js b/lib/util/ignored-paths.js index 41bc3f48c84..1d9003b655b 100644 --- a/lib/util/ignored-paths.js +++ b/lib/util/ignored-paths.js @@ -11,11 +11,12 @@ const fs = require("fs"), path = require("path"), - ignore = require("ignore"), - pathUtils = require("./path-utils"); + ignore = require("ignore"); const debug = require("debug")("eslint:ignored-paths"); +// debug.enabled = true; + //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ @@ -168,7 +169,11 @@ class IgnoredPaths { debug("Using specific ignore file"); try { - fs.statSync(options.ignorePath); + const stat = fs.statSync(options.ignorePath); + + if (!stat.isFile()) { + throw new Error(`${options.ignorePath} is not a file`); + } ignorePath = options.ignorePath; } catch (e) { e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`; @@ -325,57 +330,30 @@ class IgnoredPaths { * @returns {boolean} true if the file path matches one or more patterns, false otherwise */ contains(filepath, category) { - let result = false; + const basePath = this.getBaseDir(); const absolutePath = path.resolve(this.options.cwd, filepath); - const relativePath = pathUtils.getRelativePath(absolutePath, this.getBaseDir()); + const relativePath = path.relative(basePath, absolutePath); - if (typeof category === "undefined") { - result = (this.ig.default.filter([relativePath]).length === 0) || - (this.ig.custom.filter([relativePath]).length === 0); - } else { - result = (this.ig[category].filter([relativePath]).length === 0); + if (relativePath) { + if (typeof category === "undefined") { + result = + (this.ig.default.filter([relativePath]).length === 0) || + (this.ig.custom.filter([relativePath]).length === 0); + } else { + result = + (this.ig[category].filter([relativePath]).length === 0); + } } debug("contains:"); - debug(" target = %j", filepath); - debug(" result = %j", result); + debug(" target = %j", filepath); + debug(" base = %j", basePath); + debug(" relative = %j", relativePath); + debug(" result = %j", result); return result; } - - /** - * Returns a list of dir patterns for glob to ignore - * @returns {function()} method to check whether a folder should be ignored by glob. - */ - getIgnoredFoldersGlobChecker() { - const baseDir = this.getBaseDir(); - const ig = ignore(); - - DEFAULT_IGNORE_DIRS.forEach(ignoreDir => this.addPatternRelativeToCwd(ig, ignoreDir)); - - if (this.options.dotfiles !== true) { - - // Ignore hidden folders. (This cannot be ".*", or else it's not possible to unignore hidden files) - ig.add([".*/*", "!../*"]); - } - - if (this.options.ignore) { - ig.add(this.ig.custom); - } - - const filter = ig.createFilter(); - - return function(absolutePath) { - const relative = pathUtils.getRelativePath(absolutePath, baseDir); - - if (!relative) { - return false; - } - - return !filter(relative); - }; - } } -module.exports = IgnoredPaths; +module.exports = { IgnoredPaths }; diff --git a/lib/util/lint-result-cache.js b/lib/util/lint-result-cache.js index f1e5aabfebf..9408780fb02 100644 --- a/lib/util/lint-result-cache.js +++ b/lib/util/lint-result-cache.js @@ -8,12 +8,12 @@ // Requirements //----------------------------------------------------------------------------- -const assert = require("assert"), - fs = require("fs"), - fileEntryCache = require("file-entry-cache"), - hash = require("./hash"), - pkg = require("../../package.json"), - stringify = require("json-stable-stringify-without-jsonify"); +const assert = require("assert"); +const fs = require("fs"); +const fileEntryCache = require("file-entry-cache"); +const stringify = require("json-stable-stringify-without-jsonify"); +const pkg = require("../../package.json"); +const hash = require("./hash"); //----------------------------------------------------------------------------- // Helpers @@ -22,14 +22,11 @@ const assert = require("assert"), const configHashCache = new WeakMap(); /** - * Calculates the hash of the config file used to validate a given file - * @param {Object} configHelper The config helper for retrieving configuration information - * @param {string} filename The path of the file to retrieve a config object for to calculate the hash + * Calculates the hash of the config + * @param {ConfigArray} config The config. * @returns {string} The hash of the config */ -function hashOfConfigFor(configHelper, filename) { - const config = configHelper.getConfig(filename); - +function hashOfConfigFor(config) { if (!configHashCache.has(config)) { configHashCache.set(config, hash(`${pkg.version}_${stringify(config)}`)); } @@ -52,15 +49,12 @@ class LintResultCache { * Creates a new LintResultCache instance. * @constructor * @param {string} cacheFileLocation The cache file location. - * @param {Object} configHelper The configuration helper (used for * configuration lookup by file path). */ - constructor(cacheFileLocation, configHelper) { + constructor(cacheFileLocation) { assert(cacheFileLocation, "Cache file location is required"); - assert(configHelper, "Config helper is required"); this.fileEntryCache = fileEntryCache.create(cacheFileLocation); - this.configHelper = configHelper; } /** @@ -68,10 +62,11 @@ class LintResultCache { * cache. If the file is present and has not been changed, rebuild any * missing result information. * @param {string} filePath The file for which to retrieve lint results. + * @param {ConfigArray} config The config of the file. * @returns {Object|null} The rebuilt lint results, or null if the file is * changed or not in the filesystem. */ - getCachedLintResults(filePath) { + getCachedLintResults(filePath, config) { /* * Cached lint results are valid if and only if: @@ -83,7 +78,7 @@ class LintResultCache { */ const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath); - const hashOfConfig = hashOfConfigFor(this.configHelper, filePath); + const hashOfConfig = hashOfConfigFor(config); const changed = fileDescriptor.changed || fileDescriptor.meta.hashOfConfig !== hashOfConfig; if (fileDescriptor.notFound || changed) { @@ -105,10 +100,11 @@ class LintResultCache { * applied), to prevent potentially incorrect results if fixes are not * written to disk. * @param {string} filePath The file for which to set lint results. + * @param {ConfigArray} config The config of the file. * @param {Object} result The lint result to be set for the file. * @returns {void} */ - setCachedLintResults(filePath, result) { + setCachedLintResults(filePath, config, result) { if (result && Object.prototype.hasOwnProperty.call(result, "output")) { return; } @@ -130,7 +126,7 @@ class LintResultCache { } fileDescriptor.meta.results = resultToSerialize; - fileDescriptor.meta.hashOfConfig = hashOfConfigFor(this.configHelper, result.filePath); + fileDescriptor.meta.hashOfConfig = hashOfConfigFor(config); } } diff --git a/lib/util/naming.js b/lib/util/naming.js index ea1cc9518ea..b99155f15ca 100644 --- a/lib/util/naming.js +++ b/lib/util/naming.js @@ -3,16 +3,6 @@ */ "use strict"; -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const pathUtils = require("../util/path-utils"); - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - const NAMESPACE_REGEX = /^@.*\//iu; /** @@ -30,8 +20,8 @@ function normalizePackageName(name, prefix) { * Normalize to Unix first to avoid errors later on. * https://github.com/eslint/eslint/issues/5644 */ - if (normalizedName.indexOf("\\") > -1) { - normalizedName = pathUtils.convertPathToPosix(normalizedName); + if (normalizedName.includes("\\")) { + normalizedName = normalizedName.replace(/\\/gu, "/"); } if (normalizedName.charAt(0) === "@") { @@ -53,7 +43,7 @@ function normalizePackageName(name, prefix) { */ normalizedName = normalizedName.replace(/^@([^/]+)\/(.*)$/u, `@$1/${prefix}-$2`); } - } else if (normalizedName.indexOf(`${prefix}-`) !== 0) { + } else if (!normalizedName.startsWith(`${prefix}-`)) { normalizedName = `${prefix}-${normalizedName}`; } diff --git a/lib/util/path-utils.js b/lib/util/path-utils.js deleted file mode 100644 index c96254df6b8..00000000000 --- a/lib/util/path-utils.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @fileoverview Common helpers for operations on filenames and paths - * @author Ian VanSchooten - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const path = require("path"); - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -/** - * Replace Windows with posix style paths - * - * @param {string} filepath Path to convert - * @returns {string} Converted filepath - */ -function convertPathToPosix(filepath) { - const normalizedFilepath = path.normalize(filepath); - const posixFilepath = normalizedFilepath.replace(/\\/gu, "/"); - - return posixFilepath; -} - -/** - * Converts an absolute filepath to a relative path from a given base path - * - * For example, if the filepath is `/my/awesome/project/foo.bar`, - * and the base directory is `/my/awesome/project/`, - * then this function should return `foo.bar`. - * - * path.relative() does something similar, but it requires a baseDir (`from` argument). - * This function makes it optional and just removes a leading slash if the baseDir is not given. - * - * It does not take into account symlinks (for now). - * - * @param {string} filepath Path to convert to relative path. If already relative, - * it will be assumed to be relative to process.cwd(), - * converted to absolute, and then processed. - * @param {string} [baseDir] Absolute base directory to resolve the filepath from. - * If not provided, all this function will do is remove - * a leading slash. - * @returns {string} Relative filepath - */ -function getRelativePath(filepath, baseDir) { - const absolutePath = path.isAbsolute(filepath) - ? filepath - : path.resolve(filepath); - - if (baseDir) { - if (!path.isAbsolute(baseDir)) { - throw new Error(`baseDir should be an absolute path: ${baseDir}`); - } - return path.relative(baseDir, absolutePath); - } - return absolutePath.replace(/^\//u, ""); - -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -module.exports = { - convertPathToPosix, - getRelativePath -}; diff --git a/lib/util/relative-module-resolver.js b/lib/util/relative-module-resolver.js index 9e86909c5de..fa45acd1261 100644 --- a/lib/util/relative-module-resolver.js +++ b/lib/util/relative-module-resolver.js @@ -18,13 +18,24 @@ const createRequireFromPath = Module.createRequireFromPath || (filename => { return mod.exports; }); -/** - * Resolves a Node module relative to another module - * @param {string} moduleName The name of a Node module, or a path to a Node module. - * - * @param {string} relativeToPath An absolute path indicating the module that `moduleName` should be resolved relative to. This must be - * a file rather than a directory, but the file need not actually exist. - * @returns {string} The absolute path that would result from calling `require.resolve(moduleName)` in a file located at `relativeToPath` - */ -module.exports = (moduleName, relativeToPath) => - createRequireFromPath(relativeToPath).resolve(moduleName); +module.exports = { + + /** + * Resolves a Node module relative to another module + * @param {string} moduleName The name of a Node module, or a path to a Node module. + * + * @param {string} relativeToPath An absolute path indicating the module that `moduleName` should be resolved relative to. This must be + * a file rather than a directory, but the file need not actually exist. + * @returns {string} The absolute path that would result from calling `require.resolve(moduleName)` in a file located at `relativeToPath` + */ + resolve(moduleName, relativeToPath) { + try { + return createRequireFromPath(relativeToPath).resolve(moduleName); + } catch (error) { + if (error && error.code === "MODULE_NOT_FOUND" && error.message.includes(moduleName)) { + error.message += ` relative to '${relativeToPath}'`; + } + throw error; + } + } +}; diff --git a/lib/util/report-translator.js b/lib/util/report-translator.js index 3dfdca0e494..8c9ed007a25 100644 --- a/lib/util/report-translator.js +++ b/lib/util/report-translator.js @@ -227,7 +227,7 @@ function createProblem(options) { /** * Returns a function that converts the arguments of a `context.report` call from a rule into a reported * problem for the Node.js API. - * @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object}} metadata Metadata for the reported problem + * @param {{ruleId: string, severity: number, sourceCode: SourceCode, messageIds: Object, disableFixes: boolean}} metadata Metadata for the reported problem * @param {SourceCode} sourceCode The `SourceCode` instance for the text being linted * @returns {function(...args): ReportInfo} Function that returns information about the report */ @@ -275,7 +275,7 @@ module.exports = function createReportTranslator(metadata) { message: interpolate(computedMessage, descriptor.data), messageId: descriptor.messageId, loc: normalizeReportLoc(descriptor), - fix: normalizeFixes(descriptor, metadata.sourceCode) + fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode) }); }; }; diff --git a/lib/util/source-code-utils.js b/lib/util/source-code-utils.js index cac13d37067..63dc2446458 100644 --- a/lib/util/source-code-utils.js +++ b/lib/util/source-code-utils.js @@ -9,10 +9,7 @@ // Requirements //------------------------------------------------------------------------------ -const CLIEngine = require("../cli-engine"), - globUtils = require("./glob-utils"), - baseDefaultOptions = require("../../conf/default-cli-options"); - +const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine"); const debug = require("debug")("eslint:source-code-utils"); //------------------------------------------------------------------------------ @@ -22,22 +19,23 @@ const debug = require("debug")("eslint:source-code-utils"); /** * Get the SourceCode object for a single file * @param {string} filename The fully resolved filename to get SourceCode from. - * @param {Object} options A CLIEngine options object. + * @param {Object} engine A CLIEngine. * @returns {Array} Array of the SourceCode object representing the file * and fatal error message. */ -function getSourceCodeOfFile(filename, options) { +function getSourceCodeOfFile(filename, engine) { debug("getting sourceCode of", filename); - const opts = Object.assign({}, options, { rules: {} }); - const cli = new CLIEngine(opts); - const results = cli.executeOnFiles([filename]); + const results = engine.executeOnFiles([filename]); if (results && results.results[0] && results.results[0].messages[0] && results.results[0].messages[0].fatal) { const msg = results.results[0].messages[0]; throw new Error(`(${filename}:${msg.line}:${msg.column}) ${msg.message}`); } - const sourceCode = cli.linter.getSourceCode(); + + // TODO: extract the logic that creates source code objects to `SourceCode#parse(text, options)` or something like. + const { linter } = getCLIEngineInternalSlots(engine); + const sourceCode = linter.getSourceCode(); return sourceCode; } @@ -56,38 +54,28 @@ function getSourceCodeOfFile(filename, options) { /** * Gets the SourceCode of a single file, or set of files. * @param {string[]|string} patterns A filename, directory name, or glob, or an array of them - * @param {Object} [providedOptions] A CLIEngine options object. If not provided, the default cli options will be used. - * @param {progressCallback} [providedCallback] Callback for reporting execution status + * @param {Object} options A CLIEngine options object. If not provided, the default cli options will be used. + * @param {progressCallback} callback Callback for reporting execution status * @returns {Object} The SourceCode of all processed files. */ -function getSourceCodeOfFiles(patterns, providedOptions, providedCallback) { +function getSourceCodeOfFiles(patterns, options, callback) { const sourceCodes = {}; const globPatternsList = typeof patterns === "string" ? [patterns] : patterns; - let options, callback; - - const defaultOptions = Object.assign({}, baseDefaultOptions, { cwd: process.cwd() }); - - if (typeof providedOptions === "undefined") { - options = defaultOptions; - callback = null; - } else if (typeof providedOptions === "function") { - callback = providedOptions; - options = defaultOptions; - } else if (typeof providedOptions === "object") { - options = Object.assign({}, defaultOptions, providedOptions); - callback = providedCallback; - } - debug("constructed options:", options); + const engine = new CLIEngine({ ...options, rules: {} }); - const filenames = globUtils.listFilesToProcess(globPatternsList, options) - .filter(fileInfo => !fileInfo.ignored) - .reduce((files, fileInfo) => files.concat(fileInfo.filename), []); + // TODO: make file iteration as a public API and use it. + const { fileEnumerator } = getCLIEngineInternalSlots(engine); + const filenames = + Array.from(fileEnumerator.iterateFiles(globPatternsList)) + .filter(entry => !entry.ignored) + .map(entry => entry.filePath); if (filenames.length === 0) { debug(`Did not find any files matching pattern(s): ${globPatternsList}`); } + filenames.forEach(filename => { - const sourceCode = getSourceCodeOfFile(filename, options); + const sourceCode = getSourceCodeOfFile(filename, engine); if (sourceCode) { debug("got sourceCode of", filename); @@ -97,6 +85,7 @@ function getSourceCodeOfFiles(patterns, providedOptions, providedCallback) { callback(filenames.length); // eslint-disable-line callback-return } }); + return sourceCodes; } diff --git a/lib/util/types.js b/lib/util/types.js new file mode 100644 index 00000000000..157b322d853 --- /dev/null +++ b/lib/util/types.js @@ -0,0 +1,126 @@ +/** + * @fileoverview Define common types for input completion. + * @author Toru Nagashima + */ +"use strict"; + +/** @type {any} */ +module.exports = {}; + +/** @typedef {boolean | "off" | "readable" | "readonly" | "writable" | "writeable"} GlobalConf */ +/** @typedef {0 | 1 | 2 | "off" | "warn" | "error"} SeverityConf */ +/** @typedef {SeverityConf | [SeverityConf, ...any[]]} RuleConf */ + +/** + * @typedef {Object} EcmaFeatures + * @property {boolean} [globalReturn] Enabling `return` statements at the top-level. + * @property {boolean} [jsx] Enabling JSX syntax. + * @property {boolean} [impliedStrict] Enabling strict mode always. + */ + +/** + * @typedef {Object} ParserOptions + * @property {EcmaFeatures} [ecmaFeatures] The optional features. + * @property {3|5|6|7|8|9|10|2015|2016|2017|2018|2019} [ecmaVersion] The ECMAScript version (or revision number). + * @property {"script"|"module"} [sourceType] The source code type. + */ + +/** + * @typedef {Object} ConfigData + * @property {Record} [env] The environment settings. + * @property {string | string[]} [extends] The path to other config files or the package name of shareable configs. + * @property {Record} [globals] The global variable settings. + * @property {OverrideConfigData[]} [overrides] The override settings per kind of files. + * @property {string} [parser] The path to a parser or the package name of a parser. + * @property {ParserOptions} [parserOptions] The parser options. + * @property {string[]} [plugins] The plugin specifiers. + * @property {boolean} [root] The root flag. + * @property {Record} [rules] The rule settings. + * @property {Object} [settings] The shared settings. + */ + +/** + * @typedef {Object} OverrideConfigData + * @property {Record} [env] The environment settings. + * @property {string | string[]} [excludedFiles] The glob pattarns for excluded files. + * @property {string | string[]} files The glob pattarns for target files. + * @property {Record} [globals] The global variable settings. + * @property {string} [parser] The path to a parser or the package name of a parser. + * @property {ParserOptions} [parserOptions] The parser options. + * @property {string[]} [plugins] The plugin specifiers. + * @property {Record} [rules] The rule settings. + * @property {Object} [settings] The shared settings. + */ + +/** + * @typedef {Object} ParseResult + * @property {Object} ast The AST. + * @property {ScopeManager} [scopeManager] The scope manager of the AST. + * @property {Record} [services] The services that the parser provides. + * @property {Record} [visitorKeys] The visitor keys of the AST. + */ + +/** + * @typedef {Object} Parser + * @property {(text:string, options:ParserOptions) => Object} parse The definition of global variables. + * @property {(text:string, options:ParserOptions) => ParseResult} [parseForESLint] The parser options that will be enabled under this environment. + */ + +/** + * @typedef {Object} Environment + * @property {Record} [globals] The definition of global variables. + * @property {ParserOptions} [parserOptions] The parser options that will be enabled under this environment. + */ + +/** + * @typedef {Object} LintMessage + * @property {number} column The 1-based column number. + * @property {number} [endColumn] The 1-based column number of the end location. + * @property {number} [endLine] The 1-based line number of the end location. + * @property {boolean} fatal If `true` then this is a fatal error. + * @property {{range:[number,number], text:string}} [fix] Information for autofix. + * @property {number} line The 1-based line number. + * @property {string} message The error message. + * @property {string|null} ruleId The ID of the rule which makes this message. + * @property {0|1|2} severity The severity of this message. + */ + +/** + * @typedef {Object} Processor + * @property {(text:string, filename:string) => Array} [preprocess] The function to extract code blocks. + * @property {(messagesList:LintMessage[][], filename:string) => LintMessage[]} [postprocess] The function to merge messages. + * @property {boolean} [supportsAutofix] If `true` then it means the processor supports autofix. + */ + +/** + * @typedef {Object} RuleMetaDocs + * @property {string} category The category of the rule. + * @property {string} description The description of the rule. + * @property {boolean} recommended If `true` then the rule is included in `eslint:recommended` preset. + * @property {string} url The URL of the rule documentation. + */ + +/** + * @typedef {Object} RuleMeta + * @property {boolean} [deprecated] If `true` then the rule has been deprecated. + * @property {RuleMetaDocs} docs The document information of the rule. + * @property {"code"|"whitespace"} [fixable] The autofix type. + * @property {Record} [messages] The messages the rule reports. + * @property {string[]} [replacedBy] The IDs of the alternative rules. + * @property {Array|Object} schema The option schema of the rule. + * @property {"problem"|"suggestion"|"layout"} type The rule type. + */ + +/** + * @typedef {Object} Rule + * @property {Function} create The factory of the rule. + * @property {RuleMeta} meta The meta data of the rule. + */ + +/** + * @typedef {Object} Plugin + * @property {Record} [configs] The definition of plugin configs. + * @property {Record} [environments] The definition of plugin environments. + * @property {Record} [processors] The definition of plugin processors. + * @property {Record} [rules] The definition of plugin rules. + */ diff --git a/messages/file-not-found.txt b/messages/file-not-found.txt index 97e2d37fa94..639498eb5c6 100644 --- a/messages/file-not-found.txt +++ b/messages/file-not-found.txt @@ -1,2 +1,2 @@ -No files matching the pattern "<%= pattern %>" were found. +No files matching the pattern "<%= pattern %>"<% if (globDisabled) { %> (with disabling globs)<% } %> were found. Please check for typing mistakes in the pattern. diff --git a/messages/no-config-found.txt b/messages/no-config-found.txt index 2f95c41b8c7..348f6dcd25f 100644 --- a/messages/no-config-found.txt +++ b/messages/no-config-found.txt @@ -2,6 +2,6 @@ ESLint couldn't find a configuration file. To set up a configuration file for th eslint --init -ESLint looked for configuration files in <%= directory %> and its ancestors. If it found none, it then looked in your home directory. +ESLint looked for configuration files in <%= directoryPath %> and its ancestors. If it found none, it then looked in your home directory. If you think you already have a configuration file or if you need more help, please stop by the ESLint chat room: https://gitter.im/eslint/eslint diff --git a/messages/plugin-missing.txt b/messages/plugin-missing.txt index aa9a298bff4..92507d0dd0b 100644 --- a/messages/plugin-missing.txt +++ b/messages/plugin-missing.txt @@ -6,6 +6,6 @@ It's likely that the plugin isn't installed correctly. Try reinstalling by runni npm install <%- pluginName %>@latest --save-dev -The plugin "<%- pluginName %>" was referenced from the config file in <%- configStack.join("\n\t...loaded in ") %>. +The plugin "<%- pluginName %>" was referenced from the config file in "<%- importerName %>". If you still can't figure out the problem, please stop by https://gitter.im/eslint/eslint to chat with the team. diff --git a/package.json b/package.json index d15aa4d1fd1..944fc93ac43 100644 --- a/package.json +++ b/package.json @@ -49,12 +49,13 @@ "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", + "glob-parent": "^3.1.0", "globals": "^11.7.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "inquirer": "^6.2.2", + "is-glob": "^4.0.0", "js-yaml": "^3.13.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", @@ -91,6 +92,7 @@ "eslint-release": "^1.2.0", "eslump": "^2.0.0", "esprima": "^4.0.1", + "glob": "^7.1.3", "jsdoc": "^3.5.5", "karma": "^4.0.1", "karma-chrome-launcher": "^2.2.0", @@ -100,8 +102,8 @@ "leche": "^2.2.3", "load-perf": "^0.2.0", "markdownlint": "^0.12.0", + "metro-memory-fs": "^0.53.1", "mocha": "^5.0.5", - "mock-fs": "^4.8.0", "npm-license": "^0.3.3", "nyc": "^13.3.0", "proxyquire": "^2.0.1", diff --git a/tests/bench/bench.js b/tests/bench/bench.js index be574cf336a..ad97af7cefd 100644 --- a/tests/bench/bench.js +++ b/tests/bench/bench.js @@ -1,4 +1,4 @@ -var Linter = require("../../lib/linter"), +var Linter = require("../../lib/linter").Linter, fs = require("fs"); var config = require("../../conf/eslint-recommended"); diff --git a/tests/fixtures/config-file/extends-chain-2/parser.js b/tests/fixtures/config-file/extends-chain-2/parser.js index e69de29bb2d..7df9df83306 100644 --- a/tests/fixtures/config-file/extends-chain-2/parser.js +++ b/tests/fixtures/config-file/extends-chain-2/parser.js @@ -0,0 +1,5 @@ +"use strict" + +module.exports = { + parse() {} +} diff --git a/tests/fixtures/config-file/js/node_modules/foo/index.js b/tests/fixtures/config-file/js/node_modules/foo/index.js index c8ac80af70b..3918c74e446 100644 --- a/tests/fixtures/config-file/js/node_modules/foo/index.js +++ b/tests/fixtures/config-file/js/node_modules/foo/index.js @@ -1 +1 @@ -"use strict": +"use strict"; diff --git a/tests/fixtures/configurations/plugins-with-prefix-and-namespace.json b/tests/fixtures/configurations/plugins-with-prefix-and-namespace.json index c6a6dbaae81..b83d83bd44f 100644 --- a/tests/fixtures/configurations/plugins-with-prefix-and-namespace.json +++ b/tests/fixtures/configurations/plugins-with-prefix-and-namespace.json @@ -4,7 +4,7 @@ ], "rules": { - "example/example-rule": 1 + "@eslint/example/example-rule": 1 } } diff --git a/tests/fixtures/configurations/plugins-without-prefix-with-namespace.json b/tests/fixtures/configurations/plugins-without-prefix-with-namespace.json index 9d10438f6ee..d5cb34f2d93 100644 --- a/tests/fixtures/configurations/plugins-without-prefix-with-namespace.json +++ b/tests/fixtures/configurations/plugins-without-prefix-with-namespace.json @@ -4,7 +4,7 @@ ], "rules": { - "example/example-rule": 1 + "@eslint/example/example-rule": 1 } } diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index cff648d49d0..71415088146 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -14,10 +14,11 @@ const assert = require("chai").assert, sinon = require("sinon"), leche = require("leche"), shell = require("shelljs"), - Config = require("../../lib/config"), fs = require("fs"), os = require("os"), - hash = require("../../lib/util/hash"); + hash = require("../../lib/util/hash"), + { CascadingConfigArrayFactory } = require("../../lib/cli-engine/cascading-config-array-factory"), + { defineCLIEngineWithInmemoryFileSystem } = require("./cli-engine/_utils"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); @@ -31,7 +32,6 @@ describe("CLIEngine", () => { const examplePluginName = "eslint-plugin-example", examplePluginNameWithNamespace = "@eslint/eslint-plugin-example", - requireStubs = {}, examplePlugin = { rules: { "example-rule": require("../fixtures/rules/custom-rule"), @@ -39,9 +39,14 @@ describe("CLIEngine", () => { } }, examplePreprocessorName = "eslint-plugin-processor", - originalDir = process.cwd(); - let CLIEngine, - fixtureDir; + originalDir = process.cwd(), + fixtureDir = path.resolve(os.tmpdir(), "eslint/fixtures"); + + /** @type {import("../../lib/cli-engine")["CLIEngine"]} */ + let CLIEngine; + + /** @type {import("../../lib/cli-engine")["getCLIEngineInternalSlots"]} */ + let getCLIEngineInternalSlots; /** * Returns the path inside of the fixture directory. @@ -65,29 +70,24 @@ describe("CLIEngine", () => { * @private */ function cliEngineWithPlugins(options) { - const engine = new CLIEngine(Object.assign({}, options, { configFile: null })); + const engine = new CLIEngine(options); // load the mocked plugins - engine.config.plugins.define(examplePluginName, examplePlugin); - engine.config.plugins.define(examplePluginNameWithNamespace, examplePlugin); - engine.config.plugins.define(examplePreprocessorName, require("../fixtures/processors/custom-processor")); - - // load the real file now so that it can consume the loaded plugins - engine.config.loadSpecificConfig(options.configFile); + engine.addPlugin(examplePluginName, examplePlugin); + engine.addPlugin(examplePluginNameWithNamespace, examplePlugin); + engine.addPlugin(examplePreprocessorName, require("../fixtures/processors/custom-processor")); return engine; } // copy into clean area so as not to get "infected" by this project's .eslintrc files before(() => { - fixtureDir = path.join(os.tmpdir(), "/eslint/fixtures"); shell.mkdir("-p", fixtureDir); shell.cp("-r", "./tests/fixtures/.", fixtureDir); - fixtureDir = fs.realpathSync(fixtureDir); }); beforeEach(() => { - CLIEngine = proxyquire("../../lib/cli-engine", requireStubs); + ({ CLIEngine, getCLIEngineInternalSlots } = require("../../lib/cli-engine")); }); after(() => { @@ -99,8 +99,9 @@ describe("CLIEngine", () => { process.chdir(__dirname); try { const engine = new CLIEngine(); + const internalSlots = getCLIEngineInternalSlots(engine); - assert.strictEqual(engine.options.cwd, __dirname); + assert.strictEqual(internalSlots.options.cwd, __dirname); } finally { process.chdir(originalDir); } @@ -110,7 +111,7 @@ describe("CLIEngine", () => { assert.throws(() => { // eslint-disable-next-line no-new new CLIEngine({ ignorePath: fixtureDir }); - }, `Error: Could not load file ${fixtureDir}\nError: ${fixtureDir} is not a file`); + }, `Cannot read ignore file: ${fixtureDir}\nError: ${fixtureDir} is not a file`); }); }); @@ -430,8 +431,12 @@ describe("CLIEngine", () => { fix: true, fixTypes: ["layout"] }); + const internalSlots = getCLIEngineInternalSlots(engine); - engine.linter.defineRule("no-program", require(getFixturePath("rules", "fix-types-test", "no-program.js"))); + internalSlots.linter.defineRule( + "no-program", + require(getFixturePath("rules", "fix-types-test", "no-program.js")) + ); const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); @@ -448,8 +453,12 @@ describe("CLIEngine", () => { fix: true, fixTypes: ["layout"] }); + const internalSlots = getCLIEngineInternalSlots(engine); - engine.linter.defineRule("no-program", require(getFixturePath("rules", "fix-types-test", "no-program.js"))); + internalSlots.linter.defineRule( + "no-program", + require(getFixturePath("rules", "fix-types-test", "no-program.js")) + ); const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); @@ -511,6 +520,7 @@ describe("CLIEngine", () => { engine = cliEngineWithPlugins({ useEslintrc: false, fix: true, + plugins: ["example"], rules: { "example/make-syntax-error": "error" }, @@ -751,6 +761,7 @@ describe("CLIEngine", () => { describe("executeOnFiles()", () => { + /** @type {InstanceType} */ let engine; it("should use correct parser when custom parser is specified", () => { @@ -901,7 +912,7 @@ describe("CLIEngine", () => { assert.throws(() => { engine.executeOnFiles(["fixtures/files/*"]); - }, `ENOENT: no such file or directory, open '${getFixturePath("..", "fixtures", "files", "*")}`); + }, "No files matching 'fixtures/files/*' were found (glob was disabled)."); }); it("should report on all files passed explicitly, even if ignored by default", () => { @@ -1214,15 +1225,15 @@ describe("CLIEngine", () => { }); assert.throws(() => { - engine.executeOnFiles([getFixturePath("./")]); - }, `All files matched by '${getFixturePath("./")}' are ignored.`); + engine.executeOnFiles([getFixturePath("./cli-engine/")]); + }, `All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`); }); it("should throw an error when all given files are ignored", () => { assert.throws(() => { - engine.executeOnFiles(["tests/fixtures/"]); - }, "All files matched by 'tests/fixtures/' are ignored."); + engine.executeOnFiles(["tests/fixtures/cli-engine/"]); + }, "All files matched by 'tests/fixtures/cli-engine/' are ignored."); }); it("should throw an error when all given files are ignored even with a `./` prefix", () => { @@ -1231,8 +1242,8 @@ describe("CLIEngine", () => { }); assert.throws(() => { - engine.executeOnFiles(["./tests/fixtures/"]); - }, "All files matched by './tests/fixtures/' are ignored."); + engine.executeOnFiles(["./tests/fixtures/cli-engine/"]); + }, "All files matched by './tests/fixtures/cli-engine/' are ignored."); }); // https://github.com/eslint/eslint/issues/3788 @@ -1997,7 +2008,7 @@ describe("CLIEngine", () => { assert.strictEqual(report.results.length, 1); assert.strictEqual(report.results[0].messages.length, 2); - assert.strictEqual(report.results[0].messages[0].ruleId, "example/example-rule"); + assert.strictEqual(report.results[0].messages[0].ruleId, "@eslint/example/example-rule"); }); it("should return two messages when executing with config file that specifies a plugin without prefix", () => { @@ -2025,7 +2036,7 @@ describe("CLIEngine", () => { assert.strictEqual(report.results.length, 1); assert.strictEqual(report.results[0].messages.length, 2); - assert.strictEqual(report.results[0].messages[0].ruleId, "example/example-rule"); + assert.strictEqual(report.results[0].messages[0].ruleId, "@eslint/example/example-rule"); }); it("should return two messages when executing with cli option that specifies a plugin", () => { @@ -2883,44 +2894,151 @@ describe("CLIEngine", () => { assert.strictEqual(ret.results[0].messages[0].ruleId, "no-unused-vars"); }); }); - }); - describe("getConfigForFile", () => { + describe("config in a config file should prior to shareable configs always; https://github.com/eslint/eslint/issues/11510", () => { + beforeEach(() => { + ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ + cwd: () => path.join(os.tmpdir(), "cli-engine/11510"), + files: { + "no-console-error-in-overrides.json": JSON.stringify({ + overrides: [{ + files: ["*.js"], + rules: { "no-console": "error" } + }] + }), + ".eslintrc.json": JSON.stringify({ + extends: "./no-console-error-in-overrides.json", + rules: { "no-console": "off" } + }), + "a.js": "console.log();" + } + })); + engine = new CLIEngine(); + }); - it("should return the info from Config#getConfig when called", () => { + it("should not report 'no-console' error.", () => { + const { results } = engine.executeOnFiles("a.js"); - const engine = new CLIEngine({ - configFile: getFixturePath("configurations", "quotes-error.json") + assert.strictEqual(results.length, 1); + assert.deepStrictEqual(results[0].messages, []); }); + }); - const configHelper = new Config(engine.options, engine.linter); + describe("configs of plugin rules should be validated even if 'plugins' key doesn't exist; https://github.com/eslint/eslint/issues/11559", () => { + beforeEach(() => { + ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ + cwd: () => path.join(os.tmpdir(), "cli-engine/11559"), + files: { + "node_modules/eslint-plugin-test/index.js": ` + exports.configs = { + recommended: { plugins: ["test"] } + }; + exports.rules = { + foo: { + meta: { schema: [{ type: "number" }] }, + create() { return {}; } + } + }; + `, + ".eslintrc.json": JSON.stringify({ - const filePath = getFixturePath("single-quoted.js"); + // Import via the recommended config. + extends: "plugin:test/recommended", - assert.deepStrictEqual( - engine.getConfigForFile(filePath), - configHelper.getConfig(filePath) - ); + // Has invalid option. + rules: { "test/foo": ["error", "invalid-option"] } + }), + "a.js": "console.log();" + } + })); + engine = new CLIEngine(); + }); + it("should throw fatal error.", () => { + assert.throws(() => { + engine.executeOnFiles("a.js"); + }, /invalid-option/u); + }); }); + describe("'--fix-type' should not crash even if plugin rules exist; https://github.com/eslint/eslint/issues/11586", () => { + beforeEach(() => { + ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ + cwd: () => path.join(os.tmpdir(), "cli-engine/11586"), + files: { + "node_modules/eslint-plugin-test/index.js": ` + exports.rules = { + "no-example": { + meta: { type: "problem", fixable: "code" }, + create(context) { + return { + Identifier(node) { + if (node.name === "example") { + context.report({ + node, + message: "fix", + fix: fixer => fixer.replaceText(node, "fixed") + }) + } + } + }; + } + } + }; + `, + ".eslintrc.json": JSON.stringify({ + plugins: ["test"], + rules: { "test/no-example": "error" } + }), + "a.js": "example;" + } + })); + engine = new CLIEngine({ fix: true, fixTypes: ["problem"] }); + }); - it("should return the config when run from within a subdir", () => { + it("should not crash.", () => { + const { results } = engine.executeOnFiles("a.js"); - const engine = new CLIEngine({ - cwd: getFixturePath("config-hierarchy", "root-true", "parent", "root", "subdir") + assert.strictEqual(results.length, 1); + assert.deepStrictEqual(results[0].messages, []); + assert.deepStrictEqual(results[0].output, "fixed;"); }); + }); + }); - const configHelper = new Config(engine.options, engine.linter); + describe("getConfigForFile", () => { + it("should return the info from Config#getConfig when called", () => { + const options = { + configFile: getFixturePath("configurations", "quotes-error.json") + }; + const engine = new CLIEngine(options); + const filePath = getFixturePath("single-quoted.js"); + + const actualConfig = engine.getConfigForFile(filePath); + const expectedConfig = new CascadingConfigArrayFactory({ specificConfigPath: options.configFile }) + .getConfigArrayForFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(actualConfig, expectedConfig); + }); + + + it("should return the config when run from within a subdir", () => { + const options = { + cwd: getFixturePath("config-hierarchy", "root-true", "parent", "root", "subdir") + }; + const engine = new CLIEngine(options); const filePath = getFixturePath("config-hierarchy", "root-true", "parent", "root", ".eslintrc"); - const config = engine.getConfigForFile("./.eslintrc"); - assert.deepStrictEqual( - config, - configHelper.getConfig(filePath) - ); + const actualConfig = engine.getConfigForFile("./.eslintrc"); + const expectedConfig = new CascadingConfigArrayFactory(options) + .getConfigArrayForFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + assert.deepStrictEqual(actualConfig, expectedConfig); }); }); @@ -3049,7 +3167,7 @@ describe("CLIEngine", () => { it("should return null when a built-in formatter doesn't exist", () => { const engine = new CLIEngine(); - const fullFormatterPath = path.resolve(__dirname, "..", "..", "lib", "formatters", "special"); + const fullFormatterPath = path.resolve(__dirname, "../../lib/formatters/special"); assert.throws(() => { engine.getFormatter("special"); @@ -3204,7 +3322,7 @@ describe("CLIEngine", () => { const fakeFS = leche.fake(fs), localCLIEngine = proxyquire("../../lib/cli-engine", { fs: fakeFS - }), + }).CLIEngine, report = { results: [ { @@ -3233,7 +3351,7 @@ describe("CLIEngine", () => { const fakeFS = leche.fake(fs), localCLIEngine = proxyquire("../../lib/cli-engine", { fs: fakeFS - }), + }).CLIEngine, report = { results: [ { @@ -3291,6 +3409,111 @@ describe("CLIEngine", () => { }); }); + describe("Moved from tests/lib/util/glob-utils.js", () => { + it("should convert a directory name with no provided extensions into a glob pattern", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + }); + + it("should not convert path with globInputPaths option false", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util"), + globInputPaths: false + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file"]); + }); + + it("should convert an absolute directory name with no provided extensions into a posix glob pattern", () => { + const patterns = [getFixturePath("glob-util", "one-js-file")]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + const expected = [`${getFixturePath("glob-util", "one-js-file").replace(/\\/gu, "/")}/**/*.js`]; + + assert.deepStrictEqual(result, expected); + }); + + it("should convert a directory name with a single provided extension into a glob pattern", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util"), + extensions: [".jsx"] + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/**/*.jsx"]); + }); + + it("should convert a directory name with multiple provided extensions into a glob pattern", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util"), + extensions: [".jsx", ".js"] + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/**/*.{jsx,js}"]); + }); + + it("should convert multiple directory names into glob patterns", () => { + const patterns = ["one-js-file", "two-js-files"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/**/*.js", "two-js-files/**/*.js"]); + }); + + it("should remove leading './' from glob patterns", () => { + const patterns = ["./one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + }); + + it("should convert a directory name with a trailing '/' into a glob pattern", () => { + const patterns = ["one-js-file/"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + }); + + it("should return filenames as they are", () => { + const patterns = ["some-file.js"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["some-file.js"]); + }); + + it("should convert backslashes into forward slashes", () => { + const patterns = ["one-js-file\\example.js"]; + const opts = { + cwd: getFixturePath() + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + + assert.deepStrictEqual(result, ["one-js-file/example.js"]); + }); + }); }); describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { @@ -3388,7 +3611,7 @@ describe("CLIEngine", () => { describe("when retreiving version number", () => { it("should return current version number", () => { - const eslintCLI = require("../../lib/cli-engine"); + const eslintCLI = require("../../lib/cli-engine").CLIEngine; const version = eslintCLI.version; assert.isString(version); @@ -3414,8 +3637,8 @@ describe("CLIEngine", () => { const fileConfig2 = engine2.getConfigForFile(filePath); // plugin - assert.strictEqual(fileConfig1.plugins[0], "example", "Plugin is present for engine 1"); - assert.isUndefined(fileConfig2.plugins, "Plugin is not present for engine 2"); + assert.deepStrictEqual(fileConfig1.plugins, ["example"], "Plugin is present for engine 1"); + assert.deepStrictEqual(fileConfig2.plugins, [], "Plugin is not present for engine 2"); }); }); @@ -3435,7 +3658,7 @@ describe("CLIEngine", () => { const fileConfig2 = engine2.getConfigForFile(filePath); // plugin - assert.strictEqual(fileConfig1.rules["example/example-rule"], 1, "example is present for engine 1"); + assert.deepStrictEqual(fileConfig1.rules["example/example-rule"], [1], "example is present for engine 1"); assert.isUndefined(fileConfig2.rules["example/example-rule"], "example is not present for engine 2"); }); }); diff --git a/tests/lib/cli-engine/_utils.js b/tests/lib/cli-engine/_utils.js new file mode 100644 index 00000000000..8fa025b110d --- /dev/null +++ b/tests/lib/cli-engine/_utils.js @@ -0,0 +1,507 @@ +/** + * @fileoverview Define classes what use the in-memory file system. + * + * This provides utilities to test `ConfigArrayFactory` and `FileEnumerator`. + * + * - `defineConfigArrayFactoryWithInmemoryFileSystem({ cwd, files })` + * - `defineFileEnumeratorWithInmemoryFileSystem({ cwd, files })` + * + * Both functions define the class `ConfigArrayFactory` or `FileEnumerator` with + * the in-memory file system. Those search config files, parsers, and plugins in + * the `files` option via the in-memory file system. + * + * For each test case, it makes more readable if we define minimal files the + * test case requires. + * + * For example: + * + * ```js + * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + * files: { + * "node_modules/eslint-config-foo/index.js": ` + * module.exports = { + * parser: "./parser", + * rules: { + * "no-undef": "error" + * } + * } + * `, + * "node_modules/eslint-config-foo/parser.js": ` + * module.exports = { + * parse() {} + * } + * `, + * ".eslintrc.json": JSON.stringify({ root: true, extends: "foo" }) + * } + * }); + * const factory = new ConfigArrayFactory(); + * const config = factory.loadFile(".eslintrc.json"); + * + * assert(config[0].name === ".eslintrc.json » eslint-config-foo"); + * assert(config[0].filePath === path.resolve("node_modules/eslint-config-foo/index.js")); + * assert(config[0].parser.filePath === path.resolve("node_modules/eslint-config-foo/parser.js")); + * + * assert(config[1].name === ".eslintrc.json"); + * assert(config[1].filePath === path.resolve(".eslintrc.json")); + * assert(config[1].root === true); + * ``` + * + * @author Toru Nagashima + */ +"use strict"; + +// To use TypeScript type annotations for VSCode intellisense. +/* eslint-disable valid-jsdoc */ + +const path = require("path"); +const vm = require("vm"); +const MemoryFs = require("metro-memory-fs"); +const Proxyquire = require("proxyquire/lib/proxyquire"); + +const CascadingConfigArrayFactoryPath = + require.resolve("../../../lib/cli-engine/cascading-config-array-factory"); +const CLIEnginePath = + require.resolve("../../../lib/cli-engine"); +const ConfigArrayFactoryPath = + require.resolve("../../../lib/cli-engine/config-array-factory"); +const FileEnumeratorPath = + require.resolve("../../../lib/cli-engine/file-enumerator"); +const IgnoredPathsPath = + require.resolve("../../../lib/util/ignored-paths"); +const LoadRulesPath = + require.resolve("../../../lib/load-rules"); +const ESLintAllPath = + require.resolve("../../../conf/eslint-all"); +const ESLintRecommendedPath = + require.resolve("../../../conf/eslint-recommended"); + +// Ensure the needed files has been loaded and cached. +require(CascadingConfigArrayFactoryPath); +require(CLIEnginePath); +require(ConfigArrayFactoryPath); +require(FileEnumeratorPath); +require(IgnoredPathsPath); +require(LoadRulesPath); +require("js-yaml"); +require("espree"); + +// Override `_require` in order to throw runtime errors in stubs. +const ERRORED = Symbol("errored"); +const proxyquire = new class extends Proxyquire { + _require(...args) { + const retv = super._require(...args); // eslint-disable-line no-underscore-dangle + + if (retv[ERRORED]) { + throw retv[ERRORED]; + } + return retv; + } +}(module).noCallThru().noPreserveCache(); + +// Separated (sandbox) context to compile fixture files. +const context = vm.createContext(); + +/** + * Check if a given path is an existing file. + * @param {import("fs")} fs The file system. + * @param {string} filePath Tha path to a file to check. + * @returns {boolean} `true` if the file existed. + */ +function isExistingFile(fs, filePath) { + try { + return fs.statSync(filePath).isFile(); + } catch (error) { + return false; + } +} + +/** + * Get some paths to test. + * @param {string} prefix The prefix to try. + * @returns {string[]} The paths to test. + */ +function getTestPaths(prefix) { + return [ + path.join(prefix), + path.join(`${prefix}.js`), + path.join(prefix, "index.js") + ]; +} + +/** + * Iterate the candidate paths of a given request to resolve. + * @param {string} request Tha package name or file path to resolve. + * @param {string} relativeTo Tha path to the file what called this resolving. + * @returns {IterableIterator} The candidate paths. + */ +function *iterateCandidatePaths(request, relativeTo) { + if (path.isAbsolute(request)) { + yield* getTestPaths(request); + return; + } + if (/^\.{1,2}[/\\]/u.test(request)) { + yield* getTestPaths(path.resolve(path.dirname(relativeTo), request)); + return; + } + + let prevPath = path.resolve(relativeTo); + let dirPath = path.dirname(prevPath); + + while (dirPath && dirPath !== prevPath) { + yield* getTestPaths(path.join(dirPath, "node_modules", request)); + prevPath = dirPath; + dirPath = path.dirname(dirPath); + } +} + +/** + * Resolve a given module name or file path relatively in the given file system. + * @param {import("fs")} fs The file system. + * @param {string} request Tha package name or file path to resolve. + * @param {string} relativeTo Tha path to the file what called this resolving. + * @returns {void} + */ +function fsResolve(fs, request, relativeTo) { + for (const filePath of iterateCandidatePaths(request, relativeTo)) { + if (isExistingFile(fs, filePath)) { + return filePath; + } + } + + throw Object.assign( + new Error(`Cannot find module '${request}'`), + { code: "MODULE_NOT_FOUND" } + ); +} + +/** + * Compile a JavaScript file. + * This is used to compile only fixture files, so this is minimam. + * @param {import("fs")} fs The file system. + * @param {Object} stubs The stubs. + * @param {string} filePath The path to a JavaScript file to compile. + * @param {string} content The source code to compile. + * @returns {any} The exported value. + */ +function compile(fs, stubs, filePath, content) { + const code = `(function(exports, require, module, __filename, __dirname) { ${content} })`; + const f = vm.runInContext(code, context); + const exports = {}; + const module = { exports }; + + f.call( + exports, + exports, + request => { + const modulePath = fsResolve(fs, request, filePath); + const stub = stubs[modulePath]; + + if (stub[ERRORED]) { + throw stub[ERRORED]; + } + return stub; + }, + module, + filePath, + path.dirname(filePath) + ); + + return module.exports; +} + +/** + * Import a given file path in the given file system. + * @param {import("fs")} fs The file system. + * @param {Object} stubs The stubs. + * @param {string} absolutePath Tha file path to import. + * @returns {void} + */ +function fsImportFresh(fs, stubs, absolutePath) { + if (absolutePath === ESLintAllPath) { + return require(ESLintAllPath); + } + if (absolutePath === ESLintRecommendedPath) { + return require(ESLintRecommendedPath); + } + + if (fs.existsSync(absolutePath)) { + return compile( + fs, + stubs, + absolutePath, + fs.readFileSync(absolutePath, "utf8") + ); + } + + throw Object.assign( + new Error(`Cannot find module '${absolutePath}'`), + { code: "MODULE_NOT_FOUND" } + ); +} + +/** + * Add support of `recursive` option. + * @param {import("fs")} fs The in-memory file system. + * @param {() => string} cwd The current working directory. + * @returns {void} + */ +function supportMkdirRecursiveOption(fs, cwd) { + const { mkdirSync } = fs; + + fs.mkdirSync = (filePath, options) => { + if (typeof options === "object" && options !== null) { + if (options.recursive) { + const absolutePath = path.resolve(cwd(), filePath); + const parentPath = path.dirname(absolutePath); + + if ( + parentPath && + parentPath !== absolutePath && + !fs.existsSync(parentPath) + ) { + fs.mkdirSync(parentPath, options); + } + } + mkdirSync(filePath, options.mode); + } else { + mkdirSync(filePath, options); + } + }; +} + +/** + * Define stubbed `ConfigArrayFactory` class what uses the in-memory file system. + * @param {Object} options The options. + * @param {() => string} [options.cwd] The current working directory. + * @param {Object} [options.files] The initial files definition in the in-memory file system. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. + */ +function defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd = process.cwd, + files = {} +} = {}) { + + /** + * The in-memory file system for this mock. + * @type {import("fs")} + */ + const fs = new MemoryFs({ + cwd, + platform: process.platform === "win32" ? "win32" : "posix" + }); + + supportMkdirRecursiveOption(fs, cwd); + fs.mkdirSync(cwd(), { recursive: true }); + + const RelativeModuleResolver = { resolve: fsResolve.bind(null, fs) }; + + /* + * Stubs for proxyquire. + * This contains the JavaScript files in `options.files`. + */ + const stubs = {}; + + stubs.fs = fs; + stubs["import-fresh"] = fsImportFresh.bind(null, fs, stubs); + stubs["../util/relative-module-resolver"] = RelativeModuleResolver; + + /* + * Write all files to the in-memory file system and compile all JavaScript + * files then set to `stubs`. + */ + (function initFiles(directoryPath, definition) { + for (const [filename, content] of Object.entries(definition)) { + const filePath = path.resolve(directoryPath, filename); + const parentPath = path.dirname(filePath); + + if (typeof content === "object") { + initFiles(filePath, content); + continue; + } + + /* + * Write this file to the in-memory file system. + * For config files that `fs.readFileSync()` or `importFresh()` will + * import. + */ + if (!fs.existsSync(parentPath)) { + fs.mkdirSync(parentPath, { recursive: true }); + } + fs.writeFileSync(filePath, content); + + /* + * Compile then stub if this file is a JavaScript file. + * For parsers and plugins that `require()` will import. + */ + if (path.extname(filePath) === ".js") { + Object.defineProperty(stubs, filePath, { + configurable: true, + enumerable: true, + get() { + let stub; + + try { + stub = compile(fs, stubs, filePath, content); + } catch (error) { + stub = { [ERRORED]: error }; + } + Object.defineProperty(stubs, filePath, { + configurable: true, + enumerable: true, + value: stub + }); + + return stub; + } + }); + } + } + }(cwd(), files)); + + // Load the stubbed one. + const { ConfigArrayFactory } = proxyquire(ConfigArrayFactoryPath, stubs); + + // Override the default cwd. + return { + fs, + RelativeModuleResolver, + ConfigArrayFactory: cwd === process.cwd + ? ConfigArrayFactory + : class extends ConfigArrayFactory { + constructor(options) { + super({ cwd: cwd(), ...options }); + } + } + }; +} + +/** + * Define stubbed `CascadingConfigArrayFactory` class what uses the in-memory file system. + * @param {Object} options The options. + * @param {() => string} [options.cwd] The current working directory. + * @param {Object} [options.files] The initial files definition in the in-memory file system. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"] }} The stubbed `CascadingConfigArrayFactory` class. + */ +function defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + cwd = process.cwd, + files = {} +} = {}) { + const { fs, RelativeModuleResolver, ConfigArrayFactory } = + defineConfigArrayFactoryWithInmemoryFileSystem({ cwd, files }); + const loadRules = proxyquire(LoadRulesPath, { fs }); + const { CascadingConfigArrayFactory } = + proxyquire(CascadingConfigArrayFactoryPath, { + "./config-array-factory": { ConfigArrayFactory }, + "../load-rules": loadRules + }); + + // Override the default cwd. + return { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory: cwd === process.cwd + ? CascadingConfigArrayFactory + : class extends CascadingConfigArrayFactory { + constructor(options) { + super({ cwd: cwd(), ...options }); + } + } + }; +} + +/** + * Define stubbed `FileEnumerator` class what uses the in-memory file system. + * @param {Object} options The options. + * @param {() => string} [options.cwd] The current working directory. + * @param {Object} [options.files] The initial files definition in the in-memory file system. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], IgnoredPaths: import("../../../lib/cli-engine/ignored-paths")["IgnoredPaths"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"] }} The stubbed `FileEnumerator` class. + */ +function defineFileEnumeratorWithInmemoryFileSystem({ + cwd = process.cwd, + files = {} +} = {}) { + const { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory + } = + defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ cwd, files }); + const { IgnoredPaths } = proxyquire(IgnoredPathsPath, { fs }); + const { FileEnumerator } = proxyquire(FileEnumeratorPath, { + fs, + "./cascading-config-array-factory": { CascadingConfigArrayFactory }, + "../util/ignored-paths": { IgnoredPaths } + }); + + // Override the default cwd. + return { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + IgnoredPaths, + FileEnumerator: cwd === process.cwd + ? FileEnumerator + : class extends FileEnumerator { + constructor(options) { + super({ cwd: cwd(), ...options }); + } + } + }; +} + +/** + * Define stubbed `CLIEngine` class what uses the in-memory file system. + * @param {Object} options The options. + * @param {() => string} [options.cwd] The current working directory. + * @param {Object} [options.files] The initial files definition in the in-memory file system. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], IgnoredPaths: import("../../../lib/cli-engine/ignored-paths")["IgnoredPaths"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"], CLIEngine: import("../../../lib/cli-engine")["CLIEngine"], getCLIEngineInternalSlots: import("../../../lib/cli-engine")["getCLIEngineInternalSlots"] }} The stubbed `CLIEngine` class. + */ +function defineCLIEngineWithInmemoryFileSystem({ + cwd = process.cwd, + files = {} +} = {}) { + const { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + IgnoredPaths, + FileEnumerator + } = + defineFileEnumeratorWithInmemoryFileSystem({ cwd, files }); + const { CLIEngine, getCLIEngineInternalSlots } = proxyquire(CLIEnginePath, { + fs, + "./cli-engine/cascading-config-array-factory": { CascadingConfigArrayFactory }, + "./cli-engine/file-enumerator": { FileEnumerator }, + "./util/ignored-paths": { IgnoredPaths }, + "./util/relative-module-resolver": RelativeModuleResolver + }); + + // Override the default cwd. + return { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + IgnoredPaths, + FileEnumerator, + CLIEngine: cwd === process.cwd + ? CLIEngine + : class extends CLIEngine { + constructor(options) { + super({ cwd: cwd(), ...options }); + } + }, + getCLIEngineInternalSlots + }; +} + +module.exports = { + defineConfigArrayFactoryWithInmemoryFileSystem, + defineCascadingConfigArrayFactoryWithInmemoryFileSystem, + defineFileEnumeratorWithInmemoryFileSystem, + defineCLIEngineWithInmemoryFileSystem +}; diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js new file mode 100644 index 00000000000..45769fab530 --- /dev/null +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -0,0 +1,1224 @@ +/** + * @fileoverview Tests for CascadingConfigArrayFactory class. + * @author Toru Nagashima + */ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const os = require("os"); +const { assert } = require("chai"); +const sh = require("shelljs"); +const sinon = require("sinon"); +const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); +const { ConfigArrayFactory } = require("../../../lib/cli-engine/config-array-factory"); +const { ExtractedConfig } = require("../../../lib/cli-engine/config-array/extracted-config"); +const { defineCascadingConfigArrayFactoryWithInmemoryFileSystem } = require("./_utils"); + +describe("CascadingConfigArrayFactory", () => { + describe("'getConfigArrayForFile(filePath)' method should retrieve the proper configuration.", () => { + const root = path.join(os.tmpdir(), "eslint/cli-engine/cascading-config-array-factory"); + const files = { + /* eslint-disable quote-props */ + "lib": { + "nested": { + "one.js": "", + "two.js": "", + "parser.js": "", + ".eslintrc.yml": "parser: './parser'" + }, + "one.js": "", + "two.js": "" + }, + "test": { + "one.js": "", + "two.js": "", + ".eslintrc.yml": "env: { mocha: true }" + }, + ".eslintignore": "/lib/nested/parser.js", + ".eslintrc.json": JSON.stringify({ + rules: { + "no-undef": "error", + "no-unused-vars": "error" + } + }) + /* eslint-enable quote-props */ + }; + + describe(`with the files ${JSON.stringify(files)}`, () => { + const { CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow + + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new CascadingConfigArrayFactory(); + }); + + it("should retrieve the config '.eslintrc.json' if the file path was not given.", () => { + const config = factory.getConfigArrayForFile(); + + assert.strictEqual(config.length, 1); + assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); + }); + + it("should retrieve the config '.eslintrc.json' if 'lib/one.js' was given.", () => { + const config = factory.getConfigArrayForFile("lib/one.js"); + + assert.strictEqual(config.length, 1); + assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); + }); + + it("should retrieve the merged config of '.eslintrc.json' and 'lib/nested/.eslintrc.yml' if 'lib/nested/one.js' was given.", () => { + const config = factory.getConfigArrayForFile("lib/nested/one.js"); + + assert.strictEqual(config.length, 2); + assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(config[1].filePath, path.join(root, "lib/nested/.eslintrc.yml")); + }); + + it("should retrieve the config '.eslintrc.json' if 'lib/non-exist.js' was given.", () => { + const config = factory.getConfigArrayForFile("lib/non-exist.js"); + + assert.strictEqual(config.length, 1); + assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); + }); + }); + + describe("Moved from tests/lib/config.js", () => { + let fixtureDir; + let sandbox; + + const DIRECTORY_CONFIG_HIERARCHY = require("../../fixtures/config-hierarchy/file-structure.json"); + + /** + * Returns the path inside of the fixture directory. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFixturePath(...args) { + return path.join(fixtureDir, "config-hierarchy", ...args); + } + + /** + * Mocks the current user's home path + * @param {string} fakeUserHomePath - fake user's home path + * @returns {void} + * @private + */ + function mockOsHomedir(fakeUserHomePath) { + sandbox.stub(os, "homedir") + .returns(fakeUserHomePath); + } + + /** + * Asserts that two configs are equal. This is necessary because assert.deepStrictEqual() + * gets confused when properties are in different orders. + * @param {Object} actual The config object to check. + * @param {Object} expected What the config object should look like. + * @returns {void} + * @private + */ + function assertConfigsEqual(actual, expected) { + const defaults = new ExtractedConfig().toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(actual, { ...defaults, ...expected }); + } + + /** + * Wait for the next tick. + * @returns {Promise} - + */ + function nextTick() { + return new Promise(resolve => process.nextTick(resolve)); + } + + /** + * Get the config data for a file. + * @param {CascadingConfigArrayFactory} factory The factory to get config. + * @param {string} filePath The path to a source code. + * @returns {Object} The gotten config. + */ + function getConfig(factory, filePath = "a.js") { + const { cwd } = factory; + const absolutePath = path.resolve(cwd, filePath); + + return factory + .getConfigArrayForFile(absolutePath) + .extractConfig(absolutePath) + .toCompatibleObjectAsConfigFileContent(); + } + + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(() => { + fixtureDir = `${os.tmpdir()}/eslint/fixtures`; + sh.mkdir("-p", fixtureDir); + sh.cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); + sh.cp("-r", "./tests/fixtures/rules", fixtureDir); + }); + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + after(() => { + sh.rm("-r", fixtureDir); + }); + + describe("new Config()", () => { + + // https://github.com/eslint/eslint/issues/2380 + it("should not modify baseConfig when format is specified", () => { + const customBaseConfig = { root: true }; + + new CascadingConfigArrayFactory({ baseConfig: customBaseConfig, format: "foo" }); // eslint-disable-line no-new + + assert.deepStrictEqual(customBaseConfig, { root: true }); + }); + + it("should create config object when using baseConfig with extends", () => { + const customBaseConfig = { + extends: path.resolve(__dirname, "../../fixtures/config-extends/array/.eslintrc") + }; + const factory = new CascadingConfigArrayFactory({ baseConfig: customBaseConfig, useEslintrc: false }); + const config = getConfig(factory); + + assert.deepStrictEqual(config.env, { + browser: false, + es6: true, + node: true + }); + assert.deepStrictEqual(config.rules, { + "no-empty": [1], + "comma-dangle": [2], + "no-console": [2] + }); + }); + }); + + describe("getConfig()", () => { + it("should return the project config when called in current working directory", () => { + const factory = new CascadingConfigArrayFactory(); + const actual = getConfig(factory); + + assert.strictEqual(actual.rules.strict[1], "global"); + }); + + it("should not retain configs from previous directories when called multiple times", () => { + const firstpath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/subdir/.eslintrc"); + const secondpath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/.eslintrc"); + const factory = new CascadingConfigArrayFactory(); + let config; + + config = getConfig(factory, firstpath); + assert.deepStrictEqual(config.rules["no-new"], [0]); + config = getConfig(factory, secondpath); + assert.deepStrictEqual(config.rules["no-new"], [1]); + }); + + it("should throw error when a configuration file doesn't exist", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/.eslintrc"); + const factory = new CascadingConfigArrayFactory(); + + sandbox.stub(fs, "readFileSync").throws(new Error()); + + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); + + }); + + it("should throw error when a configuration file is not require-able", () => { + const configPath = ".eslintrc"; + const factory = new CascadingConfigArrayFactory(); + + sandbox.stub(fs, "readFileSync").throws(new Error()); + + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); + + }); + + it("should cache config when the same directory is passed twice", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/.eslintrc"); + const configArrayFactory = new ConfigArrayFactory(); + const factory = new CascadingConfigArrayFactory({ configArrayFactory }); + + sandbox.spy(configArrayFactory, "loadOnDirectory"); + + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadOnDirectory.callcount; + + getConfig(factory, configPath); + + assert.strictEqual(configArrayFactory.loadOnDirectory.callcount, callcount); + }); + + // make sure JS-style comments don't throw an error + it("should load the config file when there are JS-style comments in the text", () => { + const specificConfigPath = path.resolve(__dirname, "../../fixtures/configurations/comments.json"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath, useEslintrc: false }); + const config = getConfig(factory); + const { semi, strict } = config.rules; + + assert.deepStrictEqual(semi, [1]); + assert.deepStrictEqual(strict, [0]); + }); + + // make sure YAML files work correctly + it("should load the config file when a YAML file is used", () => { + const specificConfigPath = path.resolve(__dirname, "../../fixtures/configurations/env-browser.yaml"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath, useEslintrc: false }); + const config = getConfig(factory); + const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; + + assert.deepStrictEqual(noAlert, [0]); + assert.deepStrictEqual(noUndef, [2]); + }); + + it("should contain the correct value for parser when a custom parser is specified", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/parser/.eslintrc.json"); + const factory = new CascadingConfigArrayFactory(); + const config = getConfig(factory, configPath); + + assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.js")); + }); + + /* + * Configuration hierarchy --------------------------------------------- + * https://github.com/eslint/eslint/issues/3915 + */ + it("should correctly merge environment settings", () => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: true }); + const file = getFixturePath("envs", "sub", "foo.js"); + const expected = { + rules: {}, + env: { + browser: true, + node: false + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Default configuration - blank + it("should return a blank config when using no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: false }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {} + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ baseConfig: false, useEslintrc: false }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {} + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // No default configuration + it("should return an empty config when not using .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: false }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, {}); + }); + + it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }, + useEslintrc: false + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + }, + plugins: ["example-with-rules-config"] + }, + cwd: getFixturePath("plugins"), + useEslintrc: false + }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + plugins: ["example-with-rules-config"], + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Project configuration - second level .eslintrc + it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [1], + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Project configuration - third level .eslintrc + it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [0], + quotes: [1, "double"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Project configuration - root set in second level .eslintrc + it("should not return or traverse configurations in parents of config with root:true", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Project configuration - root set in second level .eslintrc + it("should return project config when called with a relative path from a subdir", () => { + const factory = new CascadingConfigArrayFactory({ cwd: getFixturePath("root-true", "parent", "root", "subdir") }); + const dir = "."; + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, dir); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file adds to local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml") + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml") + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "double"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml") + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "single"], + "no-console": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file overrides local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml") + }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "single"], + "no-console": [1] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { + quotes: [1, "double"] + } + }, + specificConfigPath: getFixturePath("broken", "override-conf.yaml") + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [1, "double"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --plugin + it("should merge command line plugin with local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + plugins: ["another-plugin"] + }, + cwd: getFixturePath("plugins") + }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + plugins: [ + "example", + "another-plugin" + ], + rules: { + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + + it("should merge multiple different config file formats", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); + const expected = { + env: { + browser: true + }, + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + + it("should load user config globals", () => { + const configPath = path.resolve(__dirname, "../../fixtures/globals/conf.yaml"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath, useEslintrc: false }); + const expected = { + globals: { + foo: true + } + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not load disabled environments", () => { + const configPath = path.resolve(__dirname, "../../fixtures/environments/disable.yaml"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath, useEslintrc: false }); + const config = getConfig(factory, configPath); + + assert.isUndefined(config.globals.window); + }); + + it("should gracefully handle empty files", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/env-node.json"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath }); + + getConfig(factory, path.resolve(__dirname, "../../fixtures/configurations/empty/empty.json")); + }); + + // Meaningful stack-traces + it("should include references to where an `extends` configuration was loaded from", () => { + const configPath = path.resolve(__dirname, "../../fixtures/config-extends/error.json"); + + assert.throws(() => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); + + getConfig(factory, configPath); + }, /Referenced from:.*?error\.json/u); + }); + + // Keep order with the last array element taking highest precedence + it("should make the last element in an array take the highest precedence", () => { + const configPath = path.resolve(__dirname, "../../fixtures/config-extends/array/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); + const expected = { + rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, + env: { browser: false, node: true, es6: true } + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + describe("with env in a child configuration file", () => { + it("should not overwrite parserOptions of the parent with env of the child", () => { + const factory = new CascadingConfigArrayFactory(); + const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); + const expected = { + rules: {}, + env: { commonjs: true }, + parserOptions: { ecmaFeatures: { globalReturn: false } } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); + + describe("personal config file within home directory", () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY + } + }); + + /** + * Returns the path inside of the fixture directory. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should load the personal config if no local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "home-folder-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if a local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should ignore the personal config if config is passed through cli", () => { + const configPath = getFakeFixturePath("quotes-error.json"); + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: projectPath, + specificConfigPath: configPath + }); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + quotes: [2, "double"] + } + }; + + assertConfigsEqual(actual, expected); + }); + + it("should still load the project config if the current working directory is the same as the home folder", () => { + const projectPath = getFakeFixturePath("personal-config", "project-with-config"); + const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(projectPath); + + const actual = getConfig(factory, filePath); + const expected = { + rules: { + "project-level-rule": [2], + "subfolder-level-rule": [2] + } + }; + + assertConfigsEqual(actual, expected); + }); + }); + + describe("when no local or personal config is found", () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY + } + }); + + /** + * Returns the path inside of the fixture directory. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should throw an error if no local config and no personal config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); + }); + + it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath, useEslintrc: false }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but rules are specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { quotes: [2, "single"] } + }, + cwd: projectPath + }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ baseConfig: {}, cwd: projectPath }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + }); + + describe("with overrides", () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY + } + }); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...pathSegments) { + return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); + } + + it("should merge override config when the pattern matches the file name", () => { + const factory = new CascadingConfigArrayFactory({}); + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const expected = { + rules: { + quotes: [2, "single"], + "no-else-return": [0], + "no-unused-vars": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should merge override config when the pattern matches the file path relative to the config file", () => { + const factory = new CascadingConfigArrayFactory({}); + const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); + const expected = { + rules: { + curly: ["error", "multi", "consistent"], + "no-else-return": [0], + "no-unused-vars": [1], + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not merge override config when the pattern matches the absolute file path", () => { + const resolvedPath = path.resolve(__dirname, "../../fixtures/config-hierarchy/overrides/bar.js"); + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: resolvedPath, + rules: { + quotes: [1, "double"] + } + }] + }, + useEslintrc: false + }), /Invalid override pattern/u); + }); + + it("should not merge override config when the pattern traverses up the directory tree", () => { + const parentPath = "overrides/../**/*.js"; + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: parentPath, + rules: { + quotes: [1, "single"] + } + }] + }, + useEslintrc: false + }), /Invalid override pattern/u); + }); + + it("should merge all local configs (override and non-override) before non-local configs", () => { + const factory = new CascadingConfigArrayFactory({}); + const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); + const expected = { + rules: { + "no-console": [0], + "no-else-return": [0], + "no-unused-vars": [2], + quotes: [2, "double"], + semi: [2, "never"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { + const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "three/**/*.js", + rules: { + "semi-style": [2, "last"] + } + } + ] + }, + useEslintrc: false + }); + const expected = { + rules: { + "semi-style": [2, "last"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides even if some glob patterns do not match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not apply overrides if any excluded glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*one.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false + }); + const expected = { + rules: {} + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should apply overrides if all excluded glob patterns fail to match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false + }); + const expected = { + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + + it("should cascade", () => { + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "single"] + } + }, + { + files: "foo.js", + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + } + ] + }, + useEslintrc: false + }); + const expected = { + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, targetPath); + + assertConfigsEqual(actual, expected); + }); + }); + + describe("deprecation warnings", () => { + const cwd = path.resolve(__dirname, "../../fixtures/config-file/"); + let warning = null; + + function onWarning(w) { // eslint-disable-line require-jsdoc + + // Node.js 6.x does not have 'w.code' property. + if (!Object.prototype.hasOwnProperty.call(w, "code") || typeof w.code === "string" && w.code.startsWith("ESLINT_")) { + warning = w; + } + } + + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new CascadingConfigArrayFactory({ cwd }); + warning = null; + process.on("warning", onWarning); + }); + afterEach(() => { + process.removeListener("warning", onWarning); + }); + + it("should emit a deprecation warning if 'ecmaFeatures' is given.", async() => { + getConfig(factory, "ecma-features/test.js"); + + // Wait for "warning" event. + await nextTick(); + + assert.notStrictEqual(warning, null); + assert.strictEqual( + warning.message, + `The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` + ); + }); + }); + }); + }); + }); + + describe("'clearCache()' method should clear cache.", () => { + const root = path.join(os.tmpdir(), "eslint/cli-engine/cascading-config-array-factory"); + const files = { + ".eslintrc.js": "" + }; + + describe(`with the files ${JSON.stringify(files)}`, () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ cwd: () => root, files }); + + /** @type {Map} */ + let additionalPluginPool; + + /** @type {CascadingConfigArrayFactory} */ + let factory; + + beforeEach(() => { + additionalPluginPool = new Map(); + factory = new CascadingConfigArrayFactory({ + additionalPluginPool, + cliConfig: { plugins: ["test"] } + }); + }); + + it("should use cached instance.", () => { + const one = factory.getConfigArrayForFile(); + const two = factory.getConfigArrayForFile(); + + assert.strictEqual(one, two); + }); + + it("should not use cached instance if it call 'clearCache()' method between two getting.", () => { + const one = factory.getConfigArrayForFile(); + + factory.clearCache(); + const two = factory.getConfigArrayForFile(); + + assert.notStrictEqual(one, two); + }); + + it("should have a loading error in CLI config.", () => { + const config = factory.getConfigArrayForFile(); + + assert.strictEqual(config[1].plugins.test.definition, null); + }); + + it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { + factory.getConfigArrayForFile(); + + // Add plugin. + const plugin = {}; + + additionalPluginPool.set("test", plugin); + factory.clearCache(); + + // Check. + const config = factory.getConfigArrayForFile(); + + assert.strictEqual(config[1].plugins.test.definition, plugin); + }); + }); + }); +}); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js new file mode 100644 index 00000000000..156982f2c29 --- /dev/null +++ b/tests/lib/cli-engine/config-array-factory.js @@ -0,0 +1,2185 @@ +/** + * @fileoverview Tests for ConfigArrayFactory class. + * @author Toru Nagashima + */ +"use strict"; + +const os = require("os"); +const path = require("path"); +const { assert } = require("chai"); +const { spy } = require("sinon"); +const { ConfigArray } = require("../../../lib/cli-engine/config-array"); +const { OverrideTester } = require("../../../lib/cli-engine/config-array"); +const { defineConfigArrayFactoryWithInmemoryFileSystem } = require("./_utils"); + +const tempDir = path.join(os.tmpdir(), "eslint/config-array-factory"); + +// For VSCode intellisense. +/** @typedef {InstanceType["ConfigArrayFactory"]>} ConfigArrayFactory */ + +/** + * Assert a config array element. + * @param {Object} actual The actual value. + * @param {Object} providedExpected The expected value. + * @returns {void} + */ +function assertConfigArrayElement(actual, providedExpected) { + const expected = { + name: "", + filePath: "", + criteria: null, + env: void 0, + globals: void 0, + parser: void 0, + parserOptions: void 0, + plugins: void 0, + processor: void 0, + root: void 0, + rules: void 0, + settings: void 0, + ...providedExpected + }; + + assert.deepStrictEqual(actual, expected); +} + +/** + * Assert a config array element. + * @param {Object} actual The actual value. + * @param {Object} providedExpected The expected value. + * @returns {void} + */ +function assertConfig(actual, providedExpected) { + const expected = { + env: {}, + globals: {}, + parser: null, + parserOptions: {}, + plugins: [], + rules: {}, + settings: {}, + ...providedExpected + }; + + assert.deepStrictEqual(actual, expected); +} + +describe("ConfigArrayFactory", () => { + describe("'create(configData, options)' method should normalize the config data.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir + }); + const factory = new ConfigArrayFactory(); + + it("should return an empty config array if 'configData' is null.", () => { + assert.strictEqual(factory.create(null).length, 0); + }); + + it("should throw an error if the config data had invalid properties,", () => { + assert.throws(() => { + factory.create({ files: true }); + }, /Unexpected top-level property "files"/u); + }); + + it("should call '_normalizeConfigData(configData, options)' with given arguments except 'options.parent'.", () => { + const configData = {}; + const filePath = __filename; + const name = "example"; + const parent = new ConfigArray(); + const normalizeConfigData = spy(factory, "_normalizeConfigData"); + + factory.create(configData, { filePath, name, parent }); + + assert.strictEqual(normalizeConfigData.callCount, 1); + assert.strictEqual(normalizeConfigData.args[0].length, 3); + assert.strictEqual(normalizeConfigData.args[0][0], configData); + assert.strictEqual(normalizeConfigData.args[0][1], filePath); + assert.strictEqual(normalizeConfigData.args[0][2], name); + }); + + it("should return a config array that contains the yielded elements from '_normalizeConfigData(configData, options)'.", () => { + const elements = [{}, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.create({}); + + assert.strictEqual(configArray.length, 2); + assert.strictEqual(configArray[0], elements[0]); + assert.strictEqual(configArray[1], elements[1]); + }); + + it("should concatenate the elements of `options.parent` and the yielded elements from '_normalizeConfigData(configData, options)'.", () => { + const parent = new ConfigArray({}, {}); + const elements = [{}, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.create({}, { parent }); + + assert.strictEqual(configArray.length, 4); + assert.strictEqual(configArray[0], parent[0]); + assert.strictEqual(configArray[1], parent[1]); + assert.strictEqual(configArray[2], elements[0]); + assert.strictEqual(configArray[3], elements[1]); + }); + + it("should not concatenate the elements of `options.parent` if the yielded elements from '_normalizeConfigData(configData, options)' has 'root:true'.", () => { + const parent = new ConfigArray({}, {}); + const elements = [{ root: true }, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.create({}, { parent }); + + assert.strictEqual(configArray.length, 2); + assert.strictEqual(configArray[0], elements[0]); + assert.strictEqual(configArray[1], elements[1]); + }); + }); + + describe("'loadFile(filePath, options)' method should load a config file.", () => { + const basicFiles = { + "js/.eslintrc.js": "exports.settings = { name: 'js/.eslintrc.js' }", + "json/.eslintrc.json": "{ \"settings\": { \"name\": \"json/.eslintrc.json\" } }", + "legacy-json/.eslintrc": "{ \"settings\": { \"name\": \"legacy-json/.eslintrc\" } }", + "legacy-yml/.eslintrc": "settings:\n name: legacy-yml/.eslintrc", + "package-json/package.json": "{ \"eslintConfig\": { \"settings\": { \"name\": \"package-json/package.json\" } } }", + "yml/.eslintrc.yml": "settings:\n name: yml/.eslintrc.yml", + "yaml/.eslintrc.yaml": "settings:\n name: yaml/.eslintrc.yaml" + }; + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + ...basicFiles, + "invalid-property.json": "{ \"files\": \"*.js\" }", + "package-json-no-config/package.json": "{ \"name\": \"foo\" }" + } + }); + const factory = new ConfigArrayFactory(); + + it("should throw an error if 'filePath' is null.", () => { + assert.throws(() => factory.loadFile(null)); + }); + + it("should throw an error if 'filePath' doesn't exist.", () => { + assert.throws(() => { + factory.loadFile("non-exist"); + }, /Cannot read config file:.*non-exist/su); + }); + + it("should throw an error if 'filePath' was 'package.json' and it doesn't have 'eslintConfig' field.", () => { + assert.throws(() => { + factory.loadFile("package-json-no-config/package.json"); + }, /Cannot read config file:.*package.json/su); + }); + + it("should throw an error if the config data had invalid properties,", () => { + assert.throws(() => { + factory.loadFile("invalid-property.json"); + }, /Unexpected top-level property "files"/u); + }); + + for (const filePath of Object.keys(basicFiles)) { + it(`should load '${filePath}' then return a config array what contains that file content.`, () => { + const configArray = factory.loadFile(filePath); + + assert.strictEqual(configArray.length, 1); + assertConfigArrayElement(configArray[0], { + filePath: path.resolve(tempDir, filePath), + name: path.relative(tempDir, path.resolve(tempDir, filePath)), + settings: { name: filePath } + }); + }); + } + + it("should call '_normalizeConfigData(configData, options)' with the loaded config data and given options except 'options.parent'.", () => { + const filePath = "js/.eslintrc.js"; + const name = "example"; + const parent = new ConfigArray(); + const normalizeConfigData = spy(factory, "_normalizeConfigData"); + + factory.loadFile(filePath, { name, parent }); + + assert.strictEqual(normalizeConfigData.callCount, 1); + assert.strictEqual(normalizeConfigData.args[0].length, 3); + assert.deepStrictEqual(normalizeConfigData.args[0][0], { settings: { name: filePath } }); + assert.strictEqual(normalizeConfigData.args[0][1], path.resolve(tempDir, filePath)); + assert.strictEqual(normalizeConfigData.args[0][2], name); + }); + + it("should return a config array that contains the yielded elements from '_normalizeConfigData(configData, options)'.", () => { + const elements = [{}, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.loadFile("js/.eslintrc.js"); + + assert.strictEqual(configArray.length, 2); + assert.strictEqual(configArray[0], elements[0]); + assert.strictEqual(configArray[1], elements[1]); + }); + + it("should concatenate the elements of `options.parent` and the yielded elements from '_normalizeConfigData(configData, options)'.", () => { + const parent = new ConfigArray({}, {}); + const elements = [{}, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.loadFile("js/.eslintrc.js", { parent }); + + assert.strictEqual(configArray.length, 4); + assert.strictEqual(configArray[0], parent[0]); + assert.strictEqual(configArray[1], parent[1]); + assert.strictEqual(configArray[2], elements[0]); + assert.strictEqual(configArray[3], elements[1]); + }); + + it("should not concatenate the elements of `options.parent` if the yielded elements from '_normalizeConfigData(configData, options)' has 'root:true'.", () => { + const parent = new ConfigArray({}, {}); + const elements = [{ root: true }, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.loadFile("js/.eslintrc.js", { parent }); + + assert.strictEqual(configArray.length, 2); + assert.strictEqual(configArray[0], elements[0]); + assert.strictEqual(configArray[1], elements[1]); + }); + }); + + describe("'loadOnDirectory(directoryPath, options)' method should load the config file of a directory.", () => { + const basicFiles = { + "js/.eslintrc.js": "exports.settings = { name: 'js/.eslintrc.js' }", + "json/.eslintrc.json": "{ \"settings\": { \"name\": \"json/.eslintrc.json\" } }", + "legacy-json/.eslintrc": "{ \"settings\": { \"name\": \"legacy-json/.eslintrc\" } }", + "legacy-yml/.eslintrc": "settings:\n name: legacy-yml/.eslintrc", + "package-json/package.json": "{ \"eslintConfig\": { \"settings\": { \"name\": \"package-json/package.json\" } } }", + "yml/.eslintrc.yml": "settings:\n name: yml/.eslintrc.yml", + "yaml/.eslintrc.yaml": "settings:\n name: yaml/.eslintrc.yaml" + }; + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + ...basicFiles, + "invalid-property/.eslintrc.json": "{ \"files\": \"*.js\" }", + "package-json-no-config/package.json": "{ \"name\": \"foo\" }" + } + }); + const factory = new ConfigArrayFactory(); + + it("should throw an error if 'directoryPath' is null.", () => { + assert.throws(() => factory.loadOnDirectory(null)); + }); + + it("should return an empty config array if the config file of 'directoryPath' doesn't exist.", () => { + assert.strictEqual(factory.loadOnDirectory("non-exist").length, 0); + }); + + it("should return an empty config array if the config file of 'directoryPath' was package.json and it didn't have 'eslintConfig' field.", () => { + assert.strictEqual(factory.loadOnDirectory("package-json-no-config").length, 0); + }); + + it("should throw an error if the config data had invalid properties,", () => { + assert.throws(() => { + factory.loadOnDirectory("invalid-property"); + }, /Unexpected top-level property "files"/u); + }); + + for (const filePath of Object.keys(basicFiles)) { + const directoryPath = filePath.split("/")[0]; + + it(`should load '${directoryPath}' then return a config array what contains the config file of that directory.`, () => { + const configArray = factory.loadOnDirectory(directoryPath); + + assert.strictEqual(configArray.length, 1); + assertConfigArrayElement(configArray[0], { + filePath: path.resolve(tempDir, filePath), + name: path.relative(tempDir, path.resolve(tempDir, filePath)), + settings: { name: filePath } + }); + }); + } + + it("should call '_normalizeConfigData(configData, options)' with the loaded config data and given options except 'options.parent'.", () => { + const directoryPath = "js"; + const name = "example"; + const parent = new ConfigArray(); + const normalizeConfigData = spy(factory, "_normalizeConfigData"); + + factory.loadOnDirectory(directoryPath, { name, parent }); + + assert.strictEqual(normalizeConfigData.callCount, 1); + assert.strictEqual(normalizeConfigData.args[0].length, 3); + assert.deepStrictEqual(normalizeConfigData.args[0][0], { settings: { name: `${directoryPath}/.eslintrc.js` } }); + assert.strictEqual(normalizeConfigData.args[0][1], path.resolve(tempDir, directoryPath, ".eslintrc.js")); + assert.strictEqual(normalizeConfigData.args[0][2], name); + }); + + it("should return a config array that contains the yielded elements from '_normalizeConfigData(configData, options)'.", () => { + const elements = [{}, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.loadOnDirectory("js"); + + assert.strictEqual(configArray.length, 2); + assert.strictEqual(configArray[0], elements[0]); + assert.strictEqual(configArray[1], elements[1]); + }); + + it("should concatenate the elements of `options.parent` and the yielded elements from '_normalizeConfigData(configData, options)'.", () => { + const parent = new ConfigArray({}, {}); + const elements = [{}, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.loadOnDirectory("js", { parent }); + + assert.strictEqual(configArray.length, 4); + assert.strictEqual(configArray[0], parent[0]); + assert.strictEqual(configArray[1], parent[1]); + assert.strictEqual(configArray[2], elements[0]); + assert.strictEqual(configArray[3], elements[1]); + }); + + it("should not concatenate the elements of `options.parent` if the yielded elements from '_normalizeConfigData(configData, options)' has 'root:true'.", () => { + const parent = new ConfigArray({}, {}); + const elements = [{ root: true }, {}]; + + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + + const configArray = factory.loadOnDirectory("js", { parent }); + + assert.strictEqual(configArray.length, 2); + assert.strictEqual(configArray[0], elements[0]); + assert.strictEqual(configArray[1], elements[1]); + }); + }); + + /* + * All of `create`, `loadFile`, and `loadOnDirectory` call this method. + * So this section tests the common part of the three. + */ + describe("'_normalizeConfigData(configData, options)' method should normalize the config data.", () => { + + /** @type {ConfigArrayFactory} */ + let factory = null; + + /** + * Call `_normalizeConfigData` method with given arguments. + * @param {ConfigData} configData The config data to normalize. + * @param {Object} [options] The options. + * @param {string} [options.filePath] The path to the config file of the config data. + * @param {string} [options.name] The name of the config file of the config data. + * @returns {ConfigArray} The created config array. + */ + function create(configData, { filePath, name } = {}) { + return new ConfigArray(...factory._normalizeConfigData(configData, filePath, name)); // eslint-disable-line no-underscore-dangle + } + + describe("misc", () => { + before(() => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir + }); + + factory = new ConfigArrayFactory(); + }); + + describe("if the config data was empty, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create({}); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the default values in the element.", () => { + assertConfigArrayElement(configArray[0], {}); + }); + }); + + describe("if the config data had 'env' property, the returned value", () => { + const env = { node: true }; + let configArray; + + beforeEach(() => { + configArray = create({ env }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'env' value in the element.", () => { + assertConfigArrayElement(configArray[0], { env }); + }); + }); + + describe("if the config data had 'globals' property, the returned value", () => { + const globals = { window: "readonly" }; + let configArray; + + beforeEach(() => { + configArray = create({ globals }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'globals' value in the element.", () => { + assertConfigArrayElement(configArray[0], { globals }); + }); + }); + + describe("if the config data had 'parser' property, the returned value", () => { + const parser = "espree"; + let configArray; + + beforeEach(() => { + configArray = create({ parser }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'parser' value in the element.", () => { + assert.strictEqual(configArray[0].parser.id, parser); + }); + }); + + describe("if the config data had 'parserOptions' property, the returned value", () => { + const parserOptions = { ecmaVersion: 2015 }; + let configArray; + + beforeEach(() => { + configArray = create({ parserOptions }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'parserOptions' value in the element.", () => { + assertConfigArrayElement(configArray[0], { parserOptions }); + }); + }); + + describe("if the config data had 'plugins' property, the returned value", () => { + const plugins = []; + let configArray; + + beforeEach(() => { + configArray = create({ plugins }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'plugins' value in the element.", () => { + assertConfigArrayElement(configArray[0], { plugins: {} }); + }); + }); + + describe("if the config data had 'root' property, the returned value", () => { + const root = true; + let configArray; + + beforeEach(() => { + configArray = create({ root }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'root' value in the element.", () => { + assertConfigArrayElement(configArray[0], { root }); + }); + }); + + describe("if the config data had 'rules' property, the returned value", () => { + const rules = { eqeqeq: "error" }; + let configArray; + + beforeEach(() => { + configArray = create({ rules }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'rules' value in the element.", () => { + assertConfigArrayElement(configArray[0], { rules }); + }); + }); + + describe("if the config data had 'settings' property, the returned value", () => { + const settings = { foo: 777 }; + let configArray; + + beforeEach(() => { + configArray = create({ settings }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'settings' value in the element.", () => { + assertConfigArrayElement(configArray[0], { settings }); + }); + }); + }); + + describe("'parser' details", () => { + before(() => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/xxx-parser/index.js": "exports.name = 'xxx-parser';", + "subdir/node_modules/xxx-parser/index.js": "exports.name = 'subdir/xxx-parser';", + "parser.js": "exports.name = './parser.js';" + } + }); + + factory = new ConfigArrayFactory(); + }); + + describe("if the 'parser' property was a valid package, the first config array element", () => { + let element; + + beforeEach(() => { + element = create({ parser: "xxx-parser" })[0]; + }); + + it("should have the package ID at 'parser.id' property.", () => { + assert.strictEqual(element.parser.id, "xxx-parser"); + }); + + it("should have the package object at 'parser.definition' property.", () => { + assert.deepStrictEqual(element.parser.definition, { name: "xxx-parser" }); + }); + + it("should have the path to the package at 'parser.filePath' property.", () => { + assert.strictEqual(element.parser.filePath, path.join(tempDir, "node_modules/xxx-parser/index.js")); + }); + }); + + describe("if the 'parser' property was an invalid package, the first config array element", () => { + let element; + + beforeEach(() => { + element = create({ parser: "invalid-parser" })[0]; + }); + + it("should have the package ID at 'parser.id' property.", () => { + assert.strictEqual(element.parser.id, "invalid-parser"); + }); + + it("should have the loading error at 'parser.error' property.", () => { + assert.match(element.parser.error.message, /Cannot find module 'invalid-parser'/u); + }); + }); + + describe("if the 'parser' property was a valid relative path, the first config array element", () => { + let element; + + beforeEach(() => { + element = create({ parser: "./parser" })[0]; + }); + + it("should have the given path at 'parser.id' property.", () => { + assert.strictEqual(element.parser.id, "./parser"); + }); + + it("should have the file's object at 'parser.definition' property.", () => { + assert.deepStrictEqual(element.parser.definition, { name: "./parser.js" }); + }); + + it("should have the absolute path to the file at 'parser.filePath' property.", () => { + assert.strictEqual(element.parser.filePath, path.join(tempDir, "./parser.js")); + }); + }); + + describe("if the 'parser' property was an invalid relative path, the first config array element", () => { + let element; + + beforeEach(() => { + element = create({ parser: "./invalid-parser" })[0]; + }); + + it("should have the given path at 'parser.id' property.", () => { + assert.strictEqual(element.parser.id, "./invalid-parser"); + }); + + it("should have the loading error at 'parser.error' property.", () => { + assert.match(element.parser.error.message, /Cannot find module '.\/invalid-parser'/u); + }); + }); + + describe("if 'parser' property was given and 'filePath' option was given, the parser", () => { + let element; + + beforeEach(() => { + element = create( + { parser: "xxx-parser" }, + { filePath: path.join(tempDir, "subdir/.eslintrc") } + )[0]; + }); + + it("should be resolved relative to the 'filePath' option.", () => { + assert.strictEqual( + element.parser.filePath, + + // rather than "xxx-parser" at the project root. + path.join(tempDir, "subdir/node_modules/xxx-parser/index.js") + ); + }); + }); + }); + + describe("'plugins' details", () => { + before(() => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/eslint-plugin-ext/index.js": "exports.processors = { '.abc': {}, '.xyz': {}, other: {} };", + "node_modules/eslint-plugin-subdir/index.js": "", + "node_modules/eslint-plugin-xxx/index.js": "exports.name = 'eslint-plugin-xxx';", + "subdir/node_modules/eslint-plugin-subdir/index.js": "", + "parser.js": "" + } + }); + + factory = new ConfigArrayFactory(); + }); + + it("should throw an error if a 'plugins' value is a file path.", () => { + assert.throws(() => { + create({ plugins: ["./path/to/plugin"] }); + }, /Plugins array cannot includes file paths/u); + }); + + describe("if the 'plugins' property was a valid package, the first config array element", () => { + let element; + + beforeEach(() => { + element = create({ plugins: ["xxx"] })[0]; + }); + + it("should have 'plugins[id]' property.", () => { + assert.notStrictEqual(element.plugins.xxx, void 0); + }); + + it("should have the package ID at 'plugins[id].id' property.", () => { + assert.strictEqual(element.plugins.xxx.id, "xxx"); + }); + + it("should have the package object at 'plugins[id].definition' property.", () => { + assert.deepStrictEqual(element.plugins.xxx.definition, { name: "eslint-plugin-xxx" }); + }); + + it("should have the path to the package at 'plugins[id].filePath' property.", () => { + assert.strictEqual(element.plugins.xxx.filePath, path.join(tempDir, "node_modules/eslint-plugin-xxx/index.js")); + }); + }); + + describe("if the 'plugins' property was an invalid package, the first config array element", () => { + let element; + + beforeEach(() => { + element = create({ plugins: ["invalid"] })[0]; + }); + + it("should have 'plugins[id]' property.", () => { + assert.notStrictEqual(element.plugins.invalid, void 0); + }); + + it("should have the package ID at 'plugins[id].id' property.", () => { + assert.strictEqual(element.plugins.invalid.id, "invalid"); + }); + + it("should have the loading error at 'plugins[id].error' property.", () => { + assert.match(element.plugins.invalid.error.message, /Cannot find module 'eslint-plugin-invalid'/u); + }); + }); + + describe("even if 'plugins' property was given and 'filePath' option was given,", () => { + it("should load the plugin from the project root.", () => { + const configArray = create( + { plugins: ["subdir"] }, + { filePath: path.resolve(tempDir, "subdir/a.js") } + ); + + assert.strictEqual( + configArray[0].plugins.subdir.filePath, + + // "subdir/node_modules/eslint-plugin-subdir/index.js" exists, but not it. + path.resolve(tempDir, "node_modules/eslint-plugin-subdir/index.js") + ); + }); + }); + + describe("if 'plugins' property was given and the plugin has two file extension processors, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create({ plugins: ["ext"] }); + }); + + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); + }); + + describe("the first element", () => { + let element; + + beforeEach(() => { + element = configArray[0]; + }); + + it("should be named '#processors[\"ext/.abc\"]'.", () => { + assert.strictEqual(element.name, "#processors[\"ext/.abc\"]"); + }); + + it("should not have 'plugins' property.", () => { + assert.strictEqual(element.plugins, void 0); + }); + + it("should have 'processor' property.", () => { + assert.strictEqual(element.processor, "ext/.abc"); + }); + + it("should have 'criteria' property what matches '.abc'.", () => { + assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.abc")), true); + assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.xyz")), false); + }); + }); + + describe("the secand element", () => { + let element; + + beforeEach(() => { + element = configArray[1]; + }); + + it("should be named '#processors[\"ext/.xyz\"]'.", () => { + assert.strictEqual(element.name, "#processors[\"ext/.xyz\"]"); + }); + + it("should not have 'plugins' property.", () => { + assert.strictEqual(element.plugins, void 0); + }); + + it("should have 'processor' property.", () => { + assert.strictEqual(element.processor, "ext/.xyz"); + }); + + it("should have 'criteria' property what matches '.xyz'.", () => { + assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.abc")), false); + assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.xyz")), true); + }); + }); + + describe("the third element", () => { + let element; + + beforeEach(() => { + element = configArray[2]; + }); + + it("should have 'plugins' property.", () => { + assert.strictEqual(element.plugins.ext.id, "ext"); + }); + + it("should not have 'processor' property.", () => { + assert.strictEqual(element.processor, void 0); + }); + }); + }); + }); + + describe("'extends' details", () => { + before(() => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-config-override/index.js": ` + module.exports = { + rules: { regular: 1 }, + overrides: [ + { files: '*.xxx', rules: { override: 1 } }, + { files: '*.yyy', rules: { override: 2 } } + ] + } + `, + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: { env: { es6: true } } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "node_modules/eslint-plugin-error/index.js": "throw new Error('xxx error')", + "base.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + }); + + factory = new ConfigArrayFactory(); + }); + + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + create({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); + }); + + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); + + it("should throw an error when a plugin threw while loading.", () => { + assert.throws(() => { + create({ + extends: "plugin:error/foo", + rules: { eqeqeq: 2 } + }); + }, /xxx error/u); + }); + + it("should throw an error when a plugin extend is a file path.", () => { + assert.throws(() => { + create({ + extends: "plugin:./path/to/foo", + rules: { eqeqeq: 2 } + }); + }, /'extends' cannot use a file path for plugins/u); + }); + + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + create({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); + + describe("if 'extends' property was 'eslint:all', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:all", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint:all' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:all", + filePath: require.resolve("../../../conf/eslint-all.js"), + ...require("../../../conf/eslint-all.js") + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'eslint:recommended', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "eslint:recommended", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint:recommended' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint:recommended", + filePath: require.resolve("../../../conf/eslint-recommended.js"), + ...require("../../../conf/eslint-recommended.js") + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'foo', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "foo", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'eslint-config-foo' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-foo", + filePath: path.join(tempDir, "node_modules/eslint-config-foo/index.js"), + env: { browser: true } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'plugin:foo/bar', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "plugin:foo/bar", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of 'plugin:foo/bar' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » plugin:foo/bar", + filePath: path.join(tempDir, "node_modules/eslint-plugin-foo/index.js"), + env: { es6: true } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was './base', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "./base", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have two elements.", () => { + assert.strictEqual(configArray.length, 2); + }); + + it("should have the config data of './base' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » ./base", + filePath: path.join(tempDir, "base.js"), + rules: { semi: [2, "always"] } + }); + }); + + it("should have the given config data at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'one' and the 'one' extends 'two', the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "one", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); + }); + + it("should have the config data of 'eslint-config-two' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-one » eslint-config-two", + filePath: path.join(tempDir, "node_modules/eslint-config-two/index.js"), + env: { node: true } + }); + }); + + it("should have the config data of 'eslint-config-one' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-one", + filePath: path.join(tempDir, "node_modules/eslint-config-one/index.js"), + env: { browser: true } + }); + }); + + it("should have the given config data at the thrid element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + + describe("if 'extends' property was 'override' and the 'override' has 'overrides' property, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create( + { extends: "override", rules: { eqeqeq: 1 } }, + { name: ".eslintrc" } + ); + }); + + it("should have four elements.", () => { + assert.strictEqual(configArray.length, 4); + }); + + it("should have the config data of 'eslint-config-override' at the first element.", () => { + assertConfigArrayElement(configArray[0], { + name: ".eslintrc » eslint-config-override", + filePath: path.join(tempDir, "node_modules/eslint-config-override/index.js"), + rules: { regular: 1 } + }); + }); + + it("should have the 'overrides[0]' config data of 'eslint-config-override' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: ".eslintrc » eslint-config-override#overrides[0]", + filePath: path.join(tempDir, "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.xxx"], [], tempDir), + rules: { override: 1 } + }); + }); + + it("should have the 'overrides[1]' config data of 'eslint-config-override' at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: ".eslintrc » eslint-config-override#overrides[1]", + filePath: path.join(tempDir, "node_modules/eslint-config-override/index.js"), + criteria: OverrideTester.create(["*.yyy"], [], tempDir), + rules: { override: 2 } + }); + }); + + it("should have the given config data at the fourth element.", () => { + assertConfigArrayElement(configArray[3], { + name: ".eslintrc", + rules: { eqeqeq: 1 } + }); + }); + }); + }); + + describe("'overrides' details", () => { + before(() => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + } + }); + + factory = new ConfigArrayFactory(); + }); + + describe("if 'overrides' property was given, the returned value", () => { + let configArray; + + beforeEach(() => { + configArray = create({ + rules: { regular: 1 }, + overrides: [ + { files: "*.xxx", rules: { override: 1 } }, + { files: "*.yyy", rules: { override: 2 } } + ] + }); + }); + + it("should have three elements.", () => { + assert.strictEqual(configArray.length, 3); + }); + + it("should have the given config data at the first element.", () => { + assertConfigArrayElement(configArray[0], { + rules: { regular: 1 } + }); + }); + + it("should have the config data of 'overrides[0]' at the second element.", () => { + assertConfigArrayElement(configArray[1], { + name: "#overrides[0]", + criteria: OverrideTester.create(["*.xxx"], [], tempDir), + rules: { override: 1 } + }); + }); + + it("should have the config data of 'overrides[1]' at the third element.", () => { + assertConfigArrayElement(configArray[2], { + name: "#overrides[1]", + criteria: OverrideTester.create(["*.yyy"], [], tempDir), + rules: { override: 2 } + }); + }); + }); + }); + + describe("additional plugin pool", () => { + const plugin = {}; + + beforeEach(() => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir + }); + + factory = new ConfigArrayFactory({ + additionalPluginPool: new Map([["abc", plugin], ["eslint-plugin-def", plugin]]) + }); + }); + + it("should use the matched plugin in the additional plugin pool; short to short", () => { + const configArray = create({ plugins: ["abc"] }); + + assert.strictEqual(configArray[0].plugins.abc.id, "abc"); + assert.strictEqual(configArray[0].plugins.abc.definition, plugin); + }); + + it("should use the matched plugin in the additional plugin pool; long to short", () => { + const configArray = create({ plugins: ["eslint-plugin-abc"] }); + + assert.strictEqual(configArray[0].plugins.abc.id, "abc"); + assert.strictEqual(configArray[0].plugins.abc.definition, plugin); + }); + + it("should use the matched plugin in the additional plugin pool; short to long", () => { + const configArray = create({ plugins: ["def"] }); + + assert.strictEqual(configArray[0].plugins.def.id, "def"); + assert.strictEqual(configArray[0].plugins.def.definition, plugin); + }); + + it("should use the matched plugin in the additional plugin pool; long to long", () => { + const configArray = create({ plugins: ["eslint-plugin-def"] }); + + assert.strictEqual(configArray[0].plugins.def.id, "def"); + assert.strictEqual(configArray[0].plugins.def.definition, plugin); + }); + }); + }); + + describe("Moved from tests/lib/config/config-file.js", () => { + describe("applyExtends()", () => { + const files = { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-plugin-invalid-parser/index.js": "exports.configs = { foo: { parser: 'nonexistent-parser' } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };", + "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }", + "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }", + "yaml/.eslintrc.yaml": "env:\n browser: true" + }; + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files }); + const factory = new ConfigArrayFactory(); + + /** + * Apply `extends` property. + * @param {Object} configData The config that has `extends` property. + * @param {string} [filePath] The path to the config data. + * @returns {Object} The applied config data. + */ + function applyExtends(configData, filePath = "whatever") { + return factory + .create(configData, { filePath }) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + } + + it("should apply extension 'foo' when specified from root directory config", () => { + const config = applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true }, + rules: { eqeqeq: [2] } + }); + }); + + it("should apply all rules when extends config includes 'eslint:all'", () => { + const config = applyExtends({ + extends: "eslint:all" + }); + + assert.strictEqual(config.rules.eqeqeq[0], "error"); + assert.strictEqual(config.rules.curly[0], "error"); + }); + + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "not-exist", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "not-exist" to extend from./u); + }); + + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); + + it("should throw an error when a parser in a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-parser/foo", + rules: { eqeqeq: 2 } + }); + }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); + }); + + it("should fall back to default parser when a parser called 'espree' is not found", () => { + const config = applyExtends({ parser: "espree" }); + + assertConfig(config, { + parser: require.resolve("espree") + }); + }); + + it("should throw an error when a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-config/bar", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + }); + + it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { + try { + applyExtends({ + extends: "plugin:nonexistent-plugin/baz", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + pluginRootPath: process.cwd(), + importerName: "whatever" + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when a plugin in the plugins list is not found", () => { + try { + applyExtends({ + plugins: ["nonexistent-plugin"] + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + pluginRootPath: process.cwd(), + importerName: "whatever" + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should apply extensions recursively when specified from package", () => { + const config = applyExtends({ + extends: "one", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true, node: true }, + rules: { eqeqeq: [2] } + }); + }); + + it("should apply extensions when specified from a JavaScript file", () => { + const config = applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, "js/foo.js"); + + assertConfig(config, { + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }); + }); + + it("should apply extensions when specified from a YAML file", () => { + const config = applyExtends({ + extends: ".eslintrc.yaml", + rules: { eqeqeq: 2 } + }, "yaml/foo.js"); + + assertConfig(config, { + env: { browser: true }, + rules: { + eqeqeq: [2] + } + }); + }); + + it("should apply extensions when specified from a JSON file", () => { + const config = applyExtends({ + extends: ".eslintrc.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + rules: { + eqeqeq: [2], + quotes: [2, "double"] + } + }); + }); + + it("should apply extensions when specified from a package.json file in a sibling directory", () => { + const config = applyExtends({ + extends: "../package-json/package.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); + + assertConfig(config, { + env: { es6: true }, + rules: { + eqeqeq: [2] + } + }); + }); + }); + + describe("load()", () => { + + /** + * Load a given config file. + * @param {ConfigArrayFactory} factory The factory to load. + * @param {string} filePath The path to a config file. + * @returns {Object} The applied config data. + */ + function load(factory, filePath) { + return factory + .loadFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + } + + it("should throw error if file doesnt exist", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + + assert.throws(() => { + load(factory, "legacy/nofile.js"); + }); + + assert.throws(() => { + load(factory, "legacy/package.json"); + }); + }); + + it("should load information from a legacy file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "legacy/.eslintrc": "{ rules: { eqeqeq: 2 } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "legacy/.eslintrc"); + + assertConfig(config, { + rules: { + eqeqeq: [2] + } + }); + }); + + it("should load information from a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.js"); + + assertConfig(config, { + rules: { + semi: [2, "always"] + } + }); + }); + + it("should throw error when loading invalid JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.broken.js": "module.exports = { rules: { semi: [2, 'always'] }" + } + }); + const factory = new ConfigArrayFactory(); + + assert.throws(() => { + load(factory, "js/.eslintrc.broken.js"); + }, /Cannot read config file/u); + }); + + it("should interpret parser module name when present in a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "node_modules/foo/index.js": "", + "js/node_modules/foo/index.js": "", + "js/.eslintrc.parser.js": `module.exports = { + parser: 'foo', + rules: { semi: [2, 'always'] } + };` + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.parser.js"); + + assertConfig(config, { + parser: path.resolve("js/node_modules/foo/index.js"), + rules: { + semi: [2, "always"] + } + }); + }); + + it("should interpret parser path when present in a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.parser2.js": `module.exports = { + parser: './not-a-config.js', + rules: { semi: [2, 'always'] } + };`, + "js/not-a-config.js": "" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.parser2.js"); + + assertConfig(config, { + parser: path.resolve("js/not-a-config.js"), + rules: { + semi: [2, "always"] + } + }); + }); + + it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.parser3.js": `module.exports = { + parser: 'espree', + rules: { semi: [2, 'always'] } + };` + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.parser3.js"); + + assertConfig(config, { + parser: require.resolve("espree"), + rules: { + semi: [2, "always"] + } + }); + }); + + it("should load information from a JSON file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "json/.eslintrc.json"); + + assertConfig(config, { + rules: { + quotes: [2, "double"] + } + }); + }); + + it("should load fresh information from a JSON file", () => { + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + const initialConfig = { + rules: { + quotes: [2, "double"] + } + }; + const updatedConfig = { + rules: { + quotes: [0] + } + }; + let config; + + fs.writeFileSync("fresh-test.json", JSON.stringify(initialConfig)); + config = load(factory, "fresh-test.json"); + assertConfig(config, initialConfig); + + fs.writeFileSync("fresh-test.json", JSON.stringify(updatedConfig)); + config = load(factory, "fresh-test.json"); + assertConfig(config, updatedConfig); + }); + + it("should load information from a package.json file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "package-json/package.json"); + + assertConfig(config, { + env: { es6: true } + }); + }); + + it("should throw error when loading invalid package.json file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "broken-package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } }" + } + }); + const factory = new ConfigArrayFactory(); + + assert.throws(() => { + try { + load(factory, "broken-package-json/package.json"); + } catch (error) { + assert.strictEqual(error.messageTemplate, "failed-to-read-json"); + throw error; + } + }, /Cannot read config file/u); + }); + + it("should load fresh information from a package.json file", () => { + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + const initialConfig = { + eslintConfig: { + rules: { + quotes: [2, "double"] + } + } + }; + const updatedConfig = { + eslintConfig: { + rules: { + quotes: [0] + } + } + }; + let config; + + fs.writeFileSync("package.json", JSON.stringify(initialConfig)); + config = load(factory, "package.json"); + assertConfig(config, initialConfig.eslintConfig); + + fs.writeFileSync("package.json", JSON.stringify(updatedConfig)); + config = load(factory, "package.json"); + assertConfig(config, updatedConfig.eslintConfig); + }); + + it("should load fresh information from a .eslintrc.js file", () => { + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + const initialConfig = { + rules: { + quotes: [2, "double"] + } + }; + const updatedConfig = { + rules: { + quotes: [0] + } + }; + let config; + + fs.writeFileSync(".eslintrc.js", `module.exports = ${JSON.stringify(initialConfig)}`); + config = load(factory, ".eslintrc.js"); + assertConfig(config, initialConfig); + + fs.writeFileSync(".eslintrc.js", `module.exports = ${JSON.stringify(updatedConfig)}`); + config = load(factory, ".eslintrc.js"); + assertConfig(config, updatedConfig); + }); + + it("should load information from a YAML file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "yaml/.eslintrc.yaml": "env:\n browser: true" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "yaml/.eslintrc.yaml"); + + assertConfig(config, { + env: { browser: true } + }); + }); + + it("should load information from an empty YAML file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "yaml/.eslintrc.empty.yaml": "{}" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "yaml/.eslintrc.empty.yaml"); + + assertConfig(config, {}); + }); + + it("should load information from a YML file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "yml/.eslintrc.yml": "env:\n node: true" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "yml/.eslintrc.yml"); + + assertConfig(config, { + env: { node: true } + }); + }); + + it("should load information from a YML file and apply extensions", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends/.eslintrc.yml": "extends: ../package-json/package.json\nrules:\n booya: 2", + "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends/.eslintrc.yml"); + + assertConfig(config, { + env: { es6: true }, + rules: { booya: [2] } + }); + }); + + it("should load information from `extends` chain.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain": { + "node_modules/eslint-config-a": { + "node_modules/eslint-config-b": { + "node_modules/eslint-config-c": { + "index.js": "module.exports = { rules: { c: 2 } };" + }, + "index.js": "module.exports = { extends: 'c', rules: { b: 2 } };" + }, + "index.js": "module.exports = { extends: 'b', rules: { a: 2 } };" + }, + ".eslintrc.json": "{ \"extends\": \"a\" }" + } + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain/.eslintrc.json"); + + assertConfig(config, { + rules: { + a: [2], // from node_modules/eslint-config-a + b: [2], // from node_modules/eslint-config-a/node_modules/eslint-config-b + c: [2] // from node_modules/eslint-config-a/node_modules/eslint-config-b/node_modules/eslint-config-c + } + }); + }); + + it("should load information from `extends` chain with relative path.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain-2": { + "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", + "node_modules/eslint-config-a/relative.js": "module.exports = { rules: { relative: 2 } };", + ".eslintrc.json": "{ \"extends\": \"a\" }" + } + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain-2/.eslintrc.json"); + + assertConfig(config, { + rules: { + a: [2], // from node_modules/eslint-config-a/index.js + relative: [2] // from node_modules/eslint-config-a/relative.js + } + }); + }); + + it("should load information from `extends` chain in .eslintrc with relative path.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain-2": { + "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", + "node_modules/eslint-config-a/relative.js": "module.exports = { rules: { relative: 2 } };", + "relative.eslintrc.json": "{ \"extends\": \"./node_modules/eslint-config-a/index.js\" }" + } + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain-2/relative.eslintrc.json"); + + assertConfig(config, { + rules: { + a: [2], // from node_modules/eslint-config-a/index.js + relative: [2] // from node_modules/eslint-config-a/relative.js + } + }); + }); + + it("should load information from `parser` in .eslintrc with relative path.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain-2": { + "parser.eslintrc.json": "{ \"parser\": \"./parser.js\" }", + "parser.js": "" + } + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain-2/parser.eslintrc.json"); + + assertConfig(config, { + parser: path.resolve("extends-chain-2/parser.js") + }); + }); + + describe("Plugins", () => { + it("should load information from a YML file and load plugins", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "node_modules/eslint-plugin-test/index.js": ` + module.exports = { + environments: { + bar: { globals: { bar: true } } + } + } + `, + "plugins/.eslintrc.yml": ` + plugins: + - test + rules: + test/foo: 2 + env: + test/bar: true + ` + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "plugins/.eslintrc.yml"); + + assertConfig(config, { + env: { "test/bar": true }, + plugins: ["test"], + rules: { + "test/foo": [2] + } + }); + }); + + it("should load two separate configs from a plugin", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "node_modules/eslint-plugin-test/index.js": ` + module.exports = { + configs: { + foo: { rules: { semi: 2, quotes: 1 } }, + bar: { rules: { quotes: 2, yoda: 2 } } + } + } + `, + "plugins/.eslintrc.yml": ` + extends: + - plugin:test/foo + - plugin:test/bar + ` + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "plugins/.eslintrc.yml"); + + assertConfig(config, { + rules: { + semi: [2], + quotes: [2], + yoda: [2] + } + }); + }); + }); + + describe("even if config files have Unicode BOM,", () => { + it("should read the JSON config file correctly.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "bom/.eslintrc.json": "\uFEFF{ \"rules\": { \"semi\": \"error\" } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "bom/.eslintrc.json"); + + assertConfig(config, { + rules: { + semi: ["error"] + } + }); + }); + + it("should read the YAML config file correctly.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "bom/.eslintrc.yaml": "\uFEFFrules:\n semi: error" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "bom/.eslintrc.yaml"); + + assertConfig(config, { + rules: { + semi: ["error"] + } + }); + }); + + it("should read the config in package.json correctly.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "bom/package.json": "\uFEFF{ \"eslintConfig\": { \"rules\": { \"semi\": \"error\" } } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "bom/package.json"); + + assertConfig(config, { + rules: { + semi: ["error"] + } + }); + }); + }); + + it("throws an error including the config file name if the config file is invalid", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "invalid/invalid-top-level-property.yml": "invalidProperty: 3" + } + }); + const factory = new ConfigArrayFactory(); + + try { + load(factory, "invalid/invalid-top-level-property.yml"); + } catch (err) { + assert.include(err.message, `ESLint configuration in ${`invalid${path.sep}invalid-top-level-property.yml`} is invalid`); + return; + } + assert.fail(); + }); + }); + + describe("resolve()", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "", + "node_modules/eslint-config-foo/bar.js": "", + "node_modules/eslint-config-eslint-configfoo/index.js": "", + "node_modules/@foo/eslint-config/index.js": "", + "node_modules/@foo/eslint-config-bar/index.js": "", + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: {} }", + "node_modules/@foo/eslint-plugin/index.js": "exports.configs = { bar: {} }", + "node_modules/@foo/eslint-plugin-bar/index.js": "exports.configs = { baz: {} }", + "foo/bar/.eslintrc": "", + ".eslintrc": "" + } + }); + const factory = new ConfigArrayFactory(); + + /** + * Resolve `extends` module. + * @param {string} request The module name to resolve. + * @param {string} [relativeTo] The importer path to resolve. + * @returns {string} The resolved path. + */ + function resolve(request, relativeTo) { + return factory.create( + { extends: request }, + { filePath: relativeTo } + )[0]; + } + + describe("Relative to CWD", () => { + for (const { input, expected } of [ + { input: ".eslintrc", expected: path.resolve(tempDir, ".eslintrc") }, + { input: "eslint-config-foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "eslint-config-foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "eslint-configfoo", expected: path.resolve(tempDir, "node_modules/eslint-config-eslint-configfoo/index.js") }, + { input: "@foo/eslint-config", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config-bar/index.js") }, + { input: "plugin:foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-plugin-foo/index.js") }, + { input: "plugin:@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin/index.js") }, + { input: "plugin:@foo/bar/baz", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin-bar/index.js") } + ]) { + it(`should return ${expected} when passed ${input}`, () => { + const result = resolve(input); + + assert.strictEqual(result.filePath, expected); + }); + } + }); + + describe("Relative to config file", () => { + const relativePath = path.resolve(tempDir, "./foo/bar/.eslintrc"); + + for (const { input, expected } of [ + { input: ".eslintrc", expected: path.join(path.dirname(relativePath), ".eslintrc") }, + { input: "eslint-config-foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "eslint-config-foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "eslint-configfoo", expected: path.resolve(tempDir, "node_modules/eslint-config-eslint-configfoo/index.js") }, + { input: "@foo/eslint-config", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config-bar/index.js") }, + { input: "plugin:foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-plugin-foo/index.js") }, + { input: "plugin:@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin/index.js") }, + { input: "plugin:@foo/bar/baz", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin-bar/index.js") } + ]) { + it(`should return ${expected} when passed ${input}`, () => { + const result = resolve(input, relativePath); + + assert.strictEqual(result.filePath, expected); + }); + } + }); + }); + }); + + describe("Moved from tests/lib/config/plugins.js", () => { + describe("load()", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/@scope/eslint-plugin-example/index.js": "exports.name = '@scope/eslint-plugin-example';", + "node_modules/eslint-plugin-example/index.js": "exports.name = 'eslint-plugin-example';", + "node_modules/eslint-plugin-throws-on-load/index.js": "throw new Error('error thrown while loading this module')" + } + }); + const factory = new ConfigArrayFactory(); + + /** + * Load a plugin. + * @param {string} request A request to load a plugin. + * @returns {Map} The loaded plugins. + */ + function load(request) { + const config = factory.create({ plugins: [request] }); + + return new Map( + Object + .entries(config[0].plugins) + .map(([id, entry]) => { + if (entry.error) { + throw entry.error; + } + return [id, entry.definition]; + }) + ); + } + + it("should load a plugin when referenced by short name", () => { + const loadedPlugins = load("example"); + + assert.deepStrictEqual( + loadedPlugins.get("example"), + { name: "eslint-plugin-example" } + ); + }); + + it("should load a plugin when referenced by long name", () => { + const loadedPlugins = load("eslint-plugin-example"); + + assert.deepStrictEqual( + loadedPlugins.get("example"), + { name: "eslint-plugin-example" } + ); + }); + + it("should throw an error when a plugin has whitespace", () => { + assert.throws(() => { + load("whitespace "); + }, /Whitespace found in plugin name 'whitespace '/u); + assert.throws(() => { + load("whitespace\t"); + }, /Whitespace found in plugin name/u); + assert.throws(() => { + load("whitespace\n"); + }, /Whitespace found in plugin name/u); + assert.throws(() => { + load("whitespace\r"); + }, /Whitespace found in plugin name/u); + }); + + it("should throw an error when a plugin doesn't exist", () => { + assert.throws(() => { + load("nonexistentplugin"); + }, /Failed to load plugin/u); + }); + + it("should rethrow an error that a plugin throws on load", () => { + assert.throws(() => { + load("throws-on-load"); + }, /error thrown while loading this module/u); + }); + + it("should load a scoped plugin when referenced by short name", () => { + const loadedPlugins = load("@scope/example"); + + assert.deepStrictEqual( + loadedPlugins.get("@scope/example"), + { name: "@scope/eslint-plugin-example" } + ); + }); + + it("should load a scoped plugin when referenced by long name", () => { + const loadedPlugins = load("@scope/eslint-plugin-example"); + + assert.deepStrictEqual( + loadedPlugins.get("@scope/example"), + { name: "@scope/eslint-plugin-example" } + ); + }); + + describe("when referencing a scope plugin and omitting @scope/", () => { + it("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", () => { + const loadedPlugins = load("@scope/example"); + + assert.strictEqual(loadedPlugins.get("example"), void 0); + }); + + it("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { + const loadedPlugins = load("@scope/eslint-plugin-example"); + + assert.strictEqual(loadedPlugins.get("example"), void 0); + }); + }); + }); + + describe("loadAll()", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/eslint-plugin-example1/index.js": "exports.name = 'eslint-plugin-example1';", + "node_modules/eslint-plugin-example2/index.js": "exports.name = 'eslint-plugin-example2';" + } + }); + const factory = new ConfigArrayFactory(); + + /** + * Load a plugin. + * @param {string[]} request A request to load a plugin. + * @returns {Map} The loaded plugins. + */ + function loadAll(request) { + const config = factory.create({ plugins: request }); + + return new Map( + Object + .entries(config[0].plugins) + .map(([id, entry]) => { + if (entry.error) { + throw entry.error; + } + return [id, entry.definition]; + }) + ); + } + + it("should load plugins when passed multiple plugins", () => { + const loadedPlugins = loadAll(["example1", "example2"]); + + assert.deepStrictEqual( + loadedPlugins.get("example1"), + { name: "eslint-plugin-example1" } + ); + assert.deepStrictEqual( + loadedPlugins.get("example2"), + { name: "eslint-plugin-example2" } + ); + }); + }); + }); +}); diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js new file mode 100644 index 00000000000..50aa8f1ff8b --- /dev/null +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -0,0 +1,724 @@ +/** + * @fileoverview Tests for ConfigArray class. + * @author Toru Nagashima + */ +"use strict"; + +const path = require("path"); +const { assert } = require("chai"); +const { ConfigArray, OverrideTester, getUsedExtractedConfigs } = require("../../../../lib/cli-engine/config-array"); + +describe("ConfigArray", () => { + it("should be a sub class of Array.", () => { + assert(new ConfigArray() instanceof Array); + }); + + describe("'constructor(...elements)' should adopt the elements as array elements.", () => { + const patterns = [ + { elements: [] }, + { elements: [{ value: 1 }] }, + { elements: [{ value: 2 }, { value: 3 }] }, + { elements: [{ value: 4 }, { value: 5 }, { value: 6 }] } + ]; + + for (const { elements } of patterns) { + describe(`if it gave ${JSON.stringify(elements)} then`, () => { + let configArray; + + beforeEach(() => { + configArray = new ConfigArray(...elements); + }); + + it(`should have ${elements.length} as the length.`, () => { + assert.strictEqual(configArray.length, elements.length); + }); + + for (let i = 0; i < elements.length; ++i) { + it(`should have ${JSON.stringify(elements[i])} at configArray[${i}].`, () => { // eslint-disable-line no-loop-func + assert.strictEqual(configArray[i], elements[i]); + }); + } + }); + } + }); + + describe("'root' property should be the value of the last element which has 'root' property.", () => { + const patterns = [ + { elements: [], expected: false }, + { elements: [{}], expected: false }, + { elements: [{}, {}], expected: false }, + { elements: [{ root: false }], expected: false }, + { elements: [{ root: true }], expected: true }, + { elements: [{ root: true }, { root: false }], expected: false }, + { elements: [{ root: false }, { root: true }], expected: true }, + { elements: [{ root: true }, { root: 1 }], expected: true } // ignore non-boolean value + ]; + + for (const { elements, expected } of patterns) { + it(`should be ${expected} if the elements are ${JSON.stringify(elements)}.`, () => { + assert.strictEqual(new ConfigArray(...elements).root, expected); + }); + } + }); + + describe("'pluginEnvironments' property should be the environments of all plugins.", () => { + const env = { + "aaa/xxx": {}, + "bbb/xxx": {} + }; + let configArray; + + beforeEach(() => { + configArray = new ConfigArray( + { + plugins: { + aaa: { + definition: { + environments: { + xxx: env["aaa/xxx"] + } + } + } + } + }, + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + plugins: { + bbb: { + definition: { + environments: { + xxx: env["bbb/xxx"] + } + } + } + } + } + ); + }); + + it("should return null for built-in env", () => { + assert.strictEqual(configArray.pluginEnvironments.get("node"), void 0); + }); + + it("should return 'aaa/xxx' if it exists.", () => { + assert.strictEqual(configArray.pluginEnvironments.get("aaa/xxx"), env["aaa/xxx"]); + }); + + it("should return 'bbb/xxx' if it exists.", () => { + assert.strictEqual(configArray.pluginEnvironments.get("bbb/xxx"), env["bbb/xxx"]); + }); + + it("should throw an error if it tried to mutate.", () => { + assert.throws(() => { + configArray.pluginEnvironments.set("ccc/xxx", {}); + }); + }); + }); + + describe("'pluginProcessors' property should be the processors of all plugins.", () => { + const processors = { + "aaa/.xxx": {}, + "bbb/.xxx": {} + }; + let configArray; + + beforeEach(() => { + configArray = new ConfigArray( + { + plugins: { + aaa: { + definition: { + processors: { + ".xxx": processors["aaa/.xxx"] + } + } + } + } + }, + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + plugins: { + bbb: { + definition: { + processors: { + ".xxx": processors["bbb/.xxx"] + } + } + } + } + } + ); + }); + + it("should return 'aaa/.xxx' if it exists.", () => { + assert.strictEqual(configArray.pluginProcessors.get("aaa/.xxx"), processors["aaa/.xxx"]); + }); + + it("should return both 'bbb/.xxx' if it exists.", () => { + assert.strictEqual(configArray.pluginProcessors.get("bbb/.xxx"), processors["bbb/.xxx"]); + }); + + it("should throw an error if it tried to mutate.", () => { + assert.throws(() => { + configArray.pluginProcessors.set("ccc/.xxx", {}); + }); + }); + }); + + describe("'pluginRules' property should be the rules of all plugins.", () => { + const rules = { + "aaa/xxx": {}, + "bbb/xxx": {} + }; + let configArray; + + beforeEach(() => { + configArray = new ConfigArray( + { + plugins: { + aaa: { + definition: { + rules: { + xxx: rules["aaa/xxx"] + } + } + } + } + }, + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + plugins: { + bbb: { + definition: { + rules: { + xxx: rules["bbb/xxx"] + } + } + } + } + } + ); + }); + + it("should return null for built-in rules", () => { + assert.strictEqual(configArray.pluginRules.get("eqeqeq"), void 0); + }); + + it("should return 'aaa/xxx' if it exists.", () => { + assert.strictEqual(configArray.pluginRules.get("aaa/xxx"), rules["aaa/xxx"]); + }); + + it("should return 'bbb/xxx' if it exists.", () => { + assert.strictEqual(configArray.pluginRules.get("bbb/xxx"), rules["bbb/xxx"]); + }); + + it("should throw an error if it tried to mutate.", () => { + assert.throws(() => { + configArray.pluginRules.set("ccc/xxx", {}); + }); + }); + }); + + describe("'extractConfig(filePath)' method should retrieve the merged config for a given file.", () => { + it("should throw an error if a 'parser' has the loading error.", () => { + assert.throws(() => { + new ConfigArray( + { + parser: { error: new Error("Failed to load a parser.") } + } + ).extractConfig(__filename); + }, "Failed to load a parser."); + }); + + it("should not throw if the errored 'parser' was not used; overwriten", () => { + const parser = { id: "a parser" }; + const config = new ConfigArray( + { + parser: { error: new Error("Failed to load a parser.") } + }, + { + parser + } + ).extractConfig(__filename); + + assert.strictEqual(config.parser, parser); + }); + + it("should not throw if the errored 'parser' was not used; not matched", () => { + const config = new ConfigArray( + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + parser: { error: new Error("Failed to load a parser.") } + } + ).extractConfig(__filename); + + assert.strictEqual(config.parser, null); + }); + + it("should throw an error if a 'plugins' value has the loading error.", () => { + assert.throws(() => { + new ConfigArray( + { + plugins: { + foo: { error: new Error("Failed to load a plugin.") } + } + } + ).extractConfig(__filename); + }, "Failed to load a plugin."); + }); + + it("should not throw if the errored 'plugins' value was not used; not matched", () => { + const config = new ConfigArray( + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + plugins: { + foo: { error: new Error("Failed to load a plugin.") } + } + } + ).extractConfig(__filename); + + assert.deepStrictEqual(config.plugins, {}); + }); + + it("should not merge the elements which were not matched.", () => { + const config = new ConfigArray( + { + rules: { + "no-redeclare": "error" + } + }, + { + criteria: OverrideTester.create(["*.js"], [], process.cwd()), + rules: { + "no-undef": "error" + } + }, + { + criteria: OverrideTester.create(["*.js"], [path.basename(__filename)], process.cwd()), + rules: { + "no-use-before-define": "error" + } + }, + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + rules: { + "no-unused-vars": "error" + } + } + ).extractConfig(__filename); + + assert.deepStrictEqual(config.rules, { + "no-redeclare": ["error"], + "no-undef": ["error"] + }); + }); + + it("should return the same instance for every the same matching.", () => { + const configArray = new ConfigArray( + { + rules: { + "no-redeclare": "error" + } + }, + { + criteria: OverrideTester.create(["*.js"], [], process.cwd()), + rules: { + "no-undef": "error" + } + }, + { + criteria: OverrideTester.create(["*.js"], [path.basename(__filename)], process.cwd()), + rules: { + "no-use-before-define": "error" + } + }, + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + rules: { + "no-unused-vars": "error" + } + } + ); + + assert.strictEqual( + configArray.extractConfig(path.join(__dirname, "a.js")), + configArray.extractConfig(path.join(__dirname, "b.js")) + ); + }); + + describe("Moved from 'merge()' in tests/lib/config/config-ops.js", () => { + + /** + * Merge two config data. + * @param {Object} target A config data. + * @param {Object} source Another config data. + * @returns {Object} The merged config data. + */ + function merge(target, source) { + return new ConfigArray(target, source).extractConfig(__filename); + } + + it("should combine two objects when passed two objects with different top-level properties", () => { + const config = [ + { env: { browser: true } }, + { globals: { foo: "bar" } } + ]; + + const result = merge(config[0], config[1]); + + assert.strictEqual(result.globals.foo, "bar"); + assert.isTrue(result.env.browser); + }); + + it("should combine without blowing up on null values", () => { + const config = [ + { env: { browser: true } }, + { env: { node: null } } + ]; + + const result = merge(config[0], config[1]); + + assert.strictEqual(result.env.node, null); + assert.isTrue(result.env.browser); + }); + + it("should combine two objects with parser when passed two objects with different top-level properties", () => { + const config = [ + { env: { browser: true }, parser: "espree" }, + { globals: { foo: "bar" } } + ]; + + const result = merge(config[0], config[1]); + + assert.strictEqual(result.parser, "espree"); + }); + + it("should combine configs and override rules when passed configs with the same rules", () => { + const config = [ + { rules: { "no-mixed-requires": [0, false] } }, + { rules: { "no-mixed-requires": [1, true] } } + ]; + + const result = merge(config[0], config[1]); + + assert.isArray(result.rules["no-mixed-requires"]); + assert.strictEqual(result.rules["no-mixed-requires"][0], 1); + assert.strictEqual(result.rules["no-mixed-requires"][1], true); + }); + + it("should combine configs when passed configs with parserOptions", () => { + const config = [ + { parserOptions: { ecmaFeatures: { jsx: true } } }, + { parserOptions: { ecmaFeatures: { globalReturn: true } } } + ]; + + const result = merge(config[0], config[1]); + + assert.deepStrictEqual(result, { + env: {}, + globals: {}, + parser: null, + parserOptions: { + ecmaFeatures: { + jsx: true, + globalReturn: true + } + }, + plugins: {}, + processor: null, + rules: {}, + settings: {} + }); + + // double-check that originals were not changed + assert.deepStrictEqual(config[0], { parserOptions: { ecmaFeatures: { jsx: true } } }); + assert.deepStrictEqual(config[1], { parserOptions: { ecmaFeatures: { globalReturn: true } } }); + }); + + it("should override configs when passed configs with the same ecmaFeatures", () => { + const config = [ + { parserOptions: { ecmaFeatures: { globalReturn: false } } }, + { parserOptions: { ecmaFeatures: { globalReturn: true } } } + ]; + + const result = merge(config[0], config[1]); + + assert.deepStrictEqual(result, { + env: {}, + globals: {}, + parser: null, + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + }, + plugins: {}, + processor: null, + rules: {}, + settings: {} + }); + }); + + it("should combine configs and override rules when merging two configs with arrays and int", () => { + + const config = [ + { rules: { "no-mixed-requires": [0, false] } }, + { rules: { "no-mixed-requires": 1 } } + ]; + + const result = merge(config[0], config[1]); + + assert.isArray(result.rules["no-mixed-requires"]); + assert.strictEqual(result.rules["no-mixed-requires"][0], 1); + assert.strictEqual(result.rules["no-mixed-requires"][1], false); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [0, false] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": 1 } }); + }); + + it("should combine configs and override rules options completely", () => { + + const config = [ + { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }, + { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } } + ]; + + const result = merge(config[0], config[1]); + + assert.isArray(result.rules["no-mixed-requires1"]); + assert.deepStrictEqual(result.rules["no-mixed-requires1"][1], { err: ["error", "e"] }); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } }); + }); + + it("should combine configs and override rules options without array or object", () => { + + const config = [ + { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }, + { rules: { "no-mixed-requires1": [2, "requirejs"] } } + ]; + + const result = merge(config[0], config[1]); + + assert.strictEqual(result.rules["no-mixed-requires1"][0], 2); + assert.strictEqual(result.rules["no-mixed-requires1"][1], "requirejs"); + assert.isUndefined(result.rules["no-mixed-requires1"][2]); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [2, "requirejs"] } }); + }); + + it("should combine configs and override rules options without array or object but special case", () => { + + const config = [ + { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }, + { rules: { "no-mixed-requires1": "error" } } + ]; + + const result = merge(config[0], config[1]); + + assert.strictEqual(result.rules["no-mixed-requires1"][0], "error"); + assert.strictEqual(result.rules["no-mixed-requires1"][1], "nconf"); + assert.strictEqual(result.rules["no-mixed-requires1"][2], "underscore"); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": "error" } }); + }); + + it("should combine configs correctly", () => { + + const config = [ + { + rules: { + "no-mixed-requires1": [1, { event: ["evt", "e"] }], + "valid-jsdoc": 1, + semi: 1, + quotes1: [2, { exception: ["hi"] }], + smile: [1, ["hi", "bye"]] + }, + parserOptions: { + ecmaFeatures: { jsx: true } + }, + env: { browser: true }, + globals: { foo: false } + }, + { + rules: { + "no-mixed-requires1": [1, { err: ["error", "e"] }], + "valid-jsdoc": 2, + test: 1, + smile: [1, ["xxx", "yyy"]] + }, + parserOptions: { + ecmaFeatures: { globalReturn: true } + }, + env: { browser: false }, + globals: { foo: true } + } + ]; + + const result = merge(config[0], config[1]); + + assert.deepStrictEqual(result, { + parser: null, + parserOptions: { + ecmaFeatures: { + jsx: true, + globalReturn: true + } + }, + plugins: {}, + env: { + browser: false + }, + globals: { + foo: true + }, + rules: { + "no-mixed-requires1": [1, + { + err: [ + "error", + "e" + ] + } + ], + quotes1: [2, + { + exception: [ + "hi" + ] + } + ], + semi: [1], + smile: [1, ["xxx", "yyy"]], + test: [1], + "valid-jsdoc": [2] + }, + settings: {}, + processor: null + }); + assert.deepStrictEqual(config[0], { + rules: { + "no-mixed-requires1": [1, { event: ["evt", "e"] }], + "valid-jsdoc": 1, + semi: 1, + quotes1: [2, { exception: ["hi"] }], + smile: [1, ["hi", "bye"]] + }, + parserOptions: { + ecmaFeatures: { jsx: true } + }, + env: { browser: true }, + globals: { foo: false } + }); + assert.deepStrictEqual(config[1], { + rules: { + "no-mixed-requires1": [1, { err: ["error", "e"] }], + "valid-jsdoc": 2, + test: 1, + smile: [1, ["xxx", "yyy"]] + }, + parserOptions: { + ecmaFeatures: { globalReturn: true } + }, + env: { browser: false }, + globals: { foo: true } + }); + }); + + it("should copy deeply if there is not the destination's property", () => { + const a = {}; + const b = { settings: { bar: 1 } }; + + const result = merge(a, b); + + assert(a.settings === void 0); + assert(b.settings.bar === 1); + assert(result.settings.bar === 1); + + result.settings.bar = 2; + assert(b.settings.bar === 1); + assert(result.settings.bar === 2); + }); + }); + }); + + describe("'getUsedExtractedConfigs(instance)' function should retrieve used extracted configs from the instance's internal cache.", () => { + let configArray; + + beforeEach(() => { + configArray = new ConfigArray( + { + rules: { + "no-redeclare": "error" + } + }, + { + criteria: OverrideTester.create(["*.js"], [], process.cwd()), + rules: { + "no-undef": "error" + } + }, + { + criteria: OverrideTester.create(["*.js"], [path.basename(__filename)], process.cwd()), + rules: { + "no-use-before-define": "error" + } + }, + { + criteria: OverrideTester.create(["*.ts"], [], process.cwd()), + rules: { + "no-unused-vars": "error" + } + } + ); + }); + + it("should return empty array before it called 'extractConfig(filePath)'.", () => { + assert.deepStrictEqual(getUsedExtractedConfigs(configArray), []); + }); + + for (const { filePaths } of [ + { filePaths: [__filename] }, + { filePaths: [__filename, `${__filename}.ts`] }, + { filePaths: [__filename, `${__filename}.ts`, path.join(__dirname, "foo.js")] } + ]) { + describe(`after it called 'extractConfig(filePath)' ${filePaths.length} time(s) with ${JSON.stringify(filePaths, null, 4)}, the returned array`, () => { // eslint-disable-line no-loop-func + let configs; + let usedConfigs; + + beforeEach(() => { + configs = filePaths.map(filePath => configArray.extractConfig(filePath)); + usedConfigs = getUsedExtractedConfigs(configArray); + }); + + it(`should have ${filePaths.length} as the length.`, () => { + assert.strictEqual(usedConfigs.length, configs.length); + }); + + for (let i = 0; i < filePaths.length; ++i) { + it(`should contain 'configs[${i}]'.`, () => { // eslint-disable-line no-loop-func + assert(usedConfigs.includes(configs[i])); + }); + } + }); + } + + it("should not contain duplicate values.", () => { + const configs = [ + configArray.extractConfig(__filename), + configArray.extractConfig(`${__filename}.ts`), + configArray.extractConfig(path.join(__dirname, "foo.js")) + ]; + + configArray.extractConfig(__filename); + configArray.extractConfig(path.join(__dirname, "foo.js")); + configArray.extractConfig(path.join(__dirname, "bar.js")); + configArray.extractConfig(path.join(__dirname, "baz.js")); + + const usedConfigs = getUsedExtractedConfigs(configArray); + + assert.deepStrictEqual(usedConfigs.filter(c => c === configs[0]).length, 1); + assert.deepStrictEqual(usedConfigs.filter(c => c === configs[1]).length, 1); + assert.deepStrictEqual(usedConfigs.filter(c => c === configs[2]).length, 1); + }); + }); +}); diff --git a/tests/lib/cli-engine/config-array/config-dependency.js b/tests/lib/cli-engine/config-array/config-dependency.js new file mode 100644 index 00000000000..e844a3afad0 --- /dev/null +++ b/tests/lib/cli-engine/config-array/config-dependency.js @@ -0,0 +1,92 @@ +/** + * @fileoverview Tests for ConfigDependency class. + * @author Toru Nagashima + */ +"use strict"; + +const assert = require("assert"); +const { ConfigDependency } = require("../../../../lib/cli-engine/config-array/config-dependency"); + +describe("ConfigDependency", () => { + describe("'constructor(data)' should initialize properties.", () => { + + /** @type {ConfigDependency} */ + let dep; + + beforeEach(() => { + dep = new ConfigDependency({ + definition: { name: "definition?" }, + error: new Error("error?"), + filePath: "filePath?", + id: "id?", + importerName: "importerName?", + importerPath: "importerPath?" + }); + }); + + it("should set 'data.definition' to 'definition' property.", () => { + assert.deepStrictEqual(dep.definition, { name: "definition?" }); + }); + + it("should set 'data.error' to 'error' property.", () => { + assert.deepStrictEqual(dep.error.message, "error?"); + }); + + it("should set 'data.filePath' to 'filePath' property.", () => { + assert.deepStrictEqual(dep.filePath, "filePath?"); + }); + + it("should set 'data.id' to 'id' property.", () => { + assert.deepStrictEqual(dep.id, "id?"); + }); + + it("should set 'data.importerName' to 'importerName' property.", () => { + assert.deepStrictEqual(dep.importerName, "importerName?"); + }); + + it("should set 'data.importerPath' to 'importerPath' property.", () => { + assert.deepStrictEqual(dep.importerPath, "importerPath?"); + }); + }); + + describe("'JSON.stringify(...)' should return readable JSON; not include 'definition' objects", () => { + it("should return an object that has five properties.", () => { + const dep = new ConfigDependency({ + definition: { name: "definition?" }, + error: new Error("error?"), + filePath: "filePath?", + id: "id?", + importerName: "importerName?", + importerPath: "importerPath?" + }); + + assert.strictEqual( + JSON.stringify(dep), + "{\"error\":{},\"filePath\":\"filePath?\",\"id\":\"id?\",\"importerName\":\"importerName?\",\"importerPath\":\"importerPath?\"}" + ); + }); + }); + + describe("'console.log(...)' should print readable string; not include 'Minimatch' objects", () => { + it("should use 'toJSON()' method.", () => { + const dep = new ConfigDependency({ + definition: { name: "definition?" }, + error: new Error("error?"), + filePath: "filePath?", + id: "id?", + importerName: "importerName?", + importerPath: "importerPath?" + }); + let called = false; + + dep.toJSON = () => { + called = true; + return ""; + }; + + console.log(dep); // eslint-disable-line no-console + + assert(called); + }); + }); +}); diff --git a/tests/lib/cli-engine/config-array/extracted-config.js b/tests/lib/cli-engine/config-array/extracted-config.js new file mode 100644 index 00000000000..9d700477b5e --- /dev/null +++ b/tests/lib/cli-engine/config-array/extracted-config.js @@ -0,0 +1,139 @@ +/** + * @fileoverview Tests for ExtractedConfig class. + * @author Toru Nagashima + */ +"use strict"; + +const assert = require("assert"); +const { ExtractedConfig } = require("../../../../lib/cli-engine/config-array/extracted-config"); + +describe("'ExtractedConfig' class", () => { + describe("'constructor()' should create an instance.", () => { + + /** @type {ExtractedConfig} */ + let config; + + beforeEach(() => { + config = new ExtractedConfig(); + }); + + it("should have 'env' property.", () => { + assert.deepStrictEqual(config.env, {}); + }); + + it("should have 'globals' property.", () => { + assert.deepStrictEqual(config.globals, {}); + }); + + it("should have 'parser' property.", () => { + assert.deepStrictEqual(config.parser, null); + }); + + it("should have 'parserOptions' property.", () => { + assert.deepStrictEqual(config.parserOptions, {}); + }); + + it("should have 'plugins' property.", () => { + assert.deepStrictEqual(config.plugins, {}); + }); + + it("should have 'processor' property.", () => { + assert.deepStrictEqual(config.processor, null); + }); + + it("should have 'rules' property.", () => { + assert.deepStrictEqual(config.rules, {}); + }); + + it("should have 'settings' property.", () => { + assert.deepStrictEqual(config.settings, {}); + }); + }); + + describe("'toCompatibleObjectAsConfigFileContent()' method should return a valid config data.", () => { + + /** @type {ExtractedConfig} */ + let config; + + beforeEach(() => { + config = new ExtractedConfig(); + }); + + it("should use 'env' property as is.", () => { + config.env = { a: true }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.env, { a: true }); + }); + + it("should use 'globals' as is.", () => { + config.globals = { a: true }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.globals, { a: true }); + }); + + it("should use 'parser.filePath' for 'parser' property.", () => { + config.parser = { + definition: {}, + error: null, + filePath: "/path/to/a/parser", + id: "parser", + importerName: "importer name", + importerPath: "importer path" + }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.parser, "/path/to/a/parser"); + }); + + it("should use 'null' for 'parser' property if 'parser' property is 'null'.", () => { + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.parser, null); + }); + + it("should use 'parserOptions' property as is.", () => { + config.parserOptions = { a: true }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.parserOptions, { a: true }); + }); + + it("should use the keys of 'plugins' property for 'plugins' property.", () => { + config.plugins = { a: {}, b: {} }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.plugins, ["b", "a"]); + }); + + it("should not use 'processor' property.", () => { + config.processor = "foo/.md"; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.processor, void 0); + }); + + it("should use 'rules' property as is.", () => { + config.rules = { a: 1, b: 2 }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.rules, { a: 1, b: 2 }); + }); + + it("should use 'settings' property as is.", () => { + config.settings = { a: 1 }; + + const data = config.toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(data.settings, { a: 1 }); + }); + }); +}); diff --git a/tests/lib/cli-engine/config-array/override-tester.js b/tests/lib/cli-engine/config-array/override-tester.js new file mode 100644 index 00000000000..d0e3d691ba8 --- /dev/null +++ b/tests/lib/cli-engine/config-array/override-tester.js @@ -0,0 +1,254 @@ +/** + * @fileoverview Tests for OverrideTester class. + * @author Toru Nagashima + */ +"use strict"; + +const path = require("path"); +const assert = require("assert"); +const { OverrideTester } = require("../../../../lib/cli-engine/config-array/override-tester"); + +describe("OverrideTester", () => { + describe("'create(files, excludedFiles, basePath)' should create a tester.", () => { + for (const { files, excludedFiles, basePath } of [ + { files: void 0, excludedFiles: void 0, basePath: process.cwd() }, + { files: [], excludedFiles: [], basePath: process.cwd() } + ]) { + it(`should return null if ${JSON.stringify({ files, excludedFiles, basePath })} was given.`, () => { + assert.strictEqual( + OverrideTester.create(files, excludedFiles, basePath), + null + ); + }); + } + + it("should return an 'OverrideTester' instance that has given parameters if strings were given.", () => { + const files = "*.js"; + const excludedFiles = "ignore/*"; + const basePath = process.cwd(); + const tester = OverrideTester.create(files, excludedFiles, basePath); + + assert.strictEqual(tester.patterns.length, 1); + assert.strictEqual(tester.patterns[0].includes.length, 1); + assert.strictEqual(tester.patterns[0].excludes.length, 1); + assert.strictEqual(tester.patterns[0].includes[0].pattern, files); + assert.strictEqual(tester.patterns[0].excludes[0].pattern, excludedFiles); + assert.strictEqual(tester.basePath, basePath); + }); + + it("should return an 'OverrideTester' instance that has given parameters if arrays were given.", () => { + const files = ["*.js"]; + const excludedFiles = ["ignore/*"]; + const basePath = process.cwd(); + const tester = OverrideTester.create(files, excludedFiles, basePath); + + assert.strictEqual(tester.patterns.length, 1); + assert.strictEqual(tester.patterns[0].includes.length, 1); + assert.strictEqual(tester.patterns[0].excludes.length, 1); + assert.strictEqual(tester.patterns[0].includes[0].pattern, files[0]); + assert.strictEqual(tester.patterns[0].excludes[0].pattern, excludedFiles[0]); + assert.strictEqual(tester.basePath, basePath); + }); + }); + + describe("'and(a, b)' should return either or create another tester what includes both.", () => { + it("should return null if both were null.", () => { + assert.strictEqual(OverrideTester.and(null, null), null); + }); + + it("should return the first one if the second one was null.", () => { + const tester = OverrideTester.create("*.js"); + + assert.strictEqual(OverrideTester.and(tester, null), tester); + }); + + it("should return the second one if the first one was null.", () => { + const tester = OverrideTester.create("*.js"); + + assert.strictEqual(OverrideTester.and(null, tester), tester); + }); + + it("should return another one what includes both patterns if both are testers.", () => { + const tester1 = OverrideTester.create("*.js"); + const tester2 = OverrideTester.create("*.js"); + const tester3 = OverrideTester.and(tester1, tester2); + + assert.strictEqual(tester3.patterns.length, 2); + assert.strictEqual(tester3.patterns[0], tester1.patterns[0]); + assert.strictEqual(tester3.patterns[1], tester2.patterns[0]); + }); + }); + + describe("'test(filePath)' method", () => { + it("should throw an error if no arguments were given.", () => { + assert.throws(() => { + OverrideTester.create("*.js").test(); + }, /'filePath' should be an absolute path, but got undefined/u); + }); + + it("should throw an error if a non-string value was given.", () => { + assert.throws(() => { + OverrideTester.create("*.js").test(100); + }, /'filePath' should be an absolute path, but got 100/u); + }); + + it("should throw an error if a relative path was given.", () => { + assert.throws(() => { + OverrideTester.create("*.js").test("foo/bar.js"); + }, /'filePath' should be an absolute path, but got foo\/bar\.js/u); + }); + + describe("Moved from 'pathMatchesGlobs()' in tests/lib/config/config-ops.js", () => { + + /** + * Test if a given file path matches to the given condition. + * @param {string} filePath The file path to test patterns against + * @param {string|string[]} files One or more glob patterns + * @param {string|string[]} [excludedFiles] One or more glob patterns + * @returns {boolean} The result. + */ + function test(filePath, files, excludedFiles) { + const basePath = process.cwd(); + const tester = OverrideTester.create(files, excludedFiles, basePath); + + return tester.test(path.resolve(basePath, filePath)); + } + + /** + * Emits a test that confirms the specified file path matches the specified combination of patterns. + * @param {string} filePath The file path to test patterns against + * @param {string|string[]} patterns One or more glob patterns + * @param {string|string[]} [excludedPatterns] One or more glob patterns + * @returns {void} + */ + function match(filePath, patterns, excludedPatterns) { + it(`matches ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { + const result = test(filePath, patterns, excludedPatterns); + + assert.strictEqual(result, true); + }); + } + + /** + * Emits a test that confirms the specified file path does not match the specified combination of patterns. + * @param {string} filePath The file path to test patterns against + * @param {string|string[]} patterns One or more glob patterns + * @param {string|string[]} [excludedPatterns] One or more glob patterns + * @returns {void} + */ + function noMatch(filePath, patterns, excludedPatterns) { + it(`does not match ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { + const result = test(filePath, patterns, excludedPatterns); + + assert.strictEqual(result, false); + }); + } + + /** + * Emits a test that confirms the specified pattern throws an error. + * @param {string} filePath The file path to test the pattern against + * @param {string} pattern The glob pattern that should trigger the error condition + * @param {string} expectedMessage The expected error's message + * @returns {void} + */ + function error(filePath, pattern, expectedMessage) { + it(`emits an error given '${pattern}'`, () => { + let errorMessage; + + try { + test(filePath, pattern); + } catch (e) { + errorMessage = e.message; + } + + assert.strictEqual(errorMessage, expectedMessage); + }); + } + + // files in the project root + match("foo.js", ["foo.js"], []); + match("foo.js", ["*"], []); + match("foo.js", ["*.js"], []); + match("foo.js", ["**/*.js"], []); + match("bar.js", ["*.js"], ["foo.js"]); + + noMatch("foo.js", ["./foo.js"], []); + noMatch("foo.js", ["./*"], []); + noMatch("foo.js", ["./**"], []); + noMatch("foo.js", ["*"], ["foo.js"]); + noMatch("foo.js", ["*.js"], ["foo.js"]); + noMatch("foo.js", ["**/*.js"], ["foo.js"]); + + // files in a subdirectory + match("subdir/foo.js", ["foo.js"], []); + match("subdir/foo.js", ["*"], []); + match("subdir/foo.js", ["*.js"], []); + match("subdir/foo.js", ["**/*.js"], []); + match("subdir/foo.js", ["subdir/*.js"], []); + match("subdir/foo.js", ["subdir/foo.js"], []); + match("subdir/foo.js", ["subdir/*"], []); + match("subdir/second/foo.js", ["subdir/**"], []); + + noMatch("subdir/foo.js", ["./foo.js"], []); + noMatch("subdir/foo.js", ["./**"], []); + noMatch("subdir/foo.js", ["./subdir/**"], []); + noMatch("subdir/foo.js", ["./subdir/*"], []); + noMatch("subdir/foo.js", ["*"], ["subdir/**"]); + noMatch("subdir/very/deep/foo.js", ["*.js"], ["subdir/**"]); + noMatch("subdir/second/foo.js", ["subdir/*"], []); + noMatch("subdir/second/foo.js", ["subdir/**"], ["subdir/second/*"]); + + // error conditions + error("foo.js", ["/*.js"], "Invalid override pattern (expected relative path not containing '..'): /*.js"); + error("foo.js", ["/foo.js"], "Invalid override pattern (expected relative path not containing '..'): /foo.js"); + error("foo.js", ["../**"], "Invalid override pattern (expected relative path not containing '..'): ../**"); + }); + }); + + describe("'JSON.stringify(...)' should return readable JSON; not include 'Minimatch' objects", () => { + it("should return an object that has three properties 'includes', 'excludes', and 'basePath' if that 'patterns' property include one object.", () => { + const files = "*.js"; + const excludedFiles = "test/*"; + const basePath = process.cwd(); + const tester = OverrideTester.create(files, excludedFiles, basePath); + + assert.strictEqual( + JSON.stringify(tester), + `{"includes":["${files}"],"excludes":["${excludedFiles}"],"basePath":${JSON.stringify(basePath)}}` + ); + }); + + it("should return an object that has two properties 'AND' and 'basePath' if that 'patterns' property include two or more objects.", () => { + const files1 = "*.js"; + const excludedFiles1 = "test/*"; + const files2 = "*.story.js"; + const excludedFiles2 = "src/*"; + const basePath = process.cwd(); + const tester = OverrideTester.and( + OverrideTester.create(files1, excludedFiles1, basePath), + OverrideTester.create(files2, excludedFiles2, basePath) + ); + + assert.strictEqual( + JSON.stringify(tester), + `{"AND":[{"includes":["${files1}"],"excludes":["${excludedFiles1}"]},{"includes":["${files2}"],"excludes":["${excludedFiles2}"]}],"basePath":${JSON.stringify(basePath)}}` + ); + }); + }); + + describe("'console.log(...)' should print readable string; not include 'Minimatch' objects", () => { + it("should use 'toJSON()' method.", () => { + const tester = OverrideTester.create("*.js", "", process.cwd()); + let called = false; + + tester.toJSON = () => { + called = true; + return ""; + }; + + console.log(tester); // eslint-disable-line no-console + + assert(called); + }); + }); +}); diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js new file mode 100644 index 00000000000..8240f2550d4 --- /dev/null +++ b/tests/lib/cli-engine/file-enumerator.js @@ -0,0 +1,454 @@ +/** + * @fileoverview Tests for FileEnumerator class. + * @author Toru Nagashima + */ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const os = require("os"); +const { assert } = require("chai"); +const sh = require("shelljs"); +const { CascadingConfigArrayFactory } = + require("../../../lib/cli-engine/cascading-config-array-factory"); +const { FileEnumerator } = require("../../../lib/cli-engine/file-enumerator"); +const { IgnoredPaths } = require("../../../lib/util/ignored-paths"); +const { defineFileEnumeratorWithInmemoryFileSystem } = require("./_utils"); + +describe("FileEnumerator", () => { + describe("'iterateFiles(patterns)' method should iterate files and configs.", () => { + const root = path.join(os.tmpdir(), "eslint/file-enumerator"); + const files = { + /* eslint-disable quote-props */ + "lib": { + "nested": { + "one.js": "", + "two.js": "", + "parser.js": "", + ".eslintrc.yml": "parser: './parser'" + }, + "one.js": "", + "two.js": "" + }, + "test": { + "one.js": "", + "two.js": "", + ".eslintrc.yml": "env: { mocha: true }" + }, + ".eslintignore": "/lib/nested/parser.js", + ".eslintrc.json": JSON.stringify({ + rules: { + "no-undef": "error", + "no-unused-vars": "error" + } + }) + /* eslint-enable quote-props */ + }; + + describe(`with the files ${JSON.stringify(files)}`, () => { + const { FileEnumerator } = defineFileEnumeratorWithInmemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow + + /** @type {FileEnumerator} */ + let enumerator; + + beforeEach(() => { + enumerator = new FileEnumerator(); + }); + + it("should ignore empty strings.", () => { + Array.from(enumerator.iterateFiles(["lib/*.js", ""])); // don't throw "file not found" error. + }); + + describe("if 'lib/*.js' was given,", () => { + + /** @type {Array<{config:(typeof import('../../../lib/cli-engine'))["ConfigArray"], filePath:string, ignored:boolean}>} */ + let list; + + beforeEach(() => { + list = [...enumerator.iterateFiles("lib/*.js")]; + }); + + it("should list two files.", () => { + assert.strictEqual(list.length, 2); + }); + + it("should list 'lib/one.js' and 'lib/two.js'.", () => { + assert.deepStrictEqual( + list.map(entry => entry.filePath), + [ + path.join(root, "lib/one.js"), + path.join(root, "lib/two.js") + ] + ); + }); + + it("should use the config '.eslintrc.json' for both files.", () => { + assert.strictEqual(list[0].config, list[1].config); + assert.strictEqual(list[0].config.length, 1); + assert.strictEqual(list[0].config[0].filePath, path.join(root, ".eslintrc.json")); + }); + }); + + describe("if 'lib/**/*.js' was given,", () => { + + /** @type {Array<{config:(typeof import('../../../lib/cli-engine'))["ConfigArray"], filePath:string, ignored:boolean}>} */ + let list; + + beforeEach(() => { + list = [...enumerator.iterateFiles("lib/**/*.js")]; + }); + + it("should list four files.", () => { + assert.strictEqual(list.length, 4); + }); + + it("should list 'lib/nested/one.js', 'lib/nested/two.js', 'lib/one.js', 'lib/two.js'.", () => { + assert.deepStrictEqual( + list.map(entry => entry.filePath), + [ + path.join(root, "lib/nested/one.js"), + path.join(root, "lib/nested/two.js"), + path.join(root, "lib/one.js"), + path.join(root, "lib/two.js") + ] + ); + }); + + it("should use the merged config of '.eslintrc.json' and 'lib/nested/.eslintrc.yml' for 'lib/nested/one.js' and 'lib/nested/two.js'.", () => { + assert.strictEqual(list[0].config, list[1].config); + assert.strictEqual(list[0].config.length, 2); + assert.strictEqual(list[0].config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[0].config[1].filePath, path.join(root, "lib/nested/.eslintrc.yml")); + }); + + it("should use the config '.eslintrc.json' for 'lib/one.js' and 'lib/two.js'.", () => { + assert.strictEqual(list[2].config, list[3].config); + assert.strictEqual(list[2].config.length, 1); + assert.strictEqual(list[2].config[0].filePath, path.join(root, ".eslintrc.json")); + }); + }); + + describe("if 'lib/*.js' snf 'test/*.js' were given,", () => { + + /** @type {Array<{config:(typeof import('../../../lib/cli-engine'))["ConfigArray"], filePath:string, ignored:boolean}>} */ + let list; + + beforeEach(() => { + list = [...enumerator.iterateFiles(["lib/*.js", "test/*.js"])]; + }); + + it("should list four files.", () => { + assert.strictEqual(list.length, 4); + }); + + it("should list 'lib/one.js', 'lib/two.js', 'test/one.js', 'test/two.js'.", () => { + assert.deepStrictEqual( + list.map(entry => entry.filePath), + [ + path.join(root, "lib/one.js"), + path.join(root, "lib/two.js"), + path.join(root, "test/one.js"), + path.join(root, "test/two.js") + ] + ); + }); + + it("should use the config '.eslintrc.json' for 'lib/one.js' and 'lib/two.js'.", () => { + assert.strictEqual(list[0].config, list[1].config); + assert.strictEqual(list[0].config.length, 1); + assert.strictEqual(list[0].config[0].filePath, path.join(root, ".eslintrc.json")); + }); + + it("should use the merged config of '.eslintrc.json' and 'test/.eslintrc.yml' for 'test/one.js' and 'test/two.js'.", () => { + assert.strictEqual(list[2].config, list[3].config); + assert.strictEqual(list[2].config.length, 2); + assert.strictEqual(list[2].config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[2].config[1].filePath, path.join(root, "test/.eslintrc.yml")); + }); + }); + }); + + describe("Moved from tests/lib/util/glob-utils.js", () => { + let fixtureDir; + + /** + * Returns the path inside of the fixture directory. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFixturePath(...args) { + return path.join(fs.realpathSync(fixtureDir), ...args); + } + + /** + * List files as a compatible shape with glob-utils. + * @param {string|string[]} patterns The patterns to list files. + * @param {Object} options The option for FileEnumerator. + * @returns {{filename:string,ignored:boolean}[]} The listed files. + */ + function listFiles(patterns, options) { + return Array.from( + new FileEnumerator({ + ...options, + configArrayFactory: new CascadingConfigArrayFactory({ + ...options, + + // Disable "No Configuration Found" error. + useEslintrc: false + }), + ignoredPaths: new IgnoredPaths(options) + }).iterateFiles(patterns), + ({ filePath, ignored }) => ({ filename: filePath, ignored }) + ); + } + + before(() => { + fixtureDir = `${os.tmpdir()}/eslint/tests/fixtures/`; + sh.mkdir("-p", fixtureDir); + sh.cp("-r", "./tests/fixtures/*", fixtureDir); + }); + + after(() => { + sh.rm("-r", fixtureDir); + }); + + describe("listFilesToProcess()", () => { + it("should return an array with a resolved (absolute) filename", () => { + const patterns = [getFixturePath("glob-util", "one-js-file", "**/*.js")]; + const result = listFiles(patterns, { + cwd: getFixturePath() + }); + + const file1 = getFixturePath("glob-util", "one-js-file", "baz.js"); + + assert.isArray(result); + assert.deepStrictEqual(result, [{ filename: file1, ignored: false }]); + }); + + it("should return all files matching a glob pattern", () => { + const patterns = [getFixturePath("glob-util", "two-js-files", "**/*.js")]; + const result = listFiles(patterns, { + cwd: getFixturePath() + }); + + const file1 = getFixturePath("glob-util", "two-js-files", "bar.js"); + const file2 = getFixturePath("glob-util", "two-js-files", "foo.js"); + + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result, [ + { filename: file1, ignored: false }, + { filename: file2, ignored: false } + ]); + }); + + it("should return all files matching multiple glob patterns", () => { + const patterns = [ + getFixturePath("glob-util", "two-js-files", "**/*.js"), + getFixturePath("glob-util", "one-js-file", "**/*.js") + ]; + const result = listFiles(patterns, { + cwd: getFixturePath() + }); + + const file1 = getFixturePath("glob-util", "two-js-files", "bar.js"); + const file2 = getFixturePath("glob-util", "two-js-files", "foo.js"); + const file3 = getFixturePath("glob-util", "one-js-file", "baz.js"); + + assert.strictEqual(result.length, 3); + assert.deepStrictEqual(result, [ + { filename: file1, ignored: false }, + { filename: file2, ignored: false }, + { filename: file3, ignored: false } + ]); + }); + + it("should ignore hidden files for standard glob patterns", () => { + const patterns = [getFixturePath("glob-util", "hidden", "**/*.js")]; + + assert.throws(() => { + listFiles(patterns, { + cwd: getFixturePath() + }); + }, `All files matched by '${patterns[0]}' are ignored.`); + }); + + it("should return hidden files if included in glob pattern", () => { + const patterns = [getFixturePath("glob-util", "hidden", "**/.*.js")]; + const result = listFiles(patterns, { + cwd: getFixturePath() + }); + + const file1 = getFixturePath("glob-util", "hidden", ".foo.js"); + + assert.strictEqual(result.length, 1); + assert.deepStrictEqual(result, [ + { filename: file1, ignored: false } + ]); + }); + + it("should ignore default ignored files if not passed explicitly", () => { + const directory = getFixturePath("glob-util", "hidden"); + const patterns = [directory]; + + assert.throws(() => { + listFiles(patterns, { + cwd: getFixturePath() + }); + }, `All files matched by '${directory}' are ignored.`); + }); + + it("should ignore and warn for default ignored files when passed explicitly", () => { + const filename = getFixturePath("glob-util", "hidden", ".foo.js"); + const patterns = [filename]; + const result = listFiles(patterns, { + cwd: getFixturePath() + }); + + assert.strictEqual(result.length, 1); + assert.deepStrictEqual(result[0], { filename, ignored: true }); + }); + + it("should ignore default ignored files if not passed explicitly even if ignore is false", () => { + const directory = getFixturePath("glob-util", "hidden"); + const patterns = [directory]; + + assert.throws(() => { + listFiles(patterns, { + cwd: getFixturePath(), + ignore: false + }); + }, `All files matched by '${directory}' are ignored.`); + }); + + it("should not ignore default ignored files when passed explicitly if ignore is false", () => { + const filename = getFixturePath("glob-util", "hidden", ".foo.js"); + const patterns = [filename]; + const result = listFiles(patterns, { + cwd: getFixturePath(), + ignore: false + }); + + assert.strictEqual(result.length, 1); + assert.deepStrictEqual(result[0], { filename, ignored: false }); + }); + + it("should throw an error for a file which does not exist", () => { + const filename = getFixturePath("glob-util", "hidden", "bar.js"); + const patterns = [filename]; + + assert.throws(() => { + listFiles(patterns, { + cwd: getFixturePath(), + allowMissingGlobs: true + }); + }, `No files matching '${filename}' were found.`); + }); + + it("should throw if a folder that does not have any applicable files is linted", () => { + const filename = getFixturePath("glob-util", "empty"); + const patterns = [filename]; + + assert.throws(() => { + listFiles(patterns, { + cwd: getFixturePath() + }); + }, `No files matching '${filename}' were found.`); + }); + + it("should throw if only ignored files match a glob", () => { + const pattern = getFixturePath("glob-util", "ignored"); + const options = { ignore: true, ignorePath: getFixturePath("glob-util", "ignored", ".eslintignore") }; + + assert.throws(() => { + listFiles([pattern], options); + }, `All files matched by '${pattern}' are ignored.`); + }); + + it("should throw an error if no files match a glob", () => { + + // Relying here on the .eslintignore from the repo root + const patterns = ["tests/fixtures/glob-util/ignored/**/*.js"]; + + assert.throws(() => { + listFiles(patterns); + }, `No files matching '${patterns[0]}' were found.`); + }); + + it("should return an ignored file, if ignore option is turned off", () => { + const options = { ignore: false }; + const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; + const result = listFiles(patterns, options); + + assert.strictEqual(result.length, 1); + }); + + it("should ignore a file from a glob if it matches a pattern in an ignore file", () => { + const options = { ignore: true, ignorePath: getFixturePath("glob-util", "ignored", ".eslintignore") }; + const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; + + assert.throws(() => { + listFiles(patterns, options); + }, `All files matched by '${patterns[0]}' are ignored.`); + }); + + it("should ignore a file from a glob if matching a specified ignore pattern", () => { + const options = { ignore: true, ignorePattern: "foo.js", cwd: getFixturePath() }; + const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; + + assert.throws(() => { + listFiles(patterns, options); + }, `All files matched by '${patterns[0]}' are ignored.`); + }); + + it("should return a file only once if listed in more than 1 pattern", () => { + const patterns = [ + getFixturePath("glob-util", "one-js-file", "**/*.js"), + getFixturePath("glob-util", "one-js-file", "baz.js") + ]; + const result = listFiles(patterns, { + cwd: path.join(fixtureDir, "..") + }); + + const file1 = getFixturePath("glob-util", "one-js-file", "baz.js"); + + assert.isArray(result); + assert.deepStrictEqual(result, [ + { filename: file1, ignored: false } + ]); + }); + + it("should set 'ignored: true' for files that are explicitly specified but ignored", () => { + const options = { ignore: true, ignorePattern: "foo.js", cwd: getFixturePath() }; + const filename = getFixturePath("glob-util", "ignored", "foo.js"); + const patterns = [filename]; + const result = listFiles(patterns, options); + + assert.strictEqual(result.length, 1); + assert.deepStrictEqual(result, [ + { filename, ignored: true } + ]); + }); + + it("should not return files from default ignored folders", () => { + const options = { cwd: getFixturePath("glob-util") }; + const glob = getFixturePath("glob-util", "**/*.js"); + const patterns = [glob]; + const result = listFiles(patterns, options); + const resultFilenames = result.map(resultObj => resultObj.filename); + + assert.notInclude(resultFilenames, getFixturePath("glob-util", "node_modules", "dependency.js")); + }); + + it("should return unignored files from default ignored folders", () => { + const options = { ignorePattern: "!/node_modules/dependency.js", cwd: getFixturePath("glob-util") }; + const glob = getFixturePath("glob-util", "**/*.js"); + const patterns = [glob]; + const result = listFiles(patterns, options); + const unignoredFilename = getFixturePath("glob-util", "node_modules", "dependency.js"); + + assert.includeDeepMembers(result, [{ filename: unignoredFilename, ignored: false }]); + }); + }); + }); + }); +}); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index a0a197d88b1..a0a5c409a3d 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -15,7 +15,7 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - CLIEngine = require("../../lib/cli-engine"), + CLIEngine = require("../../lib/cli-engine").CLIEngine, path = require("path"), sinon = require("sinon"), leche = require("leche"), @@ -57,7 +57,7 @@ describe("cli", () => { sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(sinon.spy()); const localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -323,7 +323,7 @@ describe("cli", () => { describe("when given a directory with eslint excluded files in the directory", () => { it("should throw an error and not process any files", () => { const ignorePath = getFixturePath(".eslintignore"); - const filePath = getFixturePath("."); + const filePath = getFixturePath("cli"); assert.throws(() => { cli.execute(`--ignore-path ${ignorePath} ${filePath}`); @@ -706,7 +706,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -729,7 +729,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -766,7 +766,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().once(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -804,7 +804,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().withExactArgs(report); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -842,7 +842,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().withExactArgs(report); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -858,7 +858,7 @@ describe("cli", () => { const fakeCLIEngine = sandbox.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -893,7 +893,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -924,7 +924,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -960,7 +960,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -998,7 +998,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -1035,7 +1035,7 @@ describe("cli", () => { fakeCLIEngine.outputFixes = sandbox.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); @@ -1050,7 +1050,7 @@ describe("cli", () => { const fakeCLIEngine = sandbox.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine": fakeCLIEngine, + "./cli-engine": { CLIEngine: fakeCLIEngine }, "./util/logging": log }); diff --git a/tests/lib/code-path-analysis/code-path-analyzer.js b/tests/lib/code-path-analysis/code-path-analyzer.js index 3304672d140..f9bc6c2cff9 100644 --- a/tests/lib/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/code-path-analysis/code-path-analyzer.js @@ -12,7 +12,7 @@ const assert = require("assert"), fs = require("fs"), path = require("path"), - Linter = require("../../../lib/linter"), + { Linter } = require("../../../lib/linter"), EventGeneratorTester = require("../../../tools/internal-testers/event-generator-tester"), createEmitter = require("../../../lib/util/safe-emitter"), debug = require("../../../lib/code-path-analysis/debug-helpers"), diff --git a/tests/lib/code-path-analysis/code-path.js b/tests/lib/code-path-analysis/code-path.js index be1039fb47d..5f50a754a92 100644 --- a/tests/lib/code-path-analysis/code-path.js +++ b/tests/lib/code-path-analysis/code-path.js @@ -10,7 +10,7 @@ //------------------------------------------------------------------------------ const assert = require("assert"), - Linter = require("../../../lib/linter"); + { Linter } = require("../../../lib/linter"); const linter = new Linter(); //------------------------------------------------------------------------------ diff --git a/tests/lib/config.js b/tests/lib/config.js deleted file mode 100644 index e400dd4753a..00000000000 --- a/tests/lib/config.js +++ /dev/null @@ -1,1373 +0,0 @@ -/** - * @fileoverview Tests for config object. - * @author Seth McLaughlin - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - path = require("path"), - fs = require("fs"), - os = require("os"), - Config = require("../../lib/config"), - Linter = require("../../lib/linter"), - environments = require("../../conf/environments"), - sinon = require("sinon"), - mockFs = require("mock-fs"); - -const DIRECTORY_CONFIG_HIERARCHY = require("../fixtures/config-hierarchy/file-structure.json"); - -const linter = new Linter(); - -const { mkdir, rm, cp } = require("shelljs"); - -const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); - -/** - * Asserts that two configs are equal. This is necessary because assert.deepStrictEqual() - * gets confused when properties are in different orders. - * @param {Object} actual The config object to check. - * @param {Object} expected What the config object should look like. - * @returns {void} - * @private - */ -function assertConfigsEqual(actual, expected) { - if (actual.env && expected.env) { - assert.deepStrictEqual(actual.env, expected.env); - } - - if (actual.parserOptions && expected.parserOptions) { - assert.deepStrictEqual(actual.parserOptions, expected.parserOptions); - } - - if (actual.globals && expected.globals) { - assert.deepStrictEqual(actual.globals, expected.globals); - } - - if (actual.rules && expected.rules) { - assert.deepStrictEqual(actual.rules, expected.rules); - } - - if (actual.plugins && expected.plugins) { - assert.deepStrictEqual(actual.plugins, expected.plugins); - } -} - -/** - * Wait for the next tick. - * @returns {Promise} - - */ -function nextTick() { - return new Promise(resolve => process.nextTick(resolve)); -} - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("Config", () => { - - let fixtureDir, - sandbox; - - /** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFixturePath(...args) { - return path.join(fixtureDir, "config-hierarchy", ...args); - } - - /** - * Mocks the current CWD path - * @param {string} fakeCWDPath - fake CWD path - * @returns {void} - * @private - */ - function mockCWDResponse(fakeCWDPath) { - sandbox.stub(process, "cwd") - .returns(fakeCWDPath); - } - - /** - * Mocks the current user's home path - * @param {string} fakeUserHomePath - fake user's home path - * @returns {void} - * @private - */ - function mockOsHomedir(fakeUserHomePath) { - sandbox.stub(os, "homedir") - .returns(fakeUserHomePath); - } - - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(() => { - fixtureDir = `${os.tmpdir()}/eslint/fixtures`; - mkdir("-p", fixtureDir); - cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); - cp("-r", "./tests/fixtures/rules", fixtureDir); - }); - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - }); - - afterEach(() => { - sandbox.verifyAndRestore(); - }); - - after(() => { - rm("-r", fixtureDir); - }); - - describe("new Config()", () => { - - // https://github.com/eslint/eslint/issues/2380 - it("should not modify baseConfig when format is specified", () => { - const customBaseConfig = {}, - configHelper = new Config({ baseConfig: customBaseConfig, cwd: process.cwd(), format: "foo" }, linter); - - // at one point, customBaseConfig.format would end up equal to "foo"...that's bad - assert.deepStrictEqual(customBaseConfig, {}); - assert.strictEqual(configHelper.options.format, "foo"); - }); - - it("should create config object when using baseConfig with extends", () => { - const customBaseConfig = { - extends: path.resolve(__dirname, "..", "fixtures", "config-extends", "array", ".eslintrc") - }; - const configHelper = new Config({ baseConfig: customBaseConfig, cwd: process.cwd() }, linter); - - assert.deepStrictEqual(configHelper.baseConfig.env, { - browser: false, - es6: true, - node: true - }); - assert.deepStrictEqual(configHelper.baseConfig.rules, { - "no-empty": 1, - "comma-dangle": 2, - "no-console": 2 - }); - }); - }); - - describe("findLocalConfigFiles()", () => { - - /** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); - } - - before(() => { - mockFs({ - eslint: { - fixtures: { - "config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - } - }); - }); - - after(() => { - mockFs.restore(); - }); - - it("should return the path when an .eslintrc file is found", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - expected = getFakeFixturePath("broken", ".eslintrc"), - actual = Array.from( - configHelper.findLocalConfigFiles(getFakeFixturePath("broken")) - ); - - assert.strictEqual(actual[0], expected); - }); - - it("should return an empty array when an .eslintrc file is not found", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - actual = Array.from( - configHelper.findLocalConfigFiles(getFakeFixturePath()) - ); - - assert.isArray(actual); - assert.lengthOf(actual, 0); - }); - - it("should return package.json only when no other config files are found", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - expected0 = getFakeFixturePath("packagejson", "subdir", "package.json"), - expected1 = getFakeFixturePath("packagejson", ".eslintrc"), - actual = Array.from( - configHelper.findLocalConfigFiles(getFakeFixturePath("packagejson", "subdir")) - ); - - assert.strictEqual(actual[0], expected0); - assert.strictEqual(actual[1], expected1); - }); - - it("should return the only one config file even if there are multiple found", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - expected = getFakeFixturePath("broken", ".eslintrc"), - - // The first element of the array is the .eslintrc in the same directory. - actual = Array.from( - configHelper.findLocalConfigFiles(getFakeFixturePath("broken")) - ); - - assert.strictEqual(actual.length, 1); - assert.deepStrictEqual(actual, [expected]); - }); - - it("should return all possible files when multiple are found", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - expected = [ - getFakeFixturePath("fileexts/subdir/subsubdir/", ".eslintrc.json"), - getFakeFixturePath("fileexts/subdir/", ".eslintrc.yml"), - getFakeFixturePath("fileexts", ".eslintrc.js") - ], - - actual = Array.from( - configHelper.findLocalConfigFiles(getFakeFixturePath("fileexts/subdir/subsubdir")) - ); - - - assert.deepStrictEqual(actual.length, expected.length); - }); - - it("should return an empty array when a package.json file is not found", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - actual = Array.from(configHelper.findLocalConfigFiles(getFakeFixturePath())); - - assert.isArray(actual); - assert.lengthOf(actual, 0); - }); - }); - - describe("getConfig()", () => { - - it("should return the project config when called in current working directory", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - actual = configHelper.getConfig(); - - assert.strictEqual(actual.rules.strict[1], "global"); - }); - - it("should not retain configs from previous directories when called multiple times", () => { - - const firstpath = path.resolve(__dirname, "..", "fixtures", "configurations", "single-quotes", "subdir", ".eslintrc"); - const secondpath = path.resolve(__dirname, "..", "fixtures", "configurations", "single-quotes", ".eslintrc"); - - const configHelper = new Config({ cwd: process.cwd() }, linter); - let config; - - config = configHelper.getConfig(firstpath); - assert.strictEqual(config.rules["no-new"], 0); - config = configHelper.getConfig(secondpath); - assert.strictEqual(config.rules["no-new"], 1); - }); - - it("should throw an error when an invalid path is given", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "foobaz", ".eslintrc"); - const homePath = "does-not-exist"; - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - const configHelper = new StubbedConfig({ cwd: process.cwd() }, linter); - - sandbox.stub(fs, "readdirSync").throws(new Error()); - - assert.throws(() => { - configHelper.getConfig(configPath); - }, "No ESLint configuration found."); - }); - - it("should throw error when a configuration file doesn't exist", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", ".eslintrc"); - const configHelper = new Config({ cwd: process.cwd() }, linter); - - sandbox.stub(fs, "readFileSync").throws(new Error()); - - assert.throws(() => { - configHelper.getConfig(configPath); - }, "Cannot read config file"); - - }); - - it("should throw error when a configuration file is not require-able", () => { - const configPath = ".eslintrc"; - const configHelper = new Config({ cwd: process.cwd() }, linter); - - sandbox.stub(fs, "readFileSync").throws(new Error()); - - assert.throws(() => { - configHelper.getConfig(configPath); - }, "Cannot read config file"); - - }); - - it("should cache config when the same directory is passed twice", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "single-quotes", ".eslintrc"); - const configHelper = new Config({ cwd: process.cwd() }, linter); - - sandbox.spy(configHelper, "findLocalConfigFiles"); - - // If cached this should be called only once - configHelper.getConfig(configPath); - const callcount = configHelper.findLocalConfigFiles.callcount; - - configHelper.getConfig(configPath); - - assert.strictEqual(configHelper.findLocalConfigFiles.callcount, callcount); - }); - - // make sure JS-style comments don't throw an error - it("should load the config file when there are JS-style comments in the text", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "comments.json"), - configHelper = new Config({ configFile: configPath, cwd: process.cwd() }, linter), - semi = configHelper.specificConfigInfo.config.rules.semi, - strict = configHelper.specificConfigInfo.config.rules.strict; - - assert.strictEqual(semi, 1); - assert.strictEqual(strict, 0); - }); - - // make sure YAML files work correctly - it("should load the config file when a YAML file is used", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "env-browser.yaml"), - configHelper = new Config({ configFile: configPath, cwd: process.cwd() }, linter), - noAlert = configHelper.specificConfigInfo.config.rules["no-alert"], - noUndef = configHelper.specificConfigInfo.config.rules["no-undef"]; - - assert.strictEqual(noAlert, 0); - assert.strictEqual(noUndef, 2); - }); - - it("should contain the correct value for parser when a custom parser is specified", () => { - const configPath = path.resolve(__dirname, "../fixtures/configurations/parser/.eslintrc.json"), - configHelper = new Config({ cwd: process.cwd() }, linter), - config = configHelper.getConfig(configPath); - - assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.js")); - }); - - /* - * Configuration hierarchy --------------------------------------------- - * https://github.com/eslint/eslint/issues/3915 - */ - it("should correctly merge environment settings", () => { - const configHelper = new Config({ useEslintrc: true, cwd: process.cwd() }, linter), - file = getFixturePath("envs", "sub", "foo.js"), - expected = { - rules: {}, - env: { - browser: true, - node: false - }, - globals: environments.browser.globals - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - // Default configuration - blank - it("should return a blank config when using no .eslintrc", () => { - - const configHelper = new Config({ useEslintrc: false, cwd: process.cwd() }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - expected = { - rules: {}, - globals: {}, - env: {} - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { - const configHelper = new Config({ baseConfig: false, useEslintrc: false, cwd: process.cwd() }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - expected = { - rules: {}, - globals: {}, - env: {} - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - // No default configuration - it("should return an empty config when not using .eslintrc", () => { - - const configHelper = new Config({ useEslintrc: false, cwd: process.cwd() }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, {}); - }); - - it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { - const configHelper = new Config({ - baseConfig: { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - } - }, - useEslintrc: false, - cwd: process.cwd() - }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - } - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { - const examplePluginName = "eslint-plugin-example-with-rules-config"; - const configHelper = new Config({ - baseConfig: { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - }, - plugins: [examplePluginName] - }, - useEslintrc: false, - cwd: getFixturePath("plugins") - }, linter), - file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - } - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - // Project configuration - second level .eslintrc - it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { - - const configHelper = new Config({ cwd: process.cwd() }, linter), - file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - "no-console": 1, - quotes: [2, "single"] - } - }, - actual = configHelper.getConfig(file); - - expected.env.node = true; - - assertConfigsEqual(actual, expected); - }); - - // Project configuration - third level .eslintrc - it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { - - const configHelper = new Config({ cwd: process.cwd() }, linter), - file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - "no-console": 0, - quotes: [1, "double"] - } - }, - actual = configHelper.getConfig(file); - - expected.env.node = true; - - assertConfigsEqual(actual, expected); - }); - - // Project configuration - root set in second level .eslintrc - it("should not return or traverse configurations in parents of config with root:true", () => { - const configHelper = new Config({ cwd: process.cwd() }, linter), - file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"), - expected = { - rules: { - semi: [2, "never"] - } - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - // Project configuration - root set in second level .eslintrc - it("should return project config when called with a relative path from a subdir", () => { - const configHelper = new Config({ cwd: getFixturePath("root-true", "parent", "root", "subdir") }, linter), - dir = ".", - expected = { - rules: { - semi: [2, "never"] - } - }, - actual = configHelper.getConfig(dir); - - assertConfigsEqual(actual, expected); - }); - - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file adds to local .eslintrc", () => { - - const configHelper = new Config({ - configFile: getFixturePath("broken", "add-conf.yaml"), - cwd: process.cwd() - }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [2, "double"], - semi: [1, "never"] - } - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file overrides local .eslintrc", () => { - - const configHelper = new Config({ - configFile: getFixturePath("broken", "override-conf.yaml"), - cwd: process.cwd() - }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [0, "double"] - } - }, - actual = configHelper.getConfig(file); - - expected.env.node = true; - - assertConfigsEqual(actual, expected); - }); - - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file adds to local and parent .eslintrc", () => { - - const configHelper = new Config({ - configFile: getFixturePath("broken", "add-conf.yaml"), - cwd: process.cwd() - }, linter), - file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [2, "single"], - "no-console": 1, - semi: [1, "never"] - } - }, - actual = configHelper.getConfig(file); - - expected.env.node = true; - - assertConfigsEqual(actual, expected); - }); - - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file overrides local and parent .eslintrc", () => { - - const configHelper = new Config({ - configFile: getFixturePath("broken", "override-conf.yaml"), - cwd: process.cwd() - }, linter), - file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [0, "single"], - "no-console": 1 - } - }, - actual = configHelper.getConfig(file); - - expected.env.node = true; - - assertConfigsEqual(actual, expected); - }); - - // Command line configuration - --rule with --config and first level .eslintrc - it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { - - const configHelper = new Config({ - configFile: getFixturePath("broken", "override-conf.yaml"), - rules: { - quotes: [1, "double"] - }, - cwd: process.cwd() - }, linter), - file = getFixturePath("broken", "console-wrong-quotes.js"), - expected = { - env: { - node: true - }, - rules: { - quotes: [1, "double"] - } - }, - actual = configHelper.getConfig(file); - - expected.env.node = true; - - assertConfigsEqual(actual, expected); - }); - - // Command line configuration - --plugin - it("should merge command line plugin with local .eslintrc", () => { - const configHelper = new Config({ - plugins: ["another-plugin"], - cwd: getFixturePath("plugins") - }, linter), - file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"), - expected = { - plugins: [ - "example", - "another-plugin" - ] - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - - it("should merge multiple different config file formats", () => { - - const configHelper = new Config({ cwd: process.cwd() }, linter), - file = getFixturePath("fileexts/subdir/subsubdir/foo.js"), - expected = { - env: { - browser: true - }, - rules: { - semi: [2, "always"], - eqeqeq: 2 - } - }, - actual = configHelper.getConfig(file); - - assertConfigsEqual(actual, expected); - }); - - - it("should load user config globals", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "globals", "conf.yaml"), - configHelper = new Config({ configFile: configPath, useEslintrc: false, cwd: process.cwd() }, linter); - - const expected = { - globals: { - foo: true - } - }; - - const actual = configHelper.getConfig(configPath); - - assertConfigsEqual(actual, expected); - }); - - it("should not load disabled environments", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "environments", "disable.yaml"); - - const configHelper = new Config({ configFile: configPath, useEslintrc: false, cwd: process.cwd() }, linter); - - const config = configHelper.getConfig(configPath); - - assert.isUndefined(config.globals.window); - }); - - it("should error on fake environments", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "environments", "fake.yaml"); - - assert.throw(() => { - new Config({ configFile: configPath, useEslintrc: false, cwd: process.cwd() }, linter); // eslint-disable-line no-new - }); - }); - - it("should gracefully handle empty files", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "env-node.json"), - configHelper = new Config({ configFile: configPath, cwd: process.cwd() }, linter); - - configHelper.getConfig(path.resolve(__dirname, "..", "fixtures", "configurations", "empty", "empty.json")); - }); - - // Meaningful stack-traces - it("should include references to where an `extends` configuration was loaded from", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "config-extends", "error.json"); - - assert.throws(() => { - const configHelper = new Config({ useEslintrc: false, configFile: configPath, cwd: process.cwd() }, linter); - - configHelper.getConfig(configPath); - }, /Referenced from:.*?error\.json/u); - }); - - // Keep order with the last array element taking highest precedence - it("should make the last element in an array take the highest precedence", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "config-extends", "array", ".eslintrc"), - configHelper = new Config({ useEslintrc: false, configFile: configPath, cwd: process.cwd() }, linter), - expected = { - rules: { "no-empty": 1, "comma-dangle": 2, "no-console": 2 }, - env: { browser: false, node: true, es6: true } - }, - actual = configHelper.getConfig(configPath); - - assertConfigsEqual(actual, expected); - }); - - describe("with env in a child configuration file", () => { - it("should not overwrite parserOptions of the parent with env of the child", () => { - const config = new Config({ cwd: process.cwd() }, linter); - const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); - const expected = { - rules: {}, - env: { commonjs: true }, - parserOptions: { ecmaFeatures: { globalReturn: false } } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - }); - - describe("personal config file within home directory", () => { - - /** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); - } - - /** - * Mocks the file system for personal-config files - * @returns {undefined} - * @private - */ - function mockPersonalConfigFileSystem() { - mockFs({ - eslint: { - fixtures: { - "config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - } - }); - } - - afterEach(() => { - mockFs.restore(); - }); - - it("should load the personal config if no local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "home-folder"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ cwd: process.cwd() }, linter), - actual = config.getConfig(filePath), - expected = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - "home-folder-rule": 2 - } - }; - - assert.deepStrictEqual(actual, expected); - - // Ensure that the personal config is cached and isn't reloaded on every call - assert.strictEqual(config.getPersonalConfig(), config.getPersonalConfig()); - }); - - it("should ignore the personal config if a local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"), - homePath = getFakeFixturePath("personal-config", "home-folder"), - filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ cwd: process.cwd() }, linter), - actual = config.getConfig(filePath), - expected = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - "project-level-rule": 2 - } - }; - - assert.deepStrictEqual(actual, expected); - }); - - it("should ignore the personal config if config is passed through cli", () => { - const configPath = getFakeFixturePath("quotes-error.json"); - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "home-folder"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ configFile: configPath, cwd: process.cwd() }, linter), - actual = config.getConfig(filePath), - expected = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: [2, "double"] - } - }; - - assert.deepStrictEqual(actual, expected); - }); - - it("should still load the project config if the current working directory is the same as the home folder", () => { - const projectPath = getFakeFixturePath("personal-config", "project-with-config"), - filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); - - mockOsHomedir(projectPath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ cwd: process.cwd() }, linter), - actual = config.getConfig(filePath), - expected = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - "project-level-rule": 2, - "subfolder-level-rule": 2 - } - }; - - assert.deepStrictEqual(actual, expected); - }); - }); - - describe("when no local or personal config is found", () => { - - /** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); - } - - /** - * Mocks the file system for personal-config files - * @returns {undefined} - * @private - */ - function mockPersonalConfigFileSystem() { - mockFs({ - eslint: { - fixtures: { - "config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - } - }); - } - - afterEach(() => { - mockFs.restore(); - }); - - it("should throw an error if no local config and no personal config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ cwd: process.cwd() }, linter); - - assert.throws(() => { - config.getConfig(filePath); - }, "No ESLint configuration found"); - }); - - it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const configHelper = new StubbedConfig({ cwd: process.cwd() }, linter); - - assert.throws(() => { - configHelper.getConfig(filePath); - }, "No ESLint configuration found"); - }); - - it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ - cwd: process.cwd(), - useEslintrc: false - }, linter); - - config.getConfig(filePath); - }); - - it("should not throw an error if no local config and no personal config was found but rules are specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ - cwd: process.cwd(), - rules: { quotes: [2, "single"] } - }, linter); - - config.getConfig(filePath); - }); - - it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"), - homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"), - filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - - mockOsHomedir(homePath); - const StubbedConfig = proxyquire("../../lib/config", {}); - - mockPersonalConfigFileSystem(); - mockCWDResponse(projectPath); - - const config = new StubbedConfig({ - cwd: process.cwd(), - baseConfig: {} - }, linter); - - config.getConfig(filePath); - }); - }); - - describe("with overrides", () => { - - /** - * Returns the path inside of the fixture directory. - * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...pathSegments) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); - } - - before(() => { - mockFs({ - eslint: { - fixtures: { - "config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - } - }); - }); - - after(() => { - mockFs.restore(); - }); - - it("should merge override config when the pattern matches the file name", () => { - const config = new Config({ cwd: process.cwd() }, linter); - const targetPath = getFakeFixturePath("overrides", "foo.js"); - const expected = { - rules: { - quotes: [2, "single"], - "no-else-return": 0, - "no-unused-vars": 1, - semi: [1, "never"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should merge override config when the pattern matches the file path relative to the config file", () => { - const config = new Config({ cwd: process.cwd() }, linter); - const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); - const expected = { - rules: { - curly: ["error", "multi", "consistent"], - "no-else-return": 0, - "no-unused-vars": 1, - quotes: [2, "double"], - semi: [1, "never"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should not merge override config when the pattern matches the absolute file path", () => { - const targetPath = getFakeFixturePath("overrides", "bar.js"); - const resolvedPath = path.resolve(__dirname, "..", "fixtures", "config-hierarchy", "overrides", "bar.js"); - const config = new Config({ - cwd: process.cwd(), - baseConfig: { - overrides: [{ - files: resolvedPath, - rules: { - quotes: [1, "double"] - } - }] - }, - useEslintrc: false - }, linter); - - assert.throws(() => config.getConfig(targetPath), /Invalid override pattern/u); - }); - - it("should not merge override config when the pattern traverses up the directory tree", () => { - const targetPath = getFakeFixturePath("overrides", "bar.js"); - const parentPath = "overrides/../**/*.js"; - - const config = new Config({ - cwd: process.cwd(), - baseConfig: { - overrides: [{ - files: parentPath, - rules: { - quotes: [1, "single"] - } - }] - }, - useEslintrc: false - }, linter); - - assert.throws(() => config.getConfig(targetPath), /Invalid override pattern/u); - }); - - it("should merge all local configs (override and non-override) before non-local configs", () => { - const config = new Config({ cwd: process.cwd() }, linter); - const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); - const expected = { - rules: { - "no-console": 0, - "no-else-return": 0, - "no-unused-vars": 2, - quotes: [2, "double"], - semi: [2, "never"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { - const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); - const config = new Config({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "three/**/*.js", - rules: { - "semi-style": [2, "last"] - } - } - ] - }, - useEslintrc: false - }, linter); - const expected = { - rules: { - "semi-style": [2, "last"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides if all glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const config = new Config({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false - }, linter); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides even if some glob patterns do not match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const config = new Config({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false - }, linter); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should not apply overrides if any excluded glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const config = new Config({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*one.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false - }, linter); - const expected = { - rules: {} - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides if all excluded glob patterns fail to match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const config = new Config({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false - }, linter); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should cascade", () => { - const targetPath = getFakeFixturePath("overrides", "foo.js"); - const config = new Config({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "single"] - } - }, - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } - } - ] - }, - useEslintrc: false - }, linter); - const expected = { - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } - }; - const actual = config.getConfig(targetPath); - - assertConfigsEqual(actual, expected); - }); - }); - - describe("deprecation warnings", () => { - let warning = null; - - function onWarning(w) { // eslint-disable-line require-jsdoc - - // Node.js 6.x does not have 'w.code' property. - if (!Object.prototype.hasOwnProperty.call(w, "code") || typeof w.code === "string" && w.code.startsWith("ESLINT_")) { - warning = w; - } - } - - beforeEach(() => { - warning = null; - process.on("warning", onWarning); - }); - afterEach(() => { - process.removeListener("warning", onWarning); - }); - - it("should emit a deprecation warning if 'ecmaFeatures' is given.", () => Promise.resolve() - .then(() => { - const cwd = path.resolve(__dirname, "../fixtures/config-file/ecma-features/"); - const config = new Config({ cwd }, linter); - - config.getConfig("test.js"); - - // Wait for "warning" event. - return nextTick(); - }) - .then(() => { - assert.notStrictEqual(warning, null); - assert.strictEqual( - warning.message, - `The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in "tests${path.sep}fixtures${path.sep}config-file${path.sep}ecma-features${path.sep}.eslintrc.yml")` - ); - })); - }); - }); - - describe("Plugin Environments", () => { - it("should load environments from plugin", () => { - const configPath = path.resolve(__dirname, "..", "fixtures", "environments", "plugin.yaml"), - configHelper = new Config({ - reset: true, - configFile: configPath, - useEslintrc: false, - cwd: getFixturePath("plugins") - }, linter), - expected = { - env: { - "test/example": true - }, - plugins: ["test"] - }, - actual = configHelper.getConfig(configPath); - - assertConfigsEqual(actual, expected); - }); - }); -}); diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js index 2d74b8ae827..4ff7336edcb 100644 --- a/tests/lib/config/config-file.js +++ b/tests/lib/config/config-file.js @@ -14,15 +14,10 @@ const assert = require("chai").assert, path = require("path"), fs = require("fs"), yaml = require("js-yaml"), - shell = require("shelljs"), espree = require("espree"), ConfigFile = require("../../../lib/config/config-file"), - Linter = require("../../../lib/linter"), - CLIEngine = require("../../../lib/cli-engine"), - Config = require("../../../lib/config"), - relativeModuleResolver = require("../../../lib/util/relative-module-resolver"); + { CLIEngine } = require("../../../lib/cli-engine"); -const temp = require("temp").track(); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); //------------------------------------------------------------------------------ @@ -39,769 +34,11 @@ function getFixturePath(filepath) { return path.resolve(__dirname, "../../fixtures/config-file", filepath); } -/** - * Helper function to write configs to temp file. - * @param {Object} config Config to write out to temp file. - * @param {string} filename Name of file to write in temp dir. - * @param {string} existingTmpDir Optional dir path if temp file exists. - * @returns {string} Full path to the temp file. - * @private - */ -function writeTempConfigFile(config, filename, existingTmpDir) { - const tmpFileDir = existingTmpDir || temp.mkdirSync("eslint-tests-"), - tmpFilePath = path.join(tmpFileDir, filename), - tmpFileContents = JSON.stringify(config); - - fs.writeFileSync(tmpFilePath, tmpFileContents); - return tmpFilePath; -} - -/** - * Helper function to write JS configs to temp file. - * @param {Object} config Config to write out to temp file. - * @param {string} filename Name of file to write in temp dir. - * @param {string} existingTmpDir Optional dir path if temp file exists. - * @returns {string} Full path to the temp file. - * @private - */ -function writeTempJsConfigFile(config, filename, existingTmpDir) { - const tmpFileDir = existingTmpDir || temp.mkdirSync("eslint-tests-"), - tmpFilePath = path.join(tmpFileDir, filename), - tmpFileContents = `module.exports = ${JSON.stringify(config)}`; - - fs.writeFileSync(tmpFilePath, tmpFileContents); - return tmpFilePath; -} - //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("ConfigFile", () => { - let configContext; - - beforeEach(() => { - configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); - }); - - describe("CONFIG_FILES", () => { - it("should be present when imported", () => { - assert.isTrue(Array.isArray(ConfigFile.CONFIG_FILES)); - }); - }); - - describe("applyExtends()", () => { - it("should apply extension 'foo' when specified from root directory config", () => { - const config = ConfigFile.applyExtends({ - extends: "enable-browser-env", - rules: { eqeqeq: 2 } - }, configContext, getFixturePath(".eslintrc")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { browser: true }, - globals: {}, - rules: { eqeqeq: 2 } - }); - }); - - it("should apply all rules when extends config includes 'eslint:all'", () => { - const config = ConfigFile.applyExtends({ - extends: "eslint:all" - }, configContext, "/whatever"); - - assert.strictEqual(config.rules.eqeqeq, "error"); - assert.strictEqual(config.rules.curly, "error"); - - }); - - it("should throw an error when extends config module is not found", () => { - assert.throws(() => { - ConfigFile.applyExtends({ - extends: "foo", - rules: { eqeqeq: 2 } - }, configContext, "/whatever"); - }, /Cannot find module 'eslint-config-foo'/u); - - }); - - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - ConfigFile.applyExtends({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } - }, configContext, "/whatever"); - }, /Failed to load config "eslint:foo" to extend from./u); - - }); - - it("should throw an error when a parser in a plugin config is not found", () => { - assert.throws(() => { - ConfigFile.applyExtends({ - extends: "plugin:enable-nonexistent-parser/bar", - rules: { eqeqeq: 2 } - }, configContext, "/whatever"); - }, /Failed to resolve parser 'nonexistent-parser' declared in '[-\w/.:\\]+'.\nReferenced from: \/whatever/u); - }); - - it("should fall back to default parser when a parser called 'espree' is not found", () => { - assert.deepStrictEqual( - ConfigFile.loadObject(configContext, { config: { parser: "espree" }, filePath: "/", configFullName: "configName" }), - { parser: require.resolve("espree") } - ); - }); - - it("should throw an error when a plugin config is not found", () => { - assert.throws(() => { - ConfigFile.applyExtends({ - extends: "plugin:enable-nonexistent-parser/baz", - rules: { eqeqeq: 2 } - }, configContext, "/whatever"); - }, /Failed to load config "plugin:enable-nonexistent-parser\/baz" to extend from./u); - }); - - it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { - try { - ConfigFile.applyExtends({ - extends: "plugin:nonexistent-plugin/baz", - rules: { eqeqeq: 2 } - }, configContext, "/whatever"); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - pluginRootPath: getFixturePath("."), - configStack: ["/whatever"] - }); - - return; - } - assert.fail("Expected to throw an error"); - }); - - it("should throw an error with a message template when a plugin in the plugins list is not found", () => { - try { - ConfigFile.loadObject(configContext, { - config: { - plugins: ["nonexistent-plugin"] - }, - filePath: "/whatever", - configFullName: "configName" - }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - pluginRootPath: getFixturePath("."), - configStack: ["/whatever"] - }); - - return; - } - assert.fail("Expected to throw an error"); - }); - - it("should apply extensions recursively when specified from package", () => { - const config = ConfigFile.applyExtends({ - extends: "recursive-dependent", - rules: { eqeqeq: 2 } - }, configContext, getFixturePath(".eslintrc.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { browser: true }, - globals: {}, - rules: { - eqeqeq: 2, - bar: 2 - } - }); - - }); - - it("should apply extensions when specified from a JavaScript file", () => { - - const extendsFile = ".eslintrc.js"; - const filePath = getFixturePath("js/foo.js"); - - const config = ConfigFile.applyExtends({ - extends: extendsFile, - rules: { eqeqeq: 2 } - }, configContext, filePath); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - semi: [2, "always"], - eqeqeq: 2 - } - }); - - }); - - it("should apply extensions when specified from a YAML file", () => { - - const extendsFile = ".eslintrc.yaml"; - const filePath = getFixturePath("yaml/foo.js"); - - const config = ConfigFile.applyExtends({ - extends: extendsFile, - rules: { eqeqeq: 2 } - }, configContext, filePath); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { browser: true }, - globals: {}, - rules: { - eqeqeq: 2 - } - }); - - }); - - it("should apply extensions when specified from a JSON file", () => { - - const extendsFile = ".eslintrc.json"; - const filePath = getFixturePath("json/foo.js"); - - const config = ConfigFile.applyExtends({ - extends: extendsFile, - rules: { eqeqeq: 2 } - }, configContext, filePath); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - eqeqeq: 2, - quotes: [2, "double"] - } - }); - - }); - - it("should apply extensions when specified from a package.json file in a sibling directory", () => { - - const extendsFile = "../package-json/package.json"; - const filePath = getFixturePath("json/foo.js"); - - const config = ConfigFile.applyExtends({ - extends: extendsFile, - rules: { eqeqeq: 2 } - }, configContext, filePath); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { es6: true }, - globals: {}, - rules: { - eqeqeq: 2 - } - }); - - }); - - }); - - describe("load()", () => { - - it("should throw error if file doesnt exist", () => { - assert.throws(() => { - ConfigFile.load(getFixturePath("legacy/nofile.js"), configContext); - }); - - assert.throws(() => { - ConfigFile.load(getFixturePath("legacy/package.json"), configContext); - }); - }); - - it("should load information from a legacy file", () => { - const configFilePath = getFixturePath("legacy/.eslintrc"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - eqeqeq: 2 - } - }); - }); - - it("should load information from a JavaScript file", () => { - const configFilePath = getFixturePath("js/.eslintrc.js"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - semi: [2, "always"] - } - }); - }); - - it("should throw error when loading invalid JavaScript file", () => { - assert.throws(() => { - ConfigFile.load(getFixturePath("js/.eslintrc.broken.js"), configContext, getFixturePath("__placeholder__.js")); - }, /Cannot read config file/u); - }); - - it("should interpret parser module name when present in a JavaScript file", () => { - const configFilePath = getFixturePath("js/.eslintrc.parser.js"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parser: path.resolve(getFixturePath("js/node_modules/foo/index.js")), - parserOptions: {}, - env: {}, - globals: {}, - rules: { - semi: [2, "always"] - } - }); - }); - - it("should interpret parser path when present in a JavaScript file", () => { - const configFilePath = getFixturePath("js/.eslintrc.parser2.js"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parser: path.resolve(getFixturePath("js/not-a-config.js")), - parserOptions: {}, - env: {}, - globals: {}, - rules: { - semi: [2, "always"] - } - }); - }); - - it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => { - const configFilePath = getFixturePath("js/.eslintrc.parser3.js"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parser: require.resolve("espree"), - parserOptions: {}, - env: {}, - globals: {}, - rules: { - semi: [2, "always"] - } - }); - }); - - it("should load information from a JSON file", () => { - const configFilePath = getFixturePath("json/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: [2, "double"] - } - }); - }); - - it("should load fresh information from a JSON file", () => { - const initialConfig = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: [2, "double"] - } - }, - updatedConfig = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: 0 - } - }, - tmpFilename = "fresh-test.json", - tmpFilePath = writeTempConfigFile(initialConfig, tmpFilename); - let config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, initialConfig); - writeTempConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath)); - configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); - config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("__placeholder__.js")); - assert.deepStrictEqual(config, updatedConfig); - }); - - it("should load information from a package.json file", () => { - const configFilePath = getFixturePath("package-json/package.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { es6: true }, - globals: {}, - rules: {} - }); - }); - - it("should throw error when loading invalid package.json file", () => { - assert.throws(() => { - try { - ConfigFile.load(getFixturePath("broken-package-json/package.json"), configContext, getFixturePath("__placeholder__.js")); - } catch (error) { - assert.strictEqual(error.messageTemplate, "failed-to-read-json"); - throw error; - } - }, /Cannot read config file/u); - }); - - it("should load fresh information from a package.json file", () => { - const initialConfig = { - eslintConfig: { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: [2, "double"] - } - } - }, - updatedConfig = { - eslintConfig: { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: 0 - } - } - }, - tmpFilename = "package.json", - tmpFilePath = writeTempConfigFile(initialConfig, tmpFilename); - let config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, initialConfig.eslintConfig); - writeTempConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath)); - configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); - config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("__placeholder__.js")); - assert.deepStrictEqual(config, updatedConfig.eslintConfig); - }); - - it("should load fresh information from a .eslintrc.js file", () => { - const initialConfig = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: [2, "double"] - } - }, - updatedConfig = { - parserOptions: {}, - env: {}, - globals: {}, - rules: { - quotes: 0 - } - }, - tmpFilename = ".eslintrc.js", - tmpFilePath = writeTempJsConfigFile(initialConfig, tmpFilename); - let config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, initialConfig); - writeTempJsConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath)); - configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); - config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("__placeholder__.js")); - assert.deepStrictEqual(config, updatedConfig); - }); - - it("should load information from a YAML file", () => { - const configFilePath = getFixturePath("yaml/.eslintrc.yaml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { browser: true }, - globals: {}, - rules: {} - }); - }); - - it("should load information from an empty YAML file", () => { - const configFilePath = getFixturePath("yaml/.eslintrc.empty.yaml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: {}, - globals: {}, - rules: {} - }); - }); - - it("should load information from a YML file", () => { - const configFilePath = getFixturePath("yml/.eslintrc.yml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { node: true }, - globals: {}, - rules: {} - }); - }); - - it("should load information from a YML file and apply extensions", () => { - const configFilePath = getFixturePath("extends/.eslintrc.yml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: { es6: true }, - globals: {}, - parserOptions: {}, - rules: { booya: 2 } - }); - }); - - it("should load information from `extends` chain.", () => { - const configFilePath = getFixturePath("extends-chain/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - a: 2, // from node_modules/eslint-config-a - b: 2, // from node_modules/eslint-config-a/node_modules/eslint-config-b - c: 2 // from node_modules/eslint-config-a/node_modules/eslint-config-b/node_modules/eslint-config-c - } - }); - }); - - it("should load information from `extends` chain with relative path.", () => { - const configFilePath = getFixturePath("extends-chain-2/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - a: 2, // from node_modules/eslint-config-a/index.js - relative: 2 // from node_modules/eslint-config-a/relative.js - } - }); - }); - - it("should load information from `extends` chain in .eslintrc with relative path.", () => { - const configFilePath = getFixturePath("extends-chain-2/relative.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - a: 2, // from node_modules/eslint-config-a/index.js - relative: 2 // from node_modules/eslint-config-a/relative.js - } - }); - }); - - it("should load information from `parser` in .eslintrc with relative path.", () => { - const configFilePath = getFixturePath("extends-chain-2/parser.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - const parserPath = getFixturePath("extends-chain-2/parser.js"); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parser: parserPath, - parserOptions: {}, - rules: {} - }); - }); - - describe("even if it's in another directory,", () => { - let fixturePath = ""; - - before(() => { - const tempDir = temp.mkdirSync("eslint-test-chain"); - const chain2 = getFixturePath("extends-chain-2"); - - fixturePath = path.join(tempDir, "extends-chain-2"); - shell.cp("-r", chain2, fixturePath); - }); - - after(() => { - temp.cleanupSync(); - }); - - it("should load information from `extends` chain in .eslintrc with relative path.", () => { - const configFilePath = path.join(fixturePath, "relative.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - a: 2, // from node_modules/eslint-config-a/index.js - relative: 2 // from node_modules/eslint-config-a/relative.js - } - }); - }); - - it("should load information from `parser` in .eslintrc with relative path.", () => { - const configFilePath = path.join(fixturePath, "parser.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - const parserPath = fs.realpathSync(path.join(fixturePath, "parser.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parser: parserPath, - parserOptions: {}, - rules: {} - }); - }); - }); - - describe("Plugins", () => { - it("should load information from a YML file and load plugins", () => { - const configFilePath = getFixturePath("plugins/.eslintrc.yml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - env: { "with-environment/bar": true }, - globals: {}, - plugins: ["with-environment"], - rules: { - "with-environment/foo": 2 - } - }); - }); - - it("should load two separate configs from a plugin", () => { - const configFilePath = getFixturePath("plugins/.eslintrc2.yml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - parserOptions: {}, - globals: {}, - env: {}, - rules: { - semi: 2, - quotes: 2, - yoda: 2 - } - }); - }); - }); - - describe("even if config files have Unicode BOM,", () => { - it("should read the JSON config file correctly.", () => { - const configFilePath = getFixturePath("bom/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - semi: "error" - } - }); - }); - - it("should read the YAML config file correctly.", () => { - const configFilePath = getFixturePath("bom/.eslintrc.yaml"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - semi: "error" - } - }); - }); - - it("should read the config in package.json correctly.", () => { - const configFilePath = getFixturePath("bom/package.json"); - const config = ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - - assert.deepStrictEqual(config, { - env: {}, - globals: {}, - parserOptions: {}, - rules: { - semi: "error" - } - }); - }); - }); - - it("throws an error including the config file name if the config file is invalid", () => { - const configFilePath = getFixturePath("invalid/invalid-top-level-property.yml"); - - try { - ConfigFile.load(configFilePath, configContext, getFixturePath("__placeholder__.js")); - } catch (err) { - assert.include(err.message, `ESLint configuration in ${configFilePath} is invalid`); - return; - } - assert.fail(); - }); - }); - - describe("resolve() relative to config file", () => { - leche.withData([ - [".eslintrc", getFixturePath("subdir/.eslintrc")], - ["eslint-config-foo", relativeModuleResolver("eslint-config-foo", getFixturePath("subdir/__placeholder__.js"))], - ["foo", relativeModuleResolver("eslint-config-foo", getFixturePath("subdir/__placeholder__.js"))], - ["eslint-configfoo", relativeModuleResolver("eslint-config-eslint-configfoo", getFixturePath("subdir/__placeholder__.js"))], - ["@foo/eslint-config", relativeModuleResolver("@foo/eslint-config", getFixturePath("subdir/__placeholder__.js"))], - ["@foo/bar", relativeModuleResolver("@foo/eslint-config-bar", getFixturePath("subdir/__placeholder__.js"))], - ["plugin:foo/bar", relativeModuleResolver("eslint-plugin-foo", getFixturePath("__placeholder__.js"))] - ], (input, expected) => { - it(`should return ${expected} when passed ${input}`, () => { - const result = ConfigFile.resolve(input, getFixturePath("subdir/__placeholder__.js"), getFixturePath(".")); - - assert.strictEqual(result.filePath, expected); - }); - }); - }); - - describe("getFilenameFromDirectory()", () => { - - leche.withData([ - [getFixturePath("legacy"), ".eslintrc"], - [getFixturePath("yaml"), ".eslintrc.yaml"], - [getFixturePath("yml"), ".eslintrc.yml"], - [getFixturePath("json"), ".eslintrc.json"], - [getFixturePath("js"), ".eslintrc.js"] - ], (input, expected) => { - it(`should return ${expected} when passed ${input}`, () => { - const result = ConfigFile.getFilenameForDirectory(input); - - assert.strictEqual(result, path.resolve(input, expected)); - }); - }); - - }); - describe("write()", () => { let sandbox, @@ -887,7 +124,7 @@ describe("ConfigFile", () => { const StubbedConfigFile = proxyquire("../../../lib/config/config-file", { fs: fakeFS, - "../cli-engine": fakeCLIEngine + "../cli-engine": { CLIEngine: fakeCLIEngine } }); assert.throws(() => { @@ -901,5 +138,4 @@ describe("ConfigFile", () => { }, /write to unknown file type/u); }); }); - }); diff --git a/tests/lib/config/config-initializer.js b/tests/lib/config/config-initializer.js index d306db1b1c3..a979408a71d 100644 --- a/tests/lib/config/config-initializer.js +++ b/tests/lib/config/config-initializer.js @@ -42,11 +42,13 @@ describe("configInitializer", () => { }; const requireStubs = { "../util/logging": log, - "../util/relative-module-resolver"() { - if (localESLintVersion) { - return `local-eslint-${localESLintVersion}`; + "../util/relative-module-resolver": { + resolve() { + if (localESLintVersion) { + return `local-eslint-${localESLintVersion}`; + } + throw new Error("Cannot find module"); } - throw new Error("Cannot find module"); }, "local-eslint-3.18.0": { linter: { version: "3.18.0" }, "@noCallThru": true }, "local-eslint-3.19.0": { linter: { version: "3.19.0" }, "@noCallThru": true }, @@ -188,7 +190,7 @@ describe("configInitializer", () => { describe("guide", () => { it("should support the google style guide", () => { - const config = init.getConfigForStyleGuide("google"); + const config = { extends: "google" }; const modules = init.getModulesList(config); assert.deepStrictEqual(config, { extends: "google", installedESLint: true }); @@ -196,7 +198,7 @@ describe("configInitializer", () => { }); it("should support the airbnb style guide", () => { - const config = init.getConfigForStyleGuide("airbnb"); + const config = { extends: "airbnb" }; const modules = init.getModulesList(config); assert.deepStrictEqual(config, { extends: "airbnb", installedESLint: true }); @@ -204,7 +206,7 @@ describe("configInitializer", () => { }); it("should support the airbnb base style guide", () => { - const config = init.getConfigForStyleGuide("airbnb-base"); + const config = { extends: "airbnb-base" }; const modules = init.getModulesList(config); assert.deepStrictEqual(config, { extends: "airbnb-base", installedESLint: true }); @@ -212,21 +214,15 @@ describe("configInitializer", () => { }); it("should support the standard style guide", () => { - const config = init.getConfigForStyleGuide("standard"); + const config = { extends: "standard" }; const modules = init.getModulesList(config); assert.deepStrictEqual(config, { extends: "standard", installedESLint: true }); assert.include(modules, "eslint-config-standard@latest"); }); - it("should throw when encountering an unsupported style guide", () => { - assert.throws(() => { - init.getConfigForStyleGuide("non-standard"); - }, "You referenced an unsupported guide."); - }); - it("should install required sharable config", () => { - const config = init.getConfigForStyleGuide("google"); + const config = { extends: "google" }; init.installModules(init.getModulesList(config)); assert(npmInstallStub.calledOnce); @@ -234,7 +230,7 @@ describe("configInitializer", () => { }); it("should install ESLint if not installed locally", () => { - const config = init.getConfigForStyleGuide("google"); + const config = { extends: "google" }; init.installModules(init.getModulesList(config)); assert(npmInstallStub.calledOnce); @@ -242,7 +238,7 @@ describe("configInitializer", () => { }); it("should install peerDependencies of the sharable config", () => { - const config = init.getConfigForStyleGuide("airbnb"); + const config = { extends: "airbnb" }; init.installModules(init.getModulesList(config)); diff --git a/tests/lib/config/config-ops.js b/tests/lib/config/config-ops.js index ffa65660d58..c671eb125f4 100644 --- a/tests/lib/config/config-ops.js +++ b/tests/lib/config/config-ops.js @@ -11,546 +11,14 @@ const assert = require("chai").assert, leche = require("leche"), util = require("util"), - environments = require("../../../conf/environments"), - Environments = require("../../../lib/config/environments"), - ConfigCache = require("../../../lib/config/config-cache"), ConfigOps = require("../../../lib/config/config-ops"); -const envContext = new Environments(); - //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("ConfigOps", () => { - describe("applyEnvironments()", () => { - it("should apply environment settings to config without destroying original settings", () => { - const config = { - env: { - node: true - }, - rules: { - foo: 2 - } - }; - - const result = ConfigOps.applyEnvironments(config, envContext); - - assert.deepStrictEqual(result, { - env: config.env, - rules: config.rules, - parserOptions: { - ecmaFeatures: environments.node.parserOptions.ecmaFeatures - }, - globals: environments.node.globals - }); - }); - - it("should not apply environment settings to config without environments", () => { - const config = { - rules: { - foo: 2 - } - }; - - const result = ConfigOps.applyEnvironments(config, envContext); - - assert.strictEqual(result, config); - }); - - it("should apply multiple environment settings to config without destroying original settings", () => { - const config = { - env: { - node: true, - es6: true - }, - rules: { - foo: 2 - } - }; - - const result = ConfigOps.applyEnvironments(config, envContext); - - assert.deepStrictEqual(result, { - env: config.env, - rules: config.rules, - parserOptions: { - ecmaVersion: 6, - ecmaFeatures: environments.node.parserOptions.ecmaFeatures - }, - globals: Object.assign({}, environments.node.globals, environments.es6.globals) - }); - }); - }); - - describe("createEnvironmentConfig()", () => { - - it("should return empty config if called without any config", () => { - const config = ConfigOps.createEnvironmentConfig(null, envContext); - - assert.deepStrictEqual(config, { - globals: {}, - env: {}, - rules: {}, - parserOptions: {} - }); - }); - - it("should return correct config for env with no globals", () => { - const config = ConfigOps.createEnvironmentConfig({ test: true }, new Environments()); - - assert.deepStrictEqual(config, { - globals: {}, - env: { - test: true - }, - rules: {}, - parserOptions: {} - }); - }); - - it("should create the correct config for Node.js environment", () => { - const config = ConfigOps.createEnvironmentConfig({ node: true }, envContext); - - assert.deepStrictEqual(config, { - env: { - node: true - }, - parserOptions: { - ecmaFeatures: environments.node.parserOptions.ecmaFeatures - }, - globals: environments.node.globals, - rules: {} - }); - }); - - it("should create the correct config for ES6 environment", () => { - const config = ConfigOps.createEnvironmentConfig({ es6: true }, envContext); - - assert.deepStrictEqual(config, { - env: { - es6: true - }, - parserOptions: { - ecmaVersion: 6 - }, - globals: environments.es6.globals, - rules: {} - }); - }); - - it("should create empty config when no environments are specified", () => { - const config = ConfigOps.createEnvironmentConfig({}, envContext); - - assert.deepStrictEqual(config, { - env: {}, - parserOptions: {}, - globals: {}, - rules: {} - }); - }); - - it("should create empty config when an unknown environment is specified", () => { - const config = ConfigOps.createEnvironmentConfig({ foo: true }, envContext); - - assert.deepStrictEqual(config, { - env: { - foo: true - }, - parserOptions: {}, - globals: {}, - rules: {} - }); - }); - - }); - - describe("merge()", () => { - - it("should combine two objects when passed two objects with different top-level properties", () => { - const config = [ - { env: { browser: true } }, - { globals: { foo: "bar" } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.strictEqual(result.globals.foo, "bar"); - assert.isTrue(result.env.browser); - }); - - it("should combine without blowing up on null values", () => { - const config = [ - { env: { browser: true } }, - { env: { node: null } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.strictEqual(result.env.node, null); - assert.isTrue(result.env.browser); - }); - - it("should combine two objects with parser when passed two objects with different top-level properties", () => { - const config = [ - { env: { browser: true }, parser: "espree" }, - { globals: { foo: "bar" } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.strictEqual(result.parser, "espree"); - }); - - it("should combine configs and override rules when passed configs with the same rules", () => { - const config = [ - { rules: { "no-mixed-requires": [0, false] } }, - { rules: { "no-mixed-requires": [1, true] } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.isArray(result.rules["no-mixed-requires"]); - assert.strictEqual(result.rules["no-mixed-requires"][0], 1); - assert.strictEqual(result.rules["no-mixed-requires"][1], true); - }); - - it("should combine configs when passed configs with parserOptions", () => { - const config = [ - { parserOptions: { ecmaFeatures: { jsx: true } } }, - { parserOptions: { ecmaFeatures: { globalReturn: true } } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.deepStrictEqual(result, { - parserOptions: { - ecmaFeatures: { - jsx: true, - globalReturn: true - } - } - }); - - // double-check that originals were not changed - assert.deepStrictEqual(config[0], { parserOptions: { ecmaFeatures: { jsx: true } } }); - assert.deepStrictEqual(config[1], { parserOptions: { ecmaFeatures: { globalReturn: true } } }); - }); - - it("should override configs when passed configs with the same ecmaFeatures", () => { - const config = [ - { parserOptions: { ecmaFeatures: { globalReturn: false } } }, - { parserOptions: { ecmaFeatures: { globalReturn: true } } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.deepStrictEqual(result, { - parserOptions: { - ecmaFeatures: { - globalReturn: true - } - } - }); - }); - - it("should combine configs and override rules when merging two configs with arrays and int", () => { - - const config = [ - { rules: { "no-mixed-requires": [0, false] } }, - { rules: { "no-mixed-requires": 1 } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.isArray(result.rules["no-mixed-requires"]); - assert.strictEqual(result.rules["no-mixed-requires"][0], 1); - assert.strictEqual(result.rules["no-mixed-requires"][1], false); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [0, false] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": 1 } }); - }); - - it("should combine configs and override rules options completely", () => { - - const config = [ - { rules: { "no-mixed-requires": [1, { event: ["evt", "e"] }] } }, - { rules: { "no-mixed-requires": [1, { err: ["error", "e"] }] } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.isArray(result.rules["no-mixed-requires"]); - assert.deepStrictEqual(result.rules["no-mixed-requires"][1], { err: ["error", "e"] }); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [1, { event: ["evt", "e"] }] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": [1, { err: ["error", "e"] }] } }); - }); - - it("should combine configs and override rules options without array or object", () => { - - const config = [ - { rules: { "no-mixed-requires": ["warn", "nconf", "underscore"] } }, - { rules: { "no-mixed-requires": [2, "requirejs"] } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.strictEqual(result.rules["no-mixed-requires"][0], 2); - assert.strictEqual(result.rules["no-mixed-requires"][1], "requirejs"); - assert.isUndefined(result.rules["no-mixed-requires"][2]); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": ["warn", "nconf", "underscore"] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": [2, "requirejs"] } }); - }); - - it("should combine configs and override rules options without array or object but special case", () => { - - const config = [ - { rules: { "no-mixed-requires": [1, "nconf", "underscore"] } }, - { rules: { "no-mixed-requires": "error" } } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.strictEqual(result.rules["no-mixed-requires"][0], "error"); - assert.strictEqual(result.rules["no-mixed-requires"][1], "nconf"); - assert.strictEqual(result.rules["no-mixed-requires"][2], "underscore"); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [1, "nconf", "underscore"] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": "error" } }); - }); - - it("should combine extends correctly", () => { - - const config = [ - { extends: ["a", "b", "c", "d", "e"] }, - { extends: ["f", "g", "h", "i"] } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.sameDeepMembers(result.extends, ["a", "b", "c", "d", "e", "f", "g", "h", "i"]); - }); - - it("should combine configs correctly", () => { - - const config = [ - { - rules: { - "no-mixed-requires": [1, { event: ["evt", "e"] }], - "valid-jsdoc": 1, - semi: 1, - quotes: [2, { exception: ["hi"] }], - smile: [1, ["hi", "bye"]] - }, - parserOptions: { - ecmaFeatures: { jsx: true } - }, - env: { browser: true }, - globals: { foo: false } - }, - { - rules: { - "no-mixed-requires": [1, { err: ["error", "e"] }], - "valid-jsdoc": 2, - test: 1, - smile: [1, ["xxx", "yyy"]] - }, - parserOptions: { - ecmaFeatures: { globalReturn: true } - }, - env: { browser: false }, - globals: { foo: true } - } - ]; - - const result = ConfigOps.merge(config[0], config[1]); - - assert.deepStrictEqual(result, { - parserOptions: { - ecmaFeatures: { - jsx: true, - globalReturn: true - } - }, - env: { - browser: false - }, - globals: { - foo: true - }, - rules: { - "no-mixed-requires": [1, - { - err: [ - "error", - "e" - ] - } - ], - quotes: [2, - { - exception: [ - "hi" - ] - } - ], - semi: 1, - smile: [1, ["xxx", "yyy"]], - test: 1, - "valid-jsdoc": 2 - } - }); - assert.deepStrictEqual(config[0], { - rules: { - "no-mixed-requires": [1, { event: ["evt", "e"] }], - "valid-jsdoc": 1, - semi: 1, - quotes: [2, { exception: ["hi"] }], - smile: [1, ["hi", "bye"]] - }, - parserOptions: { - ecmaFeatures: { jsx: true } - }, - env: { browser: true }, - globals: { foo: false } - }); - assert.deepStrictEqual(config[1], { - rules: { - "no-mixed-requires": [1, { err: ["error", "e"] }], - "valid-jsdoc": 2, - test: 1, - smile: [1, ["xxx", "yyy"]] - }, - parserOptions: { - ecmaFeatures: { globalReturn: true } - }, - env: { browser: false }, - globals: { foo: true } - }); - }); - - it("should copy deeply if there is not the destination's property", () => { - const a = {}; - const b = { foo: { bar: 1 } }; - - const result = ConfigOps.merge(a, b); - - assert(a.foo === void 0); - assert(b.foo.bar === 1); - assert(result.foo.bar === 1); - - result.foo.bar = 2; - assert(b.foo.bar === 1); - assert(result.foo.bar === 2); - }); - - describe("plugins", () => { - let baseConfig; - - beforeEach(() => { - baseConfig = { plugins: ["foo", "bar"] }; - }); - - it("should combine the plugin entries when each config has different plugins", () => { - const customConfig = { plugins: ["baz"] }, - expectedResult = { plugins: ["foo", "bar", "baz"] }, - result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - assert.deepStrictEqual(baseConfig, { plugins: ["foo", "bar"] }); - assert.deepStrictEqual(customConfig, { plugins: ["baz"] }); - }); - - it("should avoid duplicate plugin entries when each config has the same plugin", () => { - const customConfig = { plugins: ["bar"] }, - expectedResult = { plugins: ["foo", "bar"] }, - result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - }); - - it("should create a valid config when one argument is an empty object", () => { - const customConfig = { plugins: ["foo"] }, - result = ConfigOps.merge({}, customConfig); - - assert.deepStrictEqual(result, customConfig); - assert.notStrictEqual(result, customConfig); - }); - }); - - describe("overrides", () => { - it("should combine the override entries in the correct order", () => { - const baseConfig = { overrides: [{ files: ["**/*Spec.js"], env: { mocha: true } }] }; - const customConfig = { overrides: [{ files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }] }; - const expectedResult = { - overrides: [ - { files: ["**/*Spec.js"], env: { mocha: true } }, - { files: ["**/*.jsx"], ecmaFeatures: { jsx: true } } - ] - }; - - const result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - }); - - it("should work if the base config doesn’t have an overrides property", () => { - const baseConfig = {}; - const customConfig = { overrides: [{ files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }] }; - const expectedResult = { - overrides: [ - { files: ["**/*.jsx"], ecmaFeatures: { jsx: true } } - ] - }; - - const result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - }); - - it("should work if the custom config doesn’t have an overrides property", () => { - const baseConfig = { overrides: [{ files: ["**/*Spec.js"], env: { mocha: true } }] }; - const customConfig = {}; - const expectedResult = { - overrides: [ - { files: ["**/*Spec.js"], env: { mocha: true } } - ] - }; - - const result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - }); - - it("should work if overrides are null in the base config", () => { - const baseConfig = { overrides: null }; - const customConfig = { overrides: [{ files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }] }; - const expectedResult = { - overrides: [ - { files: ["**/*.jsx"], ecmaFeatures: { jsx: true } } - ] - }; - - const result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - }); - - it("should work if overrides are null in the custom config", () => { - const baseConfig = { overrides: [{ files: ["**/*Spec.js"], env: { mocha: true } }] }; - const customConfig = { overrides: null }; - const expectedResult = { - overrides: [ - { files: ["**/*Spec.js"], env: { mocha: true } } - ] - }; - - const result = ConfigOps.merge(baseConfig, customConfig); - - assert.deepStrictEqual(result, expectedResult); - }); - }); - }); - describe("getRuleSeverity()", () => { const EXPECTED_RESULTS = new Map([ [0, 0], @@ -753,152 +221,6 @@ describe("ConfigOps", () => { }); - describe("getConfigFromVector()", () => { - let configCache; - - beforeEach(() => { - configCache = new ConfigCache(); - }); - - it("should get from merged vector cache when present", () => { - const vector = [ - { filePath: "configFile1", matchingOverrides: [1] }, - { filePath: "configFile2", matchingOverrides: [0, 1] } - ]; - const merged = { merged: true }; - - configCache.setMergedVectorConfig(vector, merged); - - const result = ConfigOps.getConfigFromVector(vector, configCache); - - assert.deepStrictEqual(result, merged); - }); - - it("should get from raw cached configs when no merged vectors are cached", () => { - const config = [ - { - rules: { foo1: "off" }, - overrides: [ - { files: "pattern1", rules: { foo1: "warn" } }, - { files: "pattern2", rules: { foo1: "error" } } - ] - }, - { - rules: { foo2: "warn" }, - overrides: [ - { files: "pattern1", rules: { foo2: "error" } }, - { files: "pattern2", rules: { foo2: "off" } } - ] - } - ]; - - configCache.setConfig("configFile1", config[0]); - configCache.setConfig("configFile2", config[1]); - - const vector = [ - { filePath: "configFile1", matchingOverrides: [1] }, - { filePath: "configFile2", matchingOverrides: [0, 1] } - ]; - - const result = ConfigOps.getConfigFromVector(vector, configCache); - - assert.strictEqual(result.rules.foo1, "error"); - assert.strictEqual(result.rules.foo2, "off"); - }); - }); - - describe("pathMatchesGlobs", () => { - - /** - * Emits a test that confirms the specified file path matches the specified combination of patterns. - * @param {string} filePath The file path to test patterns against - * @param {string|string[]} patterns One or more glob patterns - * @param {string|string[]} [excludedPatterns] One or more glob patterns - * @returns {void} - */ - function match(filePath, patterns, excludedPatterns) { - it(`matches ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { - const result = ConfigOps.pathMatchesGlobs(filePath, patterns, excludedPatterns); - - assert.strictEqual(result, true); - }); - } - - /** - * Emits a test that confirms the specified file path does not match the specified combination of patterns. - * @param {string} filePath The file path to test patterns against - * @param {string|string[]} patterns One or more glob patterns - * @param {string|string[]} [excludedPatterns] One or more glob patterns - * @returns {void} - */ - function noMatch(filePath, patterns, excludedPatterns) { - it(`does not match ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { - const result = ConfigOps.pathMatchesGlobs(filePath, patterns, excludedPatterns); - - assert.strictEqual(result, false); - }); - } - - /** - * Emits a test that confirms the specified pattern throws an error. - * @param {string} filePath The file path to test the pattern against - * @param {string} pattern The glob pattern that should trigger the error condition - * @param {string} expectedMessage The expected error's message - * @returns {void} - */ - function error(filePath, pattern, expectedMessage) { - it(`emits an error given '${pattern}'`, () => { - let errorMessage; - - try { - ConfigOps.pathMatchesGlobs(filePath, pattern); - } catch (e) { - errorMessage = e.message; - } - - assert.strictEqual(errorMessage, expectedMessage); - }); - } - - // files in the project root - match("foo.js", ["foo.js"], []); - match("foo.js", ["*"], []); - match("foo.js", ["*.js"], []); - match("foo.js", ["**/*.js"], []); - match("bar.js", ["*.js"], ["foo.js"]); - - noMatch("foo.js", ["./foo.js"], []); - noMatch("foo.js", ["./*"], []); - noMatch("foo.js", ["./**"], []); - noMatch("foo.js", ["*"], ["foo.js"]); - noMatch("foo.js", ["*.js"], ["foo.js"]); - noMatch("foo.js", ["**/*.js"], ["foo.js"]); - - // files in a subdirectory - match("subdir/foo.js", ["foo.js"], []); - match("subdir/foo.js", ["*"], []); - match("subdir/foo.js", ["*.js"], []); - match("subdir/foo.js", ["**/*.js"], []); - match("subdir/foo.js", ["subdir/*.js"], []); - match("subdir/foo.js", ["subdir/foo.js"], []); - match("subdir/foo.js", ["subdir/*"], []); - match("subdir/second/foo.js", ["subdir/**"], []); - - noMatch("subdir/foo.js", ["./foo.js"], []); - noMatch("subdir/foo.js", ["./**"], []); - noMatch("subdir/foo.js", ["./subdir/**"], []); - noMatch("subdir/foo.js", ["./subdir/*"], []); - noMatch("subdir/foo.js", ["*"], ["subdir/**"]); - noMatch("subdir/very/deep/foo.js", ["*.js"], ["subdir/**"]); - noMatch("subdir/second/foo.js", ["subdir/*"], []); - noMatch("subdir/second/foo.js", ["subdir/**"], ["subdir/second/*"]); - - // error conditions - error("foo.js", ["/*.js"], "Invalid override pattern (expected relative path not containing '..'): /*.js"); - error("foo.js", ["/foo.js"], "Invalid override pattern (expected relative path not containing '..'): /foo.js"); - error("foo.js", ["../**"], "Invalid override pattern (expected relative path not containing '..'): ../**"); - }); - describe("normalizeConfigGlobal", () => { [ ["off", "off"], diff --git a/tests/lib/config/config-rule.js b/tests/lib/config/config-rule.js index 810f8aaccd2..f3cb29a4120 100644 --- a/tests/lib/config/config-rule.js +++ b/tests/lib/config/config-rule.js @@ -294,8 +294,8 @@ describe("ConfigRule", () => { const rulesConfig = ConfigRule.createCoreRuleConfigs(); it("should create a rulesConfig containing all core rules", () => { - const coreRules = builtInRules, - expectedRules = Object.keys(coreRules), + const + expectedRules = Array.from(builtInRules.keys()), actualRules = Object.keys(rulesConfig); assert.sameMembers(actualRules, expectedRules); diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js index da4d79f320a..af66f6df9d5 100644 --- a/tests/lib/config/config-validator.js +++ b/tests/lib/config/config-validator.js @@ -10,7 +10,7 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - Linter = require("../../../lib/linter"), + { Linter } = require("../../../lib/linter"), validator = require("../../../lib/config/config-validator"), Rules = require("../../../lib/rules"); const linter = new Linter(); @@ -108,7 +108,7 @@ describe("Validator", () => { describe("validate", () => { it("should do nothing with an empty config", () => { - validator.validate({}, ruleMapper, linter.environments, "tests"); + validator.validate({}, "tests", ruleMapper); }); it("should do nothing with a valid eslint config", () => { @@ -124,9 +124,8 @@ describe("Validator", () => { parserOptions: { foo: "bar" }, rules: {} }, - ruleMapper, - linter.environments, - "tests" + "tests", + ruleMapper ); }); @@ -136,9 +135,8 @@ describe("Validator", () => { { foo: true }, - ruleMapper, - linter.environments, - "tests" + "tests", + ruleMapper ); assert.throws(fn, "Unexpected top-level property \"foo\"."); @@ -146,13 +144,13 @@ describe("Validator", () => { describe("root", () => { it("should throw with a string value", () => { - const fn = validator.validate.bind(null, { root: "true" }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { root: "true" }, null, ruleMapper); assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `\"true\"`)."); }); it("should throw with a numeric value", () => { - const fn = validator.validate.bind(null, { root: 0 }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { root: 0 }, null, ruleMapper); assert.throws(fn, "Property \"root\" is the wrong type (expected boolean but got `0`)."); }); @@ -160,13 +158,13 @@ describe("Validator", () => { describe("globals", () => { it("should throw with a string value", () => { - const fn = validator.validate.bind(null, { globals: "jQuery" }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { globals: "jQuery" }, null, ruleMapper); assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `\"jQuery\"`)."); }); it("should throw with an array value", () => { - const fn = validator.validate.bind(null, { globals: ["jQuery"] }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { globals: ["jQuery"] }, null, ruleMapper); assert.throws(fn, "Property \"globals\" is the wrong type (expected object but got `[\"jQuery\"]`)."); }); @@ -174,49 +172,49 @@ describe("Validator", () => { describe("parser", () => { it("should not throw with a null value", () => { - validator.validate({ parser: null }, ruleMapper, linter.environments, null); + validator.validate({ parser: null }, null, ruleMapper); }); }); describe("env", () => { it("should throw with an array environment", () => { - const fn = validator.validate.bind(null, { env: [] }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { env: [] }, null, ruleMapper); assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `[]`)."); }); it("should throw with a primitive environment", () => { - const fn = validator.validate.bind(null, { env: 1 }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { env: 1 }, null, ruleMapper); assert.throws(fn, "Property \"env\" is the wrong type (expected object but got `1`)."); }); it("should catch invalid environments", () => { - const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { env: { browser: true, invalid: true } }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); it("should catch disabled invalid environments", () => { - const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { env: { browser: true, invalid: false } }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); it("should do nothing with an undefined environment", () => { - validator.validate({}, null, ruleMapper, linter.environments); + validator.validate({}, null, ruleMapper); }); }); describe("plugins", () => { it("should not throw with an empty array", () => { - validator.validate({ plugins: [] }, ruleMapper, linter.environments, null); + validator.validate({ plugins: [] }, null, ruleMapper); }); it("should throw with a string", () => { - const fn = validator.validate.bind(null, { plugins: "react" }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { plugins: "react" }, null, ruleMapper); assert.throws(fn, "Property \"plugins\" is the wrong type (expected array but got `\"react\"`)."); }); @@ -224,11 +222,11 @@ describe("Validator", () => { describe("settings", () => { it("should not throw with an empty object", () => { - validator.validate({ settings: {} }, ruleMapper, linter.environments, null); + validator.validate({ settings: {} }, null, ruleMapper); }); it("should throw with an array", () => { - const fn = validator.validate.bind(null, { settings: ["foo"] }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { settings: ["foo"] }, null, ruleMapper); assert.throws(fn, "Property \"settings\" is the wrong type (expected object but got `[\"foo\"]`)."); }); @@ -236,15 +234,15 @@ describe("Validator", () => { describe("extends", () => { it("should not throw with an empty array", () => { - validator.validate({ extends: [] }, ruleMapper, linter.environments, null); + validator.validate({ extends: [] }, null, ruleMapper); }); it("should not throw with a string", () => { - validator.validate({ extends: "react" }, ruleMapper, linter.environments, null); + validator.validate({ extends: "react" }, null, ruleMapper); }); it("should throw with an object", () => { - const fn = validator.validate.bind(null, { extends: {} }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { extends: {} }, null, ruleMapper); assert.throws(fn, "Property \"extends\" is the wrong type (expected string/array but got `{}`)."); }); @@ -252,11 +250,11 @@ describe("Validator", () => { describe("parserOptions", () => { it("should not throw with an empty object", () => { - validator.validate({ parserOptions: {} }, ruleMapper, linter.environments, null); + validator.validate({ parserOptions: {} }, null, ruleMapper); }); it("should throw with an array", () => { - const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { parserOptions: ["foo"] }, null, ruleMapper); assert.throws(fn, "Property \"parserOptions\" is the wrong type (expected object but got `[\"foo\"]`)."); }); @@ -265,47 +263,47 @@ describe("Validator", () => { describe("rules", () => { it("should do nothing with an empty rules object", () => { - validator.validate({ rules: {} }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: {} }, "tests", ruleMapper); }); it("should do nothing with a valid config with rules", () => { - validator.validate({ rules: { "mock-rule": [2, "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": [2, "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is off", () => { - validator.validate({ rules: { "mock-rule": ["off", "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": ["off", "second"] } }, "tests", ruleMapper); }); it("should do nothing with an invalid config when severity is off", () => { - validator.validate({ rules: { "mock-required-options-rule": "off" } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-required-options-rule": "off" } }, "tests", ruleMapper); }); it("should do nothing with an invalid config when severity is an array with 'off'", () => { - validator.validate({ rules: { "mock-required-options-rule": ["off"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-required-options-rule": ["off"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is warn", () => { - validator.validate({ rules: { "mock-rule": ["warn", "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": ["warn", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is error", () => { - validator.validate({ rules: { "mock-rule": ["error", "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": ["error", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is Off", () => { - validator.validate({ rules: { "mock-rule": ["Off", "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": ["Off", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is Warn", () => { - validator.validate({ rules: { "mock-rule": ["Warn", "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": ["Warn", "second"] } }, "tests", ruleMapper); }); it("should do nothing with a valid config when severity is Error", () => { - validator.validate({ rules: { "mock-rule": ["Error", "second"] } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-rule": ["Error", "second"] } }, "tests", ruleMapper); }); it("should catch invalid rule options", () => { - const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { rules: { "mock-rule": [3, "third"] } }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); @@ -313,13 +311,13 @@ describe("Validator", () => { it("should allow for rules with no options", () => { linter.defineRule("mock-no-options-rule", mockNoOptionsRule); - validator.validate({ rules: { "mock-no-options-rule": 2 } }, ruleMapper, linter.environments, "tests"); + validator.validate({ rules: { "mock-no-options-rule": 2 } }, "tests", ruleMapper); }); it("should not allow options for rules with no options", () => { linter.defineRule("mock-no-options-rule", mockNoOptionsRule); - const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { rules: { "mock-no-options-rule": [2, "extra"] } }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); }); @@ -328,7 +326,7 @@ describe("Validator", () => { describe("globals", () => { it("should disallow globals set to invalid values", () => { assert.throws( - () => validator.validate({ globals: { foo: "AAAAA" } }, () => {}, linter.environments, "tests"), + () => validator.validate({ globals: { foo: "AAAAA" } }, "tests", ruleMapper), "ESLint configuration of global 'foo' in tests is invalid:\n'AAAAA' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')" ); }); @@ -336,39 +334,39 @@ describe("Validator", () => { describe("overrides", () => { it("should not throw with an empty overrides array", () => { - validator.validate({ overrides: [] }, ruleMapper, linter.environments, "tests"); + validator.validate({ overrides: [] }, "tests", ruleMapper); }); it("should not throw with a valid overrides array", () => { - validator.validate({ overrides: [{ files: "*", rules: {} }] }, ruleMapper, linter.environments, "tests"); + validator.validate({ overrides: [{ files: "*", rules: {} }] }, "tests", ruleMapper); }); it("should throw if override does not specify files", () => { - const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- \"overrides[0]\" should have required property 'files'. Value: {\"rules\":{}}.\n"); }); it("should throw if override has an empty files array", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Property \"overrides[0].files\" is the wrong type (expected string but got `[]`).\n\t- \"overrides[0].files\" should NOT have fewer than 1 items. Value: [].\n\t- \"overrides[0].files\" should match exactly one schema in oneOf. Value: [].\n"); }); it("should throw if override has nested overrides", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].overrides\".\n"); }); it("should throw if override extends", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].extends\".\n"); }); it("should throw if override tries to set root", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", ruleMapper); assert.throws(fn, "ESLint configuration in tests is invalid:\n\t- Unexpected top-level property \"overrides[0].root\".\n"); }); @@ -376,13 +374,13 @@ describe("Validator", () => { describe("env", () => { it("should catch invalid environments", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: true } }] }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: true } }] }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); it("should catch disabled invalid environments", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: false } }] }, ruleMapper, linter.environments, null); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", env: { browser: true, invalid: false } }] }, null, ruleMapper); assert.throws(fn, "Environment key \"invalid\" is unknown\n"); }); @@ -392,7 +390,7 @@ describe("Validator", () => { describe("rules", () => { it("should catch invalid rule options", () => { - const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-rule": [3, "third"] } }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-rule": [3, "third"] } }] }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); }); @@ -400,7 +398,7 @@ describe("Validator", () => { it("should not allow options for rules with no options", () => { linter.defineRule("mock-no-options-rule", mockNoOptionsRule); - const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-no-options-rule": [2, "extra"] } }] }, ruleMapper, linter.environments, "tests"); + const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: { "mock-no-options-rule": [2, "extra"] } }] }, "tests", ruleMapper); assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-no-options-rule\" is invalid:\n\tValue [\"extra\"] should NOT have more than 0 items.\n"); }); diff --git a/tests/lib/config/environments.js b/tests/lib/config/environments.js deleted file mode 100644 index c37745f5c16..00000000000 --- a/tests/lib/config/environments.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @fileoverview Tests for Environments - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - envs = require("../../../conf/environments"), - Environments = require("../../../lib/config/environments"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("Environments", () => { - let environments = null; - - beforeEach(() => { - environments = new Environments(); - }); - - describe("load()", () => { - - it("should have all default environments loaded", () => { - Object.keys(envs).forEach(envName => { - assert.deepStrictEqual(environments.get(envName), envs[envName]); - }); - }); - }); - - describe("define()", () => { - it("should add an environment with the given name", () => { - const env = { globals: { foo: true } }; - - environments.define("foo", env); - - const result = environments.get("foo"); - - assert.deepStrictEqual(result, env); - }); - }); - - describe("importPlugin()", () => { - it("should import all environments from a plugin object", () => { - const plugin = { - environments: { - foo: { - globals: { foo: true } - }, - bar: { - globals: { bar: true } - } - } - }; - - environments.importPlugin(plugin, "plugin"); - - const fooEnv = environments.get("plugin/foo"), - barEnv = environments.get("plugin/bar"); - - assert.deepStrictEqual(fooEnv, plugin.environments.foo); - assert.deepStrictEqual(barEnv, plugin.environments.bar); - }); - }); -}); diff --git a/tests/lib/config/plugins.js b/tests/lib/config/plugins.js deleted file mode 100644 index ecaaccc9ed3..00000000000 --- a/tests/lib/config/plugins.js +++ /dev/null @@ -1,238 +0,0 @@ -/** - * @fileoverview Tests for Plugins - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - path = require("path"), - Plugins = require("../../../lib/config/plugins"), - Environments = require("../../../lib/config/environments"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("Plugins", () => { - - describe("get", () => { - - it("should return null when plugin doesn't exist", () => { - assert.isNull((new Plugins(new Environments(), { defineRule: () => {}, pluginRootPath: process.cwd() })).get("foo")); - }); - }); - - describe("load()", () => { - - let RelativeLoadedPlugins, - rules, - environments, - plugin, - scopedPlugin; - - beforeEach(() => { - plugin = require("../../fixtures/plugins/node_modules/eslint-plugin-example"); - scopedPlugin = require("../../fixtures/plugins/node_modules/@scope/eslint-plugin-example"); - environments = new Environments(); - rules = new Map(); - RelativeLoadedPlugins = new Plugins(environments, { - defineRule: rules.set.bind(rules), - pluginRootPath: path.resolve(__dirname, "..", "..", "fixtures", "plugins") - }); - }); - - it("should load a plugin when referenced by short name", () => { - RelativeLoadedPlugins.load("example"); - assert.strictEqual(RelativeLoadedPlugins.get("example"), plugin); - }); - - it("should load a plugin when referenced by long name", () => { - RelativeLoadedPlugins.load("eslint-plugin-example"); - assert.strictEqual(RelativeLoadedPlugins.get("example"), plugin); - }); - - it("should register environments when plugin has environments", () => { - plugin.environments = { - foo: { - globals: { foo: true } - }, - bar: { - globals: { bar: false } - } - }; - - RelativeLoadedPlugins.load("eslint-plugin-example"); - - assert.deepStrictEqual(environments.get("example/foo"), plugin.environments.foo); - assert.deepStrictEqual(environments.get("example/bar"), plugin.environments.bar); - }); - - it("should register rules when plugin has rules", () => { - plugin.rules = { - baz: {}, - qux: {} - }; - - RelativeLoadedPlugins.load("eslint-plugin-example"); - - assert.deepStrictEqual(rules.get("example/baz"), plugin.rules.baz); - assert.deepStrictEqual(rules.get("example/qux"), plugin.rules.qux); - }); - - it("should throw an error when a plugin has whitespace", () => { - assert.throws(() => { - RelativeLoadedPlugins.load("whitespace "); - }, /Whitespace found in plugin name 'whitespace '/u); - assert.throws(() => { - RelativeLoadedPlugins.load("whitespace\t"); - }, /Whitespace found in plugin name/u); - assert.throws(() => { - RelativeLoadedPlugins.load("whitespace\n"); - }, /Whitespace found in plugin name/u); - assert.throws(() => { - RelativeLoadedPlugins.load("whitespace\r"); - }, /Whitespace found in plugin name/u); - }); - - it("should throw an error when a plugin doesn't exist", () => { - assert.throws(() => { - RelativeLoadedPlugins.load("nonexistentplugin"); - }, /Failed to load plugin/u); - }); - - it("should rethrow an error that a plugin throws on load", () => { - try { - RelativeLoadedPlugins.load("throws-on-load"); - } catch (err) { - assert.strictEqual( - err.message, - "error thrown while loading this module", - "should rethrow the same error that was thrown on plugin load" - ); - - return; - } - assert.fail(null, null, "should throw an error if a plugin fails to load"); - }); - - it("should load a scoped plugin when referenced by short name", () => { - RelativeLoadedPlugins.load("@scope/example"); - assert.strictEqual(RelativeLoadedPlugins.get("@scope/example"), scopedPlugin); - }); - - it("should load a scoped plugin when referenced by long name", () => { - RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); - assert.strictEqual(RelativeLoadedPlugins.get("@scope/example"), scopedPlugin); - }); - - it("should register environments when scoped plugin has environments", () => { - scopedPlugin.environments = { - foo: {} - }; - RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); - - assert.strictEqual(environments.get("@scope/example/foo"), scopedPlugin.environments.foo); - }); - - it("should register rules when scoped plugin has rules", () => { - scopedPlugin.rules = { - foo: {} - }; - RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); - - assert.strictEqual(rules.get("@scope/example/foo"), scopedPlugin.rules.foo); - }); - - describe("when referencing a scope plugin and omitting @scope/", () => { - it("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", () => { - RelativeLoadedPlugins.load("@scope/example"); - assert.strictEqual(RelativeLoadedPlugins.get("example"), null); - }); - - it("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { - RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); - assert.strictEqual(RelativeLoadedPlugins.get("example"), null); - }); - - it("should register environments when scoped plugin has environments, but should not get the environment if '@scope/' is omitted", () => { - scopedPlugin.environments = { - foo: {} - }; - RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); - - assert.strictEqual(environments.get("example/foo"), null); - }); - - it("should register rules when scoped plugin has rules, but should not get the rule if '@scope/' is omitted", () => { - scopedPlugin.rules = { - foo: {} - }; - RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); - - assert.isFalse(rules.has("example/foo")); - }); - }); - }); - - describe("loadAll()", () => { - - let RelativeLoadedPlugins, - plugin1, - plugin2, - rules; - const environments = new Environments(); - - beforeEach(() => { - plugin1 = require("../../fixtures/plugins/node_modules/eslint-plugin-example1"); - plugin2 = require("../../fixtures/plugins/node_modules/eslint-plugin-example2"); - rules = new Map(); - RelativeLoadedPlugins = new Plugins(environments, { - defineRule: rules.set.bind(rules), - pluginRootPath: path.resolve(__dirname, "..", "..", "fixtures", "plugins") - }); - }); - - it("should load plugins when passed multiple plugins", () => { - RelativeLoadedPlugins.loadAll(["example1", "example2"]); - assert.strictEqual(RelativeLoadedPlugins.get("example1"), plugin1); - assert.strictEqual(RelativeLoadedPlugins.get("example2"), plugin2); - }); - - it("should load environments from plugins when passed multiple plugins", () => { - plugin1.environments = { - foo: {} - }; - - plugin2.environments = { - bar: {} - }; - - RelativeLoadedPlugins.loadAll(["example1", "example2"]); - assert.strictEqual(environments.get("example1/foo"), plugin1.environments.foo); - assert.strictEqual(environments.get("example2/bar"), plugin2.environments.bar); - }); - - it("should load rules from plugins when passed multiple plugins", () => { - plugin1.rules = { - foo: {} - }; - - plugin2.rules = { - bar: {} - }; - - RelativeLoadedPlugins.loadAll(["example1", "example2"]); - assert.strictEqual(rules.get("example1/foo"), plugin1.rules.foo); - assert.strictEqual(rules.get("example2/bar"), plugin2.rules.bar); - }); - - it("should throw an error if plugins is not an array", () => { - assert.throws(() => RelativeLoadedPlugins.loadAll("example1"), "\"plugins\" value must be an array"); - }); - - }); -}); diff --git a/tests/lib/linter.js b/tests/lib/linter.js index bf8812c7bea..c877a36064a 100644 --- a/tests/lib/linter.js +++ b/tests/lib/linter.js @@ -35,7 +35,7 @@ const assert = require("chai").assert, esprima = require("esprima"), testParsers = require("../fixtures/parsers/linter-test-parsers"); -const Linter = compatRequire("../../lib/linter", "eslint"); +const { Linter } = compatRequire("../../lib/linter", "eslint"); //------------------------------------------------------------------------------ // Constants @@ -4188,19 +4188,6 @@ describe("Linter", () => { assert.isFalse(linter2.getRules().has("mock-rule"), "mock rule is not present"); }); }); - - describe("environments", () => { - it("with no changes same env are loaded", () => { - assert.sameDeepMembers([linter1.environments.getAll()], [linter2.environments.getAll()]); - }); - - it("defining env in one doesnt change the other", () => { - linter1.environments.define("mock-env", true); - - assert.isTrue(linter1.environments.get("mock-env"), "mock env is present"); - assert.isNull(linter2.environments.get("mock-env"), "mock env is not present"); - }); - }); }); describe("processors", () => { diff --git a/tests/lib/load-rules.js b/tests/lib/load-rules.js index 9d01e6b0285..09006b4aaf2 100644 --- a/tests/lib/load-rules.js +++ b/tests/lib/load-rules.js @@ -20,6 +20,6 @@ describe("when given a valid rules directory", () => { it("should load rules and not throw an error", () => { const rules = loadRules("tests/fixtures/rules", process.cwd()); - assert.strictEqual(rules["fixture-rule"], require.resolve("../../tests/fixtures/rules/fixture-rule")); + assert.strictEqual(rules["fixture-rule"], require(require.resolve("../fixtures/rules/fixture-rule"))); }); }); diff --git a/tests/lib/rules.js b/tests/lib/rules.js index 25abfd58224..8d33182df4d 100644 --- a/tests/lib/rules.js +++ b/tests/lib/rules.js @@ -11,7 +11,7 @@ const assert = require("chai").assert, Rules = require("../../lib/rules"), - Linter = require("../../lib/linter"); + { Linter } = require("../../lib/linter"); //------------------------------------------------------------------------------ // Tests @@ -90,11 +90,10 @@ describe("rules", () => { }); describe("when loading all rules", () => { - it("should return a map", () => { - const allRules = rules.getAllLoadedRules(); + it("should iterate all rules", () => { + const allRules = new Map(rules); assert.isAbove(allRules.size, 230); - assert.instanceOf(allRules, Map); assert.isObject(allRules.get("no-alert")); }); }); diff --git a/tests/lib/util/ast-utils.js b/tests/lib/util/ast-utils.js index b86fbbaca46..c879dfb053f 100644 --- a/tests/lib/util/ast-utils.js +++ b/tests/lib/util/ast-utils.js @@ -12,7 +12,7 @@ const assert = require("chai").assert, espree = require("espree"), astUtils = require("../../../lib/util/ast-utils"), - Linter = require("../../../lib/linter"), + { Linter } = require("../../../lib/linter"), SourceCode = require("../../../lib/util/source-code"); //------------------------------------------------------------------------------ diff --git a/tests/lib/util/file-finder.js b/tests/lib/util/file-finder.js deleted file mode 100644 index e2a9e147f3d..00000000000 --- a/tests/lib/util/file-finder.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * @fileoverview Tests for FileFinder class. - * @author Michael Mclaughlin - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - path = require("path"), - FileFinder = require("../../../lib/util/file-finder.js"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("FileFinder", () => { - const fixtureDir = path.resolve(__dirname, "../..", "fixtures"), - fileFinderDir = path.join(fixtureDir, "file-finder"), - subdir = path.join(fileFinderDir, "subdir"), - subsubdir = path.join(subdir, "subsubdir"), - subsubsubdir = path.join(subsubdir, "subsubsubdir"), - absentFileName = "4ktrgrtUTYjkopoohFe54676hnjyumlimn6r787", - uniqueFileName = "xvgRHtyH56756764535jkJ6jthty65tyhteHTEY"; - - describe("findAllInDirectoryAndParents()", () => { - let actual, - expected, - finder; - - describe("a file present in the cwd", () => { - - it("should be found, and returned as the first element of an array", () => { - finder = new FileFinder(uniqueFileName, process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents(fileFinderDir)); - expected = path.join(fileFinderDir, uniqueFileName); - - assert.isArray(actual); - assert.strictEqual(actual[0], expected); - }); - }); - - describe("a file present in a parent directory", () => { - - it("should be found, and returned as the first element of an array", () => { - finder = new FileFinder(uniqueFileName, process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents(subsubsubdir)); - expected = path.join(fileFinderDir, "subdir", uniqueFileName); - - assert.isArray(actual); - assert.strictEqual(actual[0], expected); - }); - }); - - describe("a relative file present in a parent directory", () => { - - it("should be found, and returned as the first element of an array", () => { - finder = new FileFinder(uniqueFileName, subsubdir); - actual = Array.from(finder.findAllInDirectoryAndParents("./subsubsubdir")); - expected = path.join(fileFinderDir, "subdir", uniqueFileName); - - assert.isArray(actual); - assert.strictEqual(actual[0], expected); - }); - }); - - describe("searching for multiple files", () => { - - it("should return only the first specified file", () => { - const firstExpected = path.join(fileFinderDir, "subdir", "empty"), - secondExpected = path.join(fileFinderDir, "empty"); - - finder = new FileFinder(["empty", uniqueFileName], process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents(subdir)); - - assert.strictEqual(actual.length, 2); - assert.strictEqual(actual[0], firstExpected); - assert.strictEqual(actual[1], secondExpected); - }); - - it("should return the second file when the first is missing", () => { - const firstExpected = path.join(fileFinderDir, "subdir", uniqueFileName), - secondExpected = path.join(fileFinderDir, uniqueFileName); - - finder = new FileFinder(["notreal", uniqueFileName], process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents(subdir)); - - assert.strictEqual(actual.length, 2); - assert.strictEqual(actual[0], firstExpected); - assert.strictEqual(actual[1], secondExpected); - }); - - it("should return multiple files when the first is missing and more than one filename is requested", () => { - const firstExpected = path.join(fileFinderDir, "subdir", uniqueFileName), - secondExpected = path.join(fileFinderDir, uniqueFileName); - - finder = new FileFinder(["notreal", uniqueFileName, "empty2"], process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents(subdir)); - - assert.strictEqual(actual.length, 2); - assert.strictEqual(actual[0], firstExpected); - assert.strictEqual(actual[1], secondExpected); - }); - - }); - - describe("two files present with the same name in parent directories", () => { - const firstExpected = path.join(fileFinderDir, "subdir", uniqueFileName), - secondExpected = path.join(fileFinderDir, uniqueFileName); - - before(() => { - finder = new FileFinder(uniqueFileName, process.cwd()); - }); - - it("should both be found, and returned in an array", () => { - actual = Array.from(finder.findAllInDirectoryAndParents(subsubsubdir)); - - assert.isArray(actual); - assert.strictEqual(actual[0], firstExpected); - assert.strictEqual(actual[1], secondExpected); - }); - - it("should be in the cache after they have been found", () => { - - assert.strictEqual(finder.cache[subsubsubdir][0], firstExpected); - assert.strictEqual(finder.cache[subsubsubdir][1], secondExpected); - assert.strictEqual(finder.cache[subsubdir][0], firstExpected); - assert.strictEqual(finder.cache[subsubdir][1], secondExpected); - assert.strictEqual(finder.cache[subdir][0], firstExpected); - assert.strictEqual(finder.cache[subdir][1], secondExpected); - assert.strictEqual(finder.cache[fileFinderDir][0], secondExpected); - assert.strictEqual(finder.cache[fileFinderDir][1], void 0); - }); - }); - - describe("an absent file", () => { - - it("should not be found, and an empty array returned", () => { - finder = new FileFinder(absentFileName, process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents()); - - assert.isArray(actual); - assert.lengthOf(actual, 0); - }); - }); - - /** - * The intention of this test case is not clear to me. It seems - * to be a special case of "a file present in a parent directory" above. - * Apart from that: Searching for package.json up to the root - * is kind of non-deterministic for testing purposes. A unique file name - * and/or restricting the search up to the workspace root (not /) would - * be better. The original code assumed there will never be a package.json - * outside of the eslint workspace, but that cannot be guaranteed. - */ - describe("Not consider directory with expected file names", () => { - it("should only find one package.json from the root", () => { - expected = path.join(process.cwd(), "package.json"); - finder = new FileFinder("package.json", process.cwd()); - actual = Array.from(finder.findAllInDirectoryAndParents(fileFinderDir)); - - /** - * Filter files outside of current workspace, otherwise test fails, - * if there is for example a ~/package.json file. - * In order to eliminate side effects of files located outside of - * workspace this should be done for all test cases here. - */ - actual = actual.filter(file => (file || "").indexOf(process.cwd()) === 0); - - assert.deepStrictEqual(actual, [expected]); - }); - }); - }); -}); diff --git a/tests/lib/util/glob-utils.js b/tests/lib/util/glob-utils.js deleted file mode 100644 index 9bafcfa0779..00000000000 --- a/tests/lib/util/glob-utils.js +++ /dev/null @@ -1,414 +0,0 @@ -/** - * @fileoverview Utilities for working with globs and the filesystem. - * @author Ian VanSchooten - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - path = require("path"), - os = require("os"), - sh = require("shelljs"), - globUtils = require("../../../lib/util/glob-utils"), - fs = require("fs"); - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -let fixtureDir; - -/** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ -function getFixturePath(...args) { - return path.join(fs.realpathSync(fixtureDir), ...args); -} - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("globUtils", () => { - - before(() => { - fixtureDir = `${os.tmpdir()}/eslint/tests/fixtures/`; - sh.mkdir("-p", fixtureDir); - sh.cp("-r", "./tests/fixtures/*", fixtureDir); - }); - - after(() => { - sh.rm("-r", fixtureDir); - }); - - describe("resolveFileGlobPatterns()", () => { - - it("should convert a directory name with no provided extensions into a glob pattern", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); - }); - - it("should not convert path with globInputPaths option false", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util"), - globInputPaths: false - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file"]); - }); - - it("should convert an absolute directory name with no provided extensions into a posix glob pattern", () => { - const patterns = [getFixturePath("glob-util", "one-js-file")]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - const expected = [`${getFixturePath("glob-util", "one-js-file").replace(/\\/gu, "/")}/**/*.js`]; - - assert.deepStrictEqual(result, expected); - }); - - it("should convert a directory name with a single provided extension into a glob pattern", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util"), - extensions: [".jsx"] - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/**/*.jsx"]); - }); - - it("should convert a directory name with multiple provided extensions into a glob pattern", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util"), - extensions: [".jsx", ".js"] - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/**/*.{jsx,js}"]); - }); - - it("should convert multiple directory names into glob patterns", () => { - const patterns = ["one-js-file", "two-js-files"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/**/*.js", "two-js-files/**/*.js"]); - }); - - it("should remove leading './' from glob patterns", () => { - const patterns = ["./one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); - }); - - it("should convert a directory name with a trailing '/' into a glob pattern", () => { - const patterns = ["one-js-file/"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); - }); - - it("should return filenames as they are", () => { - const patterns = ["some-file.js"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["some-file.js"]); - }); - - it("should convert backslashes into forward slashes", () => { - const patterns = ["one-js-file\\example.js"]; - const opts = { - cwd: getFixturePath() - }; - const result = globUtils.resolveFileGlobPatterns(patterns, opts); - - assert.deepStrictEqual(result, ["one-js-file/example.js"]); - }); - }); - - describe("listFilesToProcess()", () => { - - it("should return an array with a resolved (absolute) filename", () => { - const patterns = [getFixturePath("glob-util", "one-js-file", "**/*.js")]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - - const file1 = getFixturePath("glob-util", "one-js-file", "baz.js"); - - assert.isArray(result); - assert.deepStrictEqual(result, [{ filename: file1, ignored: false }]); - }); - - it("should return an array with a unmodified filename", () => { - const patterns = [getFixturePath("glob-util", "one-js-file", "**/*.js")]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath(), - globInputPaths: false - }); - - const file1 = getFixturePath("glob-util", "one-js-file", "**/*.js"); - - assert.isArray(result); - assert.deepStrictEqual(result, [{ filename: file1, ignored: false }]); - }); - - it("should return all files matching a glob pattern", () => { - const patterns = [getFixturePath("glob-util", "two-js-files", "**/*.js")]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - - const file1 = getFixturePath("glob-util", "two-js-files", "bar.js"); - const file2 = getFixturePath("glob-util", "two-js-files", "foo.js"); - - assert.strictEqual(result.length, 2); - assert.deepStrictEqual(result, [ - { filename: file1, ignored: false }, - { filename: file2, ignored: false } - ]); - }); - - it("should return all files matching multiple glob patterns", () => { - const patterns = [ - getFixturePath("glob-util", "two-js-files", "**/*.js"), - getFixturePath("glob-util", "one-js-file", "**/*.js") - ]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - - const file1 = getFixturePath("glob-util", "two-js-files", "bar.js"); - const file2 = getFixturePath("glob-util", "two-js-files", "foo.js"); - const file3 = getFixturePath("glob-util", "one-js-file", "baz.js"); - - assert.strictEqual(result.length, 3); - assert.deepStrictEqual(result, [ - { filename: file1, ignored: false }, - { filename: file2, ignored: false }, - { filename: file3, ignored: false } - ]); - }); - - it("should ignore hidden files for standard glob patterns", () => { - const patterns = [getFixturePath("glob-util", "hidden", "**/*.js")]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - }, `All files matched by '${patterns[0]}' are ignored.`); - }); - - it("should return hidden files if included in glob pattern", () => { - const patterns = [getFixturePath("glob-util", "hidden", "**/.*.js")]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - - const file1 = getFixturePath("glob-util", "hidden", ".foo.js"); - - assert.strictEqual(result.length, 1); - assert.deepStrictEqual(result, [ - { filename: file1, ignored: false } - ]); - }); - - it("should ignore default ignored files if not passed explicitly", () => { - const directory = getFixturePath("glob-util", "hidden"); - const patterns = [directory]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - }, `All files matched by '${directory}' are ignored.`); - }); - - it("should ignore and warn for default ignored files when passed explicitly", () => { - const filename = getFixturePath("glob-util", "hidden", ".foo.js"); - const patterns = [filename]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - - assert.strictEqual(result.length, 1); - assert.deepStrictEqual(result[0], { filename, ignored: true }); - }); - - it("should ignore default ignored files if not passed explicitly even if ignore is false", () => { - const directory = getFixturePath("glob-util", "hidden"); - const patterns = [directory]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath(), - ignore: false - }); - }, `All files matched by '${directory}' are ignored.`); - }); - - it("should not ignore default ignored files when passed explicitly if ignore is false", () => { - const filename = getFixturePath("glob-util", "hidden", ".foo.js"); - const patterns = [filename]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath(), - ignore: false - }); - - assert.strictEqual(result.length, 1); - assert.deepStrictEqual(result[0], { filename, ignored: false }); - }); - - it("should throw an error for a file which does not exist", () => { - const filename = getFixturePath("glob-util", "hidden", "bar.js"); - const patterns = [filename]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath(), - allowMissingGlobs: true - }); - }, `No files matching '${filename}' were found.`); - }); - - it("should throw if a folder that does not have any applicable files is linted", () => { - const filename = getFixturePath("glob-util", "empty"); - const patterns = [filename]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, { - cwd: getFixturePath() - }); - }, `No files matching '${filename}' were found.`); - }); - - it("should throw if only ignored files match a glob", () => { - const pattern = getFixturePath("glob-util", "ignored"); - const options = { ignore: true, ignorePath: getFixturePath("glob-util", "ignored", ".eslintignore") }; - - assert.throws(() => { - globUtils.listFilesToProcess([pattern], options); - }, `All files matched by '${pattern}' are ignored.`); - }); - - it("should throw an error if no files match a glob", () => { - - // Relying here on the .eslintignore from the repo root - const patterns = ["tests/fixtures/glob-util/ignored/**/*.js"]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns); - }, `No files matching '${patterns[0]}' were found.`); - }); - - it("should ignore empty patterns", () => { - const patterns = [""]; - const result = globUtils.listFilesToProcess(patterns); - - assert.deepStrictEqual(result, []); - }); - - it("should return an ignored file, if ignore option is turned off", () => { - const options = { ignore: false }; - const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; - const result = globUtils.listFilesToProcess(patterns, options); - - assert.strictEqual(result.length, 1); - }); - - it("should ignore a file from a glob if it matches a pattern in an ignore file", () => { - const options = { ignore: true, ignorePath: getFixturePath("glob-util", "ignored", ".eslintignore") }; - const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, options); - }, `All files matched by '${patterns[0]}' are ignored.`); - }); - - it("should ignore a file from a glob if matching a specified ignore pattern", () => { - const options = { ignore: true, ignorePattern: "foo.js", cwd: getFixturePath() }; - const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; - - assert.throws(() => { - globUtils.listFilesToProcess(patterns, options); - }, `All files matched by '${patterns[0]}' are ignored.`); - }); - - it("should return a file only once if listed in more than 1 pattern", () => { - const patterns = [ - getFixturePath("glob-util", "one-js-file", "**/*.js"), - getFixturePath("glob-util", "one-js-file", "baz.js") - ]; - const result = globUtils.listFilesToProcess(patterns, { - cwd: path.join(fixtureDir, "..") - }); - - const file1 = getFixturePath("glob-util", "one-js-file", "baz.js"); - - assert.isArray(result); - assert.deepStrictEqual(result, [ - { filename: file1, ignored: false } - ]); - }); - - it("should set 'ignored: true' for files that are explicitly specified but ignored", () => { - const options = { ignore: true, ignorePattern: "foo.js", cwd: getFixturePath() }; - const filename = getFixturePath("glob-util", "ignored", "foo.js"); - const patterns = [filename]; - const result = globUtils.listFilesToProcess(patterns, options); - - assert.strictEqual(result.length, 1); - assert.deepStrictEqual(result, [ - { filename, ignored: true } - ]); - }); - - it("should not return files from default ignored folders", () => { - const options = { cwd: getFixturePath("glob-util") }; - const glob = getFixturePath("glob-util", "**/*.js"); - const patterns = [glob]; - const result = globUtils.listFilesToProcess(patterns, options); - const resultFilenames = result.map(resultObj => resultObj.filename); - - assert.notInclude(resultFilenames, getFixturePath("glob-util", "node_modules", "dependency.js")); - }); - - it("should return unignored files from default ignored folders", () => { - const options = { ignorePattern: "!/node_modules/dependency.js", cwd: getFixturePath("glob-util") }; - const glob = getFixturePath("glob-util", "**/*.js"); - const patterns = [glob]; - const result = globUtils.listFilesToProcess(patterns, options); - const unignoredFilename = getFixturePath("glob-util", "node_modules", "dependency.js"); - - assert.includeDeepMembers(result, [{ filename: unignoredFilename, ignored: false }]); - }); - }); -}); diff --git a/tests/lib/util/ignored-paths.js b/tests/lib/util/ignored-paths.js index acf007b1810..40c943fbc0e 100644 --- a/tests/lib/util/ignored-paths.js +++ b/tests/lib/util/ignored-paths.js @@ -12,7 +12,7 @@ const assert = require("chai").assert, path = require("path"), os = require("os"), - IgnoredPaths = require("../../../lib/util/ignored-paths.js"), + { IgnoredPaths } = require("../../../lib/util/ignored-paths.js"), sinon = require("sinon"), fs = require("fs"), includes = require("lodash").includes; @@ -386,14 +386,21 @@ describe("IgnoredPaths", () => { .withArgs(".eslintignore") .returns("subdir\r\n"); sinon.stub(fs, "statSync") - .withArgs("subdir") - .returns(); - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ".eslintignore", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/undef.js"))); - - fs.readFileSync.restore(); - fs.statSync.restore(); + .withArgs(".eslintignore") + .returns({ + isFile() { + return true; + } + }); + + try { + const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ".eslintignore", cwd: getFixturePath() }); + + assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/undef.js"))); + } finally { + fs.readFileSync.restore(); + fs.statSync.restore(); + } }); it("should return false for file not matching any ignore pattern", () => { @@ -648,129 +655,4 @@ describe("IgnoredPaths", () => { }); - describe("getIgnoredFoldersGlobChecker", () => { - - /** - * Creates a function to resolve the given relative path according to the `cwd` - * @param {path} cwd The cwd of `ignorePaths` - * @returns {function()} the function described above. - */ - function createResolve(cwd) { - return function(relative) { - return path.join(cwd, relative); - }; - } - - it("should ignore default folders when there is no eslintignore file", () => { - const cwd = getFixturePath("no-ignore-file"); - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isTrue(shouldIgnore(resolve("node_modules/a"))); - assert.isTrue(shouldIgnore(resolve("node_modules/a/b"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a/b"))); - assert.isFalse(shouldIgnore(resolve(".hidden"))); - assert.isTrue(shouldIgnore(resolve(".hidden/a"))); - - assert.isFalse(shouldIgnore(resolve(".."))); - assert.isFalse(shouldIgnore(resolve("../.."))); - assert.isFalse(shouldIgnore(resolve("../foo"))); - assert.isFalse(shouldIgnore(resolve("../../.."))); - assert.isFalse(shouldIgnore(resolve("../../foo"))); - }); - - it("should ignore default folders when there is an ignore file without unignored defaults", () => { - const cwd = getFixturePath(); - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignore"), cwd }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isTrue(shouldIgnore(resolve("node_modules/a"))); - assert.isTrue(shouldIgnore(resolve("node_modules/a/b"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a/b"))); - assert.isFalse(shouldIgnore(resolve(".hidden"))); - assert.isTrue(shouldIgnore(resolve(".hidden/a"))); - }); - - it("should not ignore things which are re-included in ignore file", () => { - const cwd = getFixturePath(); - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignoreWithUnignoredDefaults"), cwd }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isTrue(shouldIgnore(resolve("node_modules/a"))); - assert.isTrue(shouldIgnore(resolve("node_modules/a/b"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a/b"))); - assert.isFalse(shouldIgnore(resolve(".hidden"))); - assert.isTrue(shouldIgnore(resolve(".hidden/a"))); - assert.isFalse(shouldIgnore(resolve("node_modules/package"))); - assert.isFalse(shouldIgnore(resolve("bower_components/package"))); - assert.isFalse(shouldIgnore(resolve(".hidden/package"))); - }); - - it("should ignore files which we try to re-include in ignore file when ignore option is disabled", () => { - const cwd = getFixturePath(); - const ignoredPaths = new IgnoredPaths({ ignore: false, ignorePath: getFixturePath(".eslintignoreWithUnignoredDefaults"), cwd }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isTrue(shouldIgnore(resolve("node_modules/a"))); - assert.isTrue(shouldIgnore(resolve("node_modules/a/b"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a/b"))); - assert.isFalse(shouldIgnore(resolve(".hidden"))); - assert.isTrue(shouldIgnore(resolve(".hidden/a"))); - assert.isTrue(shouldIgnore(resolve("node_modules/package"))); - assert.isTrue(shouldIgnore(resolve("bower_components/package"))); - assert.isTrue(shouldIgnore(resolve(".hidden/package"))); - }); - - it("should not ignore dirs which are re-included by ignorePattern", () => { - const cwd = getFixturePath("no-ignore-file"); - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd, ignorePattern: "!/node_modules/package" }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isTrue(shouldIgnore(resolve("node_modules/a"))); - assert.isTrue(shouldIgnore(resolve("node_modules/a/b"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a"))); - assert.isTrue(shouldIgnore(resolve("bower_components/a/b"))); - assert.isFalse(shouldIgnore(resolve(".hidden"))); - assert.isTrue(shouldIgnore(resolve(".hidden/a"))); - assert.isFalse(shouldIgnore(resolve("node_modules/package"))); - assert.isTrue(shouldIgnore(resolve("bower_components/package"))); - }); - - it("should not ignore hidden dirs when dotfiles is enabled", () => { - const cwd = getFixturePath("no-ignore-file"); - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd, dotfiles: true }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isFalse(shouldIgnore(resolve(".hidden"))); - assert.isFalse(shouldIgnore(resolve(".hidden/a"))); - }); - - it("should use the ignorePath's directory as the base to resolve relative paths, not cwd", () => { - const cwd = getFixturePath("subdir"); - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd, ignorePath: getFixturePath(".eslintignoreForDifferentCwd") }); - - const shouldIgnore = ignoredPaths.getIgnoredFoldersGlobChecker(); - const resolve = createResolve(cwd); - - assert.isFalse(shouldIgnore(resolve("undef.js"))); - assert.isTrue(shouldIgnore(resolve("../undef.js"))); - }); - }); - }); diff --git a/tests/lib/util/lint-result-cache.js b/tests/lib/util/lint-result-cache.js index 97ed3cf9c28..e58cf7cf675 100644 --- a/tests/lib/util/lint-result-cache.js +++ b/tests/lib/util/lint-result-cache.js @@ -9,7 +9,7 @@ //----------------------------------------------------------------------------- const assert = require("chai").assert, - CLIEngine = require("../../../lib/cli-engine"), + { CLIEngine } = require("../../../lib/cli-engine"), fs = require("fs"), path = require("path"), proxyquire = require("proxyquire"), @@ -27,17 +27,13 @@ describe("LintResultCache", () => { let LintResultCache, hashStub, sandbox, - fakeConfigHelper, + fakeConfig, fakeErrorResults, fakeErrorResultsAutofix; before(() => { sandbox = sinon.sandbox.create(); - fakeConfigHelper = { - getConfig: sandbox.stub() - }; - hashStub = sandbox.stub(); let shouldFix = false; @@ -58,7 +54,7 @@ describe("LintResultCache", () => { fakeErrorResultsAutofix = cliEngine.executeOnFiles([path.join(fixturePath, "test-with-errors.js")]).results[0]; // Set up LintResultCache with fake fileEntryCache module - LintResultCache = proxyquire("../../../lib/util/lint-result-cache", { + LintResultCache = proxyquire("../../../lib/util/lint-result-cache.js", { "file-entry-cache": fileEntryCacheStubs, "./hash": hashStub }); @@ -81,12 +77,8 @@ describe("LintResultCache", () => { assert.throws(() => new LintResultCache(), /Cache file location is required/u); }); - it("should throw an error if config helper is not provided", () => { - assert.throws(() => new LintResultCache(cacheFileLocation), /Config helper is required/u); - }); - - it("should successfully create an instance if cache file location and config helper are provided", () => { - const instance = new LintResultCache(cacheFileLocation, fakeConfigHelper); + it("should successfully create an instance if cache file location is provided", () => { + const instance = new LintResultCache(cacheFileLocation); assert.ok(instance, "Instance should have been created successfully"); }); @@ -126,11 +118,9 @@ describe("LintResultCache", () => { getFileDescriptorStub.withArgs(filePath) .returns(cacheEntry); - const fakeConfig = {}; + fakeConfig = {}; - fakeConfigHelper.getConfig.returns(fakeConfig); - - lintResultsCache = new LintResultCache(cacheFileLocation, fakeConfigHelper); + lintResultsCache = new LintResultCache(cacheFileLocation); }); describe("When file is changed", () => { @@ -140,7 +130,7 @@ describe("LintResultCache", () => { }); it("should return null", () => { - const result = lintResultsCache.getCachedLintResults(filePath); + const result = lintResultsCache.getCachedLintResults(filePath, fakeConfig); assert.ok(getFileDescriptorStub.calledOnce); assert.isNull(result); @@ -153,7 +143,7 @@ describe("LintResultCache", () => { }); it("should return null", () => { - const result = lintResultsCache.getCachedLintResults(filePath); + const result = lintResultsCache.getCachedLintResults(filePath, fakeConfig); assert.ok(getFileDescriptorStub.calledOnce); assert.isNull(result); @@ -167,7 +157,7 @@ describe("LintResultCache", () => { }); it("should return null", () => { - const result = lintResultsCache.getCachedLintResults(filePath); + const result = lintResultsCache.getCachedLintResults(filePath, fakeConfig); assert.ok(getFileDescriptorStub.calledOnce); assert.isNull(result); @@ -180,7 +170,7 @@ describe("LintResultCache", () => { }); it("should return expected results", () => { - const result = lintResultsCache.getCachedLintResults(filePath); + const result = lintResultsCache.getCachedLintResults(filePath, fakeConfig); assert.deepStrictEqual(result, fakeErrorResults); assert.ok(result.source, "source property should be hydrated from filesystem"); @@ -216,18 +206,16 @@ describe("LintResultCache", () => { getFileDescriptorStub.withArgs(filePath) .returns(cacheEntry); - const fakeConfig = {}; - - fakeConfigHelper.getConfig.returns(fakeConfig); + fakeConfig = {}; hashStub.returns(hashOfConfig); - lintResultsCache = new LintResultCache(cacheFileLocation, fakeConfigHelper); + lintResultsCache = new LintResultCache(cacheFileLocation); }); describe("When lint result has output property", () => { it("does not modify file entry", () => { - lintResultsCache.setCachedLintResults(filePath, fakeErrorResultsAutofix); + lintResultsCache.setCachedLintResults(filePath, fakeConfig, fakeErrorResultsAutofix); assert.notProperty(cacheEntry.meta, "results"); assert.notProperty(cacheEntry.meta, "hashOfConfig"); @@ -240,7 +228,7 @@ describe("LintResultCache", () => { }); it("does not modify file entry", () => { - lintResultsCache.setCachedLintResults(filePath, fakeErrorResults); + lintResultsCache.setCachedLintResults(filePath, fakeConfig, fakeErrorResults); assert.notProperty(cacheEntry.meta, "results"); assert.notProperty(cacheEntry.meta, "hashOfConfig"); @@ -249,7 +237,7 @@ describe("LintResultCache", () => { describe("When file is found on filesystem", () => { beforeEach(() => { - lintResultsCache.setCachedLintResults(filePath, fakeErrorResults); + lintResultsCache.setCachedLintResults(filePath, fakeConfig, fakeErrorResults); }); it("stores hash of config in file entry", () => { @@ -271,6 +259,7 @@ describe("LintResultCache", () => { beforeEach(() => { lintResultsCache.setCachedLintResults( filePath, + fakeConfig, Object.assign({}, fakeErrorResults, { source: "" }) ); }); @@ -308,7 +297,7 @@ describe("LintResultCache", () => { }); beforeEach(() => { - lintResultsCache = new LintResultCache(cacheFileLocation, fakeConfigHelper); + lintResultsCache = new LintResultCache(cacheFileLocation); }); it("calls reconcile on the underlying cache", () => { diff --git a/tests/lib/util/npm-utils.js b/tests/lib/util/npm-utils.js index 1b07f37153f..a1deaf9ceb4 100644 --- a/tests/lib/util/npm-utils.js +++ b/tests/lib/util/npm-utils.js @@ -8,12 +8,51 @@ // Requirements //------------------------------------------------------------------------------ -const assert = require("chai").assert, +const + path = require("path"), + assert = require("chai").assert, spawn = require("cross-spawn"), + MemoryFs = require("metro-memory-fs"), sinon = require("sinon"), npmUtils = require("../../../lib/util/npm-utils"), - log = require("../../../lib/util/logging"), - mockFs = require("mock-fs"); + log = require("../../../lib/util/logging"); + +const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Import `npm-utils` with the in-memory file system. + * @param {Object} files The file definitions. + * @returns {Object} `npm-utils`. + */ +function requireNpmUtilsWithInmemoryFileSystem(files) { + const fs = new MemoryFs({ + cwd: process.cwd, + platform: process.platform === "win32" ? "win32" : "posix" + }); + + // Make cwd. + (function mkdir(dirPath) { + const parentPath = path.dirname(dirPath); + + if (parentPath && parentPath !== dirPath && !fs.existsSync(parentPath)) { + mkdir(parentPath); + } + fs.mkdirSync(dirPath); + + }(process.cwd())); + + // Write files. + for (const [filename, content] of Object.entries(files)) { + fs.writeFileSync(filename, content); + } + + // Stub. + return proxyquire("../../../lib/util/npm-utils", { fs }); +} //------------------------------------------------------------------------------ // Tests @@ -29,7 +68,6 @@ describe("npmUtils", () => { afterEach(() => { sandbox.verifyAndRestore(); - mockFs.restore(); }); describe("checkDevDeps()", () => { @@ -61,7 +99,7 @@ describe("npmUtils", () => { }); it("should handle missing devDependencies key", () => { - mockFs({ + const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow "package.json": JSON.stringify({ private: true, dependencies: {} }) }); @@ -70,7 +108,7 @@ describe("npmUtils", () => { }); it("should throw with message when parsing invalid package.json", () => { - mockFs({ + const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow "package.json": "{ \"not: \"valid json\" }" }); @@ -92,10 +130,6 @@ describe("npmUtils", () => { installStatus = npmUtils.checkDeps(["debug", "mocha", "notarealpackage", "jshint"]); }); - afterEach(() => { - mockFs.restore(); - }); - it("should find a direct dependency of the project", () => { assert.isTrue(installStatus.debug); }); @@ -124,7 +158,7 @@ describe("npmUtils", () => { }); it("should handle missing dependencies key", () => { - mockFs({ + const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow "package.json": JSON.stringify({ private: true, devDependencies: {} }) }); @@ -133,7 +167,7 @@ describe("npmUtils", () => { }); it("should throw with message when parsing invalid package.json", () => { - mockFs({ + const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow "package.json": "{ \"not: \"valid json\" }" }); @@ -149,12 +183,8 @@ describe("npmUtils", () => { }); describe("checkPackageJson()", () => { - after(() => { - mockFs.restore(); - }); - it("should return true if package.json exists", () => { - mockFs({ + const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow "package.json": "{ \"file\": \"contents\" }" }); @@ -162,7 +192,8 @@ describe("npmUtils", () => { }); it("should return false if package.json does not exist", () => { - mockFs({}); + const npmUtils = requireNpmUtilsWithInmemoryFileSystem({}); // eslint-disable-line no-shadow + assert.strictEqual(npmUtils.checkPackageJson(), false); }); }); diff --git a/tests/lib/util/path-utils.js b/tests/lib/util/path-utils.js deleted file mode 100644 index f0e1061f518..00000000000 --- a/tests/lib/util/path-utils.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @fileoverview Common helpers for operations on filenames and paths - * @author Ian VanSchooten - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const path = require("path"), - assert = require("chai").assert, - sinon = require("sinon"), - pathUtils = require("../../../lib/util/path-utils"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("pathUtil", () => { - - describe("convertPathToPosix()", () => { - - it("should remove a leading './'", () => { - const input = "./relative/file/path.js"; - const result = pathUtils.convertPathToPosix(input); - - assert.strictEqual(result, "relative/file/path.js"); - }); - - it("should remove interior '../'", () => { - const input = "./relative/file/../path.js"; - const result = pathUtils.convertPathToPosix(input); - - assert.strictEqual(result, "relative/path.js"); - }); - - it("should not remove a leading '../'", () => { - const input = "../parent/file/path.js"; - const result = pathUtils.convertPathToPosix(input); - - assert.strictEqual(result, "../parent/file/path.js"); - }); - - it("should convert windows path seperators into posix style path seperators", () => { - const input = "windows\\style\\path.js"; - const result = pathUtils.convertPathToPosix(input); - - assert.strictEqual(result, "windows/style/path.js"); - }); - - }); - - describe("getRelativePath()", () => { - - it("should return a path relative to the provided base path", () => { - const filePath = "/absolute/file/path.js"; - const basePath = "/absolute/"; - const result = pathUtils.getRelativePath(filePath, basePath); - - assert.strictEqual(result, path.normalize("file/path.js")); - }); - - it("should throw if the base path is not absolute", () => { - const filePath = "/absolute/file/path.js"; - const basePath = "somewhere/"; - - assert.throws(() => { - pathUtils.getRelativePath(filePath, basePath); - }); - }); - - it("should treat relative file path arguments as being relative to process.cwd", () => { - const filePath = "file/path.js"; - const basePath = "/absolute/file"; - - sinon.stub(process, "cwd").returns("/absolute/"); - const result = pathUtils.getRelativePath(filePath, basePath); - - assert.strictEqual(result, "path.js"); - - process.cwd.restore(); - }); - - it("should strip a leading '/' if no baseDir is provided", () => { - const filePath = "/absolute/file/path.js"; - const result = pathUtils.getRelativePath(filePath); - - assert.strictEqual(result, "absolute/file/path.js"); - }); - - }); -}); diff --git a/tests/lib/util/source-code-utils.js b/tests/lib/util/source-code-utils.js index a8b4b6dbbc6..db100e90d03 100644 --- a/tests/lib/util/source-code-utils.js +++ b/tests/lib/util/source-code-utils.js @@ -15,7 +15,6 @@ const path = require("path"), assert = require("chai").assert, sinon = require("sinon"), sh = require("shelljs"), - globUtils = require("../../../lib/util/glob-utils"), SourceCode = require("../../../lib/util/source-code"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); @@ -122,7 +121,7 @@ describe("SourceCodeUtil", () => { const spy = sinon.spy(); process.chdir(fixtureDir); - getSourceCodeOfFiles(filename, spy); + getSourceCodeOfFiles(filename, {}, spy); process.chdir(originalDir); assert(spy.calledOnce); }); @@ -132,20 +131,11 @@ describe("SourceCodeUtil", () => { const spy = sinon.spy(); process.chdir(fixtureDir); - getSourceCodeOfFiles(filename, spy); + getSourceCodeOfFiles(filename, {}, spy); process.chdir(originalDir); assert.strictEqual(spy.firstCall.args[0], 1); }); - it("should use default options if none are provided", () => { - const filename = getFixturePath("foo.js"); - const spy = sinon.spy(globUtils, "resolveFileGlobPatterns"); - - getSourceCodeOfFiles(filename); - assert(spy.called); - assert.deepStrictEqual(spy.firstCall.args[1].extensions, [".js"]); - }); - it("should create an object with located filenames as keys", () => { const fooFilename = getFixturePath("foo.js"); const barFilename = getFixturePath("bar.js"); diff --git a/tests/lib/util/source-code.js b/tests/lib/util/source-code.js index 548ce895046..662551f5c42 100644 --- a/tests/lib/util/source-code.js +++ b/tests/lib/util/source-code.js @@ -14,7 +14,7 @@ const fs = require("fs"), espree = require("espree"), sinon = require("sinon"), leche = require("leche"), - Linter = require("../../../lib/linter"), + { Linter } = require("../../../lib/linter"), SourceCode = require("../../../lib/util/source-code"), astUtils = require("../../../lib/util/ast-utils"); From ef57db3687df1e8c357cc736c4f68c11f95de505 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 06:38:04 +0900 Subject: [PATCH 02/49] fix typo Co-Authored-By: mysticatea --- lib/cli-engine/cascading-config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 033a8170ff3..2637bcc58d5 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -5,7 +5,7 @@ * * 1. Handles cascading of config files. * - * It provies two methods: + * It provides two methods: * * - `getConfigArrayForFile(filePath)` * Get the corresponded configuration of a given file. This method doesn't From 69566ee9a5c329cbdfa95972108cde869a3555c0 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 06:41:21 +0900 Subject: [PATCH 03/49] clarify a magic number Co-Authored-By: mysticatea --- lib/cli-engine/config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index e6bd4c55bcc..6720c103742 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -632,7 +632,7 @@ class ConfigArrayFactory { */ _loadExtendedPluginConfig(extendName, importerPath, importerName) { const slashIndex = extendName.lastIndexOf("/"); - const pluginName = extendName.slice(7, slashIndex); + const pluginName = extendName.slice("plugin:".length, slashIndex); const configName = extendName.slice(slashIndex + 1); if (isFilePath(pluginName)) { From 815746455dc50cb2c2f156d40f224e5f37d357a1 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 06:45:21 +0900 Subject: [PATCH 04/49] correct a comment Co-Authored-By: mysticatea --- lib/cli-engine/config-array/override-tester.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/config-array/override-tester.js b/lib/cli-engine/config-array/override-tester.js index 2618013f6c8..d0d30dff18d 100644 --- a/lib/cli-engine/config-array/override-tester.js +++ b/lib/cli-engine/config-array/override-tester.js @@ -106,7 +106,7 @@ class OverrideTester { /** * Combine two testers by logical and. - * If either of testers was `null`, returns another. + * If either of the testers was `null`, returns the other tester. * @param {OverrideTester|null} a A tester. * @param {OverrideTester|null} b Another tester. * @returns {OverrideTester|null} Combined tester. From d96176f0c63e0b87a47afe2f1be143c58e2fd575 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 07:06:29 +0900 Subject: [PATCH 05/49] correct a test description Co-Authored-By: mysticatea --- tests/lib/cli-engine/cascading-config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 45769fab530..a4a623db682 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -1190,7 +1190,7 @@ describe("CascadingConfigArrayFactory", () => { assert.strictEqual(one, two); }); - it("should not use cached instance if it call 'clearCache()' method between two getting.", () => { + it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { const one = factory.getConfigArrayForFile(); factory.clearCache(); From 11f293dea915c478694392c389d6d083568ace23 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 07:12:20 +0900 Subject: [PATCH 06/49] fix typo Co-Authored-By: mysticatea --- tests/lib/cli-engine/config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 156982f2c29..b3ed043e1e3 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -766,7 +766,7 @@ describe("ConfigArrayFactory", () => { }); }); - describe("the secand element", () => { + describe("the second element", () => { let element; beforeEach(() => { From f6eb5fa9474cd0dfa7d0419389d98edd32b50a5b Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 07:13:10 +0900 Subject: [PATCH 07/49] fix typo Co-Authored-By: mysticatea --- tests/lib/cli-engine/config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index b3ed043e1e3..6d46b0a1615 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -760,7 +760,7 @@ describe("ConfigArrayFactory", () => { assert.strictEqual(element.processor, "ext/.abc"); }); - it("should have 'criteria' property what matches '.abc'.", () => { + it("should have 'criteria' property which matches '.abc'.", () => { assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.abc")), true); assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.xyz")), false); }); From d6538e74a4bf6c87c060ab60092e52c1900c3d24 Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 07:15:12 +0900 Subject: [PATCH 08/49] fix typo Co-Authored-By: mysticatea --- tests/lib/cli-engine/config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 6d46b0a1615..ed75a2d8e3b 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -785,7 +785,7 @@ describe("ConfigArrayFactory", () => { assert.strictEqual(element.processor, "ext/.xyz"); }); - it("should have 'criteria' property what matches '.xyz'.", () => { + it("should have 'criteria' property which matches '.xyz'.", () => { assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.abc")), false); assert.strictEqual(element.criteria.test(path.join(tempDir, "1234.xyz")), true); }); From f51d2c0cf35872a1f357a0b6b7f87161deff391a Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 07:20:49 +0900 Subject: [PATCH 09/49] fix a copy/paste mistake Co-Authored-By: mysticatea --- tests/lib/cli-engine/config-array/config-array.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index 50aa8f1ff8b..577c1c568eb 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -154,7 +154,7 @@ describe("ConfigArray", () => { assert.strictEqual(configArray.pluginProcessors.get("aaa/.xxx"), processors["aaa/.xxx"]); }); - it("should return both 'bbb/.xxx' if it exists.", () => { + it("should return 'bbb/.xxx' if it exists.", () => { assert.strictEqual(configArray.pluginProcessors.get("bbb/.xxx"), processors["bbb/.xxx"]); }); From bedce888b37373b465481839448704bd2f1fbe1d Mon Sep 17 00:00:00 2001 From: Kevin Partington Date: Fri, 12 Apr 2019 07:27:10 +0900 Subject: [PATCH 10/49] fix typo Co-Authored-By: mysticatea --- tests/lib/cli-engine/file-enumerator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index 8240f2550d4..aadddd79f78 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -128,7 +128,7 @@ describe("FileEnumerator", () => { }); }); - describe("if 'lib/*.js' snf 'test/*.js' were given,", () => { + describe("if 'lib/*.js' and 'test/*.js' were given,", () => { /** @type {Array<{config:(typeof import('../../../lib/cli-engine'))["ConfigArray"], filePath:string, ignored:boolean}>} */ let list; From 95d3dc23f5050121661a2e03ecd10ee98765407f Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 10:59:23 +0900 Subject: [PATCH 11/49] remove "a.js" from getConfigForFile (https://github.com/eslint/eslint/pull/11546#discussion_r274252053) --- lib/cli-engine.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/cli-engine.js b/lib/cli-engine.js index 65bbe96b220..f4ce8cec9b1 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -876,9 +876,10 @@ class CLIEngine { * @param {string} filePath The path of the file to retrieve a config object for. * @returns {ConfigData} A configuration object for the file. */ - getConfigForFile(filePath = "a.js") { + getConfigForFile(filePath) { const { configArrayFactory, options } = internalSlotsMap.get(this); - const absolutePath = path.resolve(options.cwd, filePath); + const absolutePath = + filePath ? path.resolve(options.cwd, filePath) : options.cwd; return configArrayFactory .getConfigArrayForFile(absolutePath) From 6a13317683684da25b4015cf6306aa3712393685 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 11:01:13 +0900 Subject: [PATCH 12/49] remove commented out debug.enabled=true --- lib/cli-engine/cascading-config-array-factory.js | 2 -- lib/cli-engine/config-array-factory.js | 2 -- lib/cli-engine/file-enumerator.js | 2 -- lib/util/ignored-paths.js | 2 -- 4 files changed, 8 deletions(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 2637bcc58d5..e21685b8f22 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -31,8 +31,6 @@ const { ConfigDependency } = require("./config-array"); const loadRules = require("../load-rules"); const debug = require("debug")("eslint:cascading-config-array-factory"); -// debug.enabled = true; - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 6720c103742..165440ec205 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -43,8 +43,6 @@ const ModuleResolver = require("../util/relative-module-resolver"); const naming = require("../util/naming"); const debug = require("debug")("eslint:config-array-factory"); -// debug.enabled = true; - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ diff --git a/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js index 9061f9df554..d796a6755b9 100644 --- a/lib/cli-engine/file-enumerator.js +++ b/lib/cli-engine/file-enumerator.js @@ -44,8 +44,6 @@ const { CascadingConfigArrayFactory } = require("./cascading-config-array-factor const { IgnoredPaths } = require("../util/ignored-paths"); const debug = require("debug")("eslint:file-enumerator"); -// debug.enabled = true; - //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ diff --git a/lib/util/ignored-paths.js b/lib/util/ignored-paths.js index 1d9003b655b..3f9e02a82a8 100644 --- a/lib/util/ignored-paths.js +++ b/lib/util/ignored-paths.js @@ -15,8 +15,6 @@ const fs = require("fs"), const debug = require("debug")("eslint:ignored-paths"); -// debug.enabled = true; - //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ From fa671b877d7e2df8ad0655a8094239ba8f80d56d Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 11:06:32 +0900 Subject: [PATCH 13/49] fix CascadingConfigArrayFactoryOptions#cliConfig comment --- lib/cli-engine/cascading-config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index e21685b8f22..205aafc56c3 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -45,7 +45,7 @@ const debug = require("debug")("eslint:cascading-config-array-factory"); * @typedef {Object} CascadingConfigArrayFactoryOptions * @property {Map} [additionalPluginPool] The map for additional plugins. * @property {ConfigData} [baseConfig] The config by `baseConfig` option. - * @property {ConfigData} [cliConfig] The config by CLI options. This is prior to regular config files. + * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files. * @property {string} [cwd] The base directory to start lookup. * @property {string[]} [rulePaths] The value of `--rulesdir` option. * @property {string} [specificConfigPath] The value of `--config` option. From e743a71013f8b42134dfb18129f7c92dc5495aa6 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 11:11:55 +0900 Subject: [PATCH 14/49] =?UTF-8?q?originalEnabled=20=E2=86=92=20originalDeb?= =?UTF-8?q?ugEnabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cli-engine/config-array-factory.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 165440ec205..fe23b104998 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -408,7 +408,7 @@ class ConfigArrayFactory { _loadConfigDataOnDirectory(directoryPath, name) { for (const filename of configFilenames) { const filePath = path.join(directoryPath, filename); - const originalEnabled = debug.enabled; + const originalDebugEnabled = debug.enabled; let configData; // Make silent temporary because of too verbose. @@ -424,7 +424,7 @@ class ConfigArrayFactory { throw error; } } finally { - debug.enabled = originalEnabled; + debug.enabled = originalDebugEnabled; } if (configData) { From ef6582502f8d0f979ebeccb2f0593319e1187c46 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 12:03:04 +0900 Subject: [PATCH 15/49] fix a test description (https://github.com/eslint/eslint/pull/11546#discussion_r274502201) --- tests/lib/cli-engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index 71415088146..430d0d95e98 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -2895,7 +2895,7 @@ describe("CLIEngine", () => { }); }); - describe("config in a config file should prior to shareable configs always; https://github.com/eslint/eslint/issues/11510", () => { + describe("a config file setting should have higher priority than a shareable config file's settings always; https://github.com/eslint/eslint/issues/11510", () => { beforeEach(() => { ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ cwd: () => path.join(os.tmpdir(), "cli-engine/11510"), From c1f7075213fbdaec2bb9279d629b4a8cd0097d82 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 14:45:14 +0900 Subject: [PATCH 16/49] remove "Moved from *" groups Please check this diff with `-w` flag. --- tests/lib/cli-engine.js | 177 +- .../cascading-config-array-factory.js | 1599 ++++++++--------- tests/lib/cli-engine/config-array-factory.js | 1549 ++++++++-------- .../cli-engine/config-array/config-array.js | 460 ++--- .../config-array/override-tester.js | 216 +-- tests/lib/cli-engine/file-enumerator.js | 3 +- 6 files changed, 2006 insertions(+), 1998 deletions(-) diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index 430d0d95e98..667ed0bb538 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -113,6 +113,15 @@ describe("CLIEngine", () => { new CLIEngine({ ignorePath: fixtureDir }); }, `Cannot read ignore file: ${fixtureDir}\nError: ${fixtureDir} is not a file`); }); + + // https://github.com/eslint/eslint/issues/2380 + it("should not modify baseConfig when format is specified", () => { + const customBaseConfig = { root: true }; + + new CLIEngine({ baseConfig: customBaseConfig, format: "foo" }); // eslint-disable-line no-new + + assert.deepStrictEqual(customBaseConfig, { root: true }); + }); }); describe("executeOnText()", () => { @@ -3409,110 +3418,108 @@ describe("CLIEngine", () => { }); }); - describe("Moved from tests/lib/util/glob-utils.js", () => { - it("should convert a directory name with no provided extensions into a glob pattern", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should convert a directory name with no provided extensions into a glob pattern", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); - }); + assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + }); - it("should not convert path with globInputPaths option false", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util"), - globInputPaths: false - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should not convert path with globInputPaths option false", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util"), + globInputPaths: false + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file"]); - }); + assert.deepStrictEqual(result, ["one-js-file"]); + }); - it("should convert an absolute directory name with no provided extensions into a posix glob pattern", () => { - const patterns = [getFixturePath("glob-util", "one-js-file")]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - const expected = [`${getFixturePath("glob-util", "one-js-file").replace(/\\/gu, "/")}/**/*.js`]; + it("should convert an absolute directory name with no provided extensions into a posix glob pattern", () => { + const patterns = [getFixturePath("glob-util", "one-js-file")]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + const expected = [`${getFixturePath("glob-util", "one-js-file").replace(/\\/gu, "/")}/**/*.js`]; - assert.deepStrictEqual(result, expected); - }); + assert.deepStrictEqual(result, expected); + }); - it("should convert a directory name with a single provided extension into a glob pattern", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util"), - extensions: [".jsx"] - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should convert a directory name with a single provided extension into a glob pattern", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util"), + extensions: [".jsx"] + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.jsx"]); - }); + assert.deepStrictEqual(result, ["one-js-file/**/*.jsx"]); + }); - it("should convert a directory name with multiple provided extensions into a glob pattern", () => { - const patterns = ["one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util"), - extensions: [".jsx", ".js"] - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should convert a directory name with multiple provided extensions into a glob pattern", () => { + const patterns = ["one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util"), + extensions: [".jsx", ".js"] + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.{jsx,js}"]); - }); + assert.deepStrictEqual(result, ["one-js-file/**/*.{jsx,js}"]); + }); - it("should convert multiple directory names into glob patterns", () => { - const patterns = ["one-js-file", "two-js-files"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should convert multiple directory names into glob patterns", () => { + const patterns = ["one-js-file", "two-js-files"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js", "two-js-files/**/*.js"]); - }); + assert.deepStrictEqual(result, ["one-js-file/**/*.js", "two-js-files/**/*.js"]); + }); - it("should remove leading './' from glob patterns", () => { - const patterns = ["./one-js-file"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should remove leading './' from glob patterns", () => { + const patterns = ["./one-js-file"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); - }); + assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + }); - it("should convert a directory name with a trailing '/' into a glob pattern", () => { - const patterns = ["one-js-file/"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should convert a directory name with a trailing '/' into a glob pattern", () => { + const patterns = ["one-js-file/"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); - }); + assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + }); - it("should return filenames as they are", () => { - const patterns = ["some-file.js"]; - const opts = { - cwd: getFixturePath("glob-util") - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should return filenames as they are", () => { + const patterns = ["some-file.js"]; + const opts = { + cwd: getFixturePath("glob-util") + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["some-file.js"]); - }); + assert.deepStrictEqual(result, ["some-file.js"]); + }); - it("should convert backslashes into forward slashes", () => { - const patterns = ["one-js-file\\example.js"]; - const opts = { - cwd: getFixturePath() - }; - const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); + it("should convert backslashes into forward slashes", () => { + const patterns = ["one-js-file\\example.js"]; + const opts = { + cwd: getFixturePath() + }; + const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/example.js"]); - }); + assert.deepStrictEqual(result, ["one-js-file/example.js"]); }); }); diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index a4a623db682..d43d27df33f 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -85,7 +85,8 @@ describe("CascadingConfigArrayFactory", () => { }); }); - describe("Moved from tests/lib/config.js", () => { + // This group moved from 'tests/lib/config.js' when refactoring to keep the cumulated test cases. + describe("with 'tests/fixtures/config-hierarchy' files", () => { let fixtureDir; let sandbox; @@ -169,990 +170,976 @@ describe("CascadingConfigArrayFactory", () => { sh.rm("-r", fixtureDir); }); - describe("new Config()", () => { + it("should create config object when using baseConfig with extends", () => { + const customBaseConfig = { + extends: path.resolve(__dirname, "../../fixtures/config-extends/array/.eslintrc") + }; + const factory = new CascadingConfigArrayFactory({ baseConfig: customBaseConfig, useEslintrc: false }); + const config = getConfig(factory); + + assert.deepStrictEqual(config.env, { + browser: false, + es6: true, + node: true + }); + assert.deepStrictEqual(config.rules, { + "no-empty": [1], + "comma-dangle": [2], + "no-console": [2] + }); + }); - // https://github.com/eslint/eslint/issues/2380 - it("should not modify baseConfig when format is specified", () => { - const customBaseConfig = { root: true }; + it("should return the project config when called in current working directory", () => { + const factory = new CascadingConfigArrayFactory(); + const actual = getConfig(factory); - new CascadingConfigArrayFactory({ baseConfig: customBaseConfig, format: "foo" }); // eslint-disable-line no-new + assert.strictEqual(actual.rules.strict[1], "global"); + }); - assert.deepStrictEqual(customBaseConfig, { root: true }); - }); + it("should not retain configs from previous directories when called multiple times", () => { + const firstpath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/subdir/.eslintrc"); + const secondpath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/.eslintrc"); + const factory = new CascadingConfigArrayFactory(); + let config; - it("should create config object when using baseConfig with extends", () => { - const customBaseConfig = { - extends: path.resolve(__dirname, "../../fixtures/config-extends/array/.eslintrc") - }; - const factory = new CascadingConfigArrayFactory({ baseConfig: customBaseConfig, useEslintrc: false }); - const config = getConfig(factory); + config = getConfig(factory, firstpath); + assert.deepStrictEqual(config.rules["no-new"], [0]); + config = getConfig(factory, secondpath); + assert.deepStrictEqual(config.rules["no-new"], [1]); + }); + + it("should throw error when a configuration file doesn't exist", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/.eslintrc"); + const factory = new CascadingConfigArrayFactory(); + + sandbox.stub(fs, "readFileSync").throws(new Error()); + + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - assert.deepStrictEqual(config.env, { - browser: false, - es6: true, - node: true - }); - assert.deepStrictEqual(config.rules, { - "no-empty": [1], - "comma-dangle": [2], - "no-console": [2] - }); - }); }); - describe("getConfig()", () => { - it("should return the project config when called in current working directory", () => { - const factory = new CascadingConfigArrayFactory(); - const actual = getConfig(factory); + it("should throw error when a configuration file is not require-able", () => { + const configPath = ".eslintrc"; + const factory = new CascadingConfigArrayFactory(); - assert.strictEqual(actual.rules.strict[1], "global"); - }); + sandbox.stub(fs, "readFileSync").throws(new Error()); - it("should not retain configs from previous directories when called multiple times", () => { - const firstpath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/subdir/.eslintrc"); - const secondpath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/.eslintrc"); - const factory = new CascadingConfigArrayFactory(); - let config; + assert.throws(() => { + getConfig(factory, configPath); + }, "Cannot read config file"); - config = getConfig(factory, firstpath); - assert.deepStrictEqual(config.rules["no-new"], [0]); - config = getConfig(factory, secondpath); - assert.deepStrictEqual(config.rules["no-new"], [1]); - }); + }); - it("should throw error when a configuration file doesn't exist", () => { - const configPath = path.resolve(__dirname, "../../fixtures/configurations/.eslintrc"); - const factory = new CascadingConfigArrayFactory(); + it("should cache config when the same directory is passed twice", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/.eslintrc"); + const configArrayFactory = new ConfigArrayFactory(); + const factory = new CascadingConfigArrayFactory({ configArrayFactory }); - sandbox.stub(fs, "readFileSync").throws(new Error()); + sandbox.spy(configArrayFactory, "loadOnDirectory"); - assert.throws(() => { - getConfig(factory, configPath); - }, "Cannot read config file"); + // If cached this should be called only once + getConfig(factory, configPath); + const callcount = configArrayFactory.loadOnDirectory.callcount; - }); + getConfig(factory, configPath); - it("should throw error when a configuration file is not require-able", () => { - const configPath = ".eslintrc"; - const factory = new CascadingConfigArrayFactory(); + assert.strictEqual(configArrayFactory.loadOnDirectory.callcount, callcount); + }); - sandbox.stub(fs, "readFileSync").throws(new Error()); + // make sure JS-style comments don't throw an error + it("should load the config file when there are JS-style comments in the text", () => { + const specificConfigPath = path.resolve(__dirname, "../../fixtures/configurations/comments.json"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath, useEslintrc: false }); + const config = getConfig(factory); + const { semi, strict } = config.rules; - assert.throws(() => { - getConfig(factory, configPath); - }, "Cannot read config file"); + assert.deepStrictEqual(semi, [1]); + assert.deepStrictEqual(strict, [0]); + }); + + // make sure YAML files work correctly + it("should load the config file when a YAML file is used", () => { + const specificConfigPath = path.resolve(__dirname, "../../fixtures/configurations/env-browser.yaml"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath, useEslintrc: false }); + const config = getConfig(factory); + const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; + + assert.deepStrictEqual(noAlert, [0]); + assert.deepStrictEqual(noUndef, [2]); + }); + + it("should contain the correct value for parser when a custom parser is specified", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/parser/.eslintrc.json"); + const factory = new CascadingConfigArrayFactory(); + const config = getConfig(factory, configPath); + + assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.js")); + }); + + /* + * Configuration hierarchy --------------------------------------------- + * https://github.com/eslint/eslint/issues/3915 + */ + it("should correctly merge environment settings", () => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: true }); + const file = getFixturePath("envs", "sub", "foo.js"); + const expected = { + rules: {}, + env: { + browser: true, + node: false + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Default configuration - blank + it("should return a blank config when using no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: false }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {} + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ baseConfig: false, useEslintrc: false }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + rules: {}, + globals: {}, + env: {} + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // No default configuration + it("should return an empty config when not using .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: false }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, {}); + }); + + it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }, + useEslintrc: false + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + baseConfig: { + env: { + node: true + }, + rules: { + quotes: [2, "single"] + }, + plugins: ["example-with-rules-config"] + }, + cwd: getFixturePath("plugins"), + useEslintrc: false }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + plugins: ["example-with-rules-config"], + rules: { + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + // Project configuration - second level .eslintrc + it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [1], + quotes: [2, "single"] + } + }; + const actual = getConfig(factory, file); - it("should cache config when the same directory is passed twice", () => { - const configPath = path.resolve(__dirname, "../../fixtures/configurations/single-quotes/.eslintrc"); - const configArrayFactory = new ConfigArrayFactory(); - const factory = new CascadingConfigArrayFactory({ configArrayFactory }); + assertConfigsEqual(actual, expected); + }); - sandbox.spy(configArrayFactory, "loadOnDirectory"); + // Project configuration - third level .eslintrc + it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + "no-console": [0], + quotes: [1, "double"] + } + }; + const actual = getConfig(factory, file); - // If cached this should be called only once - getConfig(factory, configPath); - const callcount = configArrayFactory.loadOnDirectory.callcount; + assertConfigsEqual(actual, expected); + }); - getConfig(factory, configPath); + // Project configuration - root set in second level .eslintrc + it("should not return or traverse configurations in parents of config with root:true", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); - assert.strictEqual(configArrayFactory.loadOnDirectory.callcount, callcount); + // Project configuration - root set in second level .eslintrc + it("should return project config when called with a relative path from a subdir", () => { + const factory = new CascadingConfigArrayFactory({ cwd: getFixturePath("root-true", "parent", "root", "subdir") }); + const dir = "."; + const expected = { + rules: { + semi: [2, "never"] + } + }; + const actual = getConfig(factory, dir); + + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file adds to local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml") }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "double"], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, file); - // make sure JS-style comments don't throw an error - it("should load the config file when there are JS-style comments in the text", () => { - const specificConfigPath = path.resolve(__dirname, "../../fixtures/configurations/comments.json"); - const factory = new CascadingConfigArrayFactory({ specificConfigPath, useEslintrc: false }); - const config = getConfig(factory); - const { semi, strict } = config.rules; + assertConfigsEqual(actual, expected); + }); - assert.deepStrictEqual(semi, [1]); - assert.deepStrictEqual(strict, [0]); + // Command line configuration - --config with first level .eslintrc + it("should merge command line config when config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml") }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "double"] + } + }; + const actual = getConfig(factory, file); - // make sure YAML files work correctly - it("should load the config file when a YAML file is used", () => { - const specificConfigPath = path.resolve(__dirname, "../../fixtures/configurations/env-browser.yaml"); - const factory = new CascadingConfigArrayFactory({ specificConfigPath, useEslintrc: false }); - const config = getConfig(factory); - const { "no-alert": noAlert, "no-undef": noUndef } = config.rules; + assertConfigsEqual(actual, expected); + }); - assert.deepStrictEqual(noAlert, [0]); - assert.deepStrictEqual(noUndef, [2]); + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file adds to local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "add-conf.yaml") }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [2, "single"], + "no-console": [1], + semi: [1, "never"] + } + }; + const actual = getConfig(factory, file); - it("should contain the correct value for parser when a custom parser is specified", () => { - const configPath = path.resolve(__dirname, "../../fixtures/configurations/parser/.eslintrc.json"); - const factory = new CascadingConfigArrayFactory(); - const config = getConfig(factory, configPath); + assertConfigsEqual(actual, expected); + }); - assert.strictEqual(config.parser, path.resolve(path.dirname(configPath), "./custom.js")); + // Command line configuration - --config with second level .eslintrc + it("should merge command line config when config file overrides local and parent .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + specificConfigPath: getFixturePath("broken", "override-conf.yaml") }); + const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [0, "single"], + "no-console": [1] + } + }; + const actual = getConfig(factory, file); - /* - * Configuration hierarchy --------------------------------------------- - * https://github.com/eslint/eslint/issues/3915 - */ - it("should correctly merge environment settings", () => { - const factory = new CascadingConfigArrayFactory({ useEslintrc: true }); - const file = getFixturePath("envs", "sub", "foo.js"); - const expected = { - rules: {}, - env: { - browser: true, - node: false + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + rules: { + quotes: [1, "double"] } - }; - const actual = getConfig(factory, file); + }, + specificConfigPath: getFixturePath("broken", "override-conf.yaml") + }); + const file = getFixturePath("broken", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + rules: { + quotes: [1, "double"] + } + }; + const actual = getConfig(factory, file); - assertConfigsEqual(actual, expected); + assertConfigsEqual(actual, expected); + }); + + // Command line configuration - --plugin + it("should merge command line plugin with local .eslintrc", () => { + const factory = new CascadingConfigArrayFactory({ + cliConfig: { + plugins: ["another-plugin"] + }, + cwd: getFixturePath("plugins") }); + const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + const expected = { + env: { + node: true + }, + plugins: [ + "example", + "another-plugin" + ], + rules: { + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + + it("should merge multiple different config file formats", () => { + const factory = new CascadingConfigArrayFactory(); + const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); + const expected = { + env: { + browser: true + }, + rules: { + semi: [2, "always"], + eqeqeq: [2] + } + }; + const actual = getConfig(factory, file); + + assertConfigsEqual(actual, expected); + }); + + + it("should load user config globals", () => { + const configPath = path.resolve(__dirname, "../../fixtures/globals/conf.yaml"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath, useEslintrc: false }); + const expected = { + globals: { + foo: true + } + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + it("should not load disabled environments", () => { + const configPath = path.resolve(__dirname, "../../fixtures/environments/disable.yaml"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath, useEslintrc: false }); + const config = getConfig(factory, configPath); + + assert.isUndefined(config.globals.window); + }); + + it("should gracefully handle empty files", () => { + const configPath = path.resolve(__dirname, "../../fixtures/configurations/env-node.json"); + const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath }); + + getConfig(factory, path.resolve(__dirname, "../../fixtures/configurations/empty/empty.json")); + }); - // Default configuration - blank - it("should return a blank config when using no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ useEslintrc: false }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); + // Meaningful stack-traces + it("should include references to where an `extends` configuration was loaded from", () => { + const configPath = path.resolve(__dirname, "../../fixtures/config-extends/error.json"); + + assert.throws(() => { + const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); + + getConfig(factory, configPath); + }, /Referenced from:.*?error\.json/u); + }); + + // Keep order with the last array element taking highest precedence + it("should make the last element in an array take the highest precedence", () => { + const configPath = path.resolve(__dirname, "../../fixtures/config-extends/array/.eslintrc"); + const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); + const expected = { + rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, + env: { browser: false, node: true, es6: true } + }; + const actual = getConfig(factory, configPath); + + assertConfigsEqual(actual, expected); + }); + + describe("with env in a child configuration file", () => { + it("should not overwrite parserOptions of the parent with env of the child", () => { + const factory = new CascadingConfigArrayFactory(); + const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); const expected = { rules: {}, - globals: {}, - env: {} + env: { commonjs: true }, + parserOptions: { ecmaFeatures: { globalReturn: false } } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); + }); + + describe("personal config file within home directory", () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY + } + }); + + /** + * Returns the path inside of the fixture directory. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should load the personal config if no local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(homePath); - it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ baseConfig: false, useEslintrc: false }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, filePath); const expected = { - rules: {}, - globals: {}, - env: {} + rules: { + "home-folder-rule": [2] + } }; - const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - // No default configuration - it("should return an empty config when not using .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ useEslintrc: false }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); - const actual = getConfig(factory, file); + it("should ignore the personal config if a local config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - assertConfigsEqual(actual, {}); - }); + mockOsHomedir(homePath); - it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - baseConfig: { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - } - }, - useEslintrc: false - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); + const actual = getConfig(factory, filePath); const expected = { - env: { - node: true - }, rules: { - quotes: [2, "single"] + "project-level-rule": [2] } }; - const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { + it("should ignore the personal config if config is passed through cli", () => { + const configPath = getFakeFixturePath("quotes-error.json"); + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); const factory = new CascadingConfigArrayFactory({ - baseConfig: { - env: { - node: true - }, - rules: { - quotes: [2, "single"] - }, - plugins: ["example-with-rules-config"] - }, - cwd: getFixturePath("plugins"), - useEslintrc: false + cwd: projectPath, + specificConfigPath: configPath }); - const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); + + mockOsHomedir(homePath); + + const actual = getConfig(factory, filePath); const expected = { - env: { - node: true - }, - plugins: ["example-with-rules-config"], rules: { - quotes: [2, "single"] + quotes: [2, "double"] } }; - const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); - // Project configuration - second level .eslintrc - it("should merge configs when local .eslintrc overrides parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory(); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + it("should still load the project config if the current working directory is the same as the home folder", () => { + const projectPath = getFakeFixturePath("personal-config", "project-with-config"); + const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(projectPath); + + const actual = getConfig(factory, filePath); const expected = { - env: { - node: true - }, rules: { - "no-console": [1], - quotes: [2, "single"] + "project-level-rule": [2], + "subfolder-level-rule": [2] } }; - const actual = getConfig(factory, file); assertConfigsEqual(actual, expected); }); + }); - // Project configuration - third level .eslintrc - it("should merge configs when local .eslintrc overrides parent and grandparent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory(); - const file = getFixturePath("broken", "subbroken", "subsubbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true - }, - rules: { - "no-console": [0], - quotes: [1, "double"] - } - }; - const actual = getConfig(factory, file); + describe("when no local or personal config is found", () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY + } + }); - assertConfigsEqual(actual, expected); + /** + * Returns the path inside of the fixture directory. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...args) { + return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); + } + + it("should throw an error if no local config and no personal config was found", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); }); - // Project configuration - root set in second level .eslintrc - it("should not return or traverse configurations in parents of config with root:true", () => { - const factory = new CascadingConfigArrayFactory(); - const file = getFixturePath("root-true", "parent", "root", "wrong-semi.js"); - const expected = { - rules: { - semi: [2, "never"] - } - }; - const actual = getConfig(factory, file); + it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - assertConfigsEqual(actual, expected); + mockOsHomedir(homePath); + + assert.throws(() => { + getConfig(factory, filePath); + }, "No ESLint configuration found"); }); - // Project configuration - root set in second level .eslintrc - it("should return project config when called with a relative path from a subdir", () => { - const factory = new CascadingConfigArrayFactory({ cwd: getFixturePath("root-true", "parent", "root", "subdir") }); - const dir = "."; - const expected = { - rules: { - semi: [2, "never"] - } - }; - const actual = getConfig(factory, dir); + it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ cwd: projectPath, useEslintrc: false }); - assertConfigsEqual(actual, expected); + mockOsHomedir(homePath); + + getConfig(factory, filePath); }); - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file adds to local .eslintrc", () => { + it("should not throw an error if no local config and no personal config was found but rules are specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml") + cliConfig: { + rules: { quotes: [2, "single"] } + }, + cwd: projectPath }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + + it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { + const projectPath = getFakeFixturePath("personal-config", "project-without-config"); + const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); + const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); + const factory = new CascadingConfigArrayFactory({ baseConfig: {}, cwd: projectPath }); + + mockOsHomedir(homePath); + + getConfig(factory, filePath); + }); + }); + + describe("with overrides", () => { + const { + CascadingConfigArrayFactory // eslint-disable-line no-shadow + } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY + } + }); + + /** + * Returns the path inside of the fixture directory. + * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFakeFixturePath(...pathSegments) { + return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); + } + + it("should merge override config when the pattern matches the file name", () => { + const factory = new CascadingConfigArrayFactory({}); + const targetPath = getFakeFixturePath("overrides", "foo.js"); const expected = { - env: { - node: true - }, rules: { - quotes: [2, "double"], + quotes: [2, "single"], + "no-else-return": [0], + "no-unused-vars": [1], semi: [1, "never"] } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - // Command line configuration - --config with first level .eslintrc - it("should merge command line config when config file overrides local .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "override-conf.yaml") - }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); + it("should merge override config when the pattern matches the file path relative to the config file", () => { + const factory = new CascadingConfigArrayFactory({}); + const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); const expected = { - env: { - node: true - }, rules: { - quotes: [0, "double"] + curly: ["error", "multi", "consistent"], + "no-else-return": [0], + "no-unused-vars": [1], + quotes: [2, "double"], + semi: [1, "never"] } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file adds to local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "add-conf.yaml") - }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); - const expected = { - env: { - node: true + it("should not merge override config when the pattern matches the absolute file path", () => { + const resolvedPath = path.resolve(__dirname, "../../fixtures/config-hierarchy/overrides/bar.js"); + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: resolvedPath, + rules: { + quotes: [1, "double"] + } + }] }, - rules: { - quotes: [2, "single"], - "no-console": [1], - semi: [1, "never"] - } - }; - const actual = getConfig(factory, file); + useEslintrc: false + }), /Invalid override pattern/u); + }); - assertConfigsEqual(actual, expected); + it("should not merge override config when the pattern traverses up the directory tree", () => { + const parentPath = "overrides/../**/*.js"; + + assert.throws(() => new CascadingConfigArrayFactory({ + baseConfig: { + overrides: [{ + files: parentPath, + rules: { + quotes: [1, "single"] + } + }] + }, + useEslintrc: false + }), /Invalid override pattern/u); }); - // Command line configuration - --config with second level .eslintrc - it("should merge command line config when config file overrides local and parent .eslintrc", () => { - const factory = new CascadingConfigArrayFactory({ - specificConfigPath: getFixturePath("broken", "override-conf.yaml") - }); - const file = getFixturePath("broken", "subbroken", "console-wrong-quotes.js"); + it("should merge all local configs (override and non-override) before non-local configs", () => { + const factory = new CascadingConfigArrayFactory({}); + const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); const expected = { - env: { - node: true - }, rules: { - quotes: [0, "single"], - "no-console": [1] + "no-console": [0], + "no-else-return": [0], + "no-unused-vars": [2], + quotes: [2, "double"], + semi: [2, "never"] } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - // Command line configuration - --rule with --config and first level .eslintrc - it("should merge command line config and rule when rule and config file overrides local .eslintrc", () => { + it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { + const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); const factory = new CascadingConfigArrayFactory({ - cliConfig: { - rules: { - quotes: [1, "double"] - } + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "three/**/*.js", + rules: { + "semi-style": [2, "last"] + } + } + ] }, - specificConfigPath: getFixturePath("broken", "override-conf.yaml") + useEslintrc: false }); - const file = getFixturePath("broken", "console-wrong-quotes.js"); const expected = { - env: { - node: true - }, rules: { - quotes: [1, "double"] + "semi-style": [2, "last"] } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - // Command line configuration - --plugin - it("should merge command line plugin with local .eslintrc", () => { + it("should apply overrides if all glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); const factory = new CascadingConfigArrayFactory({ - cliConfig: { - plugins: ["another-plugin"] + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*.js"], + rules: { + quotes: [2, "single"] + } + }] }, - cwd: getFixturePath("plugins") + useEslintrc: false }); - const file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"); const expected = { - env: { - node: true - }, - plugins: [ - "example", - "another-plugin" - ], rules: { - quotes: [2, "double"] + quotes: [2, "single"] } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - - it("should merge multiple different config file formats", () => { - const factory = new CascadingConfigArrayFactory(); - const file = getFixturePath("fileexts/subdir/subsubdir/foo.js"); - const expected = { - env: { - browser: true + it("should apply overrides even if some glob patterns do not match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: ["one/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] }, + useEslintrc: false + }); + const expected = { rules: { - semi: [2, "always"], - eqeqeq: [2] + quotes: [2, "single"] } }; - const actual = getConfig(factory, file); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - - it("should load user config globals", () => { - const configPath = path.resolve(__dirname, "../../fixtures/globals/conf.yaml"); - const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath, useEslintrc: false }); + it("should not apply overrides if any excluded glob patterns match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*one.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false + }); const expected = { - globals: { - foo: true - } + rules: {} }; - const actual = getConfig(factory, configPath); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - it("should not load disabled environments", () => { - const configPath = path.resolve(__dirname, "../../fixtures/environments/disable.yaml"); - const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath, useEslintrc: false }); - const config = getConfig(factory, configPath); - - assert.isUndefined(config.globals.window); - }); - - it("should gracefully handle empty files", () => { - const configPath = path.resolve(__dirname, "../../fixtures/configurations/env-node.json"); - const factory = new CascadingConfigArrayFactory({ specificConfigPath: configPath }); - - getConfig(factory, path.resolve(__dirname, "../../fixtures/configurations/empty/empty.json")); - }); - - // Meaningful stack-traces - it("should include references to where an `extends` configuration was loaded from", () => { - const configPath = path.resolve(__dirname, "../../fixtures/config-extends/error.json"); - - assert.throws(() => { - const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); - - getConfig(factory, configPath); - }, /Referenced from:.*?error\.json/u); - }); - - // Keep order with the last array element taking highest precedence - it("should make the last element in an array take the highest precedence", () => { - const configPath = path.resolve(__dirname, "../../fixtures/config-extends/array/.eslintrc"); - const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); + it("should apply overrides if all excluded glob patterns fail to match", () => { + const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [{ + files: "one/**/*", + excludedFiles: ["two/**/*", "*two.js"], + rules: { + quotes: [2, "single"] + } + }] + }, + useEslintrc: false + }); const expected = { - rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, - env: { browser: false, node: true, es6: true } + rules: { + quotes: [2, "single"] + } }; - const actual = getConfig(factory, configPath); + const actual = getConfig(factory, targetPath); assertConfigsEqual(actual, expected); }); - describe("with env in a child configuration file", () => { - it("should not overwrite parserOptions of the parent with env of the child", () => { - const factory = new CascadingConfigArrayFactory(); - const targetPath = getFixturePath("overwrite-ecmaFeatures", "child", "foo.js"); - const expected = { - rules: {}, - env: { commonjs: true }, - parserOptions: { ecmaFeatures: { globalReturn: false } } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - }); - - describe("personal config file within home directory", () => { - const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - }); - - /** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); - } - - it("should load the personal config if no local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); - const expected = { - rules: { - "home-folder-rule": [2] - } - }; - - assertConfigsEqual(actual, expected); - }); - - it("should ignore the personal config if a local config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); - const expected = { - rules: { - "project-level-rule": [2] - } - }; - - assertConfigsEqual(actual, expected); - }); - - it("should ignore the personal config if config is passed through cli", () => { - const configPath = getFakeFixturePath("quotes-error.json"); - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: projectPath, - specificConfigPath: configPath - }); - - mockOsHomedir(homePath); - - const actual = getConfig(factory, filePath); - const expected = { - rules: { - quotes: [2, "double"] - } - }; - - assertConfigsEqual(actual, expected); - }); - - it("should still load the project config if the current working directory is the same as the home folder", () => { - const projectPath = getFakeFixturePath("personal-config", "project-with-config"); - const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - - mockOsHomedir(projectPath); - - const actual = getConfig(factory, filePath); - const expected = { - rules: { - "project-level-rule": [2], - "subfolder-level-rule": [2] - } - }; - - assertConfigsEqual(actual, expected); - }); - }); - - describe("when no local or personal config is found", () => { - const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - }); - - /** - * Returns the path inside of the fixture directory. - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...args) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...args); - } - - it("should throw an error if no local config and no personal config was found", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - - mockOsHomedir(homePath); - - assert.throws(() => { - getConfig(factory, filePath); - }, "No ESLint configuration found"); - }); - - it("should throw an error if no local config was found and ~/package.json contains no eslintConfig section", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); - - mockOsHomedir(homePath); - - assert.throws(() => { - getConfig(factory, filePath); - }, "No ESLint configuration found"); - }); - - it("should not throw an error if no local config and no personal config was found but useEslintrc is false", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath, useEslintrc: false }); - - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - - it("should not throw an error if no local config and no personal config was found but rules are specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ - cliConfig: { - rules: { quotes: [2, "single"] } - }, - cwd: projectPath - }); - - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - - it("should not throw an error if no local config and no personal config was found but baseConfig is specified", () => { - const projectPath = getFakeFixturePath("personal-config", "project-without-config"); - const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); - const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ baseConfig: {}, cwd: projectPath }); - - mockOsHomedir(homePath); - - getConfig(factory, filePath); - }); - }); - - describe("with overrides", () => { - const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY - } - }); - - /** - * Returns the path inside of the fixture directory. - * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first - * @returns {string} The path inside the fixture directory. - * @private - */ - function getFakeFixturePath(...pathSegments) { - return path.join(process.cwd(), "eslint", "fixtures", "config-hierarchy", ...pathSegments); - } - - it("should merge override config when the pattern matches the file name", () => { - const factory = new CascadingConfigArrayFactory({}); - const targetPath = getFakeFixturePath("overrides", "foo.js"); - const expected = { - rules: { - quotes: [2, "single"], - "no-else-return": [0], - "no-unused-vars": [1], - semi: [1, "never"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should merge override config when the pattern matches the file path relative to the config file", () => { - const factory = new CascadingConfigArrayFactory({}); - const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); - const expected = { - rules: { - curly: ["error", "multi", "consistent"], - "no-else-return": [0], - "no-unused-vars": [1], - quotes: [2, "double"], - semi: [1, "never"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should not merge override config when the pattern matches the absolute file path", () => { - const resolvedPath = path.resolve(__dirname, "../../fixtures/config-hierarchy/overrides/bar.js"); - - assert.throws(() => new CascadingConfigArrayFactory({ - baseConfig: { - overrides: [{ - files: resolvedPath, - rules: { - quotes: [1, "double"] - } - }] - }, - useEslintrc: false - }), /Invalid override pattern/u); - }); - - it("should not merge override config when the pattern traverses up the directory tree", () => { - const parentPath = "overrides/../**/*.js"; - - assert.throws(() => new CascadingConfigArrayFactory({ - baseConfig: { - overrides: [{ - files: parentPath, - rules: { - quotes: [1, "single"] - } - }] - }, - useEslintrc: false - }), /Invalid override pattern/u); - }); - - it("should merge all local configs (override and non-override) before non-local configs", () => { - const factory = new CascadingConfigArrayFactory({}); - const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); - const expected = { - rules: { - "no-console": [0], - "no-else-return": [0], - "no-unused-vars": [2], - quotes: [2, "double"], - semi: [2, "never"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { - const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "three/**/*.js", - rules: { - "semi-style": [2, "last"] - } - } - ] - }, - useEslintrc: false - }); - const expected = { - rules: { - "semi-style": [2, "last"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides if all glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false - }); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides even if some glob patterns do not match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: ["one/**/*", "*two.js"], - rules: { - quotes: [2, "single"] - } - }] - }, - useEslintrc: false - }); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should not apply overrides if any excluded glob patterns match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*one.js"], + it("should cascade", () => { + const targetPath = getFakeFixturePath("overrides", "foo.js"); + const factory = new CascadingConfigArrayFactory({ + cwd: getFakeFixturePath("overrides"), + baseConfig: { + overrides: [ + { + files: "foo.js", rules: { + semi: [2, "never"], quotes: [2, "single"] } - }] - }, - useEslintrc: false - }); - const expected = { - rules: {} - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); - - it("should apply overrides if all excluded glob patterns fail to match", () => { - const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [{ - files: "one/**/*", - excludedFiles: ["two/**/*", "*two.js"], + }, + { + files: "foo.js", rules: { - quotes: [2, "single"] + semi: [2, "never"], + quotes: [2, "double"] } - }] - }, - useEslintrc: false - }); - const expected = { - rules: { - quotes: [2, "single"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); + } + ] + }, + useEslintrc: false }); + const expected = { + rules: { + semi: [2, "never"], + quotes: [2, "double"] + } + }; + const actual = getConfig(factory, targetPath); - it("should cascade", () => { - const targetPath = getFakeFixturePath("overrides", "foo.js"); - const factory = new CascadingConfigArrayFactory({ - cwd: getFakeFixturePath("overrides"), - baseConfig: { - overrides: [ - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "single"] - } - }, - { - files: "foo.js", - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } - } - ] - }, - useEslintrc: false - }); - const expected = { - rules: { - semi: [2, "never"], - quotes: [2, "double"] - } - }; - const actual = getConfig(factory, targetPath); - - assertConfigsEqual(actual, expected); - }); + assertConfigsEqual(actual, expected); }); + }); - describe("deprecation warnings", () => { - const cwd = path.resolve(__dirname, "../../fixtures/config-file/"); - let warning = null; + describe("deprecation warnings", () => { + const cwd = path.resolve(__dirname, "../../fixtures/config-file/"); + let warning = null; - function onWarning(w) { // eslint-disable-line require-jsdoc + function onWarning(w) { // eslint-disable-line require-jsdoc - // Node.js 6.x does not have 'w.code' property. - if (!Object.prototype.hasOwnProperty.call(w, "code") || typeof w.code === "string" && w.code.startsWith("ESLINT_")) { - warning = w; - } + // Node.js 6.x does not have 'w.code' property. + if (!Object.prototype.hasOwnProperty.call(w, "code") || typeof w.code === "string" && w.code.startsWith("ESLINT_")) { + warning = w; } + } - /** @type {CascadingConfigArrayFactory} */ - let factory; + /** @type {CascadingConfigArrayFactory} */ + let factory; - beforeEach(() => { - factory = new CascadingConfigArrayFactory({ cwd }); - warning = null; - process.on("warning", onWarning); - }); - afterEach(() => { - process.removeListener("warning", onWarning); - }); + beforeEach(() => { + factory = new CascadingConfigArrayFactory({ cwd }); + warning = null; + process.on("warning", onWarning); + }); + afterEach(() => { + process.removeListener("warning", onWarning); + }); - it("should emit a deprecation warning if 'ecmaFeatures' is given.", async() => { - getConfig(factory, "ecma-features/test.js"); + it("should emit a deprecation warning if 'ecmaFeatures' is given.", async() => { + getConfig(factory, "ecma-features/test.js"); - // Wait for "warning" event. - await nextTick(); + // Wait for "warning" event. + await nextTick(); - assert.notStrictEqual(warning, null); - assert.strictEqual( - warning.message, - `The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` - ); - }); + assert.notStrictEqual(warning, null); + assert.strictEqual( + warning.message, + `The 'ecmaFeatures' config file property is deprecated, and has no effect. (found in "ecma-features${path.sep}.eslintrc.yml")` + ); }); }); }); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index ed75a2d8e3b..01c549ba726 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -1213,973 +1213,974 @@ describe("ConfigArrayFactory", () => { }); }); - describe("Moved from tests/lib/config/config-file.js", () => { - describe("applyExtends()", () => { - const files = { - "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", - "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", - "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", - "node_modules/eslint-plugin-invalid-parser/index.js": "exports.configs = { foo: { parser: 'nonexistent-parser' } }", - "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", - "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };", - "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }", - "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }", - "yaml/.eslintrc.yaml": "env:\n browser: true" - }; - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files }); - const factory = new ConfigArrayFactory(); + // This group moved from 'tests/lib/config/config-file.js' when refactoring to keep the cumulated test cases. + describe("'extends' property should handle the content of extended configs properly.", () => { + const files = { + "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", + "node_modules/eslint-config-one/index.js": "module.exports = { extends: 'two', env: { browser: true } }", + "node_modules/eslint-config-two/index.js": "module.exports = { env: { node: true } }", + "node_modules/eslint-plugin-invalid-parser/index.js": "exports.configs = { foo: { parser: 'nonexistent-parser' } }", + "node_modules/eslint-plugin-invalid-config/index.js": "exports.configs = { foo: {} }", + "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };", + "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }", + "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }", + "yaml/.eslintrc.yaml": "env:\n browser: true" + }; + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files }); + const factory = new ConfigArrayFactory(); - /** - * Apply `extends` property. - * @param {Object} configData The config that has `extends` property. - * @param {string} [filePath] The path to the config data. - * @returns {Object} The applied config data. - */ - function applyExtends(configData, filePath = "whatever") { - return factory - .create(configData, { filePath }) - .extractConfig(filePath) - .toCompatibleObjectAsConfigFileContent(); - } + /** + * Apply `extends` property. + * @param {Object} configData The config that has `extends` property. + * @param {string} [filePath] The path to the config data. + * @returns {Object} The applied config data. + */ + function applyExtends(configData, filePath = "whatever") { + return factory + .create(configData, { filePath }) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + } + + it("should apply extension 'foo' when specified from root directory config", () => { + const config = applyExtends({ + extends: "foo", + rules: { eqeqeq: 2 } + }); + + assertConfig(config, { + env: { browser: true }, + rules: { eqeqeq: [2] } + }); + }); + + it("should apply all rules when extends config includes 'eslint:all'", () => { + const config = applyExtends({ + extends: "eslint:all" + }); + + assert.strictEqual(config.rules.eqeqeq[0], "error"); + assert.strictEqual(config.rules.curly[0], "error"); + }); - it("should apply extension 'foo' when specified from root directory config", () => { - const config = applyExtends({ - extends: "foo", + it("should throw an error when extends config module is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "not-exist", rules: { eqeqeq: 2 } }); + }, /Failed to load config "not-exist" to extend from./u); + }); - assertConfig(config, { - env: { browser: true }, - rules: { eqeqeq: [2] } + it("should throw an error when an eslint config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "eslint:foo", + rules: { eqeqeq: 2 } }); - }); + }, /Failed to load config "eslint:foo" to extend from./u); + }); - it("should apply all rules when extends config includes 'eslint:all'", () => { - const config = applyExtends({ - extends: "eslint:all" + it("should throw an error when a parser in a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-parser/foo", + rules: { eqeqeq: 2 } }); + }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); + }); - assert.strictEqual(config.rules.eqeqeq[0], "error"); - assert.strictEqual(config.rules.curly[0], "error"); - }); + it("should fall back to default parser when a parser called 'espree' is not found", () => { + const config = applyExtends({ parser: "espree" }); - it("should throw an error when extends config module is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "not-exist", - rules: { eqeqeq: 2 } - }); - }, /Failed to load config "not-exist" to extend from./u); - }); - - it("should throw an error when an eslint config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "eslint:foo", - rules: { eqeqeq: 2 } - }); - }, /Failed to load config "eslint:foo" to extend from./u); + assertConfig(config, { + parser: require.resolve("espree") }); + }); - it("should throw an error when a parser in a plugin config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "plugin:invalid-parser/foo", - rules: { eqeqeq: 2 } - }); - }, /Failed to load parser 'nonexistent-parser' declared in 'whatever » plugin:invalid-parser\/foo'/u); - }); + it("should throw an error when a plugin config is not found", () => { + assert.throws(() => { + applyExtends({ + extends: "plugin:invalid-config/bar", + rules: { eqeqeq: 2 } + }); + }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + }); - it("should fall back to default parser when a parser called 'espree' is not found", () => { - const config = applyExtends({ parser: "espree" }); + it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { + try { + applyExtends({ + extends: "plugin:nonexistent-plugin/baz", + rules: { eqeqeq: 2 } + }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + pluginRootPath: process.cwd(), + importerName: "whatever" + }); + return; + } + assert.fail("Expected to throw an error"); + }); - assertConfig(config, { - parser: require.resolve("espree") + it("should throw an error with a message template when a plugin in the plugins list is not found", () => { + try { + applyExtends({ + plugins: ["nonexistent-plugin"] }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + pluginName: "eslint-plugin-nonexistent-plugin", + pluginRootPath: process.cwd(), + importerName: "whatever" + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should apply extensions recursively when specified from package", () => { + const config = applyExtends({ + extends: "one", + rules: { eqeqeq: 2 } }); - it("should throw an error when a plugin config is not found", () => { - assert.throws(() => { - applyExtends({ - extends: "plugin:invalid-config/bar", - rules: { eqeqeq: 2 } - }); - }, /Failed to load config "plugin:invalid-config\/bar" to extend from./u); + assertConfig(config, { + env: { browser: true, node: true }, + rules: { eqeqeq: [2] } }); + }); - it("should throw an error with a message template when a plugin referenced for a plugin config is not found", () => { - try { - applyExtends({ - extends: "plugin:nonexistent-plugin/baz", - rules: { eqeqeq: 2 } - }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - pluginRootPath: process.cwd(), - importerName: "whatever" - }); - return; + it("should apply extensions when specified from a JavaScript file", () => { + const config = applyExtends({ + extends: ".eslintrc.js", + rules: { eqeqeq: 2 } + }, "js/foo.js"); + + assertConfig(config, { + rules: { + semi: [2, "always"], + eqeqeq: [2] } - assert.fail("Expected to throw an error"); }); + }); - it("should throw an error with a message template when a plugin in the plugins list is not found", () => { - try { - applyExtends({ - plugins: ["nonexistent-plugin"] - }); - } catch (err) { - assert.strictEqual(err.messageTemplate, "plugin-missing"); - assert.deepStrictEqual(err.messageData, { - pluginName: "eslint-plugin-nonexistent-plugin", - pluginRootPath: process.cwd(), - importerName: "whatever" - }); - return; + it("should apply extensions when specified from a YAML file", () => { + const config = applyExtends({ + extends: ".eslintrc.yaml", + rules: { eqeqeq: 2 } + }, "yaml/foo.js"); + + assertConfig(config, { + env: { browser: true }, + rules: { + eqeqeq: [2] } - assert.fail("Expected to throw an error"); }); + }); - it("should apply extensions recursively when specified from package", () => { - const config = applyExtends({ - extends: "one", - rules: { eqeqeq: 2 } - }); + it("should apply extensions when specified from a JSON file", () => { + const config = applyExtends({ + extends: ".eslintrc.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); - assertConfig(config, { - env: { browser: true, node: true }, - rules: { eqeqeq: [2] } - }); + assertConfig(config, { + rules: { + eqeqeq: [2], + quotes: [2, "double"] + } }); + }); - it("should apply extensions when specified from a JavaScript file", () => { - const config = applyExtends({ - extends: ".eslintrc.js", - rules: { eqeqeq: 2 } - }, "js/foo.js"); + it("should apply extensions when specified from a package.json file in a sibling directory", () => { + const config = applyExtends({ + extends: "../package-json/package.json", + rules: { eqeqeq: 2 } + }, "json/foo.js"); - assertConfig(config, { - rules: { - semi: [2, "always"], - eqeqeq: [2] - } - }); + assertConfig(config, { + env: { es6: true }, + rules: { + eqeqeq: [2] + } }); + }); + }); - it("should apply extensions when specified from a YAML file", () => { - const config = applyExtends({ - extends: ".eslintrc.yaml", - rules: { eqeqeq: 2 } - }, "yaml/foo.js"); + // This group moved from 'tests/lib/config/config-file.js' when refactoring to keep the cumulated test cases. + describe("loading config files should work properly.", () => { - assertConfig(config, { - env: { browser: true }, - rules: { - eqeqeq: [2] - } - }); - }); + /** + * Load a given config file. + * @param {ConfigArrayFactory} factory The factory to load. + * @param {string} filePath The path to a config file. + * @returns {Object} The applied config data. + */ + function load(factory, filePath) { + return factory + .loadFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + } - it("should apply extensions when specified from a JSON file", () => { - const config = applyExtends({ - extends: ".eslintrc.json", - rules: { eqeqeq: 2 } - }, "json/foo.js"); + it("should throw error if file doesnt exist", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); - assertConfig(config, { - rules: { - eqeqeq: [2], - quotes: [2, "double"] - } - }); + assert.throws(() => { + load(factory, "legacy/nofile.js"); }); - it("should apply extensions when specified from a package.json file in a sibling directory", () => { - const config = applyExtends({ - extends: "../package-json/package.json", - rules: { eqeqeq: 2 } - }, "json/foo.js"); - - assertConfig(config, { - env: { es6: true }, - rules: { - eqeqeq: [2] - } - }); + assert.throws(() => { + load(factory, "legacy/package.json"); }); }); - describe("load()", () => { + it("should load information from a legacy file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "legacy/.eslintrc": "{ rules: { eqeqeq: 2 } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "legacy/.eslintrc"); - /** - * Load a given config file. - * @param {ConfigArrayFactory} factory The factory to load. - * @param {string} filePath The path to a config file. - * @returns {Object} The applied config data. - */ - function load(factory, filePath) { - return factory - .loadFile(filePath) - .extractConfig(filePath) - .toCompatibleObjectAsConfigFileContent(); - } + assertConfig(config, { + rules: { + eqeqeq: [2] + } + }); + }); - it("should throw error if file doesnt exist", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); - const factory = new ConfigArrayFactory(); + it("should load information from a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.js"); - assert.throws(() => { - load(factory, "legacy/nofile.js"); - }); + assertConfig(config, { + rules: { + semi: [2, "always"] + } + }); + }); - assert.throws(() => { - load(factory, "legacy/package.json"); - }); + it("should throw error when loading invalid JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.broken.js": "module.exports = { rules: { semi: [2, 'always'] }" + } }); + const factory = new ConfigArrayFactory(); - it("should load information from a legacy file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "legacy/.eslintrc": "{ rules: { eqeqeq: 2 } }" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "legacy/.eslintrc"); + assert.throws(() => { + load(factory, "js/.eslintrc.broken.js"); + }, /Cannot read config file/u); + }); - assertConfig(config, { - rules: { - eqeqeq: [2] - } - }); + it("should interpret parser module name when present in a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "node_modules/foo/index.js": "", + "js/node_modules/foo/index.js": "", + "js/.eslintrc.parser.js": `module.exports = { + parser: 'foo', + rules: { semi: [2, 'always'] } + };` + } }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.parser.js"); - it("should load information from a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "js/.eslintrc.js"); - - assertConfig(config, { - rules: { - semi: [2, "always"] - } - }); + assertConfig(config, { + parser: path.resolve("js/node_modules/foo/index.js"), + rules: { + semi: [2, "always"] + } }); + }); - it("should throw error when loading invalid JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "js/.eslintrc.broken.js": "module.exports = { rules: { semi: [2, 'always'] }" - } - }); - const factory = new ConfigArrayFactory(); + it("should interpret parser path when present in a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.parser2.js": `module.exports = { + parser: './not-a-config.js', + rules: { semi: [2, 'always'] } + };`, + "js/not-a-config.js": "" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.parser2.js"); - assert.throws(() => { - load(factory, "js/.eslintrc.broken.js"); - }, /Cannot read config file/u); + assertConfig(config, { + parser: path.resolve("js/not-a-config.js"), + rules: { + semi: [2, "always"] + } }); + }); - it("should interpret parser module name when present in a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "node_modules/foo/index.js": "", - "js/node_modules/foo/index.js": "", - "js/.eslintrc.parser.js": `module.exports = { - parser: 'foo', - rules: { semi: [2, 'always'] } - };` - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "js/.eslintrc.parser.js"); + it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "js/.eslintrc.parser3.js": `module.exports = { + parser: 'espree', + rules: { semi: [2, 'always'] } + };` + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "js/.eslintrc.parser3.js"); - assertConfig(config, { - parser: path.resolve("js/node_modules/foo/index.js"), - rules: { - semi: [2, "always"] - } - }); + assertConfig(config, { + parser: require.resolve("espree"), + rules: { + semi: [2, "always"] + } }); + }); - it("should interpret parser path when present in a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "js/.eslintrc.parser2.js": `module.exports = { - parser: './not-a-config.js', - rules: { semi: [2, 'always'] } - };`, - "js/not-a-config.js": "" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "js/.eslintrc.parser2.js"); + it("should load information from a JSON file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "json/.eslintrc.json"); - assertConfig(config, { - parser: path.resolve("js/not-a-config.js"), - rules: { - semi: [2, "always"] - } - }); + assertConfig(config, { + rules: { + quotes: [2, "double"] + } }); + }); - it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "js/.eslintrc.parser3.js": `module.exports = { - parser: 'espree', - rules: { semi: [2, 'always'] } - };` - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "js/.eslintrc.parser3.js"); + it("should load fresh information from a JSON file", () => { + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + const initialConfig = { + rules: { + quotes: [2, "double"] + } + }; + const updatedConfig = { + rules: { + quotes: [0] + } + }; + let config; - assertConfig(config, { - parser: require.resolve("espree"), - rules: { - semi: [2, "always"] - } - }); + fs.writeFileSync("fresh-test.json", JSON.stringify(initialConfig)); + config = load(factory, "fresh-test.json"); + assertConfig(config, initialConfig); + + fs.writeFileSync("fresh-test.json", JSON.stringify(updatedConfig)); + config = load(factory, "fresh-test.json"); + assertConfig(config, updatedConfig); + }); + + it("should load information from a package.json file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" + } }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "package-json/package.json"); - it("should load information from a JSON file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "json/.eslintrc.json"); + assertConfig(config, { + env: { es6: true } + }); + }); - assertConfig(config, { - rules: { - quotes: [2, "double"] - } - }); + it("should throw error when loading invalid package.json file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "broken-package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } }" + } }); + const factory = new ConfigArrayFactory(); - it("should load fresh information from a JSON file", () => { - const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); - const factory = new ConfigArrayFactory(); - const initialConfig = { + assert.throws(() => { + try { + load(factory, "broken-package-json/package.json"); + } catch (error) { + assert.strictEqual(error.messageTemplate, "failed-to-read-json"); + throw error; + } + }, /Cannot read config file/u); + }); + + it("should load fresh information from a package.json file", () => { + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + const initialConfig = { + eslintConfig: { rules: { quotes: [2, "double"] } - }; - const updatedConfig = { + } + }; + const updatedConfig = { + eslintConfig: { rules: { quotes: [0] } - }; - let config; + } + }; + let config; + + fs.writeFileSync("package.json", JSON.stringify(initialConfig)); + config = load(factory, "package.json"); + assertConfig(config, initialConfig.eslintConfig); + + fs.writeFileSync("package.json", JSON.stringify(updatedConfig)); + config = load(factory, "package.json"); + assertConfig(config, updatedConfig.eslintConfig); + }); + + it("should load fresh information from a .eslintrc.js file", () => { + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const factory = new ConfigArrayFactory(); + const initialConfig = { + rules: { + quotes: [2, "double"] + } + }; + const updatedConfig = { + rules: { + quotes: [0] + } + }; + let config; + + fs.writeFileSync(".eslintrc.js", `module.exports = ${JSON.stringify(initialConfig)}`); + config = load(factory, ".eslintrc.js"); + assertConfig(config, initialConfig); - fs.writeFileSync("fresh-test.json", JSON.stringify(initialConfig)); - config = load(factory, "fresh-test.json"); - assertConfig(config, initialConfig); + fs.writeFileSync(".eslintrc.js", `module.exports = ${JSON.stringify(updatedConfig)}`); + config = load(factory, ".eslintrc.js"); + assertConfig(config, updatedConfig); + }); - fs.writeFileSync("fresh-test.json", JSON.stringify(updatedConfig)); - config = load(factory, "fresh-test.json"); - assertConfig(config, updatedConfig); + it("should load information from a YAML file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "yaml/.eslintrc.yaml": "env:\n browser: true" + } }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "yaml/.eslintrc.yaml"); - it("should load information from a package.json file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "package-json/package.json"); + assertConfig(config, { + env: { browser: true } + }); + }); - assertConfig(config, { - env: { es6: true } - }); + it("should load information from an empty YAML file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "yaml/.eslintrc.empty.yaml": "{}" + } }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "yaml/.eslintrc.empty.yaml"); - it("should throw error when loading invalid package.json file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "broken-package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } }" - } - }); - const factory = new ConfigArrayFactory(); + assertConfig(config, {}); + }); - assert.throws(() => { - try { - load(factory, "broken-package-json/package.json"); - } catch (error) { - assert.strictEqual(error.messageTemplate, "failed-to-read-json"); - throw error; - } - }, /Cannot read config file/u); + it("should load information from a YML file", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "yml/.eslintrc.yml": "env:\n node: true" + } }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "yml/.eslintrc.yml"); - it("should load fresh information from a package.json file", () => { - const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); - const factory = new ConfigArrayFactory(); - const initialConfig = { - eslintConfig: { - rules: { - quotes: [2, "double"] - } - } - }; - const updatedConfig = { - eslintConfig: { - rules: { - quotes: [0] - } - } - }; - let config; + assertConfig(config, { + env: { node: true } + }); + }); - fs.writeFileSync("package.json", JSON.stringify(initialConfig)); - config = load(factory, "package.json"); - assertConfig(config, initialConfig.eslintConfig); + it("should load information from a YML file and apply extensions", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends/.eslintrc.yml": "extends: ../package-json/package.json\nrules:\n booya: 2", + "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends/.eslintrc.yml"); - fs.writeFileSync("package.json", JSON.stringify(updatedConfig)); - config = load(factory, "package.json"); - assertConfig(config, updatedConfig.eslintConfig); + assertConfig(config, { + env: { es6: true }, + rules: { booya: [2] } }); + }); - it("should load fresh information from a .eslintrc.js file", () => { - const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); - const factory = new ConfigArrayFactory(); - const initialConfig = { - rules: { - quotes: [2, "double"] - } - }; - const updatedConfig = { - rules: { - quotes: [0] + it("should load information from `extends` chain.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain": { + "node_modules/eslint-config-a": { + "node_modules/eslint-config-b": { + "node_modules/eslint-config-c": { + "index.js": "module.exports = { rules: { c: 2 } };" + }, + "index.js": "module.exports = { extends: 'c', rules: { b: 2 } };" + }, + "index.js": "module.exports = { extends: 'b', rules: { a: 2 } };" + }, + ".eslintrc.json": "{ \"extends\": \"a\" }" } - }; - let config; - - fs.writeFileSync(".eslintrc.js", `module.exports = ${JSON.stringify(initialConfig)}`); - config = load(factory, ".eslintrc.js"); - assertConfig(config, initialConfig); + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain/.eslintrc.json"); - fs.writeFileSync(".eslintrc.js", `module.exports = ${JSON.stringify(updatedConfig)}`); - config = load(factory, ".eslintrc.js"); - assertConfig(config, updatedConfig); + assertConfig(config, { + rules: { + a: [2], // from node_modules/eslint-config-a + b: [2], // from node_modules/eslint-config-a/node_modules/eslint-config-b + c: [2] // from node_modules/eslint-config-a/node_modules/eslint-config-b/node_modules/eslint-config-c + } }); + }); - it("should load information from a YAML file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "yaml/.eslintrc.yaml": "env:\n browser: true" + it("should load information from `extends` chain with relative path.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain-2": { + "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", + "node_modules/eslint-config-a/relative.js": "module.exports = { rules: { relative: 2 } };", + ".eslintrc.json": "{ \"extends\": \"a\" }" } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "yaml/.eslintrc.yaml"); + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain-2/.eslintrc.json"); - assertConfig(config, { - env: { browser: true } - }); + assertConfig(config, { + rules: { + a: [2], // from node_modules/eslint-config-a/index.js + relative: [2] // from node_modules/eslint-config-a/relative.js + } }); + }); - it("should load information from an empty YAML file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "yaml/.eslintrc.empty.yaml": "{}" + it("should load information from `extends` chain in .eslintrc with relative path.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain-2": { + "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", + "node_modules/eslint-config-a/relative.js": "module.exports = { rules: { relative: 2 } };", + "relative.eslintrc.json": "{ \"extends\": \"./node_modules/eslint-config-a/index.js\" }" } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "yaml/.eslintrc.empty.yaml"); + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain-2/relative.eslintrc.json"); - assertConfig(config, {}); + assertConfig(config, { + rules: { + a: [2], // from node_modules/eslint-config-a/index.js + relative: [2] // from node_modules/eslint-config-a/relative.js + } }); + }); - it("should load information from a YML file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "yml/.eslintrc.yml": "env:\n node: true" + it("should load information from `parser` in .eslintrc with relative path.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + files: { + "extends-chain-2": { + "parser.eslintrc.json": "{ \"parser\": \"./parser.js\" }", + "parser.js": "" } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "yml/.eslintrc.yml"); + } + }); + const factory = new ConfigArrayFactory(); + const config = load(factory, "extends-chain-2/parser.eslintrc.json"); - assertConfig(config, { - env: { node: true } - }); + assertConfig(config, { + parser: path.resolve("extends-chain-2/parser.js") }); + }); - it("should load information from a YML file and apply extensions", () => { + describe("Plugins", () => { + it("should load information from a YML file and load plugins", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files: { - "extends/.eslintrc.yml": "extends: ../package-json/package.json\nrules:\n booya: 2", - "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" + "node_modules/eslint-plugin-test/index.js": ` + module.exports = { + environments: { + bar: { globals: { bar: true } } + } + } + `, + "plugins/.eslintrc.yml": ` + plugins: + - test + rules: + test/foo: 2 + env: + test/bar: true + ` } }); const factory = new ConfigArrayFactory(); - const config = load(factory, "extends/.eslintrc.yml"); + const config = load(factory, "plugins/.eslintrc.yml"); assertConfig(config, { - env: { es6: true }, - rules: { booya: [2] } + env: { "test/bar": true }, + plugins: ["test"], + rules: { + "test/foo": [2] + } }); }); - it("should load information from `extends` chain.", () => { + it("should load two separate configs from a plugin", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files: { - "extends-chain": { - "node_modules/eslint-config-a": { - "node_modules/eslint-config-b": { - "node_modules/eslint-config-c": { - "index.js": "module.exports = { rules: { c: 2 } };" - }, - "index.js": "module.exports = { extends: 'c', rules: { b: 2 } };" - }, - "index.js": "module.exports = { extends: 'b', rules: { a: 2 } };" - }, - ".eslintrc.json": "{ \"extends\": \"a\" }" - } + "node_modules/eslint-plugin-test/index.js": ` + module.exports = { + configs: { + foo: { rules: { semi: 2, quotes: 1 } }, + bar: { rules: { quotes: 2, yoda: 2 } } + } + } + `, + "plugins/.eslintrc.yml": ` + extends: + - plugin:test/foo + - plugin:test/bar + ` } }); const factory = new ConfigArrayFactory(); - const config = load(factory, "extends-chain/.eslintrc.json"); + const config = load(factory, "plugins/.eslintrc.yml"); assertConfig(config, { rules: { - a: [2], // from node_modules/eslint-config-a - b: [2], // from node_modules/eslint-config-a/node_modules/eslint-config-b - c: [2] // from node_modules/eslint-config-a/node_modules/eslint-config-b/node_modules/eslint-config-c + semi: [2], + quotes: [2], + yoda: [2] } }); }); + }); - it("should load information from `extends` chain with relative path.", () => { + describe("even if config files have Unicode BOM,", () => { + it("should read the JSON config file correctly.", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files: { - "extends-chain-2": { - "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", - "node_modules/eslint-config-a/relative.js": "module.exports = { rules: { relative: 2 } };", - ".eslintrc.json": "{ \"extends\": \"a\" }" - } + "bom/.eslintrc.json": "\uFEFF{ \"rules\": { \"semi\": \"error\" } }" } }); const factory = new ConfigArrayFactory(); - const config = load(factory, "extends-chain-2/.eslintrc.json"); + const config = load(factory, "bom/.eslintrc.json"); assertConfig(config, { rules: { - a: [2], // from node_modules/eslint-config-a/index.js - relative: [2] // from node_modules/eslint-config-a/relative.js + semi: ["error"] } }); }); - it("should load information from `extends` chain in .eslintrc with relative path.", () => { + it("should read the YAML config file correctly.", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files: { - "extends-chain-2": { - "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", - "node_modules/eslint-config-a/relative.js": "module.exports = { rules: { relative: 2 } };", - "relative.eslintrc.json": "{ \"extends\": \"./node_modules/eslint-config-a/index.js\" }" - } + "bom/.eslintrc.yaml": "\uFEFFrules:\n semi: error" } }); const factory = new ConfigArrayFactory(); - const config = load(factory, "extends-chain-2/relative.eslintrc.json"); + const config = load(factory, "bom/.eslintrc.yaml"); assertConfig(config, { rules: { - a: [2], // from node_modules/eslint-config-a/index.js - relative: [2] // from node_modules/eslint-config-a/relative.js + semi: ["error"] } }); }); - it("should load information from `parser` in .eslintrc with relative path.", () => { + it("should read the config in package.json correctly.", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files: { - "extends-chain-2": { - "parser.eslintrc.json": "{ \"parser\": \"./parser.js\" }", - "parser.js": "" - } + "bom/package.json": "\uFEFF{ \"eslintConfig\": { \"rules\": { \"semi\": \"error\" } } }" } }); const factory = new ConfigArrayFactory(); - const config = load(factory, "extends-chain-2/parser.eslintrc.json"); + const config = load(factory, "bom/package.json"); assertConfig(config, { - parser: path.resolve("extends-chain-2/parser.js") - }); - }); - - describe("Plugins", () => { - it("should load information from a YML file and load plugins", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "node_modules/eslint-plugin-test/index.js": ` - module.exports = { - environments: { - bar: { globals: { bar: true } } - } - } - `, - "plugins/.eslintrc.yml": ` - plugins: - - test - rules: - test/foo: 2 - env: - test/bar: true - ` - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "plugins/.eslintrc.yml"); - - assertConfig(config, { - env: { "test/bar": true }, - plugins: ["test"], - rules: { - "test/foo": [2] - } - }); - }); - - it("should load two separate configs from a plugin", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "node_modules/eslint-plugin-test/index.js": ` - module.exports = { - configs: { - foo: { rules: { semi: 2, quotes: 1 } }, - bar: { rules: { quotes: 2, yoda: 2 } } - } - } - `, - "plugins/.eslintrc.yml": ` - extends: - - plugin:test/foo - - plugin:test/bar - ` - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "plugins/.eslintrc.yml"); - - assertConfig(config, { - rules: { - semi: [2], - quotes: [2], - yoda: [2] - } - }); - }); - }); - - describe("even if config files have Unicode BOM,", () => { - it("should read the JSON config file correctly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "bom/.eslintrc.json": "\uFEFF{ \"rules\": { \"semi\": \"error\" } }" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "bom/.eslintrc.json"); - - assertConfig(config, { - rules: { - semi: ["error"] - } - }); - }); - - it("should read the YAML config file correctly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "bom/.eslintrc.yaml": "\uFEFFrules:\n semi: error" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "bom/.eslintrc.yaml"); - - assertConfig(config, { - rules: { - semi: ["error"] - } - }); - }); - - it("should read the config in package.json correctly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "bom/package.json": "\uFEFF{ \"eslintConfig\": { \"rules\": { \"semi\": \"error\" } } }" - } - }); - const factory = new ConfigArrayFactory(); - const config = load(factory, "bom/package.json"); - - assertConfig(config, { - rules: { - semi: ["error"] - } - }); - }); - }); - - it("throws an error including the config file name if the config file is invalid", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - files: { - "invalid/invalid-top-level-property.yml": "invalidProperty: 3" + rules: { + semi: ["error"] } }); - const factory = new ConfigArrayFactory(); - - try { - load(factory, "invalid/invalid-top-level-property.yml"); - } catch (err) { - assert.include(err.message, `ESLint configuration in ${`invalid${path.sep}invalid-top-level-property.yml`} is invalid`); - return; - } - assert.fail(); }); }); - describe("resolve()", () => { + it("throws an error including the config file name if the config file is invalid", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - cwd: () => tempDir, files: { - "node_modules/eslint-config-foo/index.js": "", - "node_modules/eslint-config-foo/bar.js": "", - "node_modules/eslint-config-eslint-configfoo/index.js": "", - "node_modules/@foo/eslint-config/index.js": "", - "node_modules/@foo/eslint-config-bar/index.js": "", - "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: {} }", - "node_modules/@foo/eslint-plugin/index.js": "exports.configs = { bar: {} }", - "node_modules/@foo/eslint-plugin-bar/index.js": "exports.configs = { baz: {} }", - "foo/bar/.eslintrc": "", - ".eslintrc": "" + "invalid/invalid-top-level-property.yml": "invalidProperty: 3" } }); const factory = new ConfigArrayFactory(); - /** - * Resolve `extends` module. - * @param {string} request The module name to resolve. - * @param {string} [relativeTo] The importer path to resolve. - * @returns {string} The resolved path. - */ - function resolve(request, relativeTo) { - return factory.create( - { extends: request }, - { filePath: relativeTo } - )[0]; + try { + load(factory, "invalid/invalid-top-level-property.yml"); + } catch (err) { + assert.include(err.message, `ESLint configuration in ${`invalid${path.sep}invalid-top-level-property.yml`} is invalid`); + return; } - - describe("Relative to CWD", () => { - for (const { input, expected } of [ - { input: ".eslintrc", expected: path.resolve(tempDir, ".eslintrc") }, - { input: "eslint-config-foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, - { input: "eslint-config-foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, - { input: "foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, - { input: "foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, - { input: "eslint-configfoo", expected: path.resolve(tempDir, "node_modules/eslint-config-eslint-configfoo/index.js") }, - { input: "@foo/eslint-config", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, - { input: "@foo", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, - { input: "@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config-bar/index.js") }, - { input: "plugin:foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-plugin-foo/index.js") }, - { input: "plugin:@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin/index.js") }, - { input: "plugin:@foo/bar/baz", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin-bar/index.js") } - ]) { - it(`should return ${expected} when passed ${input}`, () => { - const result = resolve(input); - - assert.strictEqual(result.filePath, expected); - }); - } - }); - - describe("Relative to config file", () => { - const relativePath = path.resolve(tempDir, "./foo/bar/.eslintrc"); - - for (const { input, expected } of [ - { input: ".eslintrc", expected: path.join(path.dirname(relativePath), ".eslintrc") }, - { input: "eslint-config-foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, - { input: "eslint-config-foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, - { input: "foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, - { input: "foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, - { input: "eslint-configfoo", expected: path.resolve(tempDir, "node_modules/eslint-config-eslint-configfoo/index.js") }, - { input: "@foo/eslint-config", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, - { input: "@foo", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, - { input: "@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config-bar/index.js") }, - { input: "plugin:foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-plugin-foo/index.js") }, - { input: "plugin:@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin/index.js") }, - { input: "plugin:@foo/bar/baz", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin-bar/index.js") } - ]) { - it(`should return ${expected} when passed ${input}`, () => { - const result = resolve(input, relativePath); - - assert.strictEqual(result.filePath, expected); - }); - } - }); + assert.fail(); }); }); - describe("Moved from tests/lib/config/plugins.js", () => { - describe("load()", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - cwd: () => tempDir, - files: { - "node_modules/@scope/eslint-plugin-example/index.js": "exports.name = '@scope/eslint-plugin-example';", - "node_modules/eslint-plugin-example/index.js": "exports.name = 'eslint-plugin-example';", - "node_modules/eslint-plugin-throws-on-load/index.js": "throw new Error('error thrown while loading this module')" - } - }); - const factory = new ConfigArrayFactory(); + // This group moved from 'tests/lib/config/config-file.js' when refactoring to keep the cumulated test cases. + describe("'extends' property should resolve the location of configs properly.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/eslint-config-foo/index.js": "", + "node_modules/eslint-config-foo/bar.js": "", + "node_modules/eslint-config-eslint-configfoo/index.js": "", + "node_modules/@foo/eslint-config/index.js": "", + "node_modules/@foo/eslint-config-bar/index.js": "", + "node_modules/eslint-plugin-foo/index.js": "exports.configs = { bar: {} }", + "node_modules/@foo/eslint-plugin/index.js": "exports.configs = { bar: {} }", + "node_modules/@foo/eslint-plugin-bar/index.js": "exports.configs = { baz: {} }", + "foo/bar/.eslintrc": "", + ".eslintrc": "" + } + }); + const factory = new ConfigArrayFactory(); - /** - * Load a plugin. - * @param {string} request A request to load a plugin. - * @returns {Map} The loaded plugins. - */ - function load(request) { - const config = factory.create({ plugins: [request] }); - - return new Map( - Object - .entries(config[0].plugins) - .map(([id, entry]) => { - if (entry.error) { - throw entry.error; - } - return [id, entry.definition]; - }) - ); + /** + * Resolve `extends` module. + * @param {string} request The module name to resolve. + * @param {string} [relativeTo] The importer path to resolve. + * @returns {string} The resolved path. + */ + function resolve(request, relativeTo) { + return factory.create( + { extends: request }, + { filePath: relativeTo } + )[0]; + } + + describe("Relative to CWD", () => { + for (const { input, expected } of [ + { input: ".eslintrc", expected: path.resolve(tempDir, ".eslintrc") }, + { input: "eslint-config-foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "eslint-config-foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "eslint-configfoo", expected: path.resolve(tempDir, "node_modules/eslint-config-eslint-configfoo/index.js") }, + { input: "@foo/eslint-config", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config-bar/index.js") }, + { input: "plugin:foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-plugin-foo/index.js") }, + { input: "plugin:@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin/index.js") }, + { input: "plugin:@foo/bar/baz", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin-bar/index.js") } + ]) { + it(`should return ${expected} when passed ${input}`, () => { + const result = resolve(input); + + assert.strictEqual(result.filePath, expected); + }); + } + }); + + describe("Relative to config file", () => { + const relativePath = path.resolve(tempDir, "./foo/bar/.eslintrc"); + + for (const { input, expected } of [ + { input: ".eslintrc", expected: path.join(path.dirname(relativePath), ".eslintrc") }, + { input: "eslint-config-foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "eslint-config-foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "foo", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/index.js") }, + { input: "foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-config-foo/bar.js") }, + { input: "eslint-configfoo", expected: path.resolve(tempDir, "node_modules/eslint-config-eslint-configfoo/index.js") }, + { input: "@foo/eslint-config", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config/index.js") }, + { input: "@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-config-bar/index.js") }, + { input: "plugin:foo/bar", expected: path.resolve(tempDir, "node_modules/eslint-plugin-foo/index.js") }, + { input: "plugin:@foo/bar", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin/index.js") }, + { input: "plugin:@foo/bar/baz", expected: path.resolve(tempDir, "node_modules/@foo/eslint-plugin-bar/index.js") } + ]) { + it(`should return ${expected} when passed ${input}`, () => { + const result = resolve(input, relativePath); + + assert.strictEqual(result.filePath, expected); + }); } + }); + }); - it("should load a plugin when referenced by short name", () => { - const loadedPlugins = load("example"); + // This group moved from 'tests/lib/config/plugins.js' when refactoring to keep the cumulated test cases. + describe("'plugins' property should load a correct plugin.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/@scope/eslint-plugin-example/index.js": "exports.name = '@scope/eslint-plugin-example';", + "node_modules/eslint-plugin-example/index.js": "exports.name = 'eslint-plugin-example';", + "node_modules/eslint-plugin-throws-on-load/index.js": "throw new Error('error thrown while loading this module')" + } + }); + const factory = new ConfigArrayFactory(); - assert.deepStrictEqual( - loadedPlugins.get("example"), - { name: "eslint-plugin-example" } - ); - }); + /** + * Load a plugin. + * @param {string} request A request to load a plugin. + * @returns {Map} The loaded plugins. + */ + function load(request) { + const config = factory.create({ plugins: [request] }); + + return new Map( + Object + .entries(config[0].plugins) + .map(([id, entry]) => { + if (entry.error) { + throw entry.error; + } + return [id, entry.definition]; + }) + ); + } - it("should load a plugin when referenced by long name", () => { - const loadedPlugins = load("eslint-plugin-example"); + it("should load a plugin when referenced by short name", () => { + const loadedPlugins = load("example"); - assert.deepStrictEqual( - loadedPlugins.get("example"), - { name: "eslint-plugin-example" } - ); - }); + assert.deepStrictEqual( + loadedPlugins.get("example"), + { name: "eslint-plugin-example" } + ); + }); - it("should throw an error when a plugin has whitespace", () => { - assert.throws(() => { - load("whitespace "); - }, /Whitespace found in plugin name 'whitespace '/u); - assert.throws(() => { - load("whitespace\t"); - }, /Whitespace found in plugin name/u); - assert.throws(() => { - load("whitespace\n"); - }, /Whitespace found in plugin name/u); - assert.throws(() => { - load("whitespace\r"); - }, /Whitespace found in plugin name/u); - }); + it("should load a plugin when referenced by long name", () => { + const loadedPlugins = load("eslint-plugin-example"); - it("should throw an error when a plugin doesn't exist", () => { - assert.throws(() => { - load("nonexistentplugin"); - }, /Failed to load plugin/u); - }); + assert.deepStrictEqual( + loadedPlugins.get("example"), + { name: "eslint-plugin-example" } + ); + }); - it("should rethrow an error that a plugin throws on load", () => { - assert.throws(() => { - load("throws-on-load"); - }, /error thrown while loading this module/u); - }); + it("should throw an error when a plugin has whitespace", () => { + assert.throws(() => { + load("whitespace "); + }, /Whitespace found in plugin name 'whitespace '/u); + assert.throws(() => { + load("whitespace\t"); + }, /Whitespace found in plugin name/u); + assert.throws(() => { + load("whitespace\n"); + }, /Whitespace found in plugin name/u); + assert.throws(() => { + load("whitespace\r"); + }, /Whitespace found in plugin name/u); + }); - it("should load a scoped plugin when referenced by short name", () => { - const loadedPlugins = load("@scope/example"); + it("should throw an error when a plugin doesn't exist", () => { + assert.throws(() => { + load("nonexistentplugin"); + }, /Failed to load plugin/u); + }); - assert.deepStrictEqual( - loadedPlugins.get("@scope/example"), - { name: "@scope/eslint-plugin-example" } - ); - }); + it("should rethrow an error that a plugin throws on load", () => { + assert.throws(() => { + load("throws-on-load"); + }, /error thrown while loading this module/u); + }); - it("should load a scoped plugin when referenced by long name", () => { - const loadedPlugins = load("@scope/eslint-plugin-example"); + it("should load a scoped plugin when referenced by short name", () => { + const loadedPlugins = load("@scope/example"); - assert.deepStrictEqual( - loadedPlugins.get("@scope/example"), - { name: "@scope/eslint-plugin-example" } - ); - }); + assert.deepStrictEqual( + loadedPlugins.get("@scope/example"), + { name: "@scope/eslint-plugin-example" } + ); + }); - describe("when referencing a scope plugin and omitting @scope/", () => { - it("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", () => { - const loadedPlugins = load("@scope/example"); + it("should load a scoped plugin when referenced by long name", () => { + const loadedPlugins = load("@scope/eslint-plugin-example"); - assert.strictEqual(loadedPlugins.get("example"), void 0); - }); + assert.deepStrictEqual( + loadedPlugins.get("@scope/example"), + { name: "@scope/eslint-plugin-example" } + ); + }); - it("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { - const loadedPlugins = load("@scope/eslint-plugin-example"); + describe("when referencing a scope plugin and omitting @scope/", () => { + it("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", () => { + const loadedPlugins = load("@scope/example"); - assert.strictEqual(loadedPlugins.get("example"), void 0); - }); + assert.strictEqual(loadedPlugins.get("example"), void 0); }); - }); - describe("loadAll()", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ - cwd: () => tempDir, - files: { - "node_modules/eslint-plugin-example1/index.js": "exports.name = 'eslint-plugin-example1';", - "node_modules/eslint-plugin-example2/index.js": "exports.name = 'eslint-plugin-example2';" - } + it("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { + const loadedPlugins = load("@scope/eslint-plugin-example"); + + assert.strictEqual(loadedPlugins.get("example"), void 0); }); - const factory = new ConfigArrayFactory(); + }); + }); - /** - * Load a plugin. - * @param {string[]} request A request to load a plugin. - * @returns {Map} The loaded plugins. - */ - function loadAll(request) { - const config = factory.create({ plugins: request }); - - return new Map( - Object - .entries(config[0].plugins) - .map(([id, entry]) => { - if (entry.error) { - throw entry.error; - } - return [id, entry.definition]; - }) - ); + // This group moved from 'tests/lib/config/plugins.js' when refactoring to keep the cumulated test cases. + describe("'plugins' property should load some correct plugins.", () => { + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + cwd: () => tempDir, + files: { + "node_modules/eslint-plugin-example1/index.js": "exports.name = 'eslint-plugin-example1';", + "node_modules/eslint-plugin-example2/index.js": "exports.name = 'eslint-plugin-example2';" } + }); + const factory = new ConfigArrayFactory(); - it("should load plugins when passed multiple plugins", () => { - const loadedPlugins = loadAll(["example1", "example2"]); + /** + * Load a plugin. + * @param {string[]} request A request to load a plugin. + * @returns {Map} The loaded plugins. + */ + function loadAll(request) { + const config = factory.create({ plugins: request }); + + return new Map( + Object + .entries(config[0].plugins) + .map(([id, entry]) => { + if (entry.error) { + throw entry.error; + } + return [id, entry.definition]; + }) + ); + } - assert.deepStrictEqual( - loadedPlugins.get("example1"), - { name: "eslint-plugin-example1" } - ); - assert.deepStrictEqual( - loadedPlugins.get("example2"), - { name: "eslint-plugin-example2" } - ); - }); + it("should load plugins when passed multiple plugins", () => { + const loadedPlugins = loadAll(["example1", "example2"]); + + assert.deepStrictEqual( + loadedPlugins.get("example1"), + { name: "eslint-plugin-example1" } + ); + assert.deepStrictEqual( + loadedPlugins.get("example2"), + { name: "eslint-plugin-example2" } + ); }); }); }); diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index 577c1c568eb..4500208582a 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -346,256 +346,193 @@ describe("ConfigArray", () => { ); }); - describe("Moved from 'merge()' in tests/lib/config/config-ops.js", () => { - - /** - * Merge two config data. - * @param {Object} target A config data. - * @param {Object} source Another config data. - * @returns {Object} The merged config data. - */ - function merge(target, source) { - return new ConfigArray(target, source).extractConfig(__filename); - } - - it("should combine two objects when passed two objects with different top-level properties", () => { - const config = [ - { env: { browser: true } }, - { globals: { foo: "bar" } } - ]; - - const result = merge(config[0], config[1]); - - assert.strictEqual(result.globals.foo, "bar"); - assert.isTrue(result.env.browser); - }); - - it("should combine without blowing up on null values", () => { - const config = [ - { env: { browser: true } }, - { env: { node: null } } - ]; + /** + * Merge two config data. + * + * The test cases which depend on this function were moved from + * 'tests/lib/config/config-ops.js' when refactoring to keep the + * cumulated test cases. + * + * Previously, the merging logic of multiple config data had been + * implemented in `ConfigOps.merge()` function. But currently, it's + * implemented in `ConfigArray#extractConfig()` method. + * + * @param {Object} target A config data. + * @param {Object} source Another config data. + * @returns {Object} The merged config data. + */ + function merge(target, source) { + return new ConfigArray(target, source).extractConfig(__filename); + } - const result = merge(config[0], config[1]); + it("should combine two objects when passed two objects with different top-level properties", () => { + const config = [ + { env: { browser: true } }, + { globals: { foo: "bar" } } + ]; - assert.strictEqual(result.env.node, null); - assert.isTrue(result.env.browser); - }); + const result = merge(config[0], config[1]); - it("should combine two objects with parser when passed two objects with different top-level properties", () => { - const config = [ - { env: { browser: true }, parser: "espree" }, - { globals: { foo: "bar" } } - ]; + assert.strictEqual(result.globals.foo, "bar"); + assert.isTrue(result.env.browser); + }); - const result = merge(config[0], config[1]); + it("should combine without blowing up on null values", () => { + const config = [ + { env: { browser: true } }, + { env: { node: null } } + ]; - assert.strictEqual(result.parser, "espree"); - }); + const result = merge(config[0], config[1]); - it("should combine configs and override rules when passed configs with the same rules", () => { - const config = [ - { rules: { "no-mixed-requires": [0, false] } }, - { rules: { "no-mixed-requires": [1, true] } } - ]; + assert.strictEqual(result.env.node, null); + assert.isTrue(result.env.browser); + }); - const result = merge(config[0], config[1]); + it("should combine two objects with parser when passed two objects with different top-level properties", () => { + const config = [ + { env: { browser: true }, parser: "espree" }, + { globals: { foo: "bar" } } + ]; - assert.isArray(result.rules["no-mixed-requires"]); - assert.strictEqual(result.rules["no-mixed-requires"][0], 1); - assert.strictEqual(result.rules["no-mixed-requires"][1], true); - }); + const result = merge(config[0], config[1]); - it("should combine configs when passed configs with parserOptions", () => { - const config = [ - { parserOptions: { ecmaFeatures: { jsx: true } } }, - { parserOptions: { ecmaFeatures: { globalReturn: true } } } - ]; + assert.strictEqual(result.parser, "espree"); + }); - const result = merge(config[0], config[1]); + it("should combine configs and override rules when passed configs with the same rules", () => { + const config = [ + { rules: { "no-mixed-requires": [0, false] } }, + { rules: { "no-mixed-requires": [1, true] } } + ]; - assert.deepStrictEqual(result, { - env: {}, - globals: {}, - parser: null, - parserOptions: { - ecmaFeatures: { - jsx: true, - globalReturn: true - } - }, - plugins: {}, - processor: null, - rules: {}, - settings: {} - }); + const result = merge(config[0], config[1]); - // double-check that originals were not changed - assert.deepStrictEqual(config[0], { parserOptions: { ecmaFeatures: { jsx: true } } }); - assert.deepStrictEqual(config[1], { parserOptions: { ecmaFeatures: { globalReturn: true } } }); - }); + assert.isArray(result.rules["no-mixed-requires"]); + assert.strictEqual(result.rules["no-mixed-requires"][0], 1); + assert.strictEqual(result.rules["no-mixed-requires"][1], true); + }); - it("should override configs when passed configs with the same ecmaFeatures", () => { - const config = [ - { parserOptions: { ecmaFeatures: { globalReturn: false } } }, - { parserOptions: { ecmaFeatures: { globalReturn: true } } } - ]; + it("should combine configs when passed configs with parserOptions", () => { + const config = [ + { parserOptions: { ecmaFeatures: { jsx: true } } }, + { parserOptions: { ecmaFeatures: { globalReturn: true } } } + ]; - const result = merge(config[0], config[1]); + const result = merge(config[0], config[1]); - assert.deepStrictEqual(result, { - env: {}, - globals: {}, - parser: null, - parserOptions: { - ecmaFeatures: { - globalReturn: true - } - }, - plugins: {}, - processor: null, - rules: {}, - settings: {} - }); + assert.deepStrictEqual(result, { + env: {}, + globals: {}, + parser: null, + parserOptions: { + ecmaFeatures: { + jsx: true, + globalReturn: true + } + }, + plugins: {}, + processor: null, + rules: {}, + settings: {} }); - it("should combine configs and override rules when merging two configs with arrays and int", () => { + // double-check that originals were not changed + assert.deepStrictEqual(config[0], { parserOptions: { ecmaFeatures: { jsx: true } } }); + assert.deepStrictEqual(config[1], { parserOptions: { ecmaFeatures: { globalReturn: true } } }); + }); - const config = [ - { rules: { "no-mixed-requires": [0, false] } }, - { rules: { "no-mixed-requires": 1 } } - ]; + it("should override configs when passed configs with the same ecmaFeatures", () => { + const config = [ + { parserOptions: { ecmaFeatures: { globalReturn: false } } }, + { parserOptions: { ecmaFeatures: { globalReturn: true } } } + ]; - const result = merge(config[0], config[1]); + const result = merge(config[0], config[1]); - assert.isArray(result.rules["no-mixed-requires"]); - assert.strictEqual(result.rules["no-mixed-requires"][0], 1); - assert.strictEqual(result.rules["no-mixed-requires"][1], false); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [0, false] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": 1 } }); + assert.deepStrictEqual(result, { + env: {}, + globals: {}, + parser: null, + parserOptions: { + ecmaFeatures: { + globalReturn: true + } + }, + plugins: {}, + processor: null, + rules: {}, + settings: {} }); + }); - it("should combine configs and override rules options completely", () => { + it("should combine configs and override rules when merging two configs with arrays and int", () => { - const config = [ - { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }, - { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } } - ]; + const config = [ + { rules: { "no-mixed-requires": [0, false] } }, + { rules: { "no-mixed-requires": 1 } } + ]; - const result = merge(config[0], config[1]); + const result = merge(config[0], config[1]); - assert.isArray(result.rules["no-mixed-requires1"]); - assert.deepStrictEqual(result.rules["no-mixed-requires1"][1], { err: ["error", "e"] }); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } }); - }); + assert.isArray(result.rules["no-mixed-requires"]); + assert.strictEqual(result.rules["no-mixed-requires"][0], 1); + assert.strictEqual(result.rules["no-mixed-requires"][1], false); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires": [0, false] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires": 1 } }); + }); - it("should combine configs and override rules options without array or object", () => { + it("should combine configs and override rules options completely", () => { - const config = [ - { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }, - { rules: { "no-mixed-requires1": [2, "requirejs"] } } - ]; + const config = [ + { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }, + { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } } + ]; - const result = merge(config[0], config[1]); + const result = merge(config[0], config[1]); - assert.strictEqual(result.rules["no-mixed-requires1"][0], 2); - assert.strictEqual(result.rules["no-mixed-requires1"][1], "requirejs"); - assert.isUndefined(result.rules["no-mixed-requires1"][2]); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [2, "requirejs"] } }); - }); + assert.isArray(result.rules["no-mixed-requires1"]); + assert.deepStrictEqual(result.rules["no-mixed-requires1"][1], { err: ["error", "e"] }); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }] } }); + }); - it("should combine configs and override rules options without array or object but special case", () => { + it("should combine configs and override rules options without array or object", () => { - const config = [ - { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }, - { rules: { "no-mixed-requires1": "error" } } - ]; + const config = [ + { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }, + { rules: { "no-mixed-requires1": [2, "requirejs"] } } + ]; - const result = merge(config[0], config[1]); + const result = merge(config[0], config[1]); - assert.strictEqual(result.rules["no-mixed-requires1"][0], "error"); - assert.strictEqual(result.rules["no-mixed-requires1"][1], "nconf"); - assert.strictEqual(result.rules["no-mixed-requires1"][2], "underscore"); - assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }); - assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": "error" } }); - }); + assert.strictEqual(result.rules["no-mixed-requires1"][0], 2); + assert.strictEqual(result.rules["no-mixed-requires1"][1], "requirejs"); + assert.isUndefined(result.rules["no-mixed-requires1"][2]); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": ["warn", "nconf", "underscore"] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": [2, "requirejs"] } }); + }); - it("should combine configs correctly", () => { + it("should combine configs and override rules options without array or object but special case", () => { - const config = [ - { - rules: { - "no-mixed-requires1": [1, { event: ["evt", "e"] }], - "valid-jsdoc": 1, - semi: 1, - quotes1: [2, { exception: ["hi"] }], - smile: [1, ["hi", "bye"]] - }, - parserOptions: { - ecmaFeatures: { jsx: true } - }, - env: { browser: true }, - globals: { foo: false } - }, - { - rules: { - "no-mixed-requires1": [1, { err: ["error", "e"] }], - "valid-jsdoc": 2, - test: 1, - smile: [1, ["xxx", "yyy"]] - }, - parserOptions: { - ecmaFeatures: { globalReturn: true } - }, - env: { browser: false }, - globals: { foo: true } - } - ]; + const config = [ + { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }, + { rules: { "no-mixed-requires1": "error" } } + ]; - const result = merge(config[0], config[1]); + const result = merge(config[0], config[1]); - assert.deepStrictEqual(result, { - parser: null, - parserOptions: { - ecmaFeatures: { - jsx: true, - globalReturn: true - } - }, - plugins: {}, - env: { - browser: false - }, - globals: { - foo: true - }, - rules: { - "no-mixed-requires1": [1, - { - err: [ - "error", - "e" - ] - } - ], - quotes1: [2, - { - exception: [ - "hi" - ] - } - ], - semi: [1], - smile: [1, ["xxx", "yyy"]], - test: [1], - "valid-jsdoc": [2] - }, - settings: {}, - processor: null - }); - assert.deepStrictEqual(config[0], { + assert.strictEqual(result.rules["no-mixed-requires1"][0], "error"); + assert.strictEqual(result.rules["no-mixed-requires1"][1], "nconf"); + assert.strictEqual(result.rules["no-mixed-requires1"][2], "underscore"); + assert.deepStrictEqual(config[0], { rules: { "no-mixed-requires1": [1, "nconf", "underscore"] } }); + assert.deepStrictEqual(config[1], { rules: { "no-mixed-requires1": "error" } }); + }); + + it("should combine configs correctly", () => { + + const config = [ + { rules: { "no-mixed-requires1": [1, { event: ["evt", "e"] }], "valid-jsdoc": 1, @@ -608,8 +545,8 @@ describe("ConfigArray", () => { }, env: { browser: true }, globals: { foo: false } - }); - assert.deepStrictEqual(config[1], { + }, + { rules: { "no-mixed-requires1": [1, { err: ["error", "e"] }], "valid-jsdoc": 2, @@ -621,23 +558,92 @@ describe("ConfigArray", () => { }, env: { browser: false }, globals: { foo: true } - }); + } + ]; + + const result = merge(config[0], config[1]); + + assert.deepStrictEqual(result, { + parser: null, + parserOptions: { + ecmaFeatures: { + jsx: true, + globalReturn: true + } + }, + plugins: {}, + env: { + browser: false + }, + globals: { + foo: true + }, + rules: { + "no-mixed-requires1": [1, + { + err: [ + "error", + "e" + ] + } + ], + quotes1: [2, + { + exception: [ + "hi" + ] + } + ], + semi: [1], + smile: [1, ["xxx", "yyy"]], + test: [1], + "valid-jsdoc": [2] + }, + settings: {}, + processor: null + }); + assert.deepStrictEqual(config[0], { + rules: { + "no-mixed-requires1": [1, { event: ["evt", "e"] }], + "valid-jsdoc": 1, + semi: 1, + quotes1: [2, { exception: ["hi"] }], + smile: [1, ["hi", "bye"]] + }, + parserOptions: { + ecmaFeatures: { jsx: true } + }, + env: { browser: true }, + globals: { foo: false } }); + assert.deepStrictEqual(config[1], { + rules: { + "no-mixed-requires1": [1, { err: ["error", "e"] }], + "valid-jsdoc": 2, + test: 1, + smile: [1, ["xxx", "yyy"]] + }, + parserOptions: { + ecmaFeatures: { globalReturn: true } + }, + env: { browser: false }, + globals: { foo: true } + }); + }); - it("should copy deeply if there is not the destination's property", () => { - const a = {}; - const b = { settings: { bar: 1 } }; + it("should copy deeply if there is not the destination's property", () => { + const a = {}; + const b = { settings: { bar: 1 } }; - const result = merge(a, b); + const result = merge(a, b); - assert(a.settings === void 0); - assert(b.settings.bar === 1); - assert(result.settings.bar === 1); + assert(a.settings === void 0); + assert(b.settings.bar === 1); + assert(result.settings.bar === 1); - result.settings.bar = 2; - assert(b.settings.bar === 1); - assert(result.settings.bar === 2); - }); + result.settings.bar = 2; + assert(b.settings.bar === 1); + assert(result.settings.bar === 2); }); }); diff --git a/tests/lib/cli-engine/config-array/override-tester.js b/tests/lib/cli-engine/config-array/override-tester.js index d0e3d691ba8..32b3fa519ef 100644 --- a/tests/lib/cli-engine/config-array/override-tester.js +++ b/tests/lib/cli-engine/config-array/override-tester.js @@ -98,111 +98,117 @@ describe("OverrideTester", () => { }, /'filePath' should be an absolute path, but got foo\/bar\.js/u); }); - describe("Moved from 'pathMatchesGlobs()' in tests/lib/config/config-ops.js", () => { - - /** - * Test if a given file path matches to the given condition. - * @param {string} filePath The file path to test patterns against - * @param {string|string[]} files One or more glob patterns - * @param {string|string[]} [excludedFiles] One or more glob patterns - * @returns {boolean} The result. - */ - function test(filePath, files, excludedFiles) { - const basePath = process.cwd(); - const tester = OverrideTester.create(files, excludedFiles, basePath); - - return tester.test(path.resolve(basePath, filePath)); - } - - /** - * Emits a test that confirms the specified file path matches the specified combination of patterns. - * @param {string} filePath The file path to test patterns against - * @param {string|string[]} patterns One or more glob patterns - * @param {string|string[]} [excludedPatterns] One or more glob patterns - * @returns {void} - */ - function match(filePath, patterns, excludedPatterns) { - it(`matches ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { - const result = test(filePath, patterns, excludedPatterns); - - assert.strictEqual(result, true); - }); - } - - /** - * Emits a test that confirms the specified file path does not match the specified combination of patterns. - * @param {string} filePath The file path to test patterns against - * @param {string|string[]} patterns One or more glob patterns - * @param {string|string[]} [excludedPatterns] One or more glob patterns - * @returns {void} - */ - function noMatch(filePath, patterns, excludedPatterns) { - it(`does not match ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { - const result = test(filePath, patterns, excludedPatterns); - - assert.strictEqual(result, false); - }); - } - - /** - * Emits a test that confirms the specified pattern throws an error. - * @param {string} filePath The file path to test the pattern against - * @param {string} pattern The glob pattern that should trigger the error condition - * @param {string} expectedMessage The expected error's message - * @returns {void} - */ - function error(filePath, pattern, expectedMessage) { - it(`emits an error given '${pattern}'`, () => { - let errorMessage; - - try { - test(filePath, pattern); - } catch (e) { - errorMessage = e.message; - } - - assert.strictEqual(errorMessage, expectedMessage); - }); - } - - // files in the project root - match("foo.js", ["foo.js"], []); - match("foo.js", ["*"], []); - match("foo.js", ["*.js"], []); - match("foo.js", ["**/*.js"], []); - match("bar.js", ["*.js"], ["foo.js"]); - - noMatch("foo.js", ["./foo.js"], []); - noMatch("foo.js", ["./*"], []); - noMatch("foo.js", ["./**"], []); - noMatch("foo.js", ["*"], ["foo.js"]); - noMatch("foo.js", ["*.js"], ["foo.js"]); - noMatch("foo.js", ["**/*.js"], ["foo.js"]); - - // files in a subdirectory - match("subdir/foo.js", ["foo.js"], []); - match("subdir/foo.js", ["*"], []); - match("subdir/foo.js", ["*.js"], []); - match("subdir/foo.js", ["**/*.js"], []); - match("subdir/foo.js", ["subdir/*.js"], []); - match("subdir/foo.js", ["subdir/foo.js"], []); - match("subdir/foo.js", ["subdir/*"], []); - match("subdir/second/foo.js", ["subdir/**"], []); - - noMatch("subdir/foo.js", ["./foo.js"], []); - noMatch("subdir/foo.js", ["./**"], []); - noMatch("subdir/foo.js", ["./subdir/**"], []); - noMatch("subdir/foo.js", ["./subdir/*"], []); - noMatch("subdir/foo.js", ["*"], ["subdir/**"]); - noMatch("subdir/very/deep/foo.js", ["*.js"], ["subdir/**"]); - noMatch("subdir/second/foo.js", ["subdir/*"], []); - noMatch("subdir/second/foo.js", ["subdir/**"], ["subdir/second/*"]); - - // error conditions - error("foo.js", ["/*.js"], "Invalid override pattern (expected relative path not containing '..'): /*.js"); - error("foo.js", ["/foo.js"], "Invalid override pattern (expected relative path not containing '..'): /foo.js"); - error("foo.js", ["../**"], "Invalid override pattern (expected relative path not containing '..'): ../**"); - }); + /** + * Test if a given file path matches to the given condition. + * + * The test cases which depend on this function were moved from + * 'tests/lib/config/config-ops.js' when refactoring to keep the + * cumulated test cases. + * + * Previously, the testing logic of `overrides` properties had been + * implemented in `ConfigOps.pathMatchesGlobs()` function. But + * currently, it's implemented in `OverrideTester` class. + * + * @param {string} filePath The file path to test patterns against + * @param {string|string[]} files One or more glob patterns + * @param {string|string[]} [excludedFiles] One or more glob patterns + * @returns {boolean} The result. + */ + function test(filePath, files, excludedFiles) { + const basePath = process.cwd(); + const tester = OverrideTester.create(files, excludedFiles, basePath); + + return tester.test(path.resolve(basePath, filePath)); + } + + /** + * Emits a test that confirms the specified file path matches the specified combination of patterns. + * @param {string} filePath The file path to test patterns against + * @param {string|string[]} patterns One or more glob patterns + * @param {string|string[]} [excludedPatterns] One or more glob patterns + * @returns {void} + */ + function match(filePath, patterns, excludedPatterns) { + it(`matches ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { + const result = test(filePath, patterns, excludedPatterns); + + assert.strictEqual(result, true); + }); + } + + /** + * Emits a test that confirms the specified file path does not match the specified combination of patterns. + * @param {string} filePath The file path to test patterns against + * @param {string|string[]} patterns One or more glob patterns + * @param {string|string[]} [excludedPatterns] One or more glob patterns + * @returns {void} + */ + function noMatch(filePath, patterns, excludedPatterns) { + it(`does not match ${filePath} given '${patterns.join("','")}' includes and '${excludedPatterns.join("','")}' excludes`, () => { + const result = test(filePath, patterns, excludedPatterns); + + assert.strictEqual(result, false); + }); + } + + /** + * Emits a test that confirms the specified pattern throws an error. + * @param {string} filePath The file path to test the pattern against + * @param {string} pattern The glob pattern that should trigger the error condition + * @param {string} expectedMessage The expected error's message + * @returns {void} + */ + function error(filePath, pattern, expectedMessage) { + it(`emits an error given '${pattern}'`, () => { + let errorMessage; + + try { + test(filePath, pattern); + } catch (e) { + errorMessage = e.message; + } + + assert.strictEqual(errorMessage, expectedMessage); + }); + } + + // files in the project root + match("foo.js", ["foo.js"], []); + match("foo.js", ["*"], []); + match("foo.js", ["*.js"], []); + match("foo.js", ["**/*.js"], []); + match("bar.js", ["*.js"], ["foo.js"]); + + noMatch("foo.js", ["./foo.js"], []); + noMatch("foo.js", ["./*"], []); + noMatch("foo.js", ["./**"], []); + noMatch("foo.js", ["*"], ["foo.js"]); + noMatch("foo.js", ["*.js"], ["foo.js"]); + noMatch("foo.js", ["**/*.js"], ["foo.js"]); + + // files in a subdirectory + match("subdir/foo.js", ["foo.js"], []); + match("subdir/foo.js", ["*"], []); + match("subdir/foo.js", ["*.js"], []); + match("subdir/foo.js", ["**/*.js"], []); + match("subdir/foo.js", ["subdir/*.js"], []); + match("subdir/foo.js", ["subdir/foo.js"], []); + match("subdir/foo.js", ["subdir/*"], []); + match("subdir/second/foo.js", ["subdir/**"], []); + + noMatch("subdir/foo.js", ["./foo.js"], []); + noMatch("subdir/foo.js", ["./**"], []); + noMatch("subdir/foo.js", ["./subdir/**"], []); + noMatch("subdir/foo.js", ["./subdir/*"], []); + noMatch("subdir/foo.js", ["*"], ["subdir/**"]); + noMatch("subdir/very/deep/foo.js", ["*.js"], ["subdir/**"]); + noMatch("subdir/second/foo.js", ["subdir/*"], []); + noMatch("subdir/second/foo.js", ["subdir/**"], ["subdir/second/*"]); + + // error conditions + error("foo.js", ["/*.js"], "Invalid override pattern (expected relative path not containing '..'): /*.js"); + error("foo.js", ["/foo.js"], "Invalid override pattern (expected relative path not containing '..'): /foo.js"); + error("foo.js", ["../**"], "Invalid override pattern (expected relative path not containing '..'): ../**"); }); describe("'JSON.stringify(...)' should return readable JSON; not include 'Minimatch' objects", () => { diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index aadddd79f78..edf8e0311e4 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -168,7 +168,8 @@ describe("FileEnumerator", () => { }); }); - describe("Moved from tests/lib/util/glob-utils.js", () => { + // This group moved from 'tests/lib/util/glob-utils.js' when refactoring to keep the cumulated test cases. + describe("with 'tests/fixtures/glob-utils' files", () => { let fixtureDir; /** From 80d55d6c2674e959b57380c05200132532f88503 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 14:53:59 +0900 Subject: [PATCH 17/49] =?UTF-8?q?Inmemory=20=E2=86=92=20InMemory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/lib/cli-engine.js | 8 +- tests/lib/cli-engine/_utils.js | 39 +++++---- .../cascading-config-array-factory.js | 12 +-- tests/lib/cli-engine/config-array-factory.js | 84 +++++++++---------- tests/lib/cli-engine/file-enumerator.js | 4 +- tests/lib/util/npm-utils.js | 14 ++-- 6 files changed, 82 insertions(+), 79 deletions(-) diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index 667ed0bb538..86a607f1e50 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -18,7 +18,7 @@ const assert = require("chai").assert, os = require("os"), hash = require("../../lib/util/hash"), { CascadingConfigArrayFactory } = require("../../lib/cli-engine/cascading-config-array-factory"), - { defineCLIEngineWithInmemoryFileSystem } = require("./cli-engine/_utils"); + { defineCLIEngineWithInMemoryFileSystem } = require("./cli-engine/_utils"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); @@ -2906,7 +2906,7 @@ describe("CLIEngine", () => { describe("a config file setting should have higher priority than a shareable config file's settings always; https://github.com/eslint/eslint/issues/11510", () => { beforeEach(() => { - ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ + ({ CLIEngine } = defineCLIEngineWithInMemoryFileSystem({ cwd: () => path.join(os.tmpdir(), "cli-engine/11510"), files: { "no-console-error-in-overrides.json": JSON.stringify({ @@ -2935,7 +2935,7 @@ describe("CLIEngine", () => { describe("configs of plugin rules should be validated even if 'plugins' key doesn't exist; https://github.com/eslint/eslint/issues/11559", () => { beforeEach(() => { - ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ + ({ CLIEngine } = defineCLIEngineWithInMemoryFileSystem({ cwd: () => path.join(os.tmpdir(), "cli-engine/11559"), files: { "node_modules/eslint-plugin-test/index.js": ` @@ -2972,7 +2972,7 @@ describe("CLIEngine", () => { describe("'--fix-type' should not crash even if plugin rules exist; https://github.com/eslint/eslint/issues/11586", () => { beforeEach(() => { - ({ CLIEngine } = defineCLIEngineWithInmemoryFileSystem({ + ({ CLIEngine } = defineCLIEngineWithInMemoryFileSystem({ cwd: () => path.join(os.tmpdir(), "cli-engine/11586"), files: { "node_modules/eslint-plugin-test/index.js": ` diff --git a/tests/lib/cli-engine/_utils.js b/tests/lib/cli-engine/_utils.js index 8fa025b110d..b29da0789a6 100644 --- a/tests/lib/cli-engine/_utils.js +++ b/tests/lib/cli-engine/_utils.js @@ -1,14 +1,17 @@ /** * @fileoverview Define classes what use the in-memory file system. * - * This provides utilities to test `ConfigArrayFactory` and `FileEnumerator`. + * This provides utilities to test `ConfigArrayFactory`, + * `CascadingConfigArrayFactory`, `FileEnumerator`, and `CLIEngine`. * - * - `defineConfigArrayFactoryWithInmemoryFileSystem({ cwd, files })` - * - `defineFileEnumeratorWithInmemoryFileSystem({ cwd, files })` + * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` + * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` + * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })` + * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })` * - * Both functions define the class `ConfigArrayFactory` or `FileEnumerator` with - * the in-memory file system. Those search config files, parsers, and plugins in - * the `files` option via the in-memory file system. + * Those functions define correspond classes with the in-memory file system. + * Those search config files, parsers, and plugins in the `files` option via the + * in-memory file system. * * For each test case, it makes more readable if we define minimal files the * test case requires. @@ -16,7 +19,7 @@ * For example: * * ```js - * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ * files: { * "node_modules/eslint-config-foo/index.js": ` * module.exports = { @@ -276,7 +279,7 @@ function supportMkdirRecursiveOption(fs, cwd) { * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. */ -function defineConfigArrayFactoryWithInmemoryFileSystem({ +function defineConfigArrayFactoryWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { @@ -382,12 +385,12 @@ function defineConfigArrayFactoryWithInmemoryFileSystem({ * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"] }} The stubbed `CascadingConfigArrayFactory` class. */ -function defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ +function defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { const { fs, RelativeModuleResolver, ConfigArrayFactory } = - defineConfigArrayFactoryWithInmemoryFileSystem({ cwd, files }); + defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files }); const loadRules = proxyquire(LoadRulesPath, { fs }); const { CascadingConfigArrayFactory } = proxyquire(CascadingConfigArrayFactoryPath, { @@ -417,7 +420,7 @@ function defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], IgnoredPaths: import("../../../lib/cli-engine/ignored-paths")["IgnoredPaths"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"] }} The stubbed `FileEnumerator` class. */ -function defineFileEnumeratorWithInmemoryFileSystem({ +function defineFileEnumeratorWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { @@ -427,7 +430,7 @@ function defineFileEnumeratorWithInmemoryFileSystem({ ConfigArrayFactory, CascadingConfigArrayFactory } = - defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ cwd, files }); + defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files }); const { IgnoredPaths } = proxyquire(IgnoredPathsPath, { fs }); const { FileEnumerator } = proxyquire(FileEnumeratorPath, { fs, @@ -459,7 +462,7 @@ function defineFileEnumeratorWithInmemoryFileSystem({ * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/util/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], IgnoredPaths: import("../../../lib/cli-engine/ignored-paths")["IgnoredPaths"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"], CLIEngine: import("../../../lib/cli-engine")["CLIEngine"], getCLIEngineInternalSlots: import("../../../lib/cli-engine")["getCLIEngineInternalSlots"] }} The stubbed `CLIEngine` class. */ -function defineCLIEngineWithInmemoryFileSystem({ +function defineCLIEngineWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { @@ -471,7 +474,7 @@ function defineCLIEngineWithInmemoryFileSystem({ IgnoredPaths, FileEnumerator } = - defineFileEnumeratorWithInmemoryFileSystem({ cwd, files }); + defineFileEnumeratorWithInMemoryFileSystem({ cwd, files }); const { CLIEngine, getCLIEngineInternalSlots } = proxyquire(CLIEnginePath, { fs, "./cli-engine/cascading-config-array-factory": { CascadingConfigArrayFactory }, @@ -500,8 +503,8 @@ function defineCLIEngineWithInmemoryFileSystem({ } module.exports = { - defineConfigArrayFactoryWithInmemoryFileSystem, - defineCascadingConfigArrayFactoryWithInmemoryFileSystem, - defineFileEnumeratorWithInmemoryFileSystem, - defineCLIEngineWithInmemoryFileSystem + defineConfigArrayFactoryWithInMemoryFileSystem, + defineCascadingConfigArrayFactoryWithInMemoryFileSystem, + defineFileEnumeratorWithInMemoryFileSystem, + defineCLIEngineWithInMemoryFileSystem }; diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index d43d27df33f..74655389a5b 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -13,7 +13,7 @@ const sinon = require("sinon"); const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); const { ConfigArrayFactory } = require("../../../lib/cli-engine/config-array-factory"); const { ExtractedConfig } = require("../../../lib/cli-engine/config-array/extracted-config"); -const { defineCascadingConfigArrayFactoryWithInmemoryFileSystem } = require("./_utils"); +const { defineCascadingConfigArrayFactoryWithInMemoryFileSystem } = require("./_utils"); describe("CascadingConfigArrayFactory", () => { describe("'getConfigArrayForFile(filePath)' method should retrieve the proper configuration.", () => { @@ -46,7 +46,7 @@ describe("CascadingConfigArrayFactory", () => { }; describe(`with the files ${JSON.stringify(files)}`, () => { - const { CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow + const { CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow /** @type {CascadingConfigArrayFactory} */ let factory; @@ -671,7 +671,7 @@ describe("CascadingConfigArrayFactory", () => { describe("personal config file within home directory", () => { const { CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ files: { "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY } @@ -766,7 +766,7 @@ describe("CascadingConfigArrayFactory", () => { describe("when no local or personal config is found", () => { const { CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ files: { "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY } @@ -849,7 +849,7 @@ describe("CascadingConfigArrayFactory", () => { describe("with overrides", () => { const { CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ + } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ files: { "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY } @@ -1154,7 +1154,7 @@ describe("CascadingConfigArrayFactory", () => { describe(`with the files ${JSON.stringify(files)}`, () => { const { CascadingConfigArrayFactory // eslint-disable-line no-shadow - } = defineCascadingConfigArrayFactoryWithInmemoryFileSystem({ cwd: () => root, files }); + } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); /** @type {Map} */ let additionalPluginPool; diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 01c549ba726..00fbfae3015 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -10,12 +10,12 @@ const { assert } = require("chai"); const { spy } = require("sinon"); const { ConfigArray } = require("../../../lib/cli-engine/config-array"); const { OverrideTester } = require("../../../lib/cli-engine/config-array"); -const { defineConfigArrayFactoryWithInmemoryFileSystem } = require("./_utils"); +const { defineConfigArrayFactoryWithInMemoryFileSystem } = require("./_utils"); const tempDir = path.join(os.tmpdir(), "eslint/config-array-factory"); // For VSCode intellisense. -/** @typedef {InstanceType["ConfigArrayFactory"]>} ConfigArrayFactory */ +/** @typedef {InstanceType["ConfigArrayFactory"]>} ConfigArrayFactory */ /** * Assert a config array element. @@ -66,7 +66,7 @@ function assertConfig(actual, providedExpected) { describe("ConfigArrayFactory", () => { describe("'create(configData, options)' method should normalize the config data.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir }); const factory = new ConfigArrayFactory(); @@ -148,7 +148,7 @@ describe("ConfigArrayFactory", () => { "yml/.eslintrc.yml": "settings:\n name: yml/.eslintrc.yml", "yaml/.eslintrc.yaml": "settings:\n name: yaml/.eslintrc.yaml" }; - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { ...basicFiles, @@ -259,7 +259,7 @@ describe("ConfigArrayFactory", () => { "yml/.eslintrc.yml": "settings:\n name: yml/.eslintrc.yml", "yaml/.eslintrc.yaml": "settings:\n name: yaml/.eslintrc.yaml" }; - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { ...basicFiles, @@ -381,7 +381,7 @@ describe("ConfigArrayFactory", () => { describe("misc", () => { before(() => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir }); @@ -543,7 +543,7 @@ describe("ConfigArrayFactory", () => { describe("'parser' details", () => { before(() => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { "node_modules/xxx-parser/index.js": "exports.name = 'xxx-parser';", @@ -650,7 +650,7 @@ describe("ConfigArrayFactory", () => { describe("'plugins' details", () => { before(() => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { "node_modules/eslint-plugin-ext/index.js": "exports.processors = { '.abc': {}, '.xyz': {}, other: {} };", @@ -811,7 +811,7 @@ describe("ConfigArrayFactory", () => { describe("'extends' details", () => { before(() => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { "node_modules/eslint-config-foo/index.js": "exports.env = { browser: true }", @@ -1120,7 +1120,7 @@ describe("ConfigArrayFactory", () => { describe("'overrides' details", () => { before(() => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { } @@ -1174,7 +1174,7 @@ describe("ConfigArrayFactory", () => { const plugin = {}; beforeEach(() => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir }); @@ -1226,7 +1226,7 @@ describe("ConfigArrayFactory", () => { "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }", "yaml/.eslintrc.yaml": "env:\n browser: true" }; - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ files }); + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files }); const factory = new ConfigArrayFactory(); /** @@ -1428,7 +1428,7 @@ describe("ConfigArrayFactory", () => { } it("should throw error if file doesnt exist", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem(); const factory = new ConfigArrayFactory(); assert.throws(() => { @@ -1441,7 +1441,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a legacy file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "legacy/.eslintrc": "{ rules: { eqeqeq: 2 } }" } @@ -1457,7 +1457,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "js/.eslintrc.js": "module.exports = { rules: { semi: [2, 'always'] } };" } @@ -1473,7 +1473,7 @@ describe("ConfigArrayFactory", () => { }); it("should throw error when loading invalid JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "js/.eslintrc.broken.js": "module.exports = { rules: { semi: [2, 'always'] }" } @@ -1486,7 +1486,7 @@ describe("ConfigArrayFactory", () => { }); it("should interpret parser module name when present in a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "node_modules/foo/index.js": "", "js/node_modules/foo/index.js": "", @@ -1508,7 +1508,7 @@ describe("ConfigArrayFactory", () => { }); it("should interpret parser path when present in a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "js/.eslintrc.parser2.js": `module.exports = { parser: './not-a-config.js', @@ -1529,7 +1529,7 @@ describe("ConfigArrayFactory", () => { }); it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "js/.eslintrc.parser3.js": `module.exports = { parser: 'espree', @@ -1549,7 +1549,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a JSON file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "json/.eslintrc.json": "{ \"rules\": { \"quotes\": [2, \"double\"] } }" } @@ -1565,7 +1565,7 @@ describe("ConfigArrayFactory", () => { }); it("should load fresh information from a JSON file", () => { - const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem(); const factory = new ConfigArrayFactory(); const initialConfig = { rules: { @@ -1589,7 +1589,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a package.json file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" } @@ -1603,7 +1603,7 @@ describe("ConfigArrayFactory", () => { }); it("should throw error when loading invalid package.json file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "broken-package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } }" } @@ -1621,7 +1621,7 @@ describe("ConfigArrayFactory", () => { }); it("should load fresh information from a package.json file", () => { - const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem(); const factory = new ConfigArrayFactory(); const initialConfig = { eslintConfig: { @@ -1649,7 +1649,7 @@ describe("ConfigArrayFactory", () => { }); it("should load fresh information from a .eslintrc.js file", () => { - const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem(); + const { fs, ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem(); const factory = new ConfigArrayFactory(); const initialConfig = { rules: { @@ -1673,7 +1673,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a YAML file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "yaml/.eslintrc.yaml": "env:\n browser: true" } @@ -1687,7 +1687,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from an empty YAML file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "yaml/.eslintrc.empty.yaml": "{}" } @@ -1699,7 +1699,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a YML file", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "yml/.eslintrc.yml": "env:\n node: true" } @@ -1713,7 +1713,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from a YML file and apply extensions", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "extends/.eslintrc.yml": "extends: ../package-json/package.json\nrules:\n booya: 2", "package-json/package.json": "{ \"eslintConfig\": { \"env\": { \"es6\": true } } }" @@ -1729,7 +1729,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from `extends` chain.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "extends-chain": { "node_modules/eslint-config-a": { @@ -1758,7 +1758,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from `extends` chain with relative path.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "extends-chain-2": { "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", @@ -1779,7 +1779,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from `extends` chain in .eslintrc with relative path.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "extends-chain-2": { "node_modules/eslint-config-a/index.js": "module.exports = { extends: './relative.js', rules: { a: 2 } };", @@ -1800,7 +1800,7 @@ describe("ConfigArrayFactory", () => { }); it("should load information from `parser` in .eslintrc with relative path.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "extends-chain-2": { "parser.eslintrc.json": "{ \"parser\": \"./parser.js\" }", @@ -1818,7 +1818,7 @@ describe("ConfigArrayFactory", () => { describe("Plugins", () => { it("should load information from a YML file and load plugins", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "node_modules/eslint-plugin-test/index.js": ` module.exports = { @@ -1850,7 +1850,7 @@ describe("ConfigArrayFactory", () => { }); it("should load two separate configs from a plugin", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "node_modules/eslint-plugin-test/index.js": ` module.exports = { @@ -1882,7 +1882,7 @@ describe("ConfigArrayFactory", () => { describe("even if config files have Unicode BOM,", () => { it("should read the JSON config file correctly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "bom/.eslintrc.json": "\uFEFF{ \"rules\": { \"semi\": \"error\" } }" } @@ -1898,7 +1898,7 @@ describe("ConfigArrayFactory", () => { }); it("should read the YAML config file correctly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "bom/.eslintrc.yaml": "\uFEFFrules:\n semi: error" } @@ -1914,7 +1914,7 @@ describe("ConfigArrayFactory", () => { }); it("should read the config in package.json correctly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "bom/package.json": "\uFEFF{ \"eslintConfig\": { \"rules\": { \"semi\": \"error\" } } }" } @@ -1931,7 +1931,7 @@ describe("ConfigArrayFactory", () => { }); it("throws an error including the config file name if the config file is invalid", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ files: { "invalid/invalid-top-level-property.yml": "invalidProperty: 3" } @@ -1950,7 +1950,7 @@ describe("ConfigArrayFactory", () => { // This group moved from 'tests/lib/config/config-file.js' when refactoring to keep the cumulated test cases. describe("'extends' property should resolve the location of configs properly.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { "node_modules/eslint-config-foo/index.js": "", @@ -2031,7 +2031,7 @@ describe("ConfigArrayFactory", () => { // This group moved from 'tests/lib/config/plugins.js' when refactoring to keep the cumulated test cases. describe("'plugins' property should load a correct plugin.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { "node_modules/@scope/eslint-plugin-example/index.js": "exports.name = '@scope/eslint-plugin-example';", @@ -2141,7 +2141,7 @@ describe("ConfigArrayFactory", () => { // This group moved from 'tests/lib/config/plugins.js' when refactoring to keep the cumulated test cases. describe("'plugins' property should load some correct plugins.", () => { - const { ConfigArrayFactory } = defineConfigArrayFactoryWithInmemoryFileSystem({ + const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { "node_modules/eslint-plugin-example1/index.js": "exports.name = 'eslint-plugin-example1';", diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index edf8e0311e4..7292f46c2d0 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -13,7 +13,7 @@ const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); const { FileEnumerator } = require("../../../lib/cli-engine/file-enumerator"); const { IgnoredPaths } = require("../../../lib/util/ignored-paths"); -const { defineFileEnumeratorWithInmemoryFileSystem } = require("./_utils"); +const { defineFileEnumeratorWithInMemoryFileSystem } = require("./_utils"); describe("FileEnumerator", () => { describe("'iterateFiles(patterns)' method should iterate files and configs.", () => { @@ -46,7 +46,7 @@ describe("FileEnumerator", () => { }; describe(`with the files ${JSON.stringify(files)}`, () => { - const { FileEnumerator } = defineFileEnumeratorWithInmemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow + const { FileEnumerator } = defineFileEnumeratorWithInMemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow /** @type {FileEnumerator} */ let enumerator; diff --git a/tests/lib/util/npm-utils.js b/tests/lib/util/npm-utils.js index a1deaf9ceb4..69257c8add2 100644 --- a/tests/lib/util/npm-utils.js +++ b/tests/lib/util/npm-utils.js @@ -28,7 +28,7 @@ const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); * @param {Object} files The file definitions. * @returns {Object} `npm-utils`. */ -function requireNpmUtilsWithInmemoryFileSystem(files) { +function requireNpmUtilsWithInMemoryFileSystem(files) { const fs = new MemoryFs({ cwd: process.cwd, platform: process.platform === "win32" ? "win32" : "posix" @@ -99,7 +99,7 @@ describe("npmUtils", () => { }); it("should handle missing devDependencies key", () => { - const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow + const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow "package.json": JSON.stringify({ private: true, dependencies: {} }) }); @@ -108,7 +108,7 @@ describe("npmUtils", () => { }); it("should throw with message when parsing invalid package.json", () => { - const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow + const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow "package.json": "{ \"not: \"valid json\" }" }); @@ -158,7 +158,7 @@ describe("npmUtils", () => { }); it("should handle missing dependencies key", () => { - const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow + const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow "package.json": JSON.stringify({ private: true, devDependencies: {} }) }); @@ -167,7 +167,7 @@ describe("npmUtils", () => { }); it("should throw with message when parsing invalid package.json", () => { - const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow + const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow "package.json": "{ \"not: \"valid json\" }" }); @@ -184,7 +184,7 @@ describe("npmUtils", () => { describe("checkPackageJson()", () => { it("should return true if package.json exists", () => { - const npmUtils = requireNpmUtilsWithInmemoryFileSystem({ // eslint-disable-line no-shadow + const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow "package.json": "{ \"file\": \"contents\" }" }); @@ -192,7 +192,7 @@ describe("npmUtils", () => { }); it("should return false if package.json does not exist", () => { - const npmUtils = requireNpmUtilsWithInmemoryFileSystem({}); // eslint-disable-line no-shadow + const npmUtils = requireNpmUtilsWithInMemoryFileSystem({}); // eslint-disable-line no-shadow assert.strictEqual(npmUtils.checkPackageJson(), false); }); From 41f0d91533f279b84ed54867b1f2f7be69d59884 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 16:01:50 +0900 Subject: [PATCH 18/49] remove JSON.stringify(files) from test subjects --- .../cascading-config-array-factory.js | 62 +++++++++---------- tests/lib/cli-engine/file-enumerator.js | 51 ++++++++------- 2 files changed, 55 insertions(+), 58 deletions(-) diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 74655389a5b..dab72b138ab 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -17,35 +17,34 @@ const { defineCascadingConfigArrayFactoryWithInMemoryFileSystem } = require("./_ describe("CascadingConfigArrayFactory", () => { describe("'getConfigArrayForFile(filePath)' method should retrieve the proper configuration.", () => { - const root = path.join(os.tmpdir(), "eslint/cli-engine/cascading-config-array-factory"); - const files = { - /* eslint-disable quote-props */ - "lib": { - "nested": { + describe("with three directories ('lib', 'lib/nested', 'test') that contains 'one.js' and 'two.js'", () => { + const root = path.join(os.tmpdir(), "eslint/cli-engine/cascading-config-array-factory"); + const files = { + /* eslint-disable quote-props */ + "lib": { + "nested": { + "one.js": "", + "two.js": "", + "parser.js": "", + ".eslintrc.yml": "parser: './parser'" + }, + "one.js": "", + "two.js": "" + }, + "test": { "one.js": "", "two.js": "", - "parser.js": "", - ".eslintrc.yml": "parser: './parser'" + ".eslintrc.yml": "env: { mocha: true }" }, - "one.js": "", - "two.js": "" - }, - "test": { - "one.js": "", - "two.js": "", - ".eslintrc.yml": "env: { mocha: true }" - }, - ".eslintignore": "/lib/nested/parser.js", - ".eslintrc.json": JSON.stringify({ - rules: { - "no-undef": "error", - "no-unused-vars": "error" - } - }) - /* eslint-enable quote-props */ - }; - - describe(`with the files ${JSON.stringify(files)}`, () => { + ".eslintignore": "/lib/nested/parser.js", + ".eslintrc.json": JSON.stringify({ + rules: { + "no-undef": "error", + "no-unused-vars": "error" + } + }) + /* eslint-enable quote-props */ + }; const { CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow /** @type {CascadingConfigArrayFactory} */ @@ -1146,12 +1145,11 @@ describe("CascadingConfigArrayFactory", () => { }); describe("'clearCache()' method should clear cache.", () => { - const root = path.join(os.tmpdir(), "eslint/cli-engine/cascading-config-array-factory"); - const files = { - ".eslintrc.js": "" - }; - - describe(`with the files ${JSON.stringify(files)}`, () => { + describe("with a '.eslintrc.js' file", () => { + const root = path.join(os.tmpdir(), "eslint/cli-engine/cascading-config-array-factory"); + const files = { + ".eslintrc.js": "" + }; const { CascadingConfigArrayFactory // eslint-disable-line no-shadow } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index 7292f46c2d0..250bad2667f 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -17,35 +17,34 @@ const { defineFileEnumeratorWithInMemoryFileSystem } = require("./_utils"); describe("FileEnumerator", () => { describe("'iterateFiles(patterns)' method should iterate files and configs.", () => { - const root = path.join(os.tmpdir(), "eslint/file-enumerator"); - const files = { - /* eslint-disable quote-props */ - "lib": { - "nested": { + describe("with three directories ('lib', 'lib/nested', 'test') that contains 'one.js' and 'two.js'", () => { + const root = path.join(os.tmpdir(), "eslint/file-enumerator"); + const files = { + /* eslint-disable quote-props */ + "lib": { + "nested": { + "one.js": "", + "two.js": "", + "parser.js": "", + ".eslintrc.yml": "parser: './parser'" + }, + "one.js": "", + "two.js": "" + }, + "test": { "one.js": "", "two.js": "", - "parser.js": "", - ".eslintrc.yml": "parser: './parser'" + ".eslintrc.yml": "env: { mocha: true }" }, - "one.js": "", - "two.js": "" - }, - "test": { - "one.js": "", - "two.js": "", - ".eslintrc.yml": "env: { mocha: true }" - }, - ".eslintignore": "/lib/nested/parser.js", - ".eslintrc.json": JSON.stringify({ - rules: { - "no-undef": "error", - "no-unused-vars": "error" - } - }) - /* eslint-enable quote-props */ - }; - - describe(`with the files ${JSON.stringify(files)}`, () => { + ".eslintignore": "/lib/nested/parser.js", + ".eslintrc.json": JSON.stringify({ + rules: { + "no-undef": "error", + "no-unused-vars": "error" + } + }) + /* eslint-enable quote-props */ + }; const { FileEnumerator } = defineFileEnumeratorWithInMemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow /** @type {FileEnumerator} */ From f8e310296ea0fe19f41305e995ca992fa89c8be2 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 16:08:49 +0900 Subject: [PATCH 19/49] remove shadowing variables --- .../cascading-config-array-factory.js | 52 +++++++++---------- tests/lib/cli-engine/file-enumerator.js | 4 +- tests/lib/util/npm-utils.js | 24 ++++----- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index dab72b138ab..c247cfb278e 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -10,7 +10,6 @@ const os = require("os"); const { assert } = require("chai"); const sh = require("shelljs"); const sinon = require("sinon"); -const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); const { ConfigArrayFactory } = require("../../../lib/cli-engine/config-array-factory"); const { ExtractedConfig } = require("../../../lib/cli-engine/config-array/extracted-config"); const { defineCascadingConfigArrayFactoryWithInMemoryFileSystem } = require("./_utils"); @@ -45,7 +44,7 @@ describe("CascadingConfigArrayFactory", () => { }) /* eslint-enable quote-props */ }; - const { CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow + const { CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); /** @type {CascadingConfigArrayFactory} */ let factory; @@ -86,6 +85,7 @@ describe("CascadingConfigArrayFactory", () => { // This group moved from 'tests/lib/config.js' when refactoring to keep the cumulated test cases. describe("with 'tests/fixtures/config-hierarchy' files", () => { + const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); let fixtureDir; let sandbox; @@ -669,7 +669,7 @@ describe("CascadingConfigArrayFactory", () => { describe("personal config file within home directory", () => { const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow + CascadingConfigArrayFactory: StubbedCascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ files: { "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY @@ -689,7 +689,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "home-folder"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath }); mockOsHomedir(homePath); @@ -707,7 +707,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "home-folder", "project"); const homePath = getFakeFixturePath("personal-config", "home-folder"); const filePath = getFakeFixturePath("personal-config", "home-folder", "project", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath }); mockOsHomedir(homePath); @@ -726,7 +726,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "home-folder"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath, specificConfigPath: configPath }); @@ -746,7 +746,7 @@ describe("CascadingConfigArrayFactory", () => { it("should still load the project config if the current working directory is the same as the home folder", () => { const projectPath = getFakeFixturePath("personal-config", "project-with-config"); const filePath = getFakeFixturePath("personal-config", "project-with-config", "subfolder", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath }); mockOsHomedir(projectPath); @@ -764,7 +764,7 @@ describe("CascadingConfigArrayFactory", () => { describe("when no local or personal config is found", () => { const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow + CascadingConfigArrayFactory: StubbedCascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ files: { "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY @@ -784,7 +784,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath }); mockOsHomedir(homePath); @@ -797,7 +797,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "home-folder-with-packagejson"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath }); + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath }); mockOsHomedir(homePath); @@ -810,7 +810,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ cwd: projectPath, useEslintrc: false }); + const factory = new StubbedCascadingConfigArrayFactory({ cwd: projectPath, useEslintrc: false }); mockOsHomedir(homePath); @@ -821,7 +821,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cliConfig: { rules: { quotes: [2, "single"] } }, @@ -837,7 +837,7 @@ describe("CascadingConfigArrayFactory", () => { const projectPath = getFakeFixturePath("personal-config", "project-without-config"); const homePath = getFakeFixturePath("personal-config", "folder-does-not-exist"); const filePath = getFakeFixturePath("personal-config", "project-without-config", "foo.js"); - const factory = new CascadingConfigArrayFactory({ baseConfig: {}, cwd: projectPath }); + const factory = new StubbedCascadingConfigArrayFactory({ baseConfig: {}, cwd: projectPath }); mockOsHomedir(homePath); @@ -847,7 +847,7 @@ describe("CascadingConfigArrayFactory", () => { describe("with overrides", () => { const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow + CascadingConfigArrayFactory: StubbedCascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ files: { "eslint/fixtures/config-hierarchy": DIRECTORY_CONFIG_HIERARCHY @@ -865,7 +865,7 @@ describe("CascadingConfigArrayFactory", () => { } it("should merge override config when the pattern matches the file name", () => { - const factory = new CascadingConfigArrayFactory({}); + const factory = new StubbedCascadingConfigArrayFactory({}); const targetPath = getFakeFixturePath("overrides", "foo.js"); const expected = { rules: { @@ -881,7 +881,7 @@ describe("CascadingConfigArrayFactory", () => { }); it("should merge override config when the pattern matches the file path relative to the config file", () => { - const factory = new CascadingConfigArrayFactory({}); + const factory = new StubbedCascadingConfigArrayFactory({}); const targetPath = getFakeFixturePath("overrides", "child", "child-one.js"); const expected = { rules: { @@ -900,7 +900,7 @@ describe("CascadingConfigArrayFactory", () => { it("should not merge override config when the pattern matches the absolute file path", () => { const resolvedPath = path.resolve(__dirname, "../../fixtures/config-hierarchy/overrides/bar.js"); - assert.throws(() => new CascadingConfigArrayFactory({ + assert.throws(() => new StubbedCascadingConfigArrayFactory({ baseConfig: { overrides: [{ files: resolvedPath, @@ -916,7 +916,7 @@ describe("CascadingConfigArrayFactory", () => { it("should not merge override config when the pattern traverses up the directory tree", () => { const parentPath = "overrides/../**/*.js"; - assert.throws(() => new CascadingConfigArrayFactory({ + assert.throws(() => new StubbedCascadingConfigArrayFactory({ baseConfig: { overrides: [{ files: parentPath, @@ -930,7 +930,7 @@ describe("CascadingConfigArrayFactory", () => { }); it("should merge all local configs (override and non-override) before non-local configs", () => { - const factory = new CascadingConfigArrayFactory({}); + const factory = new StubbedCascadingConfigArrayFactory({}); const targetPath = getFakeFixturePath("overrides", "two", "child-two.js"); const expected = { rules: { @@ -948,7 +948,7 @@ describe("CascadingConfigArrayFactory", () => { it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => { const targetPath = getFakeFixturePath("overrides", "three", "foo.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: getFakeFixturePath("overrides"), baseConfig: { overrides: [ @@ -974,7 +974,7 @@ describe("CascadingConfigArrayFactory", () => { it("should apply overrides if all glob patterns match", () => { const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: getFakeFixturePath("overrides"), baseConfig: { overrides: [{ @@ -998,7 +998,7 @@ describe("CascadingConfigArrayFactory", () => { it("should apply overrides even if some glob patterns do not match", () => { const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: getFakeFixturePath("overrides"), baseConfig: { overrides: [{ @@ -1022,7 +1022,7 @@ describe("CascadingConfigArrayFactory", () => { it("should not apply overrides if any excluded glob patterns match", () => { const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: getFakeFixturePath("overrides"), baseConfig: { overrides: [{ @@ -1045,7 +1045,7 @@ describe("CascadingConfigArrayFactory", () => { it("should apply overrides if all excluded glob patterns fail to match", () => { const targetPath = getFakeFixturePath("overrides", "one", "child-one.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: getFakeFixturePath("overrides"), baseConfig: { overrides: [{ @@ -1070,7 +1070,7 @@ describe("CascadingConfigArrayFactory", () => { it("should cascade", () => { const targetPath = getFakeFixturePath("overrides", "foo.js"); - const factory = new CascadingConfigArrayFactory({ + const factory = new StubbedCascadingConfigArrayFactory({ cwd: getFakeFixturePath("overrides"), baseConfig: { overrides: [ @@ -1151,7 +1151,7 @@ describe("CascadingConfigArrayFactory", () => { ".eslintrc.js": "" }; const { - CascadingConfigArrayFactory // eslint-disable-line no-shadow + CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => root, files }); /** @type {Map} */ diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index 250bad2667f..4af721fb752 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -11,7 +11,6 @@ const { assert } = require("chai"); const sh = require("shelljs"); const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); -const { FileEnumerator } = require("../../../lib/cli-engine/file-enumerator"); const { IgnoredPaths } = require("../../../lib/util/ignored-paths"); const { defineFileEnumeratorWithInMemoryFileSystem } = require("./_utils"); @@ -45,7 +44,7 @@ describe("FileEnumerator", () => { }) /* eslint-enable quote-props */ }; - const { FileEnumerator } = defineFileEnumeratorWithInMemoryFileSystem({ cwd: () => root, files }); // eslint-disable-line no-shadow + const { FileEnumerator } = defineFileEnumeratorWithInMemoryFileSystem({ cwd: () => root, files }); /** @type {FileEnumerator} */ let enumerator; @@ -169,6 +168,7 @@ describe("FileEnumerator", () => { // This group moved from 'tests/lib/util/glob-utils.js' when refactoring to keep the cumulated test cases. describe("with 'tests/fixtures/glob-utils' files", () => { + const { FileEnumerator } = require("../../../lib/cli-engine/file-enumerator"); let fixtureDir; /** diff --git a/tests/lib/util/npm-utils.js b/tests/lib/util/npm-utils.js index 69257c8add2..2bbab8ec598 100644 --- a/tests/lib/util/npm-utils.js +++ b/tests/lib/util/npm-utils.js @@ -99,22 +99,22 @@ describe("npmUtils", () => { }); it("should handle missing devDependencies key", () => { - const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow + const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ "package.json": JSON.stringify({ private: true, dependencies: {} }) }); // Should not throw. - npmUtils.checkDevDeps(["some-package"]); + stubbedNpmUtils.checkDevDeps(["some-package"]); }); it("should throw with message when parsing invalid package.json", () => { - const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow + const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ "package.json": "{ \"not: \"valid json\" }" }); assert.throws(() => { try { - npmUtils.checkDevDeps(["some-package"]); + stubbedNpmUtils.checkDevDeps(["some-package"]); } catch (error) { assert.strictEqual(error.messageTemplate, "failed-to-read-json"); throw error; @@ -158,22 +158,22 @@ describe("npmUtils", () => { }); it("should handle missing dependencies key", () => { - const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow + const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ "package.json": JSON.stringify({ private: true, devDependencies: {} }) }); // Should not throw. - npmUtils.checkDeps(["some-package"]); + stubbedNpmUtils.checkDeps(["some-package"]); }); it("should throw with message when parsing invalid package.json", () => { - const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow + const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ "package.json": "{ \"not: \"valid json\" }" }); assert.throws(() => { try { - npmUtils.checkDeps(["some-package"]); + stubbedNpmUtils.checkDeps(["some-package"]); } catch (error) { assert.strictEqual(error.messageTemplate, "failed-to-read-json"); throw error; @@ -184,17 +184,17 @@ describe("npmUtils", () => { describe("checkPackageJson()", () => { it("should return true if package.json exists", () => { - const npmUtils = requireNpmUtilsWithInMemoryFileSystem({ // eslint-disable-line no-shadow + const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({ "package.json": "{ \"file\": \"contents\" }" }); - assert.strictEqual(npmUtils.checkPackageJson(), true); + assert.strictEqual(stubbedNpmUtils.checkPackageJson(), true); }); it("should return false if package.json does not exist", () => { - const npmUtils = requireNpmUtilsWithInMemoryFileSystem({}); // eslint-disable-line no-shadow + const stubbedNpmUtils = requireNpmUtilsWithInMemoryFileSystem({}); - assert.strictEqual(npmUtils.checkPackageJson(), false); + assert.strictEqual(stubbedNpmUtils.checkPackageJson(), false); }); }); From 3df21e8d7de4add964ce0df62411762b63037ff7 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 16:13:50 +0900 Subject: [PATCH 20/49] update wrong JSDoc comment --- tests/lib/cli-engine/cascading-config-array-factory.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index c247cfb278e..6f82a1d4b86 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -112,8 +112,13 @@ describe("CascadingConfigArrayFactory", () => { } /** - * Asserts that two configs are equal. This is necessary because assert.deepStrictEqual() - * gets confused when properties are in different orders. + * Assert that given two objects have the same properties with the + * same value for each. + * + * The `expected` object is merged with the default values of config + * data before comparing, so you can specify only the properties you + * focus on. + * * @param {Object} actual The config object to check. * @param {Object} expected What the config object should look like. * @returns {void} From f8f69501eddd9616a5ebcd0a95c09ed352d305a1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 16:18:35 +0900 Subject: [PATCH 21/49] remove a workaround for Node 6 --- .../lib/cli-engine/cascading-config-array-factory.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 6f82a1d4b86..1b307de99f3 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -1113,10 +1113,13 @@ describe("CascadingConfigArrayFactory", () => { const cwd = path.resolve(__dirname, "../../fixtures/config-file/"); let warning = null; - function onWarning(w) { // eslint-disable-line require-jsdoc - - // Node.js 6.x does not have 'w.code' property. - if (!Object.prototype.hasOwnProperty.call(w, "code") || typeof w.code === "string" && w.code.startsWith("ESLINT_")) { + /** + * Store a reported warning object if that code starts with `ESLINT_`. + * @param {{code:string, message:string}} w The warning object to store. + * @returns {void} + */ + function onWarning(w) { + if (w.code.startsWith("ESLINT_")) { warning = w; } } From d1c27520ddede2cdc023a026afb51d9023116f92 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 17:00:28 +0900 Subject: [PATCH 22/49] make `factory` instance unique for each test case --- tests/lib/cli-engine/config-array-factory.js | 28 ++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 00fbfae3015..517b123fc1a 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -69,7 +69,13 @@ describe("ConfigArrayFactory", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir }); - const factory = new ConfigArrayFactory(); + + /** @type {ConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new ConfigArrayFactory(); + }); it("should return an empty config array if 'configData' is null.", () => { assert.strictEqual(factory.create(null).length, 0); @@ -156,7 +162,13 @@ describe("ConfigArrayFactory", () => { "package-json-no-config/package.json": "{ \"name\": \"foo\" }" } }); - const factory = new ConfigArrayFactory(); + + /** @type {ConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new ConfigArrayFactory(); + }); it("should throw an error if 'filePath' is null.", () => { assert.throws(() => factory.loadFile(null)); @@ -181,7 +193,7 @@ describe("ConfigArrayFactory", () => { }); for (const filePath of Object.keys(basicFiles)) { - it(`should load '${filePath}' then return a config array what contains that file content.`, () => { + it(`should load '${filePath}' then return a config array what contains that file content.`, () => { // eslint-disable-line no-loop-func const configArray = factory.loadFile(filePath); assert.strictEqual(configArray.length, 1); @@ -267,7 +279,13 @@ describe("ConfigArrayFactory", () => { "package-json-no-config/package.json": "{ \"name\": \"foo\" }" } }); - const factory = new ConfigArrayFactory(); + + /** @type {ConfigArrayFactory} */ + let factory; + + beforeEach(() => { + factory = new ConfigArrayFactory(); + }); it("should throw an error if 'directoryPath' is null.", () => { assert.throws(() => factory.loadOnDirectory(null)); @@ -290,7 +308,7 @@ describe("ConfigArrayFactory", () => { for (const filePath of Object.keys(basicFiles)) { const directoryPath = filePath.split("/")[0]; - it(`should load '${directoryPath}' then return a config array what contains the config file of that directory.`, () => { + it(`should load '${directoryPath}' then return a config array what contains the config file of that directory.`, () => { // eslint-disable-line no-loop-func const configArray = factory.loadOnDirectory(directoryPath); assert.strictEqual(configArray.length, 1); From 75c5157457a2eebbc7198117154ff903a4d68fce Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 17:03:19 +0900 Subject: [PATCH 23/49] add a test case for `root` --- tests/lib/cli-engine/config-array/config-array.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index 4500208582a..d15f89409c7 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -51,6 +51,7 @@ describe("ConfigArray", () => { { elements: [{ root: true }], expected: true }, { elements: [{ root: true }, { root: false }], expected: false }, { elements: [{ root: false }, { root: true }], expected: true }, + { elements: [{ root: false }, { root: true }, { rules: {} }], expected: true }, // ignore undefined. { elements: [{ root: true }, { root: 1 }], expected: true } // ignore non-boolean value ]; From 8a2772e889b3a8538004ba954b0da63e2bf1272d Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 17:23:06 +0900 Subject: [PATCH 24/49] fix a test case more readable --- tests/lib/cli-engine/config-array/config-array.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index d15f89409c7..ad2cb7c83e3 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -710,12 +710,11 @@ describe("ConfigArray", () => { } it("should not contain duplicate values.", () => { - const configs = [ - configArray.extractConfig(__filename), - configArray.extractConfig(`${__filename}.ts`), - configArray.extractConfig(path.join(__dirname, "foo.js")) - ]; + // Call some times, including with the same arguments. + configArray.extractConfig(__filename); + configArray.extractConfig(`${__filename}.ts`); + configArray.extractConfig(path.join(__dirname, "foo.js")); configArray.extractConfig(__filename); configArray.extractConfig(path.join(__dirname, "foo.js")); configArray.extractConfig(path.join(__dirname, "bar.js")); @@ -723,9 +722,7 @@ describe("ConfigArray", () => { const usedConfigs = getUsedExtractedConfigs(configArray); - assert.deepStrictEqual(usedConfigs.filter(c => c === configs[0]).length, 1); - assert.deepStrictEqual(usedConfigs.filter(c => c === configs[1]).length, 1); - assert.deepStrictEqual(usedConfigs.filter(c => c === configs[2]).length, 1); + assert.strictEqual(new Set(usedConfigs).size, usedConfigs.length); }); }); }); From 6ea6c1ec627aa78565991694f7328895e8235bba Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 12 Apr 2019 18:40:21 +0900 Subject: [PATCH 25/49] improve tests for JSON.stringify and console.log --- .../config-array/config-dependency.js | 18 ++++-- .../config-array/override-tester.js | 3 + .../config-array/config-dependency.js | 58 ++++++++++++++----- .../config-array/override-tester.js | 20 +++++-- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/lib/cli-engine/config-array/config-dependency.js b/lib/cli-engine/config-array/config-dependency.js index 08a359649ad..76189a7d17a 100644 --- a/lib/cli-engine/config-array/config-dependency.js +++ b/lib/cli-engine/config-array/config-dependency.js @@ -85,6 +85,20 @@ class ConfigDependency { * @returns {Object} a JSON compatible object. */ toJSON() { + const obj = this[util.inspect.custom](); + + // Display `error.message` (`Error#message` is unenumerable). + if (obj.error instanceof Error) { + obj.error = { ...obj.error, message: obj.error.message }; + } + + return obj; + } + + /** + * @returns {Object} an object to display by `console.log()`. + */ + [util.inspect.custom]() { const { definition: _ignore, // eslint-disable-line no-unused-vars ...obj @@ -92,10 +106,6 @@ class ConfigDependency { return obj; } - - [util.inspect.custom]() { - return this.toJSON(); - } } /** @typedef {ConfigDependency} DependentParser */ diff --git a/lib/cli-engine/config-array/override-tester.js b/lib/cli-engine/config-array/override-tester.js index d0d30dff18d..de6c7c26151 100644 --- a/lib/cli-engine/config-array/override-tester.js +++ b/lib/cli-engine/config-array/override-tester.js @@ -169,6 +169,9 @@ class OverrideTester { }; } + /** + * @returns {Object} an object to display by `console.log()`. + */ [util.inspect.custom]() { return this.toJSON(); } diff --git a/tests/lib/cli-engine/config-array/config-dependency.js b/tests/lib/cli-engine/config-array/config-dependency.js index e844a3afad0..fc008fa004b 100644 --- a/tests/lib/cli-engine/config-array/config-dependency.js +++ b/tests/lib/cli-engine/config-array/config-dependency.js @@ -5,6 +5,8 @@ "use strict"; const assert = require("assert"); +const { Console } = require("console"); +const { Writable } = require("stream"); const { ConfigDependency } = require("../../../../lib/cli-engine/config-array/config-dependency"); describe("ConfigDependency", () => { @@ -49,8 +51,8 @@ describe("ConfigDependency", () => { }); }); - describe("'JSON.stringify(...)' should return readable JSON; not include 'definition' objects", () => { - it("should return an object that has five properties.", () => { + describe("'JSON.stringify(...)' should return readable JSON; not include 'definition' property", () => { + it("should not print 'definition' property.", () => { const dep = new ConfigDependency({ definition: { name: "definition?" }, error: new Error("error?"), @@ -60,33 +62,59 @@ describe("ConfigDependency", () => { importerPath: "importerPath?" }); - assert.strictEqual( - JSON.stringify(dep), - "{\"error\":{},\"filePath\":\"filePath?\",\"id\":\"id?\",\"importerName\":\"importerName?\",\"importerPath\":\"importerPath?\"}" + assert.deepStrictEqual( + JSON.parse(JSON.stringify(dep)), + { + error: { message: "error?" }, + filePath: "filePath?", + id: "id?", + importerName: "importerName?", + importerPath: "importerPath?" + } ); }); }); - describe("'console.log(...)' should print readable string; not include 'Minimatch' objects", () => { - it("should use 'toJSON()' method.", () => { + describe("'console.log(...)' should print readable string; not include 'defininition' property", () => { + + // Record the written strings to `output` variable. + let output = ""; + const localConsole = new Console( + new class extends Writable { + write(chunk) { // eslint-disable-line class-methods-use-this + output += chunk; + } + }() + ); + + it("should not print 'definition' property.", () => { + const error = new Error("error?"); // reuse error object to use the same stacktrace. const dep = new ConfigDependency({ definition: { name: "definition?" }, - error: new Error("error?"), + error, filePath: "filePath?", id: "id?", importerName: "importerName?", importerPath: "importerPath?" }); - let called = false; - dep.toJSON = () => { - called = true; - return ""; - }; + // Make actual output. + output = ""; + localConsole.log(dep); + const actual = output; - console.log(dep); // eslint-disable-line no-console + // Make expected output; no `definition` property. + output = ""; + localConsole.log({ + error, + filePath: "filePath?", + id: "id?", + importerName: "importerName?", + importerPath: "importerPath?" + }); + const expected = output; - assert(called); + assert.strictEqual(actual, expected); }); }); }); diff --git a/tests/lib/cli-engine/config-array/override-tester.js b/tests/lib/cli-engine/config-array/override-tester.js index 32b3fa519ef..db9f46a4198 100644 --- a/tests/lib/cli-engine/config-array/override-tester.js +++ b/tests/lib/cli-engine/config-array/override-tester.js @@ -4,8 +4,10 @@ */ "use strict"; -const path = require("path"); const assert = require("assert"); +const { Console } = require("console"); +const path = require("path"); +const { Writable } = require("stream"); const { OverrideTester } = require("../../../../lib/cli-engine/config-array/override-tester"); describe("OverrideTester", () => { @@ -235,14 +237,22 @@ describe("OverrideTester", () => { OverrideTester.create(files2, excludedFiles2, basePath) ); - assert.strictEqual( - JSON.stringify(tester), - `{"AND":[{"includes":["${files1}"],"excludes":["${excludedFiles1}"]},{"includes":["${files2}"],"excludes":["${excludedFiles2}"]}],"basePath":${JSON.stringify(basePath)}}` + assert.deepStrictEqual( + JSON.parse(JSON.stringify(tester)), + { + AND: [ + { includes: [files1], excludes: [excludedFiles1] }, + { includes: [files2], excludes: [excludedFiles2] } + ], + basePath + } ); }); }); describe("'console.log(...)' should print readable string; not include 'Minimatch' objects", () => { + const localConsole = new Console(new Writable()); + it("should use 'toJSON()' method.", () => { const tester = OverrideTester.create("*.js", "", process.cwd()); let called = false; @@ -252,7 +262,7 @@ describe("OverrideTester", () => { return ""; }; - console.log(tester); // eslint-disable-line no-console + localConsole.log(tester); assert(called); }); From 5bc6da01a7d8a9afc56a8788a05a163dd104be2d Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 10:58:02 +0900 Subject: [PATCH 26/49] fix typo Co-Authored-By: Teddy Katz --- lib/cli-engine/cascading-config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 205aafc56c3..548cd6f9b99 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -325,7 +325,7 @@ class CascadingConfigArrayFactory { /** * Finalize a given config array. - * Concatinate `--config` and other CLI options. + * Concatenate `--config` and other CLI options. * @param {ConfigArray} configArray The parent config array. * @param {string} directoryPath The path to the leaf directory to find config files. * @returns {ConfigArray} The loaded config. From abdea3eb9b4f796e5b03f31cce94d4f4f3921ee1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 11:04:25 +0900 Subject: [PATCH 27/49] =?UTF-8?q?"on"=20=E2=86=92=20"in"=20in=20a=20commen?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Teddy Katz --- lib/cli-engine/config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index fe23b104998..e44160f460b 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -399,7 +399,7 @@ class ConfigArrayFactory { } /** - * Load the config file on a given directory if exists. + * Load the config file in a given directory if exists. * @param {string} directoryPath The path to a directory. * @param {string} name The config name. * @returns {IterableIterator | null} Loaded config. `null` if any config doesn't exist. From 511da78365397e94eabfb685acaa92d607692557 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 11:34:06 +0900 Subject: [PATCH 28/49] change `verifyText` parameter to an object --- lib/cli-engine.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/cli-engine.js b/lib/cli-engine.js index f4ce8cec9b1..cec1bdfcd30 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -193,17 +193,18 @@ function calculateStatsPerRun(results) { /** * Processes an source code using ESLint. - * @param {string} text The source code to verify. - * @param {string} filePath The path to the file of `text`. - * @param {ConfigArray} config The config. - * @param {boolean} fix If `true` then it does fix. - * @param {boolean} allowInlineConfig If `true` then it uses directive comments. - * @param {boolean} reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments. - * @param {Linter} linter The linter instance to verify. + * @param {Object} config The config object. + * @param {string} config.text The source code to verify. + * @param {string} config.filePath The path to the file of `text`. + * @param {ConfigArray} config.config The config. + * @param {boolean} config.fix If `true` then it does fix. + * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments. + * @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments. + * @param {Linter} config.linter The linter instance to verify. * @returns {LintResult} The result of linting. * @private */ -function verifyText( +function verifyText({ text, filePath, config, @@ -211,7 +212,7 @@ function verifyText( allowInlineConfig, reportUnusedDisableDirectives, linter -) { +}) { debug(`Lint ${filePath}`); // Verify. @@ -760,15 +761,15 @@ class CLIEngine { } // Do lint. - const result = verifyText( - fs.readFileSync(filePath, "utf8"), + const result = verifyText({ + text: fs.readFileSync(filePath, "utf8"), filePath, config, fix, allowInlineConfig, reportUnusedDisableDirectives, linter - ); + }); results.push(result); @@ -845,15 +846,15 @@ class CLIEngine { lastConfigArrays.push(config); // Do lint. - results.push(verifyText( + results.push(verifyText({ text, - resolvedFilename, + filePath: resolvedFilename, config, fix, allowInlineConfig, reportUnusedDisableDirectives, linter - )); + })); } // Collect used deprecated rules. From 6a3c384882fbc18475e47d1ca6b6efde648ac5e3 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 11:39:42 +0900 Subject: [PATCH 29/49] add a comment about `basename` and `resultFilePath` --- lib/cli-engine.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/cli-engine.js b/lib/cli-engine.js index cec1bdfcd30..77f1722c953 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -227,6 +227,13 @@ function verifyText({ } ); + /* + * `filePath` is `/path/of/cwd/` if a user didn't specify filename on + * `CLIEngine#executeOnText()`, so it extracts the `` part for the + * lint result. (In another case, it may be `/path/of/cwd/`.) + * On the other hand, `ConfigArray#extractConfig(filePath)` requires an + * absolute path, so it passed `filePath` as is to the `Linter` object. + */ const basename = path.basename(filePath, path.extname(filePath)); const resultFilePath = basename.startsWith("<") && basename.endsWith(">") ? basename From bfc25ec9eb82dc7a6c9b43cc0bc0e945d6295b3c Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 11:51:06 +0900 Subject: [PATCH 30/49] simplify `dirSuffix` --- lib/cli-engine.js | 4 +--- tests/lib/cli-engine.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/cli-engine.js b/lib/cli-engine.js index 77f1722c953..1456b724a80 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -679,9 +679,7 @@ class CLIEngine { } const extensions = options.extensions.map(ext => ext.replace(/^\./u, "")); - const dirSuffix = extensions.length === 1 - ? `/**/*.${extensions[0]}` - : `/**/*.{${extensions.join(",")}}`; + const dirSuffix = `/**/*.{${extensions.join(",")}}`; return patterns.filter(Boolean).map(pathname => { const resolvedPath = path.resolve(options.cwd, pathname); diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index 86a607f1e50..2e609a65061 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -3402,9 +3402,9 @@ describe("CLIEngine", () => { describe("resolveFileGlobPatterns", () => { leche.withData([ - [".", ["**/*.js"]], - ["./", ["**/*.js"]], - ["../", ["../**/*.js"]], + [".", ["**/*.{js}"]], + ["./", ["**/*.{js}"]], + ["../", ["../**/*.{js}"]], ["", []] ], (input, expected) => { @@ -3425,7 +3425,7 @@ describe("CLIEngine", () => { }; const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + assert.deepStrictEqual(result, ["one-js-file/**/*.{js}"]); }); it("should not convert path with globInputPaths option false", () => { @@ -3445,7 +3445,7 @@ describe("CLIEngine", () => { cwd: getFixturePath("glob-util") }; const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - const expected = [`${getFixturePath("glob-util", "one-js-file").replace(/\\/gu, "/")}/**/*.js`]; + const expected = [`${getFixturePath("glob-util", "one-js-file").replace(/\\/gu, "/")}/**/*.{js}`]; assert.deepStrictEqual(result, expected); }); @@ -3458,7 +3458,7 @@ describe("CLIEngine", () => { }; const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.jsx"]); + assert.deepStrictEqual(result, ["one-js-file/**/*.{jsx}"]); }); it("should convert a directory name with multiple provided extensions into a glob pattern", () => { @@ -3479,7 +3479,7 @@ describe("CLIEngine", () => { }; const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js", "two-js-files/**/*.js"]); + assert.deepStrictEqual(result, ["one-js-file/**/*.{js}", "two-js-files/**/*.{js}"]); }); it("should remove leading './' from glob patterns", () => { @@ -3489,7 +3489,7 @@ describe("CLIEngine", () => { }; const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + assert.deepStrictEqual(result, ["one-js-file/**/*.{js}"]); }); it("should convert a directory name with a trailing '/' into a glob pattern", () => { @@ -3499,7 +3499,7 @@ describe("CLIEngine", () => { }; const result = new CLIEngine(opts).resolveFileGlobPatterns(patterns); - assert.deepStrictEqual(result, ["one-js-file/**/*.js"]); + assert.deepStrictEqual(result, ["one-js-file/**/*.{js}"]); }); it("should return filenames as they are", () => { From 57774b44c2d000e36635ee8d56f12c5aeef48ddf Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 11:55:28 +0900 Subject: [PATCH 31/49] =?UTF-8?q?"on"=20=E2=86=92=20"in"=20in=20an=20error?= =?UTF-8?q?=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Teddy Katz --- lib/cli-engine/cascading-config-array-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 548cd6f9b99..1bc09987f11 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -156,7 +156,7 @@ class ConfigurationNotFoundError extends Error { * @param {string} directoryPath - The directory path. */ constructor(directoryPath) { - super(`No ESLint configuration found on ${directoryPath}.`); + super(`No ESLint configuration found in ${directoryPath}.`); this.messageTemplate = "no-config-found"; this.messageData = { directoryPath }; } From 49d1461fb697da921a6371b3071aa2b56c7818c6 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 12:03:25 +0900 Subject: [PATCH 32/49] remove the default value from `CascadingConfigArrayFactory#getConfigArrayForFile()` method --- .../cascading-config-array-factory.js | 4 ++-- .../cascading-config-array-factory.js | 21 +++++++------------ 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 1bc09987f11..ae8bf39c10a 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -223,10 +223,10 @@ class CascadingConfigArrayFactory { /** * Get the config array of a given file. - * @param {string} [filePath] The file path to a file. + * @param {string} filePath The file path to a file. * @returns {ConfigArray} The config array of the file. */ - getConfigArrayForFile(filePath = "a.js") { + getConfigArrayForFile(filePath) { const { cwd } = internalSlotsMap.get(this); const directoryPath = path.dirname(path.resolve(cwd, filePath)); diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 1b307de99f3..d6c08c76b2d 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -53,13 +53,6 @@ describe("CascadingConfigArrayFactory", () => { factory = new CascadingConfigArrayFactory(); }); - it("should retrieve the config '.eslintrc.json' if the file path was not given.", () => { - const config = factory.getConfigArrayForFile(); - - assert.strictEqual(config.length, 1); - assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); - }); - it("should retrieve the config '.eslintrc.json' if 'lib/one.js' was given.", () => { const config = factory.getConfigArrayForFile("lib/one.js"); @@ -1177,29 +1170,29 @@ describe("CascadingConfigArrayFactory", () => { }); it("should use cached instance.", () => { - const one = factory.getConfigArrayForFile(); - const two = factory.getConfigArrayForFile(); + const one = factory.getConfigArrayForFile("a.js"); + const two = factory.getConfigArrayForFile("a.js"); assert.strictEqual(one, two); }); it("should not use cached instance if 'clearCache()' method is called after first config is retrieved", () => { - const one = factory.getConfigArrayForFile(); + const one = factory.getConfigArrayForFile("a.js"); factory.clearCache(); - const two = factory.getConfigArrayForFile(); + const two = factory.getConfigArrayForFile("a.js"); assert.notStrictEqual(one, two); }); it("should have a loading error in CLI config.", () => { - const config = factory.getConfigArrayForFile(); + const config = factory.getConfigArrayForFile("a.js"); assert.strictEqual(config[1].plugins.test.definition, null); }); it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { - factory.getConfigArrayForFile(); + factory.getConfigArrayForFile("a.js"); // Add plugin. const plugin = {}; @@ -1208,7 +1201,7 @@ describe("CascadingConfigArrayFactory", () => { factory.clearCache(); // Check. - const config = factory.getConfigArrayForFile(); + const config = factory.getConfigArrayForFile("a.js"); assert.strictEqual(config[1].plugins.test.definition, plugin); }); From bdb31238cc00a6510ccfba9405efac52cb4ef6c4 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 12:13:01 +0900 Subject: [PATCH 33/49] freeze cached config arrays --- .../cascading-config-array-factory.js | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index ae8bf39c10a..1bdbaa24c42 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -283,8 +283,7 @@ class CascadingConfigArrayFactory { // Consider this is root. if (directoryPath === homePath && cwd !== homePath) { debug("Stop traversing because of considered root."); - configCache.set(directoryPath, baseConfigArray); - return baseConfigArray; + return this._cacheConfig(directoryPath, baseConfigArray); } // Load the config on this directory. @@ -294,16 +293,14 @@ class CascadingConfigArrayFactory { /* istanbul ignore next */ if (error.code === "EACCES") { debug("Stop traversing because of 'EACCES' error."); - configCache.set(directoryPath, baseConfigArray); - return baseConfigArray; + return this._cacheConfig(directoryPath, baseConfigArray); } throw error; } if (configArray.length > 0 && configArray.root) { debug("Stop traversing because of 'root:true'."); - configCache.set(directoryPath, configArray); - return configArray; + return this._cacheConfig(directoryPath, configArray); } // Load from the ancestors and merge it. @@ -319,7 +316,21 @@ class CascadingConfigArrayFactory { } // Cache and return. + return this._cacheConfig(directoryPath, configArray); + } + + /** + * Freeze and cache a given config. + * @param {string} directoryPath The path to a directory as a cache key. + * @param {ConfigArray} configArray The config array as a cache value. + * @returns {ConfigArray} The `configArray` (frozen). + */ + _cacheConfig(directoryPath, configArray) { + const { configCache } = internalSlotsMap.get(this); + + Object.freeze(configArray); configCache.set(directoryPath, configArray); + return configArray; } @@ -367,6 +378,7 @@ class CascadingConfigArrayFactory { validateConfigArray(finalConfigArray); // Cache it. + Object.freeze(finalConfigArray); finalizeCache.set(configArray, finalConfigArray); debug( From 4fae2b2be215db1b7f068e93ea081a1374a18400 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 12:20:49 +0900 Subject: [PATCH 34/49] =?UTF-8?q?"*OnDirectory"=20=E2=86=92=20"*InDirector?= =?UTF-8?q?y"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cascading-config-array-factory.js | 4 ++-- lib/cli-engine/config-array-factory.js | 8 +++---- .../cascading-config-array-factory.js | 6 ++--- tests/lib/cli-engine/config-array-factory.js | 22 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 1bdbaa24c42..51f596ae3ba 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -288,7 +288,7 @@ class CascadingConfigArrayFactory { // Load the config on this directory. try { - configArray = configArrayFactory.loadOnDirectory(directoryPath); + configArray = configArrayFactory.loadInDirectory(directoryPath); } catch (error) { /* istanbul ignore next */ if (error.code === "EACCES") { @@ -363,7 +363,7 @@ class CascadingConfigArrayFactory { ) { debug("Loading the config file of the home directory."); - finalConfigArray = configArrayFactory.loadOnDirectory( + finalConfigArray = configArrayFactory.loadInDirectory( os.homedir(), { name: "PersonalConfig", parent: finalConfigArray } ); diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index e44160f460b..e4c62d65529 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -13,7 +13,7 @@ * - If the filename was `package.json`, an IO error or an * `ESLINT_CONFIG_FIELD_NOT_FOUND` error. * - Otherwise, an IO error such as `ENOENT`. - * - `loadOnDirectory(directoryPath, options)` + * - `loadInDirectory(directoryPath, options)` * Create a `ConfigArray` instance from a config file which is on a given * directory. This tries to load `.eslintrc.*` or `package.json`. If not * found, returns an empty `ConfigArray`. @@ -373,12 +373,12 @@ class ConfigArrayFactory { * @param {ConfigArray} [options.parent] The parent config array. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. */ - loadOnDirectory(directoryPath, { name, parent } = {}) { + loadInDirectory(directoryPath, { name, parent } = {}) { const { cwd } = internalSlotsMap.get(this); const absolutePath = path.resolve(cwd, directoryPath); return createConfigArray( - this._loadConfigDataOnDirectory(absolutePath, name), + this._loadConfigDataInDirectory(absolutePath, name), parent ); } @@ -405,7 +405,7 @@ class ConfigArrayFactory { * @returns {IterableIterator | null} Loaded config. `null` if any config doesn't exist. * @private */ - _loadConfigDataOnDirectory(directoryPath, name) { + _loadConfigDataInDirectory(directoryPath, name) { for (const filename of configFilenames) { const filePath = path.join(directoryPath, filename); const originalDebugEnabled = debug.enabled; diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index d6c08c76b2d..37da391fb50 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -234,15 +234,15 @@ describe("CascadingConfigArrayFactory", () => { const configArrayFactory = new ConfigArrayFactory(); const factory = new CascadingConfigArrayFactory({ configArrayFactory }); - sandbox.spy(configArrayFactory, "loadOnDirectory"); + sandbox.spy(configArrayFactory, "loadInDirectory"); // If cached this should be called only once getConfig(factory, configPath); - const callcount = configArrayFactory.loadOnDirectory.callcount; + const callcount = configArrayFactory.loadInDirectory.callcount; getConfig(factory, configPath); - assert.strictEqual(configArrayFactory.loadOnDirectory.callcount, callcount); + assert.strictEqual(configArrayFactory.loadInDirectory.callcount, callcount); }); // make sure JS-style comments don't throw an error diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 517b123fc1a..2a1a1103271 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -261,7 +261,7 @@ describe("ConfigArrayFactory", () => { }); }); - describe("'loadOnDirectory(directoryPath, options)' method should load the config file of a directory.", () => { + describe("'loadInDirectory(directoryPath, options)' method should load the config file of a directory.", () => { const basicFiles = { "js/.eslintrc.js": "exports.settings = { name: 'js/.eslintrc.js' }", "json/.eslintrc.json": "{ \"settings\": { \"name\": \"json/.eslintrc.json\" } }", @@ -288,20 +288,20 @@ describe("ConfigArrayFactory", () => { }); it("should throw an error if 'directoryPath' is null.", () => { - assert.throws(() => factory.loadOnDirectory(null)); + assert.throws(() => factory.loadInDirectory(null)); }); it("should return an empty config array if the config file of 'directoryPath' doesn't exist.", () => { - assert.strictEqual(factory.loadOnDirectory("non-exist").length, 0); + assert.strictEqual(factory.loadInDirectory("non-exist").length, 0); }); it("should return an empty config array if the config file of 'directoryPath' was package.json and it didn't have 'eslintConfig' field.", () => { - assert.strictEqual(factory.loadOnDirectory("package-json-no-config").length, 0); + assert.strictEqual(factory.loadInDirectory("package-json-no-config").length, 0); }); it("should throw an error if the config data had invalid properties,", () => { assert.throws(() => { - factory.loadOnDirectory("invalid-property"); + factory.loadInDirectory("invalid-property"); }, /Unexpected top-level property "files"/u); }); @@ -309,7 +309,7 @@ describe("ConfigArrayFactory", () => { const directoryPath = filePath.split("/")[0]; it(`should load '${directoryPath}' then return a config array what contains the config file of that directory.`, () => { // eslint-disable-line no-loop-func - const configArray = factory.loadOnDirectory(directoryPath); + const configArray = factory.loadInDirectory(directoryPath); assert.strictEqual(configArray.length, 1); assertConfigArrayElement(configArray[0], { @@ -326,7 +326,7 @@ describe("ConfigArrayFactory", () => { const parent = new ConfigArray(); const normalizeConfigData = spy(factory, "_normalizeConfigData"); - factory.loadOnDirectory(directoryPath, { name, parent }); + factory.loadInDirectory(directoryPath, { name, parent }); assert.strictEqual(normalizeConfigData.callCount, 1); assert.strictEqual(normalizeConfigData.args[0].length, 3); @@ -340,7 +340,7 @@ describe("ConfigArrayFactory", () => { factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle - const configArray = factory.loadOnDirectory("js"); + const configArray = factory.loadInDirectory("js"); assert.strictEqual(configArray.length, 2); assert.strictEqual(configArray[0], elements[0]); @@ -353,7 +353,7 @@ describe("ConfigArrayFactory", () => { factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle - const configArray = factory.loadOnDirectory("js", { parent }); + const configArray = factory.loadInDirectory("js", { parent }); assert.strictEqual(configArray.length, 4); assert.strictEqual(configArray[0], parent[0]); @@ -368,7 +368,7 @@ describe("ConfigArrayFactory", () => { factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle - const configArray = factory.loadOnDirectory("js", { parent }); + const configArray = factory.loadInDirectory("js", { parent }); assert.strictEqual(configArray.length, 2); assert.strictEqual(configArray[0], elements[0]); @@ -377,7 +377,7 @@ describe("ConfigArrayFactory", () => { }); /* - * All of `create`, `loadFile`, and `loadOnDirectory` call this method. + * All of `create`, `loadFile`, and `loadInDirectory` call this method. * So this section tests the common part of the three. */ describe("'_normalizeConfigData(configData, options)' method should normalize the config data.", () => { From 3447120c264e3e1f35e75db10ae3d56232540707 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 12:55:11 +0900 Subject: [PATCH 35/49] normalize plugins in `_loadPlugin` method --- lib/cli-engine/config-array-factory.js | 21 ++++- .../cascading-config-array-factory.js | 15 ++-- tests/lib/cli-engine/config-array-factory.js | 84 +++++++++++++------ 3 files changed, 87 insertions(+), 33 deletions(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index e4c62d65529..547ba49e6bb 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -309,6 +309,22 @@ function createConfigArray(elements, parentConfigArray) { return configArray; } +/** + * Normalize a given plugin. + * - Ensure the object to have four properties: configs, environments, processors, and rules. + * - Ensure the object to not have other properties. + * @param {Plugin} plugin The plugin to normalize. + * @returns {Plugin} The normalized plugin. + */ +function normalizePlugin(plugin) { + return { + configs: plugin.configs || {}, + environments: plugin.environments || {}, + processors: plugin.processors || {}, + rules: plugin.rules || {} + }; +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -640,7 +656,6 @@ class ConfigArrayFactory { const plugin = this._loadPlugin(pluginName, importerPath, importerName); const configData = plugin.definition && - plugin.definition.configs && plugin.definition.configs[configName]; if (configData) { @@ -810,7 +825,7 @@ class ConfigArrayFactory { if (plugin) { return new ConfigDependency({ - definition: plugin, + definition: normalizePlugin(plugin), filePath: importerPath, id, importerName, @@ -828,7 +843,7 @@ class ConfigArrayFactory { writeDebugLogForLoading(request, relativeTo, filePath); return new ConfigDependency({ - definition: require(filePath), + definition: normalizePlugin(require(filePath)), filePath, id, importerName, diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 37da391fb50..d601ff00ad5 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -1194,16 +1194,21 @@ describe("CascadingConfigArrayFactory", () => { it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { factory.getConfigArrayForFile("a.js"); - // Add plugin. - const plugin = {}; - - additionalPluginPool.set("test", plugin); + additionalPluginPool.set("test", { configs: { name: "test" } }); factory.clearCache(); // Check. const config = factory.getConfigArrayForFile("a.js"); - assert.strictEqual(config[1].plugins.test.definition, plugin); + assert.deepStrictEqual( + config[1].plugins.test.definition, + { + configs: { name: "test" }, + environments: {}, + processors: {}, + rules: {} + } + ); }); }); }); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 2a1a1103271..2d0b210967e 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -64,6 +64,24 @@ function assertConfig(actual, providedExpected) { assert.deepStrictEqual(actual, expected); } +/** + * Assert a plugin definition. + * @param {Object} actual The actual value. + * @param {Object} providedExpected The expected value. + * @returns {void} + */ +function assertPluginDefinition(actual, providedExpected) { + const expected = { + configs: {}, + environments: {}, + processors: {}, + rules: {}, + ...providedExpected + }; + + assert.deepStrictEqual(actual, expected); +} + describe("ConfigArrayFactory", () => { describe("'create(configData, options)' method should normalize the config data.", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ @@ -673,7 +691,7 @@ describe("ConfigArrayFactory", () => { files: { "node_modules/eslint-plugin-ext/index.js": "exports.processors = { '.abc': {}, '.xyz': {}, other: {} };", "node_modules/eslint-plugin-subdir/index.js": "", - "node_modules/eslint-plugin-xxx/index.js": "exports.name = 'eslint-plugin-xxx';", + "node_modules/eslint-plugin-xxx/index.js": "exports.configs = { name: 'eslint-plugin-xxx' };", "subdir/node_modules/eslint-plugin-subdir/index.js": "", "parser.js": "" } @@ -704,7 +722,10 @@ describe("ConfigArrayFactory", () => { }); it("should have the package object at 'plugins[id].definition' property.", () => { - assert.deepStrictEqual(element.plugins.xxx.definition, { name: "eslint-plugin-xxx" }); + assertPluginDefinition( + element.plugins.xxx.definition, + { configs: { name: "eslint-plugin-xxx" } } + ); }); it("should have the path to the package at 'plugins[id].filePath' property.", () => { @@ -1189,15 +1210,16 @@ describe("ConfigArrayFactory", () => { }); describe("additional plugin pool", () => { - const plugin = {}; - beforeEach(() => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir }); factory = new ConfigArrayFactory({ - additionalPluginPool: new Map([["abc", plugin], ["eslint-plugin-def", plugin]]) + additionalPluginPool: new Map([ + ["abc", { configs: { name: "abc" } }], + ["eslint-plugin-def", { configs: { name: "def" } }] + ]) }); }); @@ -1205,28 +1227,40 @@ describe("ConfigArrayFactory", () => { const configArray = create({ plugins: ["abc"] }); assert.strictEqual(configArray[0].plugins.abc.id, "abc"); - assert.strictEqual(configArray[0].plugins.abc.definition, plugin); + assertPluginDefinition( + configArray[0].plugins.abc.definition, + { configs: { name: "abc" } } + ); }); it("should use the matched plugin in the additional plugin pool; long to short", () => { const configArray = create({ plugins: ["eslint-plugin-abc"] }); assert.strictEqual(configArray[0].plugins.abc.id, "abc"); - assert.strictEqual(configArray[0].plugins.abc.definition, plugin); + assertPluginDefinition( + configArray[0].plugins.abc.definition, + { configs: { name: "abc" } } + ); }); it("should use the matched plugin in the additional plugin pool; short to long", () => { const configArray = create({ plugins: ["def"] }); assert.strictEqual(configArray[0].plugins.def.id, "def"); - assert.strictEqual(configArray[0].plugins.def.definition, plugin); + assertPluginDefinition( + configArray[0].plugins.def.definition, + { configs: { name: "def" } } + ); }); it("should use the matched plugin in the additional plugin pool; long to long", () => { const configArray = create({ plugins: ["eslint-plugin-def"] }); assert.strictEqual(configArray[0].plugins.def.id, "def"); - assert.strictEqual(configArray[0].plugins.def.definition, plugin); + assertPluginDefinition( + configArray[0].plugins.def.definition, + { configs: { name: "def" } } + ); }); }); }); @@ -2052,8 +2086,8 @@ describe("ConfigArrayFactory", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { - "node_modules/@scope/eslint-plugin-example/index.js": "exports.name = '@scope/eslint-plugin-example';", - "node_modules/eslint-plugin-example/index.js": "exports.name = 'eslint-plugin-example';", + "node_modules/@scope/eslint-plugin-example/index.js": "exports.configs = { name: '@scope/eslint-plugin-example' };", + "node_modules/eslint-plugin-example/index.js": "exports.configs = { name: 'eslint-plugin-example' };", "node_modules/eslint-plugin-throws-on-load/index.js": "throw new Error('error thrown while loading this module')" } }); @@ -2082,18 +2116,18 @@ describe("ConfigArrayFactory", () => { it("should load a plugin when referenced by short name", () => { const loadedPlugins = load("example"); - assert.deepStrictEqual( + assertPluginDefinition( loadedPlugins.get("example"), - { name: "eslint-plugin-example" } + { configs: { name: "eslint-plugin-example" } } ); }); it("should load a plugin when referenced by long name", () => { const loadedPlugins = load("eslint-plugin-example"); - assert.deepStrictEqual( + assertPluginDefinition( loadedPlugins.get("example"), - { name: "eslint-plugin-example" } + { configs: { name: "eslint-plugin-example" } } ); }); @@ -2127,18 +2161,18 @@ describe("ConfigArrayFactory", () => { it("should load a scoped plugin when referenced by short name", () => { const loadedPlugins = load("@scope/example"); - assert.deepStrictEqual( + assertPluginDefinition( loadedPlugins.get("@scope/example"), - { name: "@scope/eslint-plugin-example" } + { configs: { name: "@scope/eslint-plugin-example" } } ); }); it("should load a scoped plugin when referenced by long name", () => { const loadedPlugins = load("@scope/eslint-plugin-example"); - assert.deepStrictEqual( + assertPluginDefinition( loadedPlugins.get("@scope/example"), - { name: "@scope/eslint-plugin-example" } + { configs: { name: "@scope/eslint-plugin-example" } } ); }); @@ -2162,8 +2196,8 @@ describe("ConfigArrayFactory", () => { const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd: () => tempDir, files: { - "node_modules/eslint-plugin-example1/index.js": "exports.name = 'eslint-plugin-example1';", - "node_modules/eslint-plugin-example2/index.js": "exports.name = 'eslint-plugin-example2';" + "node_modules/eslint-plugin-example1/index.js": "exports.configs = { name: 'eslint-plugin-example1' };", + "node_modules/eslint-plugin-example2/index.js": "exports.configs = { name: 'eslint-plugin-example2' };" } }); const factory = new ConfigArrayFactory(); @@ -2191,13 +2225,13 @@ describe("ConfigArrayFactory", () => { it("should load plugins when passed multiple plugins", () => { const loadedPlugins = loadAll(["example1", "example2"]); - assert.deepStrictEqual( + assertPluginDefinition( loadedPlugins.get("example1"), - { name: "eslint-plugin-example1" } + { configs: { name: "eslint-plugin-example1" } } ); - assert.deepStrictEqual( + assertPluginDefinition( loadedPlugins.get("example2"), - { name: "eslint-plugin-example2" } + { configs: { name: "eslint-plugin-example2" } } ); }); }); From 6f5269bff4e6a685f8e69d46d06393cf63d934aa Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:03:32 +0900 Subject: [PATCH 36/49] rename placeholder filename --- lib/cli-engine/config-array-factory.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 547ba49e6bb..8419f661984 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -679,7 +679,7 @@ class ConfigArrayFactory { */ _loadExtendedShareableConfig(extendName, importerPath, importerName) { const { cwd } = internalSlotsMap.get(this); - const relativeTo = importerPath || path.join(cwd, ".eslintrc"); + const relativeTo = importerPath || path.join(cwd, "__placeholder__.js"); let request; if (isFilePath(extendName)) { @@ -741,7 +741,7 @@ class ConfigArrayFactory { debug("Loading parser %j from %s", nameOrPath, importerPath); const { cwd } = internalSlotsMap.get(this); - const relativeTo = importerPath || path.join(cwd, ".eslintrc"); + const relativeTo = importerPath || path.join(cwd, "__placeholder__.js"); try { const filePath = ModuleResolver.resolve(nameOrPath, relativeTo); @@ -837,7 +837,7 @@ class ConfigArrayFactory { try { // Resolve the plugin file relative to the project root. - const relativeTo = path.join(cwd, ".eslintrc"); + const relativeTo = path.join(cwd, "__placeholder__.js"); const filePath = ModuleResolver.resolve(request, relativeTo); writeDebugLogForLoading(request, relativeTo, filePath); From b5c0285ed6543da21fe7d30ee9042f83027f3c6c Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:08:40 +0900 Subject: [PATCH 37/49] remove unnecessary `if` for local plugins --- lib/cli-engine/config-array-factory.js | 76 ++++++++++++-------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 8419f661984..11b803f733b 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -783,55 +783,49 @@ class ConfigArrayFactory { /** * Load a given plugin. - * @param {string} nameOrPath The plugin name to load. + * @param {string} name The plugin name to load. * @param {string} importerPath The path to a config file that imports it. This is just a debug info. * @param {string} importerName The name of a config file that imports it. This is just a debug info. * @returns {DependentPlugin} The loaded plugin. * @private */ - _loadPlugin(nameOrPath, importerPath, importerName) { - debug("Loading plugin %j from %s", nameOrPath, importerPath); + _loadPlugin(name, importerPath, importerName) { + debug("Loading plugin %j from %s", name, importerPath); const { additionalPluginPool, cwd } = internalSlotsMap.get(this); - let request, id; - - if (isFilePath(nameOrPath)) { - request = id = nameOrPath; - } else { - request = naming.normalizePackageName(nameOrPath, "eslint-plugin"); - id = naming.getShorthandName(request, "eslint-plugin"); - - if (nameOrPath.match(/\s+/u)) { - const error = Object.assign( - new Error(`Whitespace found in plugin name '${nameOrPath}'`), - { - messageTemplate: "whitespace-found", - messageData: { pluginName: request } - } - ); + const request = naming.normalizePackageName(name, "eslint-plugin"); + const id = naming.getShorthandName(request, "eslint-plugin"); + + if (name.match(/\s+/u)) { + const error = Object.assign( + new Error(`Whitespace found in plugin name '${name}'`), + { + messageTemplate: "whitespace-found", + messageData: { pluginName: request } + } + ); - return new ConfigDependency({ - error, - id, - importerName, - importerPath - }); - } + return new ConfigDependency({ + error, + id, + importerName, + importerPath + }); + } - // Check for additional pool. - const plugin = - additionalPluginPool.get(request) || - additionalPluginPool.get(id); + // Check for additional pool. + const plugin = + additionalPluginPool.get(request) || + additionalPluginPool.get(id); - if (plugin) { - return new ConfigDependency({ - definition: normalizePlugin(plugin), - filePath: importerPath, - id, - importerName, - importerPath - }); - } + if (plugin) { + return new ConfigDependency({ + definition: normalizePlugin(plugin), + filePath: importerPath, + id, + importerName, + importerPath + }); } try { @@ -850,7 +844,7 @@ class ConfigArrayFactory { importerPath }); } catch (error) { - debug("Failed to load plugin '%s' declared in '%s'.", nameOrPath, importerName); + debug("Failed to load plugin '%s' declared in '%s'.", name, importerName); if (error && error.code === "MODULE_NOT_FOUND" && error.message.includes(request)) { error.messageTemplate = "plugin-missing"; @@ -860,7 +854,7 @@ class ConfigArrayFactory { importerName }; } - error.message = `Failed to load plugin '${nameOrPath}' declared in '${importerName}': ${error.message}`; + error.message = `Failed to load plugin '${name}' declared in '${importerName}': ${error.message}`; return new ConfigDependency({ error, From 396ff47dd9ec8946a4728181a159fb6c670a9f9b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:15:36 +0900 Subject: [PATCH 38/49] add more check for __proto__ --- lib/cli-engine/config-array/config-array.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index e74ee176af7..d2a36f93a93 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -135,6 +135,10 @@ function mergeWithoutOverwrite(target, source) { } for (const key of Object.keys(source)) { + if (key === "__proto__") { + continue; + } + if (isNonNullObject(target[key])) { mergeWithoutOverwrite(target[key], source[key]); } else if (target[key] === void 0) { @@ -162,6 +166,9 @@ function mergePlugins(target, source) { } for (const key of Object.keys(source)) { + if (key === "__proto__") { + continue; + } const targetValue = target[key]; const sourceValue = source[key]; @@ -189,6 +196,9 @@ function mergeRules(target, source) { } for (const key of Object.keys(source)) { + if (key === "__proto__") { + continue; + } const targetDef = target[key]; const sourceDef = source[key]; From 55fd2da18244c19ca27927d93ea330b5a4d29218 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:21:40 +0900 Subject: [PATCH 39/49] =?UTF-8?q?"mergeRules"=20=E2=86=92=20"mergeRuleConf?= =?UTF-8?q?igs"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cli-engine/config-array/config-array.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index d2a36f93a93..65e5352577a 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -183,14 +183,14 @@ function mergePlugins(target, source) { } /** - * Merge rules. + * Merge rule configs. * `target`'s definition is prior to `source`'s. * * @param {Record} target The destination to merge * @param {Record|undefined} source The source to merge. * @returns {void} */ -function mergeRules(target, source) { +function mergeRuleConfigs(target, source) { if (!isNonNullObject(source)) { return; } @@ -257,7 +257,7 @@ function createConfig(instance, indices) { mergeWithoutOverwrite(config.parserOptions, element.parserOptions); mergeWithoutOverwrite(config.settings, element.settings); mergePlugins(config.plugins, element.plugins, slots); - mergeRules(config.rules, element.rules); + mergeRuleConfigs(config.rules, element.rules); } return config; From 5a2453de01b9d628aaaa975fbc91e4fa3ffa7af8 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:31:58 +0900 Subject: [PATCH 40/49] =?UTF-8?q?"root"=20=E2=86=92=20"isRoot()"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cli-engine/cascading-config-array-factory.js | 2 +- lib/cli-engine/config-array-factory.js | 2 +- lib/cli-engine/config-array/config-array.js | 8 ++++---- tests/lib/cli-engine/config-array/config-array.js | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 51f596ae3ba..267ea96d486 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -298,7 +298,7 @@ class CascadingConfigArrayFactory { throw error; } - if (configArray.length > 0 && configArray.root) { + if (configArray.length > 0 && configArray.isRoot()) { debug("Stop traversing because of 'root:true'."); return this._cacheConfig(directoryPath, configArray); } diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 11b803f733b..7c413271f47 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -303,7 +303,7 @@ function createConfigArray(elements, parentConfigArray) { } const configArray = new ConfigArray(...elements); - if (parentConfigArray && !configArray.root) { + if (parentConfigArray && !configArray.isRoot()) { configArray.unshift(...parentConfigArray); } return configArray; diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index 65e5352577a..60399355ce9 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -5,7 +5,7 @@ * config file, base config files that were extended, loaded parsers, and loaded * plugins. * - * `ConfigArray` class provies four properties and one method. + * `ConfigArray` class provies three properties and two methods. * * - `pluginEnvironments` * - `pluginProcessors` @@ -13,7 +13,7 @@ * The `Map` objects that contain the members of all plugins that this * config array contains. Those map objects don't have mutation methods. * Those keys are the member ID such as `pluginId/memberName`. - * - `root` + * - `isRoot()` * If `true` then this configuration has `root:true` property. * - `extractConfig(filePath)` * Extract the final configuration for a given file. This means merging @@ -404,9 +404,9 @@ class ConfigArray extends Array { /** * Check if this config has `root` flag. - * @type {boolean} + * @returns {boolean} `true` if this config array is root. */ - get root() { + isRoot() { for (let i = this.length - 1; i >= 0; --i) { const root = this[i].root; diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index ad2cb7c83e3..07cf0f2c661 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -42,7 +42,7 @@ describe("ConfigArray", () => { } }); - describe("'root' property should be the value of the last element which has 'root' property.", () => { + describe("'isRoot()' method should be the value of the last element which has 'root' property.", () => { const patterns = [ { elements: [], expected: false }, { elements: [{}], expected: false }, @@ -57,7 +57,7 @@ describe("ConfigArray", () => { for (const { elements, expected } of patterns) { it(`should be ${expected} if the elements are ${JSON.stringify(elements)}.`, () => { - assert.strictEqual(new ConfigArray(...elements).root, expected); + assert.strictEqual(new ConfigArray(...elements).isRoot(), expected); }); } }); From f8cd0980a05ba42ace0764254571caef9a148bd4 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:43:03 +0900 Subject: [PATCH 41/49] add assertion for basePath in OverrideTester.and() --- lib/cli-engine/config-array-factory.js | 15 +++++++-------- lib/cli-engine/config-array/override-tester.js | 3 +++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 7c413271f47..1ee5902f7b1 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -491,19 +491,18 @@ class ConfigArrayFactory { // Apply the criteria to every element. for (const element of elements) { - element.criteria = OverrideTester.and(criteria, element.criteria); - /* - * Adopt the base path of the entry file (the outermost base path). - * Also, ensure the elements which came from `overrides` settings - * don't have `root` property even if it came from `extends` in - * `overrides`. - */ + // Adopt the base path of the entry file (the outermost base path). if (element.criteria) { element.criteria.basePath = basePath; - element.root = void 0; } + /* + * Merge the criteria; this is for only file extension processors in + * `overrides` section for now. + */ + element.criteria = OverrideTester.and(criteria, element.criteria); + yield element; } } diff --git a/lib/cli-engine/config-array/override-tester.js b/lib/cli-engine/config-array/override-tester.js index de6c7c26151..2aaefac7d1c 100644 --- a/lib/cli-engine/config-array/override-tester.js +++ b/lib/cli-engine/config-array/override-tester.js @@ -18,6 +18,7 @@ */ "use strict"; +const assert = require("assert"); const path = require("path"); const util = require("util"); const { Minimatch } = require("minimatch"); @@ -107,6 +108,7 @@ class OverrideTester { /** * Combine two testers by logical and. * If either of the testers was `null`, returns the other tester. + * The `basePath` property of the two must be the same value. * @param {OverrideTester|null} a A tester. * @param {OverrideTester|null} b Another tester. * @returns {OverrideTester|null} Combined tester. @@ -119,6 +121,7 @@ class OverrideTester { return b; } + assert.strictEqual(a.basePath, b.basePath); return new OverrideTester(a.patterns.concat(b.patterns), a.basePath); } From bc2340479cff4129d7c5ef7ca36df4895041f30d Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:46:34 +0900 Subject: [PATCH 42/49] Update lib/config/config-initializer.js Co-Authored-By: Teddy Katz --- lib/config/config-initializer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js index 41d1edc03d9..a655a77f6c3 100644 --- a/lib/config/config-initializer.js +++ b/lib/config/config-initializer.js @@ -560,7 +560,7 @@ function promptUser() { } else if (config.extends) { config.extends = [config.extends, earlyAnswers.styleguide]; } else { - config.extends = earlyAnswers.styleguide; + config.extends = [earlyAnswers.styleguide]; } const modules = getModulesList(config); From eb5164affcc6c81198bc9148750b2f4130e30113 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:49:59 +0900 Subject: [PATCH 43/49] =?UTF-8?q?"builtInRules.keys()"=20=E2=86=92=20"buil?= =?UTF-8?q?tInRules.entries()"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/config/config-rule.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/config/config-rule.js b/lib/config/config-rule.js index bdeb2706b04..0e277fd6f3b 100644 --- a/lib/config/config-rule.js +++ b/lib/config/config-rule.js @@ -296,8 +296,7 @@ function generateConfigsFromSchema(schema) { * @returns {rulesConfig} Hash of rule names and arrays of possible configurations */ function createCoreRuleConfigs() { - return Array.from(builtInRules.keys()).reduce((accumulator, id) => { - const rule = builtInRules.get(id); + return Array.from(builtInRules.entries()).reduce((accumulator, [id, rule]) => { const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema; accumulator[id] = generateConfigsFromSchema(schema); From 810dd777b585292170526edbf10ed30015ab59e3 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:53:42 +0900 Subject: [PATCH 44/49] =?UTF-8?q?"Function.prototype"=20=E2=86=92=20"noop"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/config/config-validator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/config/config-validator.js b/lib/config/config-validator.js index 1dda7ab73f4..d37a50f1c13 100644 --- a/lib/config/config-validator.js +++ b/lib/config/config-validator.js @@ -20,6 +20,7 @@ const const ajv = require("../util/ajv")(); const ruleValidators = new WeakMap(); +const noop = Function.prototype; //------------------------------------------------------------------------------ // Private @@ -151,7 +152,7 @@ function validateRuleOptions(rule, ruleId, options, source = null) { function validateEnvironment( environment, source, - getAdditionalEnv = Function.prototype + getAdditionalEnv = noop ) { // not having an environment is ok @@ -180,7 +181,7 @@ function validateEnvironment( function validateRules( rulesConfig, source, - getAdditionalRule = Function.prototype + getAdditionalRule = noop ) { if (!rulesConfig) { return; From fb5c29ac3dcfe22ed657451e9cb37bb9239cefd3 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Thu, 9 May 2019 13:56:36 +0900 Subject: [PATCH 45/49] remove unnecessary lodash.cloneDeep --- lib/linter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/linter.js b/lib/linter.js index 4acce362ea1..e2a1bfe8fb8 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -366,7 +366,7 @@ function normalizeVerifyOptions(providedOptions) { function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { const parserOptionsFromEnv = enabledEnvironments .filter(env => env.parserOptions) - .reduce((parserOptions, env) => lodash.merge(parserOptions, lodash.cloneDeep(env.parserOptions)), {}); + .reduce((parserOptions, env) => lodash.merge(parserOptions, env.parserOptions), {}); const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {}); const isModule = mergedParserOptions.sourceType === "module"; From a852b020eefb299fe6215bfab31ca52a32c70b58 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 10 May 2019 07:37:17 +0900 Subject: [PATCH 46/49] fix about `` --- lib/cli-engine.js | 46 +++++++++++++++++++++++----------------------- lib/linter.js | 17 +++++++---------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/lib/cli-engine.js b/lib/cli-engine.js index 1456b724a80..4422f659acb 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -195,7 +195,8 @@ function calculateStatsPerRun(results) { * Processes an source code using ESLint. * @param {Object} config The config object. * @param {string} config.text The source code to verify. - * @param {string} config.filePath The path to the file of `text`. + * @param {string} config.cwd The path to the current working directory. + * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses ``. * @param {ConfigArray} config.config The config. * @param {boolean} config.fix If `true` then it does fix. * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments. @@ -206,42 +207,38 @@ function calculateStatsPerRun(results) { */ function verifyText({ text, - filePath, + cwd, + filePath: providedFilePath, config, fix, allowInlineConfig, reportUnusedDisableDirectives, linter }) { + const filePath = providedFilePath || ""; + debug(`Lint ${filePath}`); - // Verify. + /* + * Verify. + * `config.extractConfig(filePath)` requires an absolute path, but `linter` + * doesn't know CWD, so it gives `linter` an absolute path always. + */ + const filePathToVerify = filePath === "" ? path.join(cwd, filePath) : filePath; const { fixed, messages, output } = linter.verifyAndFix( text, config, { allowInlineConfig, - filename: filePath, + filename: filePathToVerify, fix, reportUnusedDisableDirectives } ); - /* - * `filePath` is `/path/of/cwd/` if a user didn't specify filename on - * `CLIEngine#executeOnText()`, so it extracts the `` part for the - * lint result. (In another case, it may be `/path/of/cwd/`.) - * On the other hand, `ConfigArray#extractConfig(filePath)` requires an - * absolute path, so it passed `filePath` as is to the `Linter` object. - */ - const basename = path.basename(filePath, path.extname(filePath)); - const resultFilePath = basename.startsWith("<") && basename.endsWith(">") - ? basename - : filePath; - // Tweak and return. const result = { - filePath: resultFilePath, + filePath, messages, ...calculateStatsPerFile(messages) }; @@ -770,6 +767,7 @@ class CLIEngine { text: fs.readFileSync(filePath, "utf8"), filePath, config, + cwd, fix, allowInlineConfig, reportUnusedDisableDirectives, @@ -810,8 +808,8 @@ class CLIEngine { /** * Executes the current configuration on text. * @param {string} text A string of JavaScript code to lint. - * @param {string} filename An optional string representing the texts filename. - * @param {boolean} warnIgnored Always warn when a file is ignored + * @param {string} [filename] An optional string representing the texts filename. + * @param {boolean} [warnIgnored] Always warn when a file is ignored * @returns {LintReport} The results for the linting. */ executeOnText(text, filename, warnIgnored) { @@ -829,18 +827,19 @@ class CLIEngine { } = internalSlotsMap.get(this); const results = []; const startTime = Date.now(); - const resolvedFilename = path.resolve(cwd, filename || ".js"); + const resolvedFilename = filename && path.resolve(cwd, filename); // Clear the last used config arrays. lastConfigArrays.length = 0; - if (filename && ignoredPaths.contains(resolvedFilename)) { + if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) { if (warnIgnored) { results.push(createIgnoreResult(resolvedFilename, cwd)); } } else { - const config = - configArrayFactory.getConfigArrayForFile(resolvedFilename); + const config = configArrayFactory.getConfigArrayForFile( + resolvedFilename || "__placeholder__.js" + ); /* * Store used configs for: @@ -855,6 +854,7 @@ class CLIEngine { text, filePath: resolvedFilename, config, + cwd, fix, allowInlineConfig, reportUnusedDisableDirectives, diff --git a/lib/linter.js b/lib/linter.js index e2a1bfe8fb8..f2bb7425932 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -1047,17 +1047,14 @@ class Linter { const config = configArray.extractConfig(providedOptions.filename); /* - * Convert "/path/to/.js" to "". - * `CLIEngine#executeOnText()` method makes the file path as `.js` - * file that is on the CWD if it was omitted. - * This stripping is for backward compatibility. + * Convert "/path/to/" to "". + * `CLIEngine#executeOnText()` method gives "/path/to/" if the + * filename was omitted because `configArray.extractConfig()` requires + * an absolute path. But linter should pass `` to + * `RuleContext#getFilename()` in that case. */ - const basename = path.basename( - providedOptions.filename, - path.extname(providedOptions.filename) - ); - const filename = basename.startsWith("<") && basename.endsWith(">") - ? basename + const filename = path.basename(providedOptions.filename) === "" + ? "" : providedOptions.filename; // Make options. From 1fda3f27986bd9130e6165cdfefca0c572ce1371 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 10 May 2019 07:44:18 +0900 Subject: [PATCH 47/49] fix `getConfigForFile()` to require an argument --- lib/cli-engine.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/cli-engine.js b/lib/cli-engine.js index 4422f659acb..b97772133ed 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -884,8 +884,7 @@ class CLIEngine { */ getConfigForFile(filePath) { const { configArrayFactory, options } = internalSlotsMap.get(this); - const absolutePath = - filePath ? path.resolve(options.cwd, filePath) : options.cwd; + const absolutePath = path.resolve(options.cwd, filePath); return configArrayFactory .getConfigArrayForFile(absolutePath) From 40925c009e0a2c111c36311f03a2eb5e1d1ba617 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 10 May 2019 07:47:19 +0900 Subject: [PATCH 48/49] =?UTF-8?q?`builtInRules.entries()`=20=E2=86=92=20`b?= =?UTF-8?q?uiltInRules`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/config/config-rule.js | 2 +- lib/rules.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/config/config-rule.js b/lib/config/config-rule.js index 0e277fd6f3b..6e6df896531 100644 --- a/lib/config/config-rule.js +++ b/lib/config/config-rule.js @@ -296,7 +296,7 @@ function generateConfigsFromSchema(schema) { * @returns {rulesConfig} Hash of rule names and arrays of possible configurations */ function createCoreRuleConfigs() { - return Array.from(builtInRules.entries()).reduce((accumulator, [id, rule]) => { + return Array.from(builtInRules).reduce((accumulator, [id, rule]) => { const schema = (typeof rule === "function") ? rule.schema : rule.meta.schema; accumulator[id] = generateConfigsFromSchema(schema); diff --git a/lib/rules.js b/lib/rules.js index ab15fa06362..4a1e3ed1351 100644 --- a/lib/rules.js +++ b/lib/rules.js @@ -92,7 +92,7 @@ class Rules { } *[Symbol.iterator]() { - yield* builtInRules.entries(); + yield* builtInRules; for (const ruleId of Object.keys(this._rules)) { yield [ruleId, this.get(ruleId)]; From fcf82281aedcd5b45e5ffc1864052beb01c2a25e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 10 May 2019 07:56:02 +0900 Subject: [PATCH 49/49] update migration guide --- docs/user-guide/migrating-to-6.0.0.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/user-guide/migrating-to-6.0.0.md b/docs/user-guide/migrating-to-6.0.0.md index e3436de10b5..f3f110e2b19 100644 --- a/docs/user-guide/migrating-to-6.0.0.md +++ b/docs/user-guide/migrating-to-6.0.0.md @@ -180,8 +180,6 @@ Due to a bug, the glob patterns in a `files` list in an `overrides` section of a ## Overrides in an extended config file can now be overridden by a parent config file -**Note:** This update is planned, but has not been implemented in the latest alpha release yet. - Due to a bug, it was previously the case that an `overrides` block in a shareable config had precedence over the top level of a parent config. For example, with the following config setup, the `semi` rule would end up enabled even though it was explicitly disabled in the end user's config: ```js