From df753899dc0cc5f0debe2513d941332e4fc8bd0d Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Fri, 7 Feb 2020 00:12:09 -0500 Subject: [PATCH 01/71] Update: Add ESLint API --- lib/cli-engine/cli-engine.js | 2 + lib/eslint/eslint.js | 417 +++++++++++++++++++++++++++++++++++ lib/eslint/index.js | 7 + 3 files changed, 426 insertions(+) create mode 100644 lib/eslint/eslint.js create mode 100644 lib/eslint/index.js diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 9d2ef8cbcde..dc0f54949b3 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -661,6 +661,8 @@ class CLIEngine { * @returns {void} */ static outputFixes(report) { + + // TODO: Write files in parallel. report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => { fs.writeFileSync(result.filePath, result.output); }); diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js new file mode 100644 index 00000000000..d86b5f5cf66 --- /dev/null +++ b/lib/eslint/eslint.js @@ -0,0 +1,417 @@ +/** + * @fileoverview Main API Class + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const { CLIEngine } = require("../cli-engine"); + +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @typedef {import("../shared/types").Rule} Rule */ + +/** + * The options with which to configure the main API instance. + * @typedef {Object} ESLintOptions + * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. + * @property {ConfigData} baseConfig Base config object, extended by all configs used with this 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. + * @property {string} cwd The value to use for the current working directory. + * @property {string[]} envs An array of environments to load. + * @property {string[]} extensions An array of file extensions to check. + * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean. + * @property {string[]} fixTypes Array of rule types to apply fixes for. + * @property {string[]} globals An array of global variables to declare. + * @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 False disables use of .eslintignore. + * @property {string} ignorePath The ignore file to use instead of .eslintignore. + * @property {string|string[]} ignorePattern One or more glob patterns to ignore. + * @property {string} parser The name of the parser to use. + * @property {ParserOptions} parserOptions An object of parserOption settings to use. + * @property {string[]} plugins An array of plugins to load. + * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives. + * @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD. + * @property {string[]} rulePaths An array of directories to load custom rules from. + * @property {Record} rules An object of rules to use. + * @property {boolean} useEslintrc False disables looking for .eslintrc + */ + +/** + * A plugin object. + * @typedef {Object} PluginElement + * @property {string} id The plugin ID. + * @property {Object} definition The plugin definition. + */ + +/** + * A rules metadata object. + * @typedef {Object} RulesMeta + * @property {string} id The plugin ID. + * @property {Object} definition The plugin definition. + */ + +/** + * A linting result. + * @typedef {Object} LintResult + * @property {string} filePath The path to the file that was linted. + * @property {LintMessage[]} messages All of the messages for 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 {string} [source] The source code of the file that was linted. + * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible. + * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. + */ + +/** + * A formatter object. + * @typedef {Object} Formatter + * @property {(results: LintResult[]) => string} format The main formatter method. + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Normalizes an array of plugins to their respective IDs. + * @param {string[]|PluginElement[]} plugins An array of plugins to normalize. + * @returns {string[]} The normalized array of plugins. + */ +function normalizePluginIds(plugins) { + return plugins.map(p => (typeof p === "string" ? p : p.id)); +} + +/** + * Validates and normalizes options for the wrapped CLIEngine instance. + * @param {ESLintOptions} options The options to process. + * @returns {ESLintOptions} The normalized options. + */ +function processOptions({ + allowInlineConfig = true, + baseConfig = null, + cache = false, + cacheLocation = ".eslintcache", + configFile = null, + cwd = process.cwd(), + envs = [], + extensions = null, + fix = false, + fixTypes = ["problem", "suggestion", "layout"], + globals = [], + globInputPaths = true, + ignore = true, + ignorePath = null, + ignorePattern = [], + parser = "espree", + parserOptions = null, + plugins = [], + reportUnusedDisableDirectives = false, + resolvePluginsRelativeTo = cwd, + rulePaths = [], + rules = null, + useEslintrc = true, + ...unknownOptions +}) { + if (Object.keys(unknownOptions).length >= 1) { + throw new Error(`${ + unknownOptions.includes("cacheFile") + ? "cacheFile has been deprecated. Please use the cacheLocation option instead. " + : "" + }Unknown options given: ${unknownOptions.join(", ")}.`); + } + + if (typeof allowInlineConfig !== "boolean") { + throw new Error("allowInlineConfig must be a boolean."); + } + + if (typeof baseConfig !== "object") { + throw new Error("baseConfig must be an object or null."); + } + + if (typeof cache !== "boolean") { + throw new Error("cache must be a boolean."); + } + + if (typeof cacheLocation !== "string") { + throw new Error("cacheLocation must be a string."); + } + + if (typeof configFile !== "string" && cacheLocation !== null) { + throw new Error("configFile must be a string or null."); + } + + if (typeof cwd !== "string") { + throw new Error("cwd must be a string."); + } + + if (!Array.isArray(envs)) { + throw new Error("envs must be an array."); + } + + if (!Array.isArray(extensions) && extensions !== null) { + throw new Error("extensions must be an array or null."); + } + + if (typeof fix !== "boolean") { + throw new Error("fix must be a boolean."); + } + + if (!Array.isArray(fixTypes)) { + throw new Error("fixTypes must be an array."); + } + + if (!Array.isArray(globals)) { + throw new Error("globals must be an array."); + } + + if (typeof globInputPaths !== "boolean") { + throw new Error("globInputPaths must be a boolean."); + } + + if (typeof ignore !== "boolean") { + throw new Error("globInputPaths must be a boolean."); + } + + if (typeof ignorePath !== "string" && ignorePath !== null) { + throw new Error("ignorePath must be a string or null."); + } + + if (typeof ignorePattern !== "string" && !Array.isArray(ignorePattern)) { + throw new Error("ignorePattern must be a string or an array of strings."); + } + + if (typeof parser !== "string") { + throw new Error("parser must be a string."); + } + + if (typeof parserOptions !== "object") { + throw new Error("parserOptions must be an object or null."); + } + + if (!Array.isArray(plugins)) { + throw new Error("plugins must be an array."); + } + + if (typeof reportUnusedDisableDirectives !== "boolean") { + throw new Error("reportUnusedDisableDirectives must be a boolean."); + } + + if (typeof resolvePluginsRelativeTo !== "string") { + throw new Error("resolvePluginsRelativeTo must be a string."); + } + + if (!Array.isArray(rulePaths)) { + throw new Error("plugins must be an array."); + } + + if (typeof rules !== "object") { + throw new Error("rules must be an object or null."); + } + + if (typeof useEslintrc !== "boolean") { + throw new Error("useElintrc must be a boolean."); + } + + return { + allowInlineConfig, + baseConfig, + cache, + cacheLocation, + configFile, + cwd, + envs, + extensions, + fix, + fixTypes, + globals, + globInputPaths, + ignore, + ignorePath, + ignorePattern, + parser, + parserOptions, + plugins: normalizePluginIds(plugins), + reportUnusedDisableDirectives, + resolvePluginsRelativeTo, + rulePaths, + rules, + useEslintrc + }; +} + +/** + * Create rulesMeta object. + * @param {Map} rules a map of rules from which to generate the object. + * @returns {Object} metadata for all enabled rules. + */ +function createRulesMeta(rules) { + return Array.from(rules).reduce((retVal, [id, rule]) => { + retVal.rulesMeta[id] = rule.meta; + return retVal; + }, { rulesMeta: {} }); +} + +class ESLint { + + /** + * Creates a new instance of the main ESLint API. + * @param {ESLintOptions} options The options for this instance. + */ + constructor(options) { + this._cliEngine = new CLIEngine(processOptions(options)); + + if (options.plugins.length) { + for (const plugin of options.plugins) { + if (typeof plugin === "object" && plugin !== null) { + this._cliEngine.addPlugin(plugin.id, plugin.definition); + } else if (typeof plugin !== "string") { + throw new Error("Invalid plugin. Plugins must be specified as a string (e.g., eslint-plugin-example) or as an object (e.g., { id: string; definition: Object })."); + } + } + } + } + + /** + * Outputs fixes from the given results to files. + * @param {LintReport} report The report object created by CLIEngine. + * @returns {Promise} Returns a promise that is used to track side effects. + */ + static async outputFixes(report) { + CLIEngine.outputFixes(report); + } + + /** + * An Array.prototype.sort() compatible compare function to order results by their file path. + * @param {LintResult} a The first lint result. + * @param {LintResult} b The second lint result. + * @returns {number} An integer representing the order in which the two results should occur. + */ + static compareResultsByFilePath(a, b) { + if (a.filePath < b.filePath) { + return -1; + } + + if (a.filePath > b.filePath) { + return 1; + } + + return 0; + } + + /** + * Returns results that only contains errors. + * @param {LintResult[]} results The results to filter. + * @returns {LintResult[]} The filtered results. + */ + static getErrorResults(results) { + return CLIEngine.getErrorResults(results); + } + + /** + * Executes the current configuration on an array of file and directory names. + * @param {string[]} patterns An array of file and directory names. + * @returns {Promise} The results of linting the file patterns given. + */ + async lintFiles(patterns) { + const { results, usedDeprecatedRules } = this._cliEngine.executeOnFiles(patterns); + + // TODO: update caching strategy. + return results.map(result => { + result.usedDeprecatedRules = usedDeprecatedRules; + return result; + }); + } + + /** + * Executes the current configuration on text. + * @param {string} code A string of JavaScript code to lint. + * @param {Object} [options] The options. + * @param {string} [options.filePath] The path to the file of the source code. + * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. + * @returns {Promise} The results of linting the string of code given. + */ + async lintText(code, options) { + const { filePath = null, warnIgnored = false } = options; + const { results, usedDeprecatedRules } = this._cliEngine.executeOnText(code, filePath, warnIgnored); + + return [{ results, usedDeprecatedRules }]; + } + + /** + * Returns the formatter representing the given formatter or null if no formatter + * with the given name can be found. + * @param {string} name The name of the formattter to load or the path to a + * custom formatter. + * @returns {Promise} A promise resolving to the formatter object or null if not found. + */ + async getFormatter(name) { + const formatter = this._cliEngine.getFormatter(name); + + if (formatter === null) { + return null; + } + + return { + + /** + * The main formatter method. + * @param {LintResults[]} results The lint results to format. + * @returns {any} The formatted lint results. + */ + format(results) { + let rulesMeta = null; + + results.sort(ESLint.compareResultsByFilePath); + + return formatter(results, { + get rulesMeta() { + if (!rulesMeta) { + rulesMeta = createRulesMeta(this._cliEngine.getRules()); + } + + return rulesMeta; + } + }); + } + }; + } + + /** + * Returns a configuration object for the given file based on the CLI options. + * 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 {Promise} A configuration object for the file. + */ + async getConfigForFile(filePath) { + return this._cliEngine.getConfigForFile(filePath); + } + + /** + * Checks if a given path is ignored by ESLint. + * @param {string} filePath The path of the file to check. + * @returns {boolean} Whether or not the given path is ignored. + */ + async isPathIgnored(filePath) { + return this._cliEngine.isPathIgnored(filePath); + } +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + ESLint +}; diff --git a/lib/eslint/index.js b/lib/eslint/index.js new file mode 100644 index 00000000000..c9185ee0eba --- /dev/null +++ b/lib/eslint/index.js @@ -0,0 +1,7 @@ +"use strict"; + +const { ESLint } = require("./eslint"); + +module.exports = { + ESLint +}; From eb5b4862f201a11885fa3ea848b2e6fa56cf390a Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Mon, 17 Feb 2020 19:19:23 -0500 Subject: [PATCH 02/71] Refactor --- lib/cli-engine/cli-engine.js | 12 +----------- lib/eslint/eslint.js | 27 +++++++++++++++++---------- lib/shared/types.js | 11 +++++++++++ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index dc0f54949b3..959580816b0 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -44,6 +44,7 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); /** @typedef {import("../shared/types").Plugin} Plugin */ /** @typedef {import("../shared/types").RuleConf} RuleConf */ /** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {import("../shared/types").LintReport} LintReport */ /** @typedef {ReturnType} ConfigArray */ /** @typedef {ReturnType} ExtractedConfig */ @@ -95,17 +96,6 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @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 diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index d86b5f5cf66..9177870cd66 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -16,6 +16,7 @@ const { CLIEngine } = require("../cli-engine"); //------------------------------------------------------------------------------ /** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {import("../shared/types").LintReport} LintReport */ /** * The options with which to configure the main API instance. @@ -262,6 +263,19 @@ function createRulesMeta(rules) { }, { rulesMeta: {} }); } +/** + * Processes the linting results generated by a CLIEngine linting report to + * match the ESLint class's API. + * @param {LintReport} report The CLIEngine linting report to process. + * @returns {LintResult[]} The processed linting results. + */ +function processCLIEngineLintReport({ results, usedDeprecatedRules }) { + return results.map(result => { + result.usedDeprecatedRules = usedDeprecatedRules; + return result; + }); +} + class ESLint { /** @@ -324,13 +338,9 @@ class ESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { - const { results, usedDeprecatedRules } = this._cliEngine.executeOnFiles(patterns); // TODO: update caching strategy. - return results.map(result => { - result.usedDeprecatedRules = usedDeprecatedRules; - return result; - }); + return processCLIEngineLintReport(this._cliEngine.executeOnFiles(patterns)); } /** @@ -341,11 +351,8 @@ class ESLint { * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. * @returns {Promise} The results of linting the string of code given. */ - async lintText(code, options) { - const { filePath = null, warnIgnored = false } = options; - const { results, usedDeprecatedRules } = this._cliEngine.executeOnText(code, filePath, warnIgnored); - - return [{ results, usedDeprecatedRules }]; + async lintText(code, { filePath = null, warnIgnored = false }) { + return processCLIEngineLintReport(this._cliEngine.executeOnText(code, filePath, warnIgnored)); } /** diff --git a/lib/shared/types.js b/lib/shared/types.js index f3d1a7f29f0..7d6939f5b77 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -141,3 +141,14 @@ module.exports = {}; * @property {Record} [processors] The definition of plugin processors. * @property {Record} [rules] The definition of plugin rules. */ + +/** + * 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. + */ From f3137ba2e0b8a1279580b33f0c050ead062a404e Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Wed, 19 Feb 2020 11:43:50 -0500 Subject: [PATCH 03/71] Address feedback --- lib/eslint/eslint.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 9177870cd66..b4a28b2905d 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -253,7 +253,7 @@ function processOptions({ /** * Create rulesMeta object. - * @param {Map} rules a map of rules from which to generate the object. + * @param {Map} rules a map of rules from which to generate the object. * @returns {Object} metadata for all enabled rules. */ function createRulesMeta(rules) { @@ -290,7 +290,7 @@ class ESLint { if (typeof plugin === "object" && plugin !== null) { this._cliEngine.addPlugin(plugin.id, plugin.definition); } else if (typeof plugin !== "string") { - throw new Error("Invalid plugin. Plugins must be specified as a string (e.g., eslint-plugin-example) or as an object (e.g., { id: string; definition: Object })."); + throw new Error("Invalid plugin. Plugins must be specified as a string (e.g., \"eslint-plugin-example\") or as an object (e.g., { id: string; definition: Object })."); } } } @@ -338,8 +338,6 @@ class ESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { - - // TODO: update caching strategy. return processCLIEngineLintReport(this._cliEngine.executeOnFiles(patterns)); } @@ -362,7 +360,7 @@ class ESLint { * custom formatter. * @returns {Promise} A promise resolving to the formatter object or null if not found. */ - async getFormatter(name) { + async loadFormatter(name) { const formatter = this._cliEngine.getFormatter(name); if (formatter === null) { @@ -401,7 +399,7 @@ class ESLint { * @param {string} filePath The path of the file to retrieve a config object for. * @returns {Promise} A configuration object for the file. */ - async getConfigForFile(filePath) { + async calculateConfigForFile(filePath) { return this._cliEngine.getConfigForFile(filePath); } From 09fbeaa00e232cc8f4c9f2f7f3ea4c3c6d1fa127 Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Wed, 19 Feb 2020 12:09:09 -0500 Subject: [PATCH 04/71] Make CLIEngine instance a private property --- lib/eslint/eslint.js | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index b4a28b2905d..196ce8d7bd5 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -80,10 +80,22 @@ const { CLIEngine } = require("../cli-engine"); * @property {(results: LintResult[]) => string} format The main formatter method. */ +/** + * Private properties for the `ESLint` instance. + * @typedef {Object} LinterInternalSlots + * @property {CLIEngine} cliEngine The wrapped CLIEngine instance. + */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +/** + * The map with which to store private properties. + * @type {WeakMap} + */ +const internalSlotsMap = new WeakMap(); + /** * Normalizes an array of plugins to their respective IDs. * @param {string[]|PluginElement[]} plugins An array of plugins to normalize. @@ -283,17 +295,22 @@ class ESLint { * @param {ESLintOptions} options The options for this instance. */ constructor(options) { - this._cliEngine = new CLIEngine(processOptions(options)); + const cliEngine = new CLIEngine(processOptions(options)); if (options.plugins.length) { for (const plugin of options.plugins) { - if (typeof plugin === "object" && plugin !== null) { - this._cliEngine.addPlugin(plugin.id, plugin.definition); + if (typeof plugin === "object" && !Array.isArray(plugin) && plugin !== null) { + cliEngine.addPlugin(plugin.id, plugin.definition); } else if (typeof plugin !== "string") { throw new Error("Invalid plugin. Plugins must be specified as a string (e.g., \"eslint-plugin-example\") or as an object (e.g., { id: string; definition: Object })."); } } } + + // Initialize private properties. + internalSlotsMap.set(this, { + cliEngine + }); } /** @@ -338,7 +355,9 @@ class ESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { - return processCLIEngineLintReport(this._cliEngine.executeOnFiles(patterns)); + const { cliEngine } = internalSlotsMap.get(this); + + return processCLIEngineLintReport(cliEngine.executeOnFiles(patterns)); } /** @@ -350,7 +369,9 @@ class ESLint { * @returns {Promise} The results of linting the string of code given. */ async lintText(code, { filePath = null, warnIgnored = false }) { - return processCLIEngineLintReport(this._cliEngine.executeOnText(code, filePath, warnIgnored)); + const { cliEngine } = internalSlotsMap.get(this); + + return processCLIEngineLintReport(cliEngine.executeOnText(code, filePath, warnIgnored)); } /** @@ -361,7 +382,8 @@ class ESLint { * @returns {Promise} A promise resolving to the formatter object or null if not found. */ async loadFormatter(name) { - const formatter = this._cliEngine.getFormatter(name); + const { cliEngine } = internalSlotsMap.get(this); + const formatter = cliEngine.getFormatter(name); if (formatter === null) { return null; @@ -382,7 +404,7 @@ class ESLint { return formatter(results, { get rulesMeta() { if (!rulesMeta) { - rulesMeta = createRulesMeta(this._cliEngine.getRules()); + rulesMeta = createRulesMeta(cliEngine.getRules()); } return rulesMeta; @@ -400,7 +422,9 @@ class ESLint { * @returns {Promise} A configuration object for the file. */ async calculateConfigForFile(filePath) { - return this._cliEngine.getConfigForFile(filePath); + const { cliEngine } = internalSlotsMap.get(this); + + return cliEngine.getConfigForFile(filePath); } /** @@ -409,7 +433,9 @@ class ESLint { * @returns {boolean} Whether or not the given path is ignored. */ async isPathIgnored(filePath) { - return this._cliEngine.isPathIgnored(filePath); + const { cliEngine } = internalSlotsMap.get(this); + + return cliEngine.isPathIgnored(filePath); } } From 75655f801ec46cd001b0a388119f71448e92b417 Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Wed, 19 Feb 2020 12:40:25 -0500 Subject: [PATCH 05/71] Validate plugin objects --- lib/eslint/eslint.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 196ce8d7bd5..d96a17f1e7c 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -96,6 +96,15 @@ const { CLIEngine } = require("../cli-engine"); */ const internalSlotsMap = new WeakMap(); +/** + * Checks if a plugin object is valid. + * @param {PluginElement} plugin The plugin to check. + * @returns {boolean} Whether te plugin is valid or not. + */ +function isValidPluginObject(plugin) { + return typeof plugin === "object" && !Array.isArray(plugin) && plugin !== null && plugin.id && plugin.definition; +} + /** * Normalizes an array of plugins to their respective IDs. * @param {string[]|PluginElement[]} plugins An array of plugins to normalize. @@ -299,7 +308,7 @@ class ESLint { if (options.plugins.length) { for (const plugin of options.plugins) { - if (typeof plugin === "object" && !Array.isArray(plugin) && plugin !== null) { + if (isValidPluginObject(plugin)) { cliEngine.addPlugin(plugin.id, plugin.definition); } else if (typeof plugin !== "string") { throw new Error("Invalid plugin. Plugins must be specified as a string (e.g., \"eslint-plugin-example\") or as an object (e.g., { id: string; definition: Object })."); From d5f79e34238b6c839763391e96ddd38ed99f5a4c Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Wed, 19 Feb 2020 14:32:20 -0500 Subject: [PATCH 06/71] Add test clarifying CLIEngine extensions behavior --- lib/cli-engine/cli-engine.js | 1 + lib/eslint/eslint.js | 10 +++++----- tests/lib/cli-engine/cli-engine.js | 23 +++++++++++++++++++++++ tests/lib/eslint/_utils.js | 6 ++++++ tests/lib/eslint/eslint.js | 19 +++++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 tests/lib/eslint/_utils.js create mode 100644 tests/lib/eslint/eslint.js diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 959580816b0..68246775cd4 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -691,6 +691,7 @@ class CLIEngine { return patterns.filter(Boolean); } + // Question: Should we account for the possibility of empty arrays? const extensions = (options.extensions || [".js"]).map(ext => ext.replace(/^\./u, "")); const dirSuffix = `/**/*.{${extensions.join(",")}}`; diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index d96a17f1e7c..239d33eedc1 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -19,7 +19,7 @@ const { CLIEngine } = require("../cli-engine"); /** @typedef {import("../shared/types").LintReport} LintReport */ /** - * The options with which to configure the main API instance. + * The options with which to configure the ESLint instance. * @typedef {Object} ESLintOptions * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. * @property {ConfigData} baseConfig Base config object, extended by all configs used with this instance @@ -43,7 +43,7 @@ const { CLIEngine } = require("../cli-engine"); * @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD. * @property {string[]} rulePaths An array of directories to load custom rules from. * @property {Record} rules An object of rules to use. - * @property {boolean} useEslintrc False disables looking for .eslintrc + * @property {boolean} useEslintrc False disables looking for .eslintrc.* files. */ /** @@ -127,7 +127,7 @@ function processOptions({ configFile = null, cwd = process.cwd(), envs = [], - extensions = null, + extensions = null, // TODO: Should we update the CLIEngine check to account for empty arrays? fix = false, fixTypes = ["problem", "suggestion", "layout"], globals = [], @@ -136,12 +136,12 @@ function processOptions({ ignorePath = null, ignorePattern = [], parser = "espree", - parserOptions = null, + parserOptions = void 0, // TODO: Is this correct? line 11 in cli-engine/cli-engine.js will never be evaluate to true if we use `null`. plugins = [], reportUnusedDisableDirectives = false, resolvePluginsRelativeTo = cwd, rulePaths = [], - rules = null, + rules = void 0, // TODO: Is this correct? line 19 in cli-engine/cli-engine.js will never be evaluate to true if we use `null`. useEslintrc = true, ...unknownOptions }) { diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index cb1e7cc4377..898401f1a6e 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -868,6 +868,29 @@ describe("CLIEngine", () => { assert.strictEqual(report.results[0].messages.length, 0); }); + it("should fall back to defaults when extensions is set to an empty array", () => { + + engine = new CLIEngine({ + cwd: getFixturePath("configurations"), + configFile: getFixturePath("configurations", "quotes-error.json"), + extensions: [] + }); + const report = engine.executeOnFiles([getFixturePath("single-quoted.js")]); + + assert.strictEqual(report.results.length, 1); + assert.strictEqual(report.results[0].messages.length, 1); + assert.strictEqual(report.errorCount, 1); + assert.strictEqual(report.warningCount, 0); + assert.strictEqual(report.fixableErrorCount, 1); + assert.strictEqual(report.fixableWarningCount, 0); + assert.strictEqual(report.results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(report.results[0].messages[0].severity, 2); + assert.strictEqual(report.results[0].errorCount, 1); + assert.strictEqual(report.results[0].warningCount, 0); + assert.strictEqual(report.results[0].fixableErrorCount, 1); + assert.strictEqual(report.results[0].fixableWarningCount, 0); + }); + it("should report zero messages when given a directory with a .js and a .js2 file", () => { engine = new CLIEngine({ diff --git a/tests/lib/eslint/_utils.js b/tests/lib/eslint/_utils.js new file mode 100644 index 00000000000..5a6c341c152 --- /dev/null +++ b/tests/lib/eslint/_utils.js @@ -0,0 +1,6 @@ +/** + * @fileoverview Utilities for testing the ESLint class. + * @author Kai Cataldo + */ + +"use strict"; diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js new file mode 100644 index 00000000000..a3f61d5c38c --- /dev/null +++ b/tests/lib/eslint/eslint.js @@ -0,0 +1,19 @@ +/** + * @fileoverview Tests for the ESLint class. + * @author Kai Cataldo + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("ESLint", () => { + +}); From 00b2bb0bb49b6e25ff568917843855feb9e3facf Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Wed, 26 Feb 2020 18:22:32 -0500 Subject: [PATCH 07/71] Add lintText() tests --- lib/cli-engine/cli-engine.js | 1 + lib/eslint/eslint.js | 56 +-- tests/lib/eslint/eslint.js | 721 +++++++++++++++++++++++++++++++++++ 3 files changed, 756 insertions(+), 22 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 68246775cd4..027c8af6fcf 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -850,6 +850,7 @@ class CLIEngine { const startTime = Date.now(); const resolvedFilename = filename && path.resolve(cwd, filename); + // Clear the last used config arrays. lastConfigArrays.length = 0; diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 239d33eedc1..99d4d235b6b 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -81,9 +81,10 @@ const { CLIEngine } = require("../cli-engine"); */ /** - * Private properties for the `ESLint` instance. - * @typedef {Object} LinterInternalSlots + * Private members for the `ESLint` instance. + * @typedef {Object} ESLintPrivateMembers * @property {CLIEngine} cliEngine The wrapped CLIEngine instance. + * @property {ESLintOptions} options The options used to instantiate the ESLint instance. */ //------------------------------------------------------------------------------ @@ -91,10 +92,10 @@ const { CLIEngine } = require("../cli-engine"); //------------------------------------------------------------------------------ /** - * The map with which to store private properties. - * @type {WeakMap} + * The map with which to store private class members. + * @type {WeakMap} */ -const internalSlotsMap = new WeakMap(); +const privateMembersMap = new WeakMap(); /** * Checks if a plugin object is valid. @@ -129,19 +130,19 @@ function processOptions({ envs = [], extensions = null, // TODO: Should we update the CLIEngine check to account for empty arrays? fix = false, - fixTypes = ["problem", "suggestion", "layout"], + fixTypes = [], // TODO: We don't currently set a default value for this. Doing so changes the behavior. globals = [], globInputPaths = true, ignore = true, ignorePath = null, ignorePattern = [], parser = "espree", - parserOptions = void 0, // TODO: Is this correct? line 11 in cli-engine/cli-engine.js will never be evaluate to true if we use `null`. + parserOptions = {}, // TODO: Is this correct? plugins = [], reportUnusedDisableDirectives = false, resolvePluginsRelativeTo = cwd, rulePaths = [], - rules = void 0, // TODO: Is this correct? line 19 in cli-engine/cli-engine.js will never be evaluate to true if we use `null`. + rules = {}, // TODO: Is this correct? useEslintrc = true, ...unknownOptions }) { @@ -169,7 +170,7 @@ function processOptions({ throw new Error("cacheLocation must be a string."); } - if (typeof configFile !== "string" && cacheLocation !== null) { + if (typeof configFile !== "string" && configFile !== null) { throw new Error("configFile must be a string or null."); } @@ -217,7 +218,7 @@ function processOptions({ throw new Error("parser must be a string."); } - if (typeof parserOptions !== "object") { + if (typeof parserOptions !== "object" && parserOptions !== null) { throw new Error("parserOptions must be an object or null."); } @@ -303,10 +304,11 @@ class ESLint { * Creates a new instance of the main ESLint API. * @param {ESLintOptions} options The options for this instance. */ - constructor(options) { - const cliEngine = new CLIEngine(processOptions(options)); + constructor(options = {}) { + const processedOptions = processOptions(options); + const cliEngine = new CLIEngine(processedOptions); - if (options.plugins.length) { + if (options.plugins && options.plugins.length) { for (const plugin of options.plugins) { if (isValidPluginObject(plugin)) { cliEngine.addPlugin(plugin.id, plugin.definition); @@ -317,8 +319,9 @@ class ESLint { } // Initialize private properties. - internalSlotsMap.set(this, { - cliEngine + privateMembersMap.set(this, { + cliEngine, + options: processedOptions }); } @@ -364,7 +367,7 @@ class ESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { - const { cliEngine } = internalSlotsMap.get(this); + const { cliEngine } = privateMembersMap.get(this); return processCLIEngineLintReport(cliEngine.executeOnFiles(patterns)); } @@ -377,8 +380,8 @@ class ESLint { * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. * @returns {Promise} The results of linting the string of code given. */ - async lintText(code, { filePath = null, warnIgnored = false }) { - const { cliEngine } = internalSlotsMap.get(this); + async lintText(code, { filePath = null, warnIgnored = false } = {}) { + const { cliEngine } = privateMembersMap.get(this); return processCLIEngineLintReport(cliEngine.executeOnText(code, filePath, warnIgnored)); } @@ -391,7 +394,7 @@ class ESLint { * @returns {Promise} A promise resolving to the formatter object or null if not found. */ async loadFormatter(name) { - const { cliEngine } = internalSlotsMap.get(this); + const { cliEngine } = privateMembersMap.get(this); const formatter = cliEngine.getFormatter(name); if (formatter === null) { @@ -431,7 +434,7 @@ class ESLint { * @returns {Promise} A configuration object for the file. */ async calculateConfigForFile(filePath) { - const { cliEngine } = internalSlotsMap.get(this); + const { cliEngine } = privateMembersMap.get(this); return cliEngine.getConfigForFile(filePath); } @@ -442,7 +445,7 @@ class ESLint { * @returns {boolean} Whether or not the given path is ignored. */ async isPathIgnored(filePath) { - const { cliEngine } = internalSlotsMap.get(this); + const { cliEngine } = privateMembersMap.get(this); return cliEngine.isPathIgnored(filePath); } @@ -453,5 +456,14 @@ class ESLint { //------------------------------------------------------------------------------ module.exports = { - ESLint + ESLint, + + /** + * Get the private class members of a given ESLint instance for tests. + * @param {ESLint} instance The ESLint instance to get. + * @returns {ESLintPrivateMembers} The instance's private class members. + */ + getESLintPrivateMembers(instance) { + return privateMembersMap.get(instance); + } }; diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index a3f61d5c38c..77f10f013f8 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -9,11 +9,732 @@ // Requirements //------------------------------------------------------------------------------ +const path = require("path"); +const os = require("os"); +const fs = require("fs"); +const assert = require("chai").assert; +const shell = require("shelljs"); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("ESLint", () => { + const examplePluginName = "eslint-plugin-example"; + const examplePluginNameWithNamespace = "@eslint/eslint-plugin-example"; + const examplePlugin = { + rules: { + "example-rule": require("../../fixtures/rules/custom-rule"), + "make-syntax-error": require("../../fixtures/rules/make-syntax-error-rule") + } + }; + const examplePreprocessorName = "eslint-plugin-processor"; + const originalDir = process.cwd(); + const fixtureDir = path.resolve(fs.realpathSync(os.tmpdir()), "eslint/fixtures"); + /** @type {import("../../../lib/eslint")["ESLint"]} */ + let ESLint; + + /** @type {import("../../../lib/eslint/eslint")["getESLintPrivateMembers"]} */ + let getESLintPrivateMembers; + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFixturePath(...args) { + const filepath = path.join(fixtureDir, ...args); + + try { + return fs.realpathSync(filepath); + } catch (e) { + return filepath; + } + } + + /** + * Create a plugins configuration array containing mocked plugins + * @returns {import("../../../lib/eslint/eslint")["PluginElement"][]} an array of plugins + * @private + */ + function createMockedPluginsOption() { + return [ + { id: examplePluginName, definition: examplePlugin }, + { id: examplePluginNameWithNamespace, definition: examplePlugin }, + { id: examplePreprocessorName, definition: require("../../fixtures/processors/custom-processor") } + ]; + } + + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(() => { + shell.mkdir("-p", fixtureDir); + shell.cp("-r", "./tests/fixtures/.", fixtureDir); + }); + + beforeEach(() => { + ({ ESLint, getESLintPrivateMembers } = require("../../../lib/eslint/eslint")); + }); + + after(() => { + shell.rm("-r", fixtureDir); + }); + + describe("ESLint constructor function", () => { + it("the default value of 'options.cwd' should be the current working directory.", () => { + process.chdir(__dirname); + + try { + const eslint = new ESLint(); + const { options } = getESLintPrivateMembers(eslint); + + assert.strictEqual(options.cwd, __dirname); + } finally { + process.chdir(originalDir); + } + }); + + it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new ESLint({ ignorePath: fixtureDir, ignore: true }); + }, `Cannot read .eslintignore file: ${fixtureDir}\nError: EISDIR: illegal operation on a directory, read`); + }); + }); + + describe("lintText()", () => { + it("should report the total and per file errors when using local cwd .eslintrc", async() => { + const eslint = new ESLint(); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].ruleId, "strict"); + assert.strictEqual(results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(results[0].fixableErrorCount, 3); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report the total and per file warnings when using local cwd .eslintrc", async() => { + const eslint = new ESLint({ + rules: { + quotes: 1, + "no-var": 1, + "eol-last": 1, + strict: 1, + "no-unused-vars": 1 + } + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].ruleId, "strict"); + assert.strictEqual(results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 3); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report one message when using specific config file", async() => { + const eslint = new ESLint({ + configFile: "fixtures/configurations/quotes-error.json", + useEslintrc: false, + cwd: getFixturePath("..") + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.isUndefined(results[0].messages[0].output); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report the filename when passed in", async() => { + const eslint = new ESLint({ + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "test.js" }; + const results = await eslint.lintText("var foo = 'bar';", options); + + assert.strictEqual(results[0].filePath, getFixturePath("test.js")); + }); + + it("should return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is true", async() => { + const eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath("..") + }); + const options = { filePath: "fixtures/passing.js", warnIgnored: true }; + const results = await eslint.lintText("var bar = foo;", options); + + assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.isUndefined(results[0].messages[0].output); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should not return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is false", async() => { + const eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath("..") + }); + const options = { + filePath: "fixtures/passing.js", + warnIgnored: false + }; + + // intentional parsing error + const results = await eslint.lintText("va r bar = foo;", options); + + // should not report anything because the file is ignored + assert.strictEqual(results.length, 0); + }); + + it("should suppress excluded file warnings by default", async() => { + const eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath("..") + }); + const options = { filePath: "fixtures/passing.js" }; + const results = await eslint.lintText("var bar = foo;", options); + + // should not report anything because there are no errors + assert.strictEqual(results.length, 0); + }); + + it("should return a message when given a filename by --stdin-filename in excluded files list and ignore is off", async() => { + const eslint = new ESLint({ + ignorePath: "fixtures/.eslintignore", + cwd: getFixturePath(".."), + ignore: false, + useEslintrc: false, + rules: { + "no-undef": 2 + } + }); + const options = { filePath: "fixtures/passing.js" }; + const results = await eslint.lintText("var bar = foo;", options); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.isUndefined(results[0].messages[0].output); + }); + + it("should return a message and fixed text when in fix mode", async() => { + const eslint = new ESLint({ + useEslintrc: false, + fix: true, + rules: { + semi: 2 + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "passing.js" }; + const results = await eslint.lintText("var bar = foo", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("passing.js"), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var bar = foo;", + usedDeprecatedRules: [] + } + ]); + }); + + it("should return a message and omit fixed text when in fix mode and fixes aren't done", async() => { + const eslint = new ESLint({ + useEslintrc: false, + fix: true, + rules: { + "no-undef": 2 + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "passing.js" }; + const results = await eslint.lintText("var bar = foo", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("passing.js"), + messages: [ + { + ruleId: "no-undef", + severity: 2, + messageId: "undef", + message: "'foo' is not defined.", + line: 1, + column: 11, + endLine: 1, + endColumn: 14, + nodeType: "Identifier" + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var bar = foo", + usedDeprecatedRules: [] + } + ]); + }); + + it("should not delete code if there is a syntax error after trying to autofix.", async() => { + const eslint = new ESLint({ + useEslintrc: false, + fix: true, + plugins: createMockedPluginsOption(), + rules: { + "example/make-syntax-error": "error" + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "test.js" }; + const results = await eslint.lintText("var bar = foo", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("test.js"), + messages: [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Parsing error: Unexpected token is", + line: 1, + column: 19 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var bar = foothis is a syntax error.", + usedDeprecatedRules: [] + } + ]); + }); + + it("should not crash even if there are any syntax error since the first time.", async() => { + const eslint = new ESLint({ + useEslintrc: false, + fix: true, + rules: { + "example/make-syntax-error": "error" + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "test.js" }; + const results = await eslint.lintText("var bar =", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("test.js"), + messages: [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Parsing error: Unexpected token", + line: 1, + column: 10 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var bar =", + usedDeprecatedRules: [] + } + ]); + }); + + it("should return source code of file in `source` property when errors are present", async() => { + const eslint = new ESLint({ + useEslintrc: false, + rules: { semi: 2 } + }); + const results = await eslint.lintText("var foo = 'bar'"); + + assert.strictEqual(results[0].source, "var foo = 'bar'"); + }); + + it("should return source code of file in `source` property when warnings are present", async() => { + const eslint = new ESLint({ + useEslintrc: false, + rules: { semi: 1 } + }); + const results = await eslint.lintText("var foo = 'bar'"); + + assert.strictEqual(results[0].source, "var foo = 'bar'"); + }); + + + it("should not return a `source` property when no errors or warnings are present", async() => { + const eslint = new ESLint({ + useEslintrc: false, + rules: { semi: 2 } + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.lengthOf(results[0].messages, 0); + assert.isUndefined(results[0].source); + }); + + it("should not return a `source` property when fixes are applied", async() => { + const eslint = new ESLint({ + useEslintrc: false, + fix: true, + rules: { + semi: 2, + "no-unused-vars": 2 + } + }); + const results = await eslint.lintText("var msg = 'hi' + foo\n"); + + assert.isUndefined(results[0].source); + assert.strictEqual(results[0].output, "var msg = 'hi' + foo;\n"); + }); + + it("should return a `source` property when a parsing error has occurred", async() => { + const eslint = new ESLint({ + useEslintrc: false, + rules: { semi: 2 } + }); + const results = await eslint.lintText("var bar = foothis is a syntax error.\n return bar;"); + + assert.deepStrictEqual(results, [ + { + filePath: "", + messages: [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Parsing error: Unexpected token is", + line: 1, + column: 19 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var bar = foothis is a syntax error.\n return bar;", + usedDeprecatedRules: [] + } + ]); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should respect default ignore rules, even with --no-ignore", async() => { + const eslint = new ESLint({ + cwd: getFixturePath(), + ignore: false + }); + const results = await eslint.lintText("var bar = foo;", { filePath: "node_modules/passing.js", warnIgnored: true }); + const expectedMsg = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("node_modules/passing.js")); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + describe('plugin shorthand notation ("@scope" for "@scope/eslint-plugin")', () => { + const Module = require("module"); + let originalFindPath = null; + + /* eslint-disable no-underscore-dangle */ + before(() => { + originalFindPath = Module._findPath; + Module._findPath = function(id, ...otherArgs) { + if (id === "@scope/eslint-plugin") { + return path.resolve(__dirname, "../../fixtures/plugin-shorthand/basic/node_modules/@scope/eslint-plugin/index.js"); + } + return originalFindPath.call(this, id, ...otherArgs); + }; + }); + + after(() => { + Module._findPath = originalFindPath; + }); + /* eslint-enable no-underscore-dangle */ + + it("should resolve 'plugins:[\"@scope\"]' to 'node_modules/@scope/eslint-plugin'.", async() => { + const eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/basic") }); + const [result] = await eslint.lintText("var x = 0", { filePath: "index.js" }); + + assert.strictEqual(result.filePath, getFixturePath("plugin-shorthand/basic/index.js")); + assert.strictEqual(result.messages[0].ruleId, "@scope/rule"); + assert.strictEqual(result.messages[0].message, "OK"); + }); + + it("should resolve 'extends:[\"plugin:@scope/recommended\"]' to 'node_modules/@scope/eslint-plugin'.", async() => { + const eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/extends") }); + const [result] = await eslint.lintText("var x = 0", { filePath: "index.js" }); + + assert.strictEqual(result.filePath, getFixturePath("plugin-shorthand/extends/index.js")); + assert.strictEqual(result.messages[0].ruleId, "@scope/rule"); + assert.strictEqual(result.messages[0].message, "OK"); + }); + }); + + it("should warn when deprecated rules are found in a config", async() => { + const eslint = new ESLint({ + cwd: originalDir, + useEslintrc: false, + configFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml" + }); + const [result] = await eslint.lintText("foo"); + + assert.deepStrictEqual( + result.usedDeprecatedRules, + [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] + ); + }); + + /* + * describe("Fix Types", () => { + * it("should throw an error when an invalid fix type is specified", () => { + * assert.throws(() => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["layou"] + * }); + * }, /invalid fix type/iu); + * }); + */ + + /* + * it("should not fix any rules when fixTypes is used without fix", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: false, + * fixTypes: ["layout"] + * }); + */ + + /* + * const inputPath = getFixturePath("fix-types/fix-only-semi.js"); + * const report = engine.executeOnFiles([inputPath]); + */ + + /* + * assert.isUndefined(report.results[0].output); + * }); + */ + + /* + * it("should not fix non-style rules when fixTypes has only 'layout'", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["layout"] + * }); + * const inputPath = getFixturePath("fix-types/fix-only-semi.js"); + * const outputPath = getFixturePath("fix-types/fix-only-semi.expected.js"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + /* + * it("should not fix style or problem rules when fixTypes has only 'suggestion'", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["suggestion"] + * }); + * const inputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.js"); + * const outputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.expected.js"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + /* + * it("should fix both style and problem rules when fixTypes has 'suggestion' and 'layout'", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["suggestion", "layout"] + * }); + * const inputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.js"); + * const outputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.expected.js"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + /* + * it("should not throw an error when a rule doesn't have a 'meta' property", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["layout"], + * rulePaths: [getFixturePath("rules", "fix-types-test")] + * }); + */ + + /* + * const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); + * const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + /* + * it("should not throw an error when a rule is loaded after initialization with executeOnFiles()", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["layout"] + * }); + * const internalSlots = getESLintInternalSlots(engine); + */ + + /* + * 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"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + /* + * it("should not throw an error when a rule is loaded after initialization with executeOnText()", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true, + * fixTypes: ["layout"] + * }); + * const internalSlots = getESLintInternalSlots(engine); + */ + + /* + * 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"); + * const results = await eslint.lintText(fs.readFileSync(inputPath, { encoding: "utf8" }), inputPath); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + // }); + + /* + * it("correctly autofixes semicolon-conflicting-fixes", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true + * }); + * const inputPath = getFixturePath("autofix/semicolon-conflicting-fixes.js"); + * const outputPath = getFixturePath("autofix/semicolon-conflicting-fixes.expected.js"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + /* + * it("correctly autofixes return-conflicting-fixes", () => { + * const eslint = new ESLint({ + * cwd: path.join(fixtureDir, ".."), + * useEslintrc: false, + * fix: true + * }); + * const inputPath = getFixturePath("autofix/return-conflicting-fixes.js"); + * const outputPath = getFixturePath("autofix/return-conflicting-fixes.expected.js"); + * const report = engine.executeOnFiles([inputPath]); + * const expectedOutput = fs.readFileSync(outputPath, "utf8"); + */ + + /* + * assert.strictEqual(report.results[0].output, expectedOutput); + * }); + */ + + }); }); From b93a625bf3adb85e81f09f6eadcb08e2903ce068 Mon Sep 17 00:00:00 2001 From: Kai Cataldo Date: Thu, 5 Mar 2020 21:12:32 -0500 Subject: [PATCH 08/71] Add lintFiles() tests --- lib/cli-engine/cli-engine.js | 1 + lib/eslint/eslint.js | 20 +- .../_utils.js => _utils/in-memory-fs.js} | 114 +- tests/_utils/index.js | 39 + tests/lib/_utils.js | 76 - .../cascading-config-array-factory.js | 2 +- tests/lib/cli-engine/cli-engine.js | 3 +- tests/lib/cli-engine/config-array-factory.js | 2 +- tests/lib/cli-engine/file-enumerator.js | 2 +- tests/lib/eslint/_utils.js | 6 - tests/lib/eslint/eslint.js | 3388 +++++++++++++++-- tests/lib/init/npm-utils.js | 2 +- tests/lib/rules/implicit-arrow-linebreak.js | 2 +- tests/lib/rules/indent.js | 24 +- tests/lib/rules/object-shorthand.js | 2 +- tests/lib/shared/runtime-info.js | 2 +- 16 files changed, 3306 insertions(+), 379 deletions(-) rename tests/{lib/cli-engine/_utils.js => _utils/in-memory-fs.js} (78%) create mode 100644 tests/_utils/index.js delete mode 100644 tests/lib/_utils.js delete mode 100644 tests/lib/eslint/_utils.js diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 027c8af6fcf..1a8eb62dde9 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -545,6 +545,7 @@ class CLIEngine { options.cacheLocation || options.cacheFile, options.cwd ); + const configArrayFactory = new CascadingConfigArrayFactory({ additionalPluginPool, baseConfig: options.baseConfig || null, diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 99d4d235b6b..cec00ec3390 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -136,22 +136,24 @@ function processOptions({ ignore = true, ignorePath = null, ignorePattern = [], - parser = "espree", + parser = null, // Question: if this is set to a default value it ends up overriding the value set in the config. That seems like unexpected behavior to me. parserOptions = {}, // TODO: Is this correct? plugins = [], - reportUnusedDisableDirectives = false, + reportUnusedDisableDirectives = null, resolvePluginsRelativeTo = cwd, rulePaths = [], rules = {}, // TODO: Is this correct? useEslintrc = true, ...unknownOptions }) { - if (Object.keys(unknownOptions).length >= 1) { + const unknownOptionKeys = Object.keys(unknownOptions); + + if (unknownOptionKeys.length >= 1) { throw new Error(`${ - unknownOptions.includes("cacheFile") + unknownOptionKeys.includes("cacheFile") ? "cacheFile has been deprecated. Please use the cacheLocation option instead. " : "" - }Unknown options given: ${unknownOptions.join(", ")}.`); + }Unknown options given: ${unknownOptionKeys.join(", ")}.`); } if (typeof allowInlineConfig !== "boolean") { @@ -214,8 +216,8 @@ function processOptions({ throw new Error("ignorePattern must be a string or an array of strings."); } - if (typeof parser !== "string") { - throw new Error("parser must be a string."); + if (typeof parser !== "string" && parser !== null) { + throw new Error("parser must be a string or null."); } if (typeof parserOptions !== "object" && parserOptions !== null) { @@ -226,8 +228,8 @@ function processOptions({ throw new Error("plugins must be an array."); } - if (typeof reportUnusedDisableDirectives !== "boolean") { - throw new Error("reportUnusedDisableDirectives must be a boolean."); + if (typeof reportUnusedDisableDirectives !== "boolean" && reportUnusedDisableDirectives !== null) { + throw new Error("reportUnusedDisableDirectives must be a boolean or null."); } if (typeof resolvePluginsRelativeTo !== "string") { diff --git a/tests/lib/cli-engine/_utils.js b/tests/_utils/in-memory-fs.js similarity index 78% rename from tests/lib/cli-engine/_utils.js rename to tests/_utils/in-memory-fs.js index 1d1bcd275b0..fdd72a3adc2 100644 --- a/tests/lib/cli-engine/_utils.js +++ b/tests/_utils/in-memory-fs.js @@ -2,12 +2,13 @@ * @fileoverview Define classes what use the in-memory file system. * * This provides utilities to test `ConfigArrayFactory`, - * `CascadingConfigArrayFactory`, `FileEnumerator`, and `CLIEngine`. + * `CascadingConfigArrayFactory`, `FileEnumerator`, `CLIEngine`, and `ESLint`. * * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })` * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })` + * - `defineESLintWithInMemoryFileSystem({ cwd, files })` * * Those functions define correspond classes with the in-memory file system. * Those search config files, parsers, and plugins in the `files` option via the @@ -55,23 +56,25 @@ const path = require("path"); const vm = require("vm"); +const { Volume, createFsFromVolume } = require("memfs"); const Proxyquire = require("proxyquire/lib/proxyquire"); -const { defineInMemoryFs } = require("../_utils"); const CascadingConfigArrayFactoryPath = - require.resolve("../../../lib/cli-engine/cascading-config-array-factory"); + require.resolve("../../lib/cli-engine/cascading-config-array-factory"); const CLIEnginePath = - require.resolve("../../../lib/cli-engine/cli-engine"); + require.resolve("../../lib/cli-engine/cli-engine"); const ConfigArrayFactoryPath = - require.resolve("../../../lib/cli-engine/config-array-factory"); + require.resolve("../../lib/cli-engine/config-array-factory"); const FileEnumeratorPath = - require.resolve("../../../lib/cli-engine/file-enumerator"); + require.resolve("../../lib/cli-engine/file-enumerator"); const LoadRulesPath = - require.resolve("../../../lib/cli-engine/load-rules"); + require.resolve("../../lib/cli-engine/load-rules"); +const ESLintPath = + require.resolve("../../lib/eslint/eslint"); const ESLintAllPath = - require.resolve("../../../conf/eslint-all"); + require.resolve("../../conf/eslint-all"); const ESLintRecommendedPath = - require.resolve("../../../conf/eslint-recommended"); + require.resolve("../../conf/eslint-recommended"); // Ensure the needed files has been loaded and cached. require(CascadingConfigArrayFactoryPath); @@ -79,6 +82,7 @@ require(CLIEnginePath); require(ConfigArrayFactoryPath); require(FileEnumeratorPath); require(LoadRulesPath); +require(ESLintPath); require("js-yaml"); require("espree"); @@ -236,12 +240,57 @@ function fsImportFresh(fs, stubs, absolutePath) { ); } +/** + * Define 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 {import("fs")} The stubbed `ConfigArrayFactory` class. + */ +function defineInMemoryFs({ + cwd = process.cwd, + files = {} +} = {}) { + + /** + * The in-memory file system for this mock. + * @type {import("fs")} + */ + const fs = createFsFromVolume(new Volume()); + + fs.mkdirSync(cwd(), { recursive: true }); + + /* + * 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); + } else if (typeof content === "string") { + if (!fs.existsSync(parentPath)) { + fs.mkdirSync(parentPath, { recursive: true }); + } + fs.writeFileSync(filePath, content); + } else { + throw new Error(`Invalid content: ${typeof content}`); + } + } + }(cwd(), files)); + + return fs; +} + /** * 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/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. */ function defineConfigArrayFactoryWithInMemoryFileSystem({ cwd = process.cwd, @@ -438,9 +487,52 @@ function defineCLIEngineWithInMemoryFileSystem({ }; } +/** + * Define stubbed `ESLint` class that 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/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../lib/cli-engine/file-enumerator")["FileEnumerator"], ESLint: import("../../lib/eslint/eslint")["ESLint"], getCLIEngineInternalSlots: import("../../lib//eslint/eslint")["getESLintInternalSlots"] }} The stubbed `ESLint` class. + */ +function defineESLintWithInMemoryFileSystem({ + cwd = process.cwd, + files = {} +} = {}) { + const { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + FileEnumerator, + CLIEngine + } = defineCLIEngineWithInMemoryFileSystem({ cwd, files }); + const { ESLint, getESLintInternalSlots } = proxyquire(ESLintPath, { + "../cli-engine": { CLIEngine } + }); + + // Override the default cwd. + return { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + FileEnumerator, + ESLint: cwd === process.cwd + ? ESLint + : class extends ESLint { + constructor(options) { + super({ cwd: cwd(), ...options }); + } + }, + getESLintInternalSlots + }; +} + module.exports = { + defineInMemoryFs, defineConfigArrayFactoryWithInMemoryFileSystem, defineCascadingConfigArrayFactoryWithInMemoryFileSystem, defineFileEnumeratorWithInMemoryFileSystem, - defineCLIEngineWithInMemoryFileSystem + defineCLIEngineWithInMemoryFileSystem, + defineESLintWithInMemoryFileSystem }; diff --git a/tests/_utils/index.js b/tests/_utils/index.js new file mode 100644 index 00000000000..0431e3aced4 --- /dev/null +++ b/tests/_utils/index.js @@ -0,0 +1,39 @@ +"use strict"; + +const { + defineInMemoryFs, + defineConfigArrayFactoryWithInMemoryFileSystem, + defineCascadingConfigArrayFactoryWithInMemoryFileSystem, + defineFileEnumeratorWithInMemoryFileSystem, + defineCLIEngineWithInMemoryFileSystem, + defineESLintWithInMemoryFileSystem +} = require("./in-memory-fs"); + + +/** + * Prevents leading spaces in a multiline template literal from appearing in the resulting string + * @param {string[]} strings The strings in the template literal + * @param {any[]} values The interpolation values in the template literal. + * @returns {string} The template literal, with spaces removed from all lines + */ +function unIndent(strings, ...values) { + const text = strings + .map((s, i) => (i === 0 ? s : values[i - 1] + s)) + .join(""); + const lines = text.replace(/^\n/u, "").replace(/\n\s*$/u, "").split("\n"); + const lineIndents = lines.filter(line => line.trim()).map(line => line.match(/ */u)[0].length); + const minLineIndent = Math.min(...lineIndents); + + return lines.map(line => line.slice(minLineIndent)).join("\n"); +} + + +module.exports = { + unIndent, + defineInMemoryFs, + defineConfigArrayFactoryWithInMemoryFileSystem, + defineCascadingConfigArrayFactoryWithInMemoryFileSystem, + defineFileEnumeratorWithInMemoryFileSystem, + defineCLIEngineWithInMemoryFileSystem, + defineESLintWithInMemoryFileSystem +}; diff --git a/tests/lib/_utils.js b/tests/lib/_utils.js deleted file mode 100644 index 76e973cc667..00000000000 --- a/tests/lib/_utils.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @fileoverview utils for rule tests. - * @author 唯然 - */ - -"use strict"; - -const path = require("path"); -const { Volume, createFsFromVolume } = require("memfs"); - -/** - * Prevents leading spaces in a multiline template literal from appearing in the resulting string - * @param {string[]} strings The strings in the template literal - * @param {any[]} values The interpolation values in the template literal. - * @returns {string} The template literal, with spaces removed from all lines - */ -function unIndent(strings, ...values) { - const text = strings - .map((s, i) => (i === 0 ? s : values[i - 1] + s)) - .join(""); - const lines = text.replace(/^\n/u, "").replace(/\n\s*$/u, "").split("\n"); - const lineIndents = lines.filter(line => line.trim()).map(line => line.match(/ */u)[0].length); - const minLineIndent = Math.min(...lineIndents); - - return lines.map(line => line.slice(minLineIndent)).join("\n"); -} - -/** - * Define 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 {import("fs")} The stubbed `ConfigArrayFactory` class. - */ -function defineInMemoryFs({ - cwd = process.cwd, - files = {} -} = {}) { - - /** - * The in-memory file system for this mock. - * @type {import("fs")} - */ - const fs = createFsFromVolume(new Volume()); - - fs.mkdirSync(cwd(), { recursive: true }); - - /* - * 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); - } else if (typeof content === "string") { - if (!fs.existsSync(parentPath)) { - fs.mkdirSync(parentPath, { recursive: true }); - } - fs.writeFileSync(filePath, content); - } else { - throw new Error(`Invalid content: ${typeof content}`); - } - } - }(cwd(), files)); - - return fs; -} - -module.exports = { - defineInMemoryFs, - unIndent -}; diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index 0bc49b461bf..f539c8b8298 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -12,7 +12,7 @@ const sh = require("shelljs"); const sinon = require("sinon"); 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"); /** @typedef {InstanceType["CascadingConfigArrayFactory"]>} CascadingConfigArrayFactory */ /** @typedef {ReturnType} ConfigArray */ diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 898401f1a6e..fc4965bc176 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -18,8 +18,7 @@ const assert = require("chai").assert, os = require("os"), hash = require("../../../lib/cli-engine/hash"), { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"), - { unIndent } = require("../_utils"), - { defineCLIEngineWithInMemoryFileSystem } = require("./_utils"); + { unIndent, defineCLIEngineWithInMemoryFileSystem } = require("../../_utils"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); const fCache = require("file-entry-cache"); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 2e1d230a574..20dd656478a 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -10,7 +10,7 @@ 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"); diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index c62578c9162..43728862d95 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -11,7 +11,7 @@ const { assert } = require("chai"); const sh = require("shelljs"); const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); -const { defineFileEnumeratorWithInMemoryFileSystem } = require("./_utils"); +const { defineFileEnumeratorWithInMemoryFileSystem } = require("../../_utils"); describe("FileEnumerator", () => { describe("'iterateFiles(patterns)' method should iterate files and configs.", () => { diff --git a/tests/lib/eslint/_utils.js b/tests/lib/eslint/_utils.js deleted file mode 100644 index 5a6c341c152..00000000000 --- a/tests/lib/eslint/_utils.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @fileoverview Utilities for testing the ESLint class. - * @author Kai Cataldo - */ - -"use strict"; diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 77f10f013f8..1814c62ee5f 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -13,7 +13,11 @@ const path = require("path"); const os = require("os"); const fs = require("fs"); const assert = require("chai").assert; +const sinon = require("sinon"); const shell = require("shelljs"); +const hash = require("../../../lib/cli-engine/hash"); +const fCache = require("file-entry-cache"); +const { unIndent, defineESLintWithInMemoryFileSystem } = require("../../_utils"); //------------------------------------------------------------------------------ // Tests @@ -35,6 +39,9 @@ describe("ESLint", () => { /** @type {import("../../../lib/eslint")["ESLint"]} */ let ESLint; + /** @type {InstanceType} */ + let eslint; + /** @type {import("../../../lib/eslint/eslint")["getESLintPrivateMembers"]} */ let getESLintPrivateMembers; @@ -82,11 +89,11 @@ describe("ESLint", () => { }); describe("ESLint constructor function", () => { - it("the default value of 'options.cwd' should be the current working directory.", () => { + it("the default value of 'options.cwd' should be the current working directory.", async () => { process.chdir(__dirname); try { - const eslint = new ESLint(); + eslint = new ESLint(); const { options } = getESLintPrivateMembers(eslint); assert.strictEqual(options.cwd, __dirname); @@ -95,7 +102,7 @@ describe("ESLint", () => { } }); - it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", () => { + it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", async () => { assert.throws(() => { // eslint-disable-next-line no-new new ESLint({ ignorePath: fixtureDir, ignore: true }); @@ -104,8 +111,8 @@ describe("ESLint", () => { }); describe("lintText()", () => { - it("should report the total and per file errors when using local cwd .eslintrc", async() => { - const eslint = new ESLint(); + it("should report the total and per file errors when using local cwd .eslintrc", async () => { + eslint = new ESLint(); const results = await eslint.lintText("var foo = 'bar';"); assert.strictEqual(results.length, 1); @@ -120,8 +127,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].usedDeprecatedRules.length, 0); }); - it("should report the total and per file warnings when using local cwd .eslintrc", async() => { - const eslint = new ESLint({ + it("should report the total and per file warnings when using local cwd .eslintrc", async () => { + eslint = new ESLint({ rules: { quotes: 1, "no-var": 1, @@ -144,8 +151,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].usedDeprecatedRules.length, 0); }); - it("should report one message when using specific config file", async() => { - const eslint = new ESLint({ + it("should report one message when using specific config file", async () => { + eslint = new ESLint({ configFile: "fixtures/configurations/quotes-error.json", useEslintrc: false, cwd: getFixturePath("..") @@ -162,8 +169,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].usedDeprecatedRules.length, 0); }); - it("should report the filename when passed in", async() => { - const eslint = new ESLint({ + it("should report the filename when passed in", async () => { + eslint = new ESLint({ ignore: false, cwd: getFixturePath() }); @@ -173,8 +180,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].filePath, getFixturePath("test.js")); }); - it("should return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is true", async() => { - const eslint = new ESLint({ + it("should return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is true", async () => { + eslint = new ESLint({ ignorePath: getFixturePath(".eslintignore"), cwd: getFixturePath("..") }); @@ -192,8 +199,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].usedDeprecatedRules.length, 0); }); - it("should not return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is false", async() => { - const eslint = new ESLint({ + it("should not return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is false", async () => { + eslint = new ESLint({ ignorePath: getFixturePath(".eslintignore"), cwd: getFixturePath("..") }); @@ -209,8 +216,8 @@ describe("ESLint", () => { assert.strictEqual(results.length, 0); }); - it("should suppress excluded file warnings by default", async() => { - const eslint = new ESLint({ + it("should suppress excluded file warnings by default", async () => { + eslint = new ESLint({ ignorePath: getFixturePath(".eslintignore"), cwd: getFixturePath("..") }); @@ -221,8 +228,8 @@ describe("ESLint", () => { assert.strictEqual(results.length, 0); }); - it("should return a message when given a filename by --stdin-filename in excluded files list and ignore is off", async() => { - const eslint = new ESLint({ + it("should return a message when given a filename by --stdin-filename in excluded files list and ignore is off", async () => { + eslint = new ESLint({ ignorePath: "fixtures/.eslintignore", cwd: getFixturePath(".."), ignore: false, @@ -241,8 +248,8 @@ describe("ESLint", () => { assert.isUndefined(results[0].messages[0].output); }); - it("should return a message and fixed text when in fix mode", async() => { - const eslint = new ESLint({ + it("should return a message and fixed text when in fix mode", async () => { + eslint = new ESLint({ useEslintrc: false, fix: true, rules: { @@ -268,8 +275,176 @@ describe("ESLint", () => { ]); }); - it("should return a message and omit fixed text when in fix mode and fixes aren't done", async() => { - const eslint = new ESLint({ + it("correctly autofixes semicolon-conflicting-fixes", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true + }); + const inputPath = getFixturePath("autofix/semicolon-conflicting-fixes.js"); + const outputPath = getFixturePath("autofix/semicolon-conflicting-fixes.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("correctly autofixes return-conflicting-fixes", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true + }); + const inputPath = getFixturePath("autofix/return-conflicting-fixes.js"); + const outputPath = getFixturePath("autofix/return-conflicting-fixes.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + describe("Fix Types", () => { + it("should throw an error when an invalid fix type is specified", async () => { + assert.throws(() => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layou"] + }); + }, /invalid fix type/iu); + }); + + it("should not fix any rules when fixTypes is used without fix", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: false, + fixTypes: ["layout"] + }); + + const inputPath = getFixturePath("fix-types/fix-only-semi.js"); + const results = await eslint.lintFiles([inputPath]); + + assert.isUndefined(results[0].output); + }); + + it("should not fix non-style rules when fixTypes has only 'layout'", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"] + }); + const inputPath = getFixturePath("fix-types/fix-only-semi.js"); + const outputPath = getFixturePath("fix-types/fix-only-semi.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should not fix style or problem rules when fixTypes has only 'suggestion'", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["suggestion"] + }); + const inputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.js"); + const outputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should fix both style and problem rules when fixTypes has 'suggestion' and 'layout'", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["suggestion", "layout"] + }); + const inputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.js"); + const outputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should not throw an error when a rule doesn't have a 'meta' property", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"], + rulePaths: [getFixturePath("rules", "fix-types-test")] + }); + + const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); + const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + describe("should not throw an error when a rule is loaded after initialization", () => { + + /** @type {import("../../../lib/cli-engine/cli-engine")["getCLIEngineInternalSlots"]} */ + let getCLIEngineInternalSlots; + + /** @type {InstanceType} */ + let linter; + + beforeEach(() => { + ({ getCLIEngineInternalSlots } = require("../../../lib/cli-engine/cli-engine")); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"] + }); + const { cliEngine } = getESLintPrivateMembers(eslint); + + ({ linter } = getCLIEngineInternalSlots(cliEngine)); + }); + + it("with executeOnFiles()", async () => { + 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"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("with executeOnText()", async () => { + 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"); + const results = await eslint.lintText(fs.readFileSync(inputPath, { encoding: "utf8" }), inputPath); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + }); + }); + + it("should return a message and omit fixed text when in fix mode and fixes aren't done", async () => { + eslint = new ESLint({ useEslintrc: false, fix: true, rules: { @@ -307,8 +482,8 @@ describe("ESLint", () => { ]); }); - it("should not delete code if there is a syntax error after trying to autofix.", async() => { - const eslint = new ESLint({ + it("should not delete code if there is a syntax error after trying to autofix.", async () => { + eslint = new ESLint({ useEslintrc: false, fix: true, plugins: createMockedPluginsOption(), @@ -344,8 +519,8 @@ describe("ESLint", () => { ]); }); - it("should not crash even if there are any syntax error since the first time.", async() => { - const eslint = new ESLint({ + it("should not crash even if there are any syntax error since the first time.", async () => { + eslint = new ESLint({ useEslintrc: false, fix: true, rules: { @@ -380,8 +555,8 @@ describe("ESLint", () => { ]); }); - it("should return source code of file in `source` property when errors are present", async() => { - const eslint = new ESLint({ + it("should return source code of file in `source` property when errors are present", async () => { + eslint = new ESLint({ useEslintrc: false, rules: { semi: 2 } }); @@ -390,8 +565,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].source, "var foo = 'bar'"); }); - it("should return source code of file in `source` property when warnings are present", async() => { - const eslint = new ESLint({ + it("should return source code of file in `source` property when warnings are present", async () => { + eslint = new ESLint({ useEslintrc: false, rules: { semi: 1 } }); @@ -401,8 +576,8 @@ describe("ESLint", () => { }); - it("should not return a `source` property when no errors or warnings are present", async() => { - const eslint = new ESLint({ + it("should not return a `source` property when no errors or warnings are present", async () => { + eslint = new ESLint({ useEslintrc: false, rules: { semi: 2 } }); @@ -412,8 +587,8 @@ describe("ESLint", () => { assert.isUndefined(results[0].source); }); - it("should not return a `source` property when fixes are applied", async() => { - const eslint = new ESLint({ + it("should not return a `source` property when fixes are applied", async () => { + eslint = new ESLint({ useEslintrc: false, fix: true, rules: { @@ -427,8 +602,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].output, "var msg = 'hi' + foo;\n"); }); - it("should return a `source` property when a parsing error has occurred", async() => { - const eslint = new ESLint({ + it("should return a `source` property when a parsing error has occurred", async () => { + eslint = new ESLint({ useEslintrc: false, rules: { semi: 2 } }); @@ -458,8 +633,8 @@ describe("ESLint", () => { }); // https://github.com/eslint/eslint/issues/5547 - it("should respect default ignore rules, even with --no-ignore", async() => { - const eslint = new ESLint({ + it("should respect default ignore rules, even with --no-ignore", async () => { + eslint = new ESLint({ cwd: getFixturePath(), ignore: false }); @@ -491,8 +666,8 @@ describe("ESLint", () => { }); /* eslint-enable no-underscore-dangle */ - it("should resolve 'plugins:[\"@scope\"]' to 'node_modules/@scope/eslint-plugin'.", async() => { - const eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/basic") }); + it("should resolve 'plugins:[\"@scope\"]' to 'node_modules/@scope/eslint-plugin'.", async () => { + eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/basic") }); const [result] = await eslint.lintText("var x = 0", { filePath: "index.js" }); assert.strictEqual(result.filePath, getFixturePath("plugin-shorthand/basic/index.js")); @@ -500,8 +675,8 @@ describe("ESLint", () => { assert.strictEqual(result.messages[0].message, "OK"); }); - it("should resolve 'extends:[\"plugin:@scope/recommended\"]' to 'node_modules/@scope/eslint-plugin'.", async() => { - const eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/extends") }); + it("should resolve 'extends:[\"plugin:@scope/recommended\"]' to 'node_modules/@scope/eslint-plugin'.", async () => { + eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/extends") }); const [result] = await eslint.lintText("var x = 0", { filePath: "index.js" }); assert.strictEqual(result.filePath, getFixturePath("plugin-shorthand/extends/index.js")); @@ -510,8 +685,8 @@ describe("ESLint", () => { }); }); - it("should warn when deprecated rules are found in a config", async() => { - const eslint = new ESLint({ + it("should warn when deprecated rules are found in a config", async () => { + eslint = new ESLint({ cwd: originalDir, useEslintrc: false, configFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml" @@ -523,218 +698,2919 @@ describe("ESLint", () => { [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] ); }); + }); + + describe("lintFiles()", () => { + it("should use correct parser when custom parser is specified", async () => { + eslint = new ESLint({ + cwd: originalDir, + ignore: false + }); + + const filePath = path.resolve(__dirname, "../../fixtures/configurations/parser/custom.js"); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Parsing error: Boom!"); + + }); + + it("should report zero messages when given a config file and a valid file", async () => { + eslint = new ESLint({ + cwd: originalDir, + configFile: ".eslintrc.js" + }); + + const results = await eslint.lintFiles(["lib/**/cli*.js"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should handle multiple patterns with overlapping files", async () => { + eslint = new ESLint({ + cwd: originalDir, + configFile: ".eslintrc.js" + }); + + const results = await eslint.lintFiles(["lib/**/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine/cli-engine}.js"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should report zero messages when given a config file and a valid file and espree as parser", async () => { + eslint = new ESLint({ + parser: "espree", + envs: ["es6"], + useEslintrc: false + }); + + const results = await eslint.lintFiles(["lib/cli.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should report zero messages when given a config file and a valid file and esprima as parser", async () => { + + eslint = new ESLint({ + parser: "esprima", + useEslintrc: false + }); + + const results = await eslint.lintFiles(["lib/cli.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should throw an error when given a config file and a valid file and invalid parser", async () => { + eslint = new ESLint({ + parser: "test11", + useEslintrc: false + }); + + try { + await eslint.lintFiles(["lib/cli.js"]); + } catch (e) { + assert.isTrue(/Failed to load parser 'test11'/u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should report zero messages when given a directory with a .js2 file", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + extensions: [".js2"] + }); + + const results = await eslint.lintFiles([getFixturePath("files/foo.js2")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should fall back to defaults when extensions is set to an empty array", async () => { + + eslint = new ESLint({ + cwd: getFixturePath("configurations"), + configFile: getFixturePath("configurations", "quotes-error.json"), + extensions: [] + }); + const results = await eslint.lintFiles([getFixturePath("single-quoted.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should report zero messages when given a directory with a .js and a .js2 file", async () => { + + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath("..") + }); + + const results = await eslint.lintFiles(["fixtures/files/"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should report zero messages when given a '**' pattern with a .js and a .js2 file", async () => { + + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles(["fixtures/files/*"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should resolve globs when 'globInputPaths' option is true", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath("..") + }); + + const results = await eslint.lintFiles(["fixtures/files/*"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should not resolve globs when 'globInputPaths' option is false", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath(".."), + globInputPaths: false + }); + + try { + await eslint.lintFiles(["fixtures/files/*"]); + } catch (e) { + assert.strictEqual("No files matching 'fixtures/files/*' were found (glob was disabled).", e.message); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should report on all files passed explicitly, even if ignored by default", async () => { + + eslint = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + + const results = await eslint.lintFiles(["node_modules/foo.js"]); + const expectedMsg = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + it("should report on globs with explicit inclusion of dotfiles, even though ignored by default", async () => { + + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + rules: { + quotes: [2, "single"] + } + }); + + const results = await eslint.lintFiles(["hidden/.hiddenfolder/*.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should not check default ignored files without --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + + try { + await eslint.lintFiles(["node_modules"]); + } catch (e) { + assert.strictEqual("All files matched by 'node_modules' are ignored.", e.message); + return; + } + assert.fail("Expected to throw an error"); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should not check node_modules files even with --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + ignore: false + }); + + try { + await eslint.lintFiles(["node_modules"]); + } catch (e) { + assert.strictEqual("All files matched by 'node_modules' are ignored.", e.message); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should not check .hidden files if they are passed explicitly without --no-ignore flag", async () => { + + eslint = new ESLint({ + cwd: getFixturePath(".."), + useEslintrc: false, + rules: { + quotes: [2, "single"] + } + }); + + const results = await eslint.lintFiles(["fixtures/files/.bar.js"]); + const expectedMsg = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!'\") to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + it("should check .hidden files if they are passed explicitly with --no-ignore flag", async () => { + + eslint = new ESLint({ + cwd: getFixturePath(".."), + ignore: false, + useEslintrc: false, + rules: { + quotes: [2, "single"] + } + }); + + const results = await eslint.lintFiles(["fixtures/files/.bar.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + }); + + it("should check .hidden files if they are unignored with an --ignore-pattern", async () => { + + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + ignore: true, + useEslintrc: false, + ignorePattern: "!.hidden*", + rules: { + quotes: [2, "single"] + } + }); + + const results = await eslint.lintFiles(["hidden/"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + }); + + it("should report zero messages when given a pattern with a .js and a .js2 file", async () => { + + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles(["fixtures/files/*.?s*"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should return one error message when given a config with rules with options and severity level set to error", async () => { + + eslint = new ESLint({ + cwd: getFixturePath("configurations"), + configFile: getFixturePath("configurations", "quotes-error.json") + }); + const results = await eslint.lintFiles([getFixturePath("single-quoted.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should return 3 messages when given a config file and a directory of 3 valid files", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "semi-error.json") + }); + + const results = await eslint.lintFiles([getFixturePath("formatters")]); + + assert.strictEqual(results.length, 3); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + assert.strictEqual(results[2].messages.length, 0); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[1].errorCount, 0); + assert.strictEqual(results[1].warningCount, 0); + assert.strictEqual(results[1].fixableErrorCount, 0); + assert.strictEqual(results[1].fixableWarningCount, 0); + assert.strictEqual(results[2].errorCount, 0); + assert.strictEqual(results[2].warningCount, 0); + assert.strictEqual(results[2].fixableErrorCount, 0); + assert.strictEqual(results[2].fixableWarningCount, 0); + }); + + + it("should return the total number of errors when given multiple files", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "single-quotes-error.json") + }); - /* - * describe("Fix Types", () => { - * it("should throw an error when an invalid fix type is specified", () => { - * assert.throws(() => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["layou"] - * }); - * }, /invalid fix type/iu); - * }); - */ - - /* - * it("should not fix any rules when fixTypes is used without fix", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: false, - * fixTypes: ["layout"] - * }); - */ - - /* - * const inputPath = getFixturePath("fix-types/fix-only-semi.js"); - * const report = engine.executeOnFiles([inputPath]); - */ - - /* - * assert.isUndefined(report.results[0].output); - * }); - */ - - /* - * it("should not fix non-style rules when fixTypes has only 'layout'", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["layout"] - * }); - * const inputPath = getFixturePath("fix-types/fix-only-semi.js"); - * const outputPath = getFixturePath("fix-types/fix-only-semi.expected.js"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - /* - * it("should not fix style or problem rules when fixTypes has only 'suggestion'", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["suggestion"] - * }); - * const inputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.js"); - * const outputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.expected.js"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - /* - * it("should fix both style and problem rules when fixTypes has 'suggestion' and 'layout'", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["suggestion", "layout"] - * }); - * const inputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.js"); - * const outputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.expected.js"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - /* - * it("should not throw an error when a rule doesn't have a 'meta' property", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["layout"], - * rulePaths: [getFixturePath("rules", "fix-types-test")] - * }); - */ - - /* - * const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); - * const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - /* - * it("should not throw an error when a rule is loaded after initialization with executeOnFiles()", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["layout"] - * }); - * const internalSlots = getESLintInternalSlots(engine); - */ - - /* - * 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"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - /* - * it("should not throw an error when a rule is loaded after initialization with executeOnText()", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true, - * fixTypes: ["layout"] - * }); - * const internalSlots = getESLintInternalSlots(engine); - */ - - /* - * 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"); - * const results = await eslint.lintText(fs.readFileSync(inputPath, { encoding: "utf8" }), inputPath); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - // }); - - /* - * it("correctly autofixes semicolon-conflicting-fixes", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true - * }); - * const inputPath = getFixturePath("autofix/semicolon-conflicting-fixes.js"); - * const outputPath = getFixturePath("autofix/semicolon-conflicting-fixes.expected.js"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ - - /* - * it("correctly autofixes return-conflicting-fixes", () => { - * const eslint = new ESLint({ - * cwd: path.join(fixtureDir, ".."), - * useEslintrc: false, - * fix: true - * }); - * const inputPath = getFixturePath("autofix/return-conflicting-fixes.js"); - * const outputPath = getFixturePath("autofix/return-conflicting-fixes.expected.js"); - * const report = engine.executeOnFiles([inputPath]); - * const expectedOutput = fs.readFileSync(outputPath, "utf8"); - */ - - /* - * assert.strictEqual(report.results[0].output, expectedOutput); - * }); - */ + const results = await eslint.lintFiles([getFixturePath("formatters")]); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[1].errorCount, 3); + assert.strictEqual(results[1].warningCount, 0); + assert.strictEqual(results[1].fixableErrorCount, 3); + assert.strictEqual(results[1].fixableWarningCount, 0); + assert.strictEqual(results[2].errorCount, 3); + assert.strictEqual(results[2].warningCount, 0); + assert.strictEqual(results[2].fixableErrorCount, 3); + assert.strictEqual(results[2].fixableWarningCount, 0); + }); + + it("should process when file is given by not specifying extensions", async () => { + + eslint = new ESLint({ + ignore: false, + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles(["fixtures/files/foo.js2"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when given a config with environment set to browser", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "env-browser.json") + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when given an option to set environment to browser", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + envs: ["browser"], + rules: { + "no-alert": 0, + "no-undef": 2 + } + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when given a config with environment set to Node.js", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "env-node.json") + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-node.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should not return results from previous call when calling more than once", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + ignore: false, + rules: { + semi: 2 + } + }); + + const failFilePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); + const passFilePath = fs.realpathSync(getFixturePath("passing.js")); + let results = await eslint.lintFiles([failFilePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, failFilePath); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].severity, 2); + + results = await eslint.lintFiles([passFilePath]); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, passFilePath); + assert.strictEqual(results[0].messages.length, 0); + + }); + + it("should throw an error when given a directory with all eslint excluded files in the directory", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + try { + await eslint.lintFiles([getFixturePath("./cli-engine/")]); + } catch (e) { + assert.strictEqual(`All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`, e.message); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error when all given files are ignored", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + try { + await eslint.lintFiles(["tests/fixtures/cli-engine/"]); + } catch (e) { + assert.isTrue(/All files matched by 'tests\/fixtures\/cli-engine\/' are ignored./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error when all given files are ignored even with a `./` prefix", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + try { + await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); + } catch (e) { + assert.isTrue(/All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + // https://github.com/eslint/eslint/issues/3788 + it("should ignore one-level down node_modules when ignore file has 'node_modules/' in it", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath("cli-engine", "nested_node_modules", ".eslintignore"), + useEslintrc: false, + rules: { + quotes: [2, "double"] + }, + cwd: getFixturePath("cli-engine", "nested_node_modules") + }); + + const results = await eslint.lintFiles(["."]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + // https://github.com/eslint/eslint/issues/3812 + it("should ignore all files and throw an error when tests/fixtures/ is in ignore file", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath("cli-engine/.eslintignore2"), + useEslintrc: false, + rules: { + quotes: [2, "double"] + } + }); + + try { + await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); + } catch (e) { + assert.isTrue(/All files matched by '.\/tests\/fixtures\/cli-engine\/' are ignored./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error when all given files are ignored via ignore-pattern", async () => { + eslint = new ESLint({ + ignorePattern: "tests/fixtures/single-quoted.js" + }); + + try { + await eslint.lintFiles(["tests/fixtures/*-quoted.js"]); + } catch (e) { + assert.isTrue(/All files matched by 'tests\/fixtures\/\*-quoted.js' are ignored./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should return a warning when an explicitly given file is ignored", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath() + }); + + const filePath = getFixturePath("passing.js"); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should return two messages when given a file in excluded files list while ignore is off", async () => { + + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + ignore: false, + rules: { + "no-undef": 2 + } + }); + + const filePath = fs.realpathSync(getFixturePath("undef.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-undef"); + assert.strictEqual(results[0].messages[1].severity, 2); + }); + + it("should return zero messages when executing a file with a shebang", async () => { + eslint = new ESLint({ + ignore: false + }); + + const results = await eslint.lintFiles([getFixturePath("shebang.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should give a warning when loading a custom rule that doesn't exist", async () => { + eslint = new ESLint({ + ignore: false, + rulePaths: [getFixturePath("rules", "dir1")], + configFile: getFixturePath("rules", "missing-rule.json") + }); + const results = await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "missing-rule"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].message, "Definition for rule 'missing-rule' was not found."); + }); + + it("should throw an error when loading a bad custom rule", async () => { + eslint = new ESLint({ + ignore: false, + rulePaths: [getFixturePath("rules", "wrong")], + configFile: getFixturePath("rules", "eslint.json") + }); + + try { + await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); + } catch (e) { + assert.isTrue(/Error while loading rule 'custom-rule'/u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should return one message when a custom rule matches a file", async () => { + + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + rulePaths: [getFixturePath("rules/")], + configFile: getFixturePath("rules", "eslint.json") + }); + + const filePath = fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "custom-rule"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + it("should load custom rule from the provided cwd", async () => { + const cwd = path.resolve(getFixturePath("rules")); + + eslint = new ESLint({ + ignore: false, + cwd, + rulePaths: ["./"], + configFile: "eslint.json" + }); + + const filePath = fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "custom-rule"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + it("should return messages when multiple custom rules match a file", async () => { + + eslint = new ESLint({ + ignore: false, + rulePaths: [ + getFixturePath("rules", "dir1"), + getFixturePath("rules", "dir2") + ], + configFile: getFixturePath("rules", "multi-rulesdirs.json") + }); + + const filePath = fs.realpathSync(getFixturePath("rules", "test-multi-rulesdirs.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-literals"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-strings"); + assert.strictEqual(results[0].messages[1].severity, 2); + }); + + it("should return zero messages when executing without useEslintrc flag", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false + }); + + const filePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when executing without useEslintrc flag in Node.js environment", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + envs: ["node"] + }); + + const filePath = fs.realpathSync(getFixturePath("process-exit.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when executing with base-config flag set to null", async () => { + eslint = new ESLint({ + ignore: false, + baseConfig: null, + useEslintrc: false + }); + + const filePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages and ignore .eslintrc files when executing with no-eslintrc flag", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + envs: ["node"] + }); + + const filePath = fs.realpathSync(getFixturePath("eslintrc", "quotes.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages and ignore package.json files when executing with no-eslintrc flag", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + envs: ["node"] + }); + + const filePath = fs.realpathSync(getFixturePath("packagejson", "quotes.js")); + + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should warn when deprecated rules are configured", async () => { + eslint = new ESLint({ + cwd: originalDir, + configFile: ".eslintrc.js", + rules: { + "indent-legacy": 1, + "require-jsdoc": 1, + "valid-jsdoc": 1 + } + }); + + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.sameDeepMembers( + results[0].usedDeprecatedRules, + [ + { ruleId: "indent-legacy", replacedBy: ["indent"] }, + { ruleId: "require-jsdoc", replacedBy: [] }, + { ruleId: "valid-jsdoc", replacedBy: [] } + ] + ); + }); + + it("should not warn when deprecated rules are not configured", async () => { + eslint = new ESLint({ + cwd: originalDir, + configFile: ".eslintrc.js", + rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + }); + + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.deepStrictEqual(results[0].usedDeprecatedRules, []); + }); + + it("should warn when deprecated rules are found in a config", async () => { + eslint = new ESLint({ + cwd: originalDir, + configFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml", + useEslintrc: false + }); + + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.deepStrictEqual( + results[0].usedDeprecatedRules, + [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] + ); + }); + + describe("Fix Mode", () => { + + it("should return fixed text on multiple files when in fix mode", async () => { + + /** + * Converts CRLF to LF in output. + * This is a workaround for git's autocrlf option on Windows. + * @param {Object} result A result object to convert. + * @returns {void} + */ + function convertCRLF(result) { + if (result && result.output) { + result.output = result.output.replace(/\r\n/gu, "\n"); + } + } + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } + }); + + const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + results.forEach(convertCRLF); + assert.deepStrictEqual(results, [ + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/multipass.js")), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "true ? \"yes\" : \"no\";\n", + usedDeprecatedRules: [] + }, + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/ok.js")), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [] + }, + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes-semi-eqeqeq.js")), + messages: [ + { + column: 9, + line: 2, + endColumn: 11, + endLine: 2, + message: "Expected '===' and instead saw '=='.", + messageId: "unexpected", + nodeType: "BinaryExpression", + ruleId: "eqeqeq", + severity: 2 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var msg = \"hi\";\nif (msg == \"hi\") {\n\n}\n", + usedDeprecatedRules: [] + }, + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes.js")), + messages: [ + { + column: 18, + line: 1, + endColumn: 21, + endLine: 1, + messageId: "undef", + message: "'foo' is not defined.", + nodeType: "Identifier", + ruleId: "no-undef", + severity: 2 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var msg = \"hi\" + foo;\n", + usedDeprecatedRules: [] + } + ]); + }); + + it("should run autofix even if files are cached without autofix results", async () => { + const baseOptions = { + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } + }; + + eslint = new ESLint(Object.assign({}, baseOptions, { cache: true, fix: false })); + + // Do initial lint run and populate the cache file + eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + eslint = new ESLint(Object.assign({}, baseOptions, { cache: true, fix: true })); + + const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + assert.ok(results.some(result => result.output)); + }); + + }); + + // These tests have to do with https://github.com/eslint/eslint/issues/963 + + describe("configuration hierarchy", () => { + + // Default configuration - blank + it("should return zero messages when executing with no .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // No default configuration rules - conf/environments.js (/*eslint-env node*/) + it("should return zero messages when executing with no .eslintrc in the Node.js environment", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes-node.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - first level .eslintrc + it("should return zero messages when executing with .eslintrc in the Node.js environment", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/process-exit.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - first level .eslintrc + it("should return zero messages when executing with .eslintrc in the Node.js environment", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/process-exit.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - first level .eslintrc + it("should return one message when executing with .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + }); + + // Project configuration - second level .eslintrc + it("should return one message when executing with local .eslintrc that overrides parent .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Project configuration - third level .eslintrc + it("should return one message when executing with local .eslintrc that overrides parent and grandparent .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/subsubbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Project configuration - first level package.json + it("should return one message when executing with package.json", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Project configuration - second level package.json + it("should return zero messages when executing with local package.json that overrides parent package.json", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/subsubdir/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - third level package.json + it("should return one message when executing with local package.json that overrides parent and grandparent package.json", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/subsubdir/subsubsubdir/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + }); + + // Project configuration - .eslintrc overrides package.json in same directory + it("should return one message when executing with .eslintrc that overrides a package.json in the same directory", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + }); + + // Command line configuration - --config with first level .eslintrc + it("should return two messages when executing with config file that adds to local .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); + assert.strictEqual(results[0].messages[1].severity, 1); + }); + + // Command line configuration - --config with first level .eslintrc + it("should return no messages when executing with config file that overrides local .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Command line configuration - --config with second level .eslintrc + it("should return two messages when executing with config file that adds to local and parent .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); + assert.strictEqual(results[0].messages[1].severity, 1); + }); + + // Command line configuration - --config with second level .eslintrc + it("should return one message when executing with config file that overrides local and parent .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("config-hierarchy/broken/override-conf.yaml") + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Command line configuration - --config with first level .eslintrc + it("should return no messages when executing with config file that overrides local .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should return one message when executing with command line rule and config file that overrides local .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("config-hierarchy/broken/override-conf.yaml"), + rules: { + quotes: [1, "double"] + } + }); + + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should return one message when executing with command line rule and config file that overrides local .eslintrc", async () => { + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("/config-hierarchy/broken/override-conf.yaml"), + rules: { + quotes: [1, "double"] + } + }); + + const results = await eslint.lintFiles([getFixturePath("config-hierarchy/broken/console-wrong-quotes.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + }); + + describe("plugins", () => { + it("should return two messages when executing with config file that specifies a plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "plugins-with-prefix.json"), + useEslintrc: false, + plugins: createMockedPluginsOption() + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test/test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should return two messages when executing with config file that specifies a plugin with namespace", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "plugins-with-prefix-and-namespace.json"), + useEslintrc: false, + plugins: createMockedPluginsOption() + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(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", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "plugins-without-prefix.json"), + useEslintrc: false, + plugins: createMockedPluginsOption() + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should return two messages when executing with config file that specifies a plugin without prefix and with namespace", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + configFile: getFixturePath("configurations", "plugins-without-prefix-with-namespace.json"), + useEslintrc: false, + plugins: createMockedPluginsOption() + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "@eslint/example/example-rule"); + }); + + it("should return two messages when executing with cli option that specifies a plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + plugins: ["example", ...createMockedPluginsOption()], + rules: { "example/example-rule": 1 } + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should load plugins from the `loadPluginsRelativeTo` directory, if specified", async () => { + eslint = new ESLint({ + resolvePluginsRelativeTo: getFixturePath("plugins"), + baseConfig: { + plugins: ["with-rules"], + rules: { "with-rules/rule1": "error" } + }, + useEslintrc: false + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "with-rules/rule1"); + assert.strictEqual(results[0].messages[0].message, "Rule report from plugin"); + }); + }); + + describe("cache", () => { + + /** + * helper method to delete a file without caring about exceptions + * @param {string} filePath The file path + * @returns {void} + */ + function doDelete(filePath) { + try { + fs.unlinkSync(filePath); + } catch (ex) { + + /* + * we don't care if the file didn't exist, since our + * intention was to remove the file + */ + } + } + + /** + * helper method to delete the cache files created during testing + * @returns {void} + */ + function deleteCache() { + doDelete(path.resolve(".eslintcache")); + doDelete(path.resolve(".cache/custom-cache")); + } + + beforeEach(() => { + deleteCache(); + }); + + afterEach(() => { + sinon.restore(); + deleteCache(); + }); + + it("should create the cache file inside the provided directory using the cacheLocation option", async () => { + assert.isFalse(shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir/", + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"], + ignore: false + }); + + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert.isTrue(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + + sinon.restore(); + }); + + it("should create the cache file inside cwd when no cacheLocation provided", async () => { + const cwd = path.resolve(getFixturePath("cli-engine")); + + eslint = new ESLint({ + useEslintrc: false, + cache: true, + cwd, + rules: { + "no-console": 0 + }, + extensions: ["js"], + ignore: false + }); + + const file = getFixturePath("cli-engine", "console.js"); + + await eslint.lintFiles([file]); + + assert.isTrue(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd"); + }); + + it("should invalidate the cache if the configuration changed between executions", async () => { + assert.isFalse(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"], + ignore: false + }); + + let spy = sinon.spy(fs, "readFileSync"); + + let file = getFixturePath("cache/src", "test-file.js"); + + file = fs.realpathSync(file); + + const [result] = await eslint.lintFiles([file]); + + assert.strictEqual(result.errorCount + result.warningCount, 0, "the file passed without errors or warnings"); + assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); + assert.isTrue(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + + // destroy the spy + sinon.restore(); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + rules: { + "no-console": 2, + "no-unused-vars": 2 + }, + extensions: ["js"], + ignore: false + }); + + // create a new spy + spy = sinon.spy(fs, "readFileSync"); + + const [cachedResult] = await eslint.lintFiles([file]); + + assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed because the config changed"); + assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used an one error was reported"); + assert.isTrue(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + }); + + it("should remember the files from a previous run and do not operate on them if not changed", async () => { + + assert.isFalse(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"], + ignore: false + }); + + let spy = sinon.spy(fs, "readFileSync"); + + let file = getFixturePath("cache/src", "test-file.js"); + + file = fs.realpathSync(file); + + const result = await eslint.lintFiles([file]); + + assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); + assert.isTrue(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + + // destroy the spy + sinon.restore(); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"], + ignore: false + }); + + // create a new spy + spy = sinon.spy(fs, "readFileSync"); + + const cachedResult = await eslint.lintFiles([file]); + + assert.deepStrictEqual(result, cachedResult, "the result is the same regardless of using cache or not"); + + // assert the file was not processed because the cache was used + assert.isFalse(spy.calledWith(file), "the file was not loaded because it used the cache"); + }); + + it("should remember the files from a previous run and do not operate on then if not changed", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + const eslintOptions = { + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"], + cwd: path.join(fixtureDir, "..") + }; + + assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + + eslint = new ESLint(eslintOptions); + + let file = getFixturePath("cache/src", "test-file.js"); + + file = fs.realpathSync(file); + + await eslint.lintFiles([file]); + + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint was created"); + + eslintOptions.cache = false; + eslint = new ESLint(eslintOptions); + + await eslint.lintFiles([file]); + + assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint was deleted since last run did not used the cache"); + }); + + it("should store in the cache a file that failed the test", async () => { + + const cacheLocation = getFixturePath(".eslintcache"); + + assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + + const result = await eslint.lintFiles([badFile, goodFile]); + + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint was created"); + + const fileCache = fCache.createFromFile(cacheLocation); + const { cache } = fileCache; + + assert.isTrue(typeof cache.getKey(goodFile) === "object", "the entry for the good file is in the cache"); + + assert.isTrue(typeof cache.getKey(badFile) === "object", "the entry for the bad file is in the cache"); + + const cachedResult = await eslint.lintFiles([badFile, goodFile]); + + assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); + }); + + it("should not contain in the cache a file that was deleted", async () => { + + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const toBeDeletedFile = fs.realpathSync(getFixturePath("cache/src", "file-to-delete.js")); + + await eslint.lintFiles([badFile, goodFile, toBeDeletedFile]); + + const fileCache = fCache.createFromFile(cacheLocation); + let { cache } = fileCache; + + assert.isTrue(typeof cache.getKey(toBeDeletedFile) === "object", "the entry for the file to be deleted is in the cache"); + + // delete the file from the file system + fs.unlinkSync(toBeDeletedFile); + + /* + * file-entry-cache@2.0.0 will remove from the cache deleted files + * even when they were not part of the array of files to be analyzed + */ + await eslint.lintFiles([badFile, goodFile]); + + cache = JSON.parse(fs.readFileSync(cacheLocation)); + + assert.isTrue(typeof cache[toBeDeletedFile] === "undefined", "the entry for the file to be deleted is not in the cache"); + }); + + it("should contain files that were not visited in the cache provided they still exist", async () => { + + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const testFile2 = fs.realpathSync(getFixturePath("cache/src", "test-file2.js")); + + await eslint.lintFiles([badFile, goodFile, testFile2]); + + let fileCache = fCache.createFromFile(cacheLocation); + let { cache } = fileCache; + + assert.isTrue(typeof cache.getKey(testFile2) === "object", "the entry for the test-file2 is in the cache"); + + /* + * we pass a different set of files minus test-file2 + * previous version of file-entry-cache would remove the non visited + * entries. 2.0.0 version will keep them unless they don't exist + */ + await eslint.lintFiles([badFile, goodFile]); + + fileCache = fCache.createFromFile(cacheLocation); + cache = fileCache.cache; + + assert.isTrue(typeof cache.getKey(testFile2) === "object", "the entry for the test-file2 is in the cache"); + }); + + describe("cache deletion", () => { + beforeEach(async () => { + const cacheLocation = getFixturePath(".eslintcache"); + const file = getFixturePath("cli-engine", "console.js"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + cache: true, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + await eslint.lintFiles([file]); + }); + + it("should not delete cache when executing on text", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintText("var foo = 'bar';"); + + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should not delete cache when executing on text with a provided filename", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + await eslint.lintText("var bar = foo;", "fixtures/passing.js"); + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintText("var bar = foo;", "fixtures/passing.js"); + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should not delete cache when executing on files with --cache flag", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + const file = getFixturePath("cli-engine", "console.js"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + cache: true, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintFiles([file]); + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should delete cache when executing on files without --cache flag", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + const file = getFixturePath("cli-engine", "console.js"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + rules: { + "no-console": 0, + "no-unused-vars": 2 + }, + extensions: ["js"] + }); + + + assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); + await eslint.lintFiles([file]); + assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint has been deleted"); + }); + + }); + }); + + describe("processors", () => { + it("should return two messages when executing with config file that specifies a processor", async () => { + eslint = new ESLint({ + configFile: getFixturePath("configurations", "processors.json"), + useEslintrc: false, + extensions: ["js", "txt"], + plugins: createMockedPluginsOption(), + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("processors", "test", "test-processor.txt"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + }); + + xit("should return two messages when executing with config file that specifies preloaded processor", async () => { + eslint = new ESLint({ + useEslintrc: false, + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + }, + extensions: ["js", "txt"], + cwd: path.join(fixtureDir, "..") + }); + + /* + * engine.addPlugin("test-processor", { + * processors: { + * ".txt": { + * preprocess(text) { + * return [text]; + * }, + * postprocess(messages) { + * return messages[0]; + * } + * } + * } + * }); + */ + + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("processors", "test", "test-processor.txt"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + }); + + it("should run processors when calling executeOnFiles with config file that specifies a processor", async () => { + eslint = new ESLint({ + configFile: getFixturePath("configurations", "processors.json"), + useEslintrc: false, + extensions: ["js", "txt"], + plugins: createMockedPluginsOption(), + cwd: path.join(fixtureDir, "..") + }); + + const results = await eslint.lintFiles([getFixturePath("processors", "test", "test-processor.txt")]); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + xit("should run processors when calling executeOnFiles with config file that specifies preloaded processor", async () => { + eslint = new ESLint({ + useEslintrc: false, + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + }, + extensions: ["js", "txt"], + cwd: path.join(fixtureDir, "..") + }); + + /* + * engine.addPlugin("test-processor", { + * processors: { + * ".txt": { + * preprocess(text) { + * return [text.replace("a()", "b()")]; + * }, + * postprocess(messages) { + * messages[0][0].ruleId = "post-processed"; + * return messages[0]; + * } + * } + * } + * }); + */ + + const results = await eslint.lintFiles([getFixturePath("processors", "test", "test-processor.txt")]); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + it("should run processors when calling executeOnText with config file that specifies a processor", async () => { + eslint = new ESLint({ + configFile: getFixturePath("configurations", "processors.json"), + useEslintrc: false, + extensions: ["js", "txt"], + plugins: createMockedPluginsOption(), + ignore: false + }); + + const results = await eslint.lintText("function a() {console.log(\"Test\");}", { filePath: "tests/fixtures/processors/test/test-processor.txt" }); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + xit("should run processors when calling executeOnText with config file that specifies preloaded processor", async () => { + eslint = new ESLint({ + useEslintrc: false, + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + }, + extensions: ["js", "txt"], + ignore: false + }); + + /* + * engine.addPlugin("test-processor", { + * processors: { + * ".txt": { + * preprocess(text) { + * return [text.replace("a()", "b()")]; + * }, + * postprocess(messages) { + * messages[0][0].ruleId = "post-processed"; + * return messages[0]; + * } + * } + * } + * }); + */ + + const results = await eslint.lintText("function a() {console.log(\"Test\");}", "tests/fixtures/processors/test/test-processor.txt"); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + xdescribe("autofixing with processors", async () => { + + /* + * const HTML_PROCESSOR = Object.freeze({ + * preprocess(text) { + * return [text.replace(/^", "foo.html"); + + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].output, ""); + }); + + xit("should not run in autofix mode when using a processor that does not support autofixing", async () => { + eslint = new ESLint({ + useEslintrc: false, + plugins: ["test-processor"], + rules: { + semi: 2 + }, + extensions: ["js", "txt"], + ignore: false, + fix: true + }); + + // engine.addPlugin("test-processor", { processors: { ".html": HTML_PROCESSOR } }); + + const results = await eslint.lintText("", "foo.html"); + + assert.strictEqual(results[0].messages.length, 1); + assert.isFalse(Object.prototype.hasOwnProperty.call(results[0], "output")); + }); + + xit("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", async () => { + eslint = new ESLint({ + useEslintrc: false, + plugins: ["test-processor"], + rules: { + semi: 2 + }, + extensions: ["js", "txt"], + ignore: false + }); + + /* + * engine.addPlugin("test-processor", { + * processors: { + * ".html": Object.assign({ supportsAutofix: true }, HTML_PROCESSOR) + * } + * }); + */ + + const results = await eslint.lintText("", "foo.html"); + + assert.strictEqual(results[0].messages.length, 1); + assert.isFalse(Object.prototype.hasOwnProperty.call(results[0], "output")); + }); + }); + }); + + describe("Patterns which match no file should throw errors.", () => { + beforeEach(() => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + useEslintrc: false + }); + }); + + it("one file", async () => { + try { + await eslint.lintFiles(["non-exist.js"]); + } catch (e) { + assert.isTrue(/No files matching 'non-exist.js' were found./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw if the directory exists and is empty", async () => { + try { + await eslint.lintFiles(["empty"]); + } catch (e) { + assert.isTrue(/No files matching 'empty' were found./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("one glob pattern", async () => { + try { + await eslint.lintFiles(["non-exist/**/*.js"]); + } catch (e) { + assert.isTrue(/No files matching 'non-exist\/\*\*\/\*.js' were found./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("two files", async () => { + try { + await eslint.lintFiles(["aaa.js", "bbb.js"]); + } catch (e) { + assert.isTrue(/No files matching 'aaa.js' were found./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("a mix of an existing file and a non-existing file", async () => { + try { + await eslint.lintFiles(["console.js", "non-exist.js"]); + } catch (e) { + assert.isTrue(/No files matching 'non-exist.js' were found./u.test(e.message)); + return; + } + assert.fail("Expected to throw an error"); + }); + }); + + describe("overrides", () => { + it("should recognize dotfiles", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine/overrides-with-dot"), + ignore: false + }); + const results = await eslint.lintFiles([".test-target.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-unused-vars"); + }); + }); + + describe("a config file setting should have higher priority than a shareable config file's settings always; https://github.com/eslint/eslint/issues/11510", async () => { + beforeEach(() => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + 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();" + } + })); + eslint = new ESLint(); + }); + + it("should not report 'no-console' error.", async () => { + const results = await eslint.lintFiles("a.js"); + + assert.strictEqual(results.length, 1); + assert.deepStrictEqual(results[0].messages, []); + }); + }); + + describe("configs of plugin rules should be validated even if 'plugins' key doesn't exist; https://github.com/eslint/eslint/issues/11559", async () => { + beforeEach(() => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + 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({ + + // Import via the recommended config. + extends: "plugin:test/recommended", + + // Has invalid option. + rules: { "test/foo": ["error", "invalid-option"] } + }), + "a.js": "console.log();" + } + })); + eslint = new ESLint(); + }); + + it("should throw fatal error.", async () => { + try { + await eslint.lintFiles("a.js"); + } catch (e) { + assert.isTrue(/invalid-option/u.test(e.message)); + return; + } + + assert.fail("Expected to throw an error"); + }); + }); + + describe("'--fix-type' should not crash even if plugin rules exist; https://github.com/eslint/eslint/issues/11586", async () => { + beforeEach(() => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + 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;" + } + })); + eslint = new ESLint({ fix: true, fixTypes: ["problem"] }); + }); + + it("should not crash.", async () => { + const results = await eslint.lintFiles("a.js"); + + assert.strictEqual(results.length, 1); + assert.deepStrictEqual(results[0].messages, []); + assert.deepStrictEqual(results[0].output, "fixed;"); + }); + }); + + describe("multiple processors", () => { + const root = path.join(os.tmpdir(), "eslint/cli-engine/multiple-processors"); + const commonFiles = { + "node_modules/pattern-processor/index.js": fs.readFileSync( + require.resolve("../../fixtures/processors/pattern-processor"), + "utf8" + ), + "node_modules/eslint-plugin-markdown/index.js": ` + const { defineProcessor } = require("pattern-processor"); + const processor = defineProcessor(${/```(\w+)\n([\s\S]+?)\n```/gu}); + exports.processors = { + ".md": { ...processor, supportsAutofix: true }, + "non-fixable": processor + }; + `, + "node_modules/eslint-plugin-html/index.js": ` + const { defineProcessor } = require("pattern-processor"); + const processor = defineProcessor(${/ + + \`\`\` + ` + }; + + it("should lint only JavaScript blocks if '--ext' was not given.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + })); + eslint = new ESLint({ cwd: root }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should fix only JavaScript blocks if '--ext' was not given.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + })); + eslint = new ESLint({ cwd: root, fix: true }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].output, unIndent` + \`\`\`js + console.log("hello");${/* ← fixed */""} + \`\`\` + \`\`\`html +
Hello
+ + + \`\`\` + `); + }); + + it("should lint HTML blocks as well with multiple processors if '--ext' option was given.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + })); + eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS block + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); // JS block in HTML block + assert.strictEqual(results[0].messages[1].line, 7); + }); + + it("should fix HTML blocks as well with multiple processors if '--ext' option was given.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + })); + eslint = new ESLint({ cwd: root, extensions: ["js", "html"], fix: true }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].output, unIndent` + \`\`\`js + console.log("hello");${/* ← fixed */""} + \`\`\` + \`\`\`html +
Hello
+ + + \`\`\` + `); + }); + + it("should use overriden processor; should report HTML blocks but not fix HTML blocks if the processor for '*.html' didn't support autofix.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/non-fixable" // supportsAutofix: false + } + ] + }) + } + })); + eslint = new ESLint({ cwd: root, extensions: ["js", "html"], fix: true }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS Block in HTML Block + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[0].fix, void 0); + assert.strictEqual(results[0].output, unIndent` + \`\`\`js + console.log("hello");${/* ← fixed */""} + \`\`\` + \`\`\`html +
Hello
+ + + \`\`\` + `); + }); + + it("should use the config '**/*.html/*.js' to lint JavaScript blocks in HTML.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + + // this rules are not used because ESLint re-resolve configs if a code block had a different file extension. + rules: { + semi: "error", + "no-console": "off" + } + }, + { + files: "**/*.html/*.js", + rules: { + semi: "off", + "no-console": "error" + } + } + ] + }) + } + })); + eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-console"); + assert.strictEqual(results[0].messages[1].line, 7); + }); + + it("should use the same config as one which has 'processor' property in order to lint blocks in HTML if the processor was legacy style.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/legacy", // this processor returns strings rather than `{text, filename}` + rules: { + semi: "off", + "no-console": "error" + } + }, + { + files: "**/*.html/*.js", + rules: { + semi: "error", + "no-console": "off" + } + } + ] + }) + } + })); + eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 3); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-console"); + assert.strictEqual(results[0].messages[1].line, 7); + assert.strictEqual(results[0].messages[2].ruleId, "no-console"); + assert.strictEqual(results[0].messages[2].line, 10); + }); + + it("should throw an error if invalid processor was specified.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + processor: "markdown/unknown" + }) + } + })); + eslint = new ESLint({ cwd: root }); + + try { + await eslint.lintFiles(["test.md"]); + } catch (e) { + assert.isTrue(/ESLint configuration of processor in '\.eslintrc\.json' is invalid: 'markdown\/unknown' was not found\./u.test(e.message)); + return; + } + + assert.fail("Expected to throw an error"); + }); + + it("should lint HTML blocks as well with multiple processors if 'overrides[].files' is present.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/.html" + }, + { + files: "*.md", + processor: "markdown/.md" + } + ] + }) + } + })); + eslint = new ESLint({ cwd: root }); + + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS block + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); // JS block in HTML block + assert.strictEqual(results[0].messages[1].line, 7); + }); + }); + + describe("MODULE_NOT_FOUND error handling", () => { + const cwd = getFixturePath("module-not-found"); + + beforeEach(() => { + eslint = new ESLint({ cwd }); + }); + + it("should throw an error with a message template when 'extends' property has a non-existence JavaScript config.", async () => { + try { + await eslint.lintText("test", { filePath: "extends-js/test.js" }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "extend-config-missing"); + assert.deepStrictEqual(err.messageData, { + configName: "nonexistent-config", + importerName: getFixturePath("module-not-found", "extends-js", ".eslintrc.yml") + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when 'extends' property has a non-existence plugin config.", async () => { + try { + await eslint.lintText("test", { filePath: "extends-plugin/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + importerName: `extends-plugin${path.sep}.eslintrc.yml`, + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: cwd + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when 'plugins' property has a non-existence plugin.", async () => { + try { + await eslint.lintText("test", { filePath: "plugins/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + importerName: `plugins${path.sep}.eslintrc.yml`, + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: cwd + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when a JavaScript config threw a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-config-itself/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when 'extends' property has a JavaScript config that throws a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-extends-js/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when 'extends' property has a plugin config that throws a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-extends-plugin/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when 'plugins' property has a plugin config that throws a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-plugins/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + }); + + describe("with '--rulesdir' option", async () => { + it("should use the configured rules which are defined by '--rulesdir' option.", async () => { + const rootPath = getFixturePath("cli-engine/with-rulesdir"); + + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => rootPath, + files: { + "internal-rules/test.js": ` + module.exports = context => ({ + ExpressionStatement(node) { + context.report({ node, message: "ok" }) + } + }) + `, + ".eslintrc.json": JSON.stringify({ + root: true, + rules: { test: "error" } + }), + "test.js": "console.log('hello')" + } + })); + eslint = new ESLint({ + rulePaths: ["internal-rules"] + }); + const results = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "ok"); + }); + }); + + describe("glob pattern '[ab].js'", () => { + const root = getFixturePath("cli-engine/unmatched-glob"); + + it("should match '[ab].js' if existed.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "a.js": "", + "b.js": "", + "ab.js": "", + "[ab].js": "", + ".eslintrc.yml": "root: true" + } + })); + eslint = new ESLint(); + + const results = await eslint.lintFiles(["[ab].js"]); + const filenames = results.map(r => path.basename(r.filePath)); + + assert.deepStrictEqual(filenames, ["[ab].js"]); + }); + + it("should match 'a.js' and 'b.js' if '[ab].js' didn't existed.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "a.js": "", + "b.js": "", + "ab.js": "", + ".eslintrc.yml": "root: true" + } + })); + eslint = new ESLint(); + + const results = await eslint.lintFiles(["[ab].js"]); + const filenames = results.map(r => path.basename(r.filePath)); + + assert.deepStrictEqual(filenames, ["a.js", "b.js"]); + }); + }); + + describe("with 'noInlineConfig' setting", async () => { + const root = getFixturePath("cli-engine/noInlineConfig"); + + it("should warn directive comments if 'noInlineConfig' was given.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* globals foo */", + ".eslintrc.yml": "noInlineConfig: true" + } + })); + eslint = new ESLint(); + + const [{ messages }] = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config (.eslintrc.yml)."); + }); + + it("should show the config file what the 'noInlineConfig' came from.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-foo/index.js": "module.exports = {noInlineConfig: true}", + "test.js": "/* globals foo */", + ".eslintrc.yml": "extends: foo" + } + })); + eslint = new ESLint(); + + const [{ messages }] = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config (.eslintrc.yml » eslint-config-foo)."); + }); + }); + + describe("with 'reportUnusedDisableDirectives' setting", async () => { + const root = getFixturePath("cli-engine/reportUnusedDisableDirectives"); + + it("should warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives' was given.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + })); + eslint = new ESLint(); + + const [{ messages }] = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].message, "Unused eslint-disable directive (no problems were reported from 'eqeqeq')."); + }); + + describe("the runtime option overrides config files.", () => { + it("should not warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives=off' was given in runtime.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + })); + eslint = new ESLint({ reportUnusedDisableDirectives: false }); + + const [{ messages }] = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(messages.length, 0); + }); + + it("should warn unused 'eslint-disable' comments as error if 'reportUnusedDisableDirectives=error' was given in runtime.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + })); + eslint = new ESLint({ reportUnusedDisableDirectives: true }); + + const [{ messages }] = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Unused eslint-disable directive (no problems were reported from 'eqeqeq')."); + }); + }); + }); + + describe("with 'overrides[*].extends' setting on deep locations", async () => { + const root = getFixturePath("cli-engine/deeply-overrides-i-extends"); + + it("should not throw.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + overrides: [{ files: ["*test*"], extends: "two" }] + })}`, + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + overrides: [{ files: ["*.js"], extends: "three" }] + })}`, + "node_modules/eslint-config-three/index.js": `module.exports = ${JSON.stringify({ + rules: { "no-console": "error" } + })}`, + "test.js": "console.log('hello')", + ".eslintrc.yml": "extends: one" + } + })); + eslint = new ESLint(); + + const [{ messages }] = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + }); + + describe("don't ignore the entry directory.", () => { + const root = getFixturePath("cli-engine/dont-ignore-entry-dir"); + + it("'executeOnFiles(\".\")' should not load config files from outside of \".\".", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "../.eslintrc.json": "BROKEN FILE", + ".eslintrc.json": JSON.stringify({ root: true }), + "index.js": "console.log(\"hello\")" + } + })); + eslint = new ESLint(); + + // Don't throw "failed to load config file" error. + eslint.lintFiles("."); + }); + + it("'executeOnFiles(\".\")' should not ignore '.' even if 'ignorePatterns' contains it.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "../.eslintrc.json": JSON.stringify({ ignorePatterns: ["/dont-ignore-entry-dir"] }), + ".eslintrc.json": JSON.stringify({ root: true }), + "index.js": "console.log(\"hello\")" + } + })); + eslint = new ESLint(); + + // Don't throw "file not found" error. + eslint.lintFiles("."); + }); + + it("'executeOnFiles(\"subdir\")' should not ignore './subdir' even if 'ignorePatterns' contains it.", async () => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ ignorePatterns: ["/subdir"] }), + "subdir/.eslintrc.json": JSON.stringify({ root: true }), + "subdir/index.js": "console.log(\"hello\")" + } + })); + eslint = new ESLint(); + + // Don't throw "file not found" error. + eslint.lintFiles("subdir"); + }); + }); }); }); diff --git a/tests/lib/init/npm-utils.js b/tests/lib/init/npm-utils.js index d0326bd1330..8465796a367 100644 --- a/tests/lib/init/npm-utils.js +++ b/tests/lib/init/npm-utils.js @@ -14,7 +14,7 @@ const sinon = require("sinon"), npmUtils = require("../../../lib/init/npm-utils"), log = require("../../../lib/shared/logging"), - { defineInMemoryFs } = require("../_utils"); + { defineInMemoryFs } = require("../../_utils"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); diff --git a/tests/lib/rules/implicit-arrow-linebreak.js b/tests/lib/rules/implicit-arrow-linebreak.js index 089424e1c47..c65d4750a7d 100644 --- a/tests/lib/rules/implicit-arrow-linebreak.js +++ b/tests/lib/rules/implicit-arrow-linebreak.js @@ -10,7 +10,7 @@ const rule = require("../../../lib/rules/implicit-arrow-linebreak"); const { RuleTester } = require("../../../lib/rule-tester"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); const EXPECTED_LINEBREAK = { messageId: "expected" }; const UNEXPECTED_LINEBREAK = { messageId: "unexpected" }; diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js index 7496da218d1..56589be5169 100644 --- a/tests/lib/rules/indent.js +++ b/tests/lib/rules/indent.js @@ -21,7 +21,7 @@ const path = require("path"); const fixture = fs.readFileSync(path.join(__dirname, "../../fixtures/rules/indent/indent-invalid-fixture-1.js"), "utf8"); const fixedFixture = fs.readFileSync(path.join(__dirname, "../../fixtures/rules/indent/indent-valid-fixture-1.js"), "utf8"); const parser = require("../../fixtures/fixture-parser"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); /** @@ -5590,9 +5590,9 @@ ruleTester.run("indent", rule, { \${a} \${b} template literal - \`(() => { + \`(() => { foo(); - + tagTwo\`multiline template literal @@ -5609,12 +5609,12 @@ ruleTester.run("indent", rule, { tagOne\`multiline template literal - \${a} \${b}\`({ + \${a} \${b}\`({ foo: 1, bar: tagTwo\`multiline template literal\`(() => { - + baz(); }) }); @@ -5651,7 +5651,7 @@ ruleTester.run("indent", rule, { code: unIndent` foo .bar - .baz\` template + .baz\` template literal \`(() => { baz(); }) @@ -11207,9 +11207,9 @@ ruleTester.run("indent", rule, { tagOne\`multiline \${a} \${b} template literal - \`(() => { + \`(() => { foo(); - + tagTwo\`multiline template literal @@ -11223,9 +11223,9 @@ ruleTester.run("indent", rule, { tagOne\`multiline \${a} \${b} template literal - \`(() => { + \`(() => { foo(); - + tagTwo\`multiline template literal @@ -11250,7 +11250,7 @@ ruleTester.run("indent", rule, { bar: tagTwo\`multiline template literal\`(() => { - + baz(); }) }); @@ -11263,7 +11263,7 @@ ruleTester.run("indent", rule, { bar: tagTwo\`multiline template literal\`(() => { - + baz(); }) }); diff --git a/tests/lib/rules/object-shorthand.js b/tests/lib/rules/object-shorthand.js index b9b81329f91..01a72f4e6f0 100644 --- a/tests/lib/rules/object-shorthand.js +++ b/tests/lib/rules/object-shorthand.js @@ -11,7 +11,7 @@ const rule = require("../../../lib/rules/object-shorthand"), { RuleTester } = require("../../../lib/rule-tester"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); //------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/shared/runtime-info.js b/tests/lib/shared/runtime-info.js index 79c50a414ac..76ca7c7ca28 100644 --- a/tests/lib/shared/runtime-info.js +++ b/tests/lib/shared/runtime-info.js @@ -12,7 +12,7 @@ const assert = require("chai").assert; const sinon = require("sinon"); const spawn = require("cross-spawn"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); const RuntimeInfo = require("../../../lib/shared/runtime-info"); const log = require("../../../lib/shared/logging"); const packageJson = require("../../../package.json"); From c6bee0aa2449b8958cda43404fb8a8e1370bbaed Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 12 Apr 2020 18:55:51 +0900 Subject: [PATCH 09/71] update usedDeprecatedRules property --- lib/cli-engine/cli-engine.js | 55 ++++++++------ lib/cli-engine/config-array/config-array.js | 2 +- lib/eslint/eslint.js | 82 ++++++++++++++++++--- lib/shared/types.js | 12 +-- tests/_utils/in-memory-fs.js | 5 +- 5 files changed, 113 insertions(+), 43 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index b6bf1cfacb9..5edab533d50 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -39,12 +39,12 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); // For VSCode IntelliSense /** @typedef {import("../shared/types").ConfigData} ConfigData */ +/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").Plugin} Plugin */ /** @typedef {import("../shared/types").RuleConf} RuleConf */ /** @typedef {import("../shared/types").Rule} Rule */ -/** @typedef {import("../shared/types").LintReport} LintReport */ /** @typedef {ReturnType} ConfigArray */ /** @typedef {ReturnType} ExtractedConfig */ @@ -90,10 +90,14 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); */ /** - * 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. */ /** @@ -546,7 +550,6 @@ class CLIEngine { options.cacheLocation || options.cacheFile, options.cwd ); - const configArrayFactory = new CascadingConfigArrayFactory({ additionalPluginPool, baseConfig: options.baseConfig || null, @@ -653,8 +656,6 @@ class CLIEngine { * @returns {void} */ static outputFixes(report) { - - // TODO: Write files in parallel. report.results.filter(result => Object.prototype.hasOwnProperty.call(result, "output")).forEach(result => { fs.writeFileSync(result.filePath, result.output); }); @@ -693,7 +694,6 @@ class CLIEngine { return patterns.filter(Boolean); } - // Question: Should we account for the possibility of empty arrays? const extensions = (options.extensions || [".js"]).map(ext => ext.replace(/^\./u, "")); const dirSuffix = `/**/*.{${extensions.join(",")}}`; @@ -815,16 +815,22 @@ class CLIEngine { lintResultCache.reconcile(); } - // Collect used deprecated rules. - const usedDeprecatedRules = Array.from( - iterateRuleDeprecationWarnings(lastConfigArrays) - ); - debug(`Linting complete in: ${Date.now() - startTime}ms`); + let usedDeprecatedRules; + return { results, ...calculateStatsPerRun(results), - usedDeprecatedRules + + // Initialize it lazily because CLI and `ESLint` API don't use it. + get usedDeprecatedRules() { + if (!usedDeprecatedRules) { + usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(lastConfigArrays) + ); + } + return usedDeprecatedRules; + } }; } @@ -855,7 +861,6 @@ class CLIEngine { // Clear the last used config arrays. lastConfigArrays.length = 0; - if (resolvedFilename && this.isPathIgnored(resolvedFilename)) { if (warnIgnored) { results.push(createIgnoreResult(resolvedFilename, cwd)); @@ -887,16 +892,22 @@ class CLIEngine { })); } - // Collect used deprecated rules. - const usedDeprecatedRules = Array.from( - iterateRuleDeprecationWarnings(lastConfigArrays) - ); - debug(`Linting complete in: ${Date.now() - startTime}ms`); + let usedDeprecatedRules; + return { results, ...calculateStatsPerRun(results), - usedDeprecatedRules + + // Initialize it lazily because CLI and `ESLint` API don't use it. + get usedDeprecatedRules() { + if (!usedDeprecatedRules) { + usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(lastConfigArrays) + ); + } + return usedDeprecatedRules; + } }; } diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index b3434198b19..42a7362737f 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -107,7 +107,7 @@ function getMatchedIndices(elements, filePath) { for (let i = elements.length - 1; i >= 0; --i) { const element = elements[i]; - if (!element.criteria || element.criteria.test(filePath)) { + if (!element.criteria || (filePath && element.criteria.test(filePath))) { indices.push(i); } } diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index cec00ec3390..ac0750e567c 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -9,14 +9,19 @@ // Requirements //------------------------------------------------------------------------------ -const { CLIEngine } = require("../cli-engine"); +const path = require("path"); +const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); +const BuiltinRules = require("../rules"); +const { getRuleSeverity } = require("../shared/config-ops"); //------------------------------------------------------------------------------ // Typedefs //------------------------------------------------------------------------------ +/** @typedef {InstanceType} CascadingConfigArrayFactory */ +/** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ +/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").Rule} Rule */ -/** @typedef {import("../shared/types").LintReport} LintReport */ /** * The options with which to configure the ESLint instance. @@ -287,17 +292,66 @@ function createRulesMeta(rules) { }, { rulesMeta: {} }); } +/** @type {WeakMap} */ +const usedDeprecatedRulesCache = new WeakMap(); + +/** + * Create used deprecated rule list. + * @param {CascadingConfigArrayFactory} factory The config array factory. + * @param {string} filePath The path to a lint target file. + * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. + */ +function calculateUsedDeprecatedRules(factory, filePath) { + const configArray = factory.getConfigArrayForFile(filePath); + const config = configArray.extractConfig(filePath); + + // Most files use the same config, so cache it. + if (!usedDeprecatedRulesCache.has(config)) { + const pluginRules = configArray.pluginRules; + const retv = []; + + for (const [ruleId, ruleConf] of Object.entries(config.rules)) { + if (getRuleSeverity(ruleConf) === 0) { + continue; + } + const rule = pluginRules.get(ruleId) || BuiltinRules.get(ruleId); + const meta = rule && rule.meta; + + if (meta && meta.deprecated) { + retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); + } + } + + usedDeprecatedRulesCache.set(config, Object.freeze(retv)); + } + + return usedDeprecatedRulesCache.get(config); +} + /** * Processes the linting results generated by a CLIEngine linting report to * match the ESLint class's API. - * @param {LintReport} report The CLIEngine linting report to process. + * @param {CascadingConfigArrayFactory} factory The config array factory. + * @param {CLIEngineLintReport} report The CLIEngine linting report to process. * @returns {LintResult[]} The processed linting results. */ -function processCLIEngineLintReport({ results, usedDeprecatedRules }) { - return results.map(result => { - result.usedDeprecatedRules = usedDeprecatedRules; - return result; - }); +function processCLIEngineLintReport(factory, { results }) { + const descriptor = { + configurable: true, + enumerable: true, + get() { + const filePath = + path.isAbsolute(this.filePath) ? this.filePath : null; + + return calculateUsedDeprecatedRules(factory, filePath); + } + }; + + for (const result of results) { + Object.defineProperty(result, "usedDeprecatedRules", descriptor); + } + + return results; } class ESLint { @@ -370,8 +424,12 @@ class ESLint { */ async lintFiles(patterns) { const { cliEngine } = privateMembersMap.get(this); + const { configArrayFactory } = getCLIEngineInternalSlots(cliEngine); - return processCLIEngineLintReport(cliEngine.executeOnFiles(patterns)); + return processCLIEngineLintReport( + configArrayFactory, + cliEngine.executeOnFiles(patterns) + ); } /** @@ -384,8 +442,12 @@ class ESLint { */ async lintText(code, { filePath = null, warnIgnored = false } = {}) { const { cliEngine } = privateMembersMap.get(this); + const { configArrayFactory } = getCLIEngineInternalSlots(cliEngine); - return processCLIEngineLintReport(cliEngine.executeOnText(code, filePath, warnIgnored)); + return processCLIEngineLintReport( + configArrayFactory, + cliEngine.executeOnText(code, filePath, warnIgnored) + ); } /** diff --git a/lib/shared/types.js b/lib/shared/types.js index 45071fa1df2..bbd95d1b378 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -143,12 +143,8 @@ module.exports = {}; */ /** - * 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. + * Information of deprecated rules. + * @typedef {Object} DeprecatedRuleInfo + * @property {string} ruleId The rule ID. + * @property {string[]} replacedBy The rule IDs that replace this deprecated rule. */ diff --git a/tests/_utils/in-memory-fs.js b/tests/_utils/in-memory-fs.js index fdd72a3adc2..5b6ecd6c8c3 100644 --- a/tests/_utils/in-memory-fs.js +++ b/tests/_utils/in-memory-fs.js @@ -504,10 +504,11 @@ function defineESLintWithInMemoryFileSystem({ ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator, - CLIEngine + CLIEngine, + getCLIEngineInternalSlots } = defineCLIEngineWithInMemoryFileSystem({ cwd, files }); const { ESLint, getESLintInternalSlots } = proxyquire(ESLintPath, { - "../cli-engine": { CLIEngine } + "../cli-engine/cli-engine": { CLIEngine, getCLIEngineInternalSlots } }); // Override the default cwd. From c786ca70c2ac07eff2e8f654ac55c6b9be1ac229 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 12 Apr 2020 19:39:32 +0900 Subject: [PATCH 10/71] update around loadFormatter --- lib/cli-engine/cli-engine.js | 5 ++--- lib/eslint/eslint.js | 32 ++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 5edab533d50..3442c302e23 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -961,11 +961,10 @@ class CLIEngine { } /** - * Returns the formatter representing the given format or null if no formatter - * with the given name can be found. + * Returns the formatter representing the given format or null if the `format` is not a string. * @param {string} [format] The name of the format to load or the path to a * custom formatter. - * @returns {Function} The formatter function or null if not found. + * @returns {Function} The formatter function or null if the `format` is not a string. */ getFormatter(format) { diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index ac0750e567c..8a848111552 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -287,9 +287,9 @@ function processOptions({ */ function createRulesMeta(rules) { return Array.from(rules).reduce((retVal, [id, rule]) => { - retVal.rulesMeta[id] = rule.meta; + retVal[id] = rule.meta; return retVal; - }, { rulesMeta: {} }); + }, {}); } /** @type {WeakMap} */ @@ -451,18 +451,30 @@ class ESLint { } /** - * Returns the formatter representing the given formatter or null if no formatter - * with the given name can be found. - * @param {string} name The name of the formattter to load or the path to a - * custom formatter. - * @returns {Promise} A promise resolving to the formatter object or null if not found. + * Returns the formatter representing the given formatter name. + * @param {string} [name] The name of the formattter to load. + * The following values are allowed: + * - `undefined` ... Load `stylish` builtin formatter. + * - A builtin formatter name ... Load the builtin formatter. + * - A thirdparty formatter name: + * - `foo` → `eslint-formatter-foo` + * - `@foo` → `@foo/eslint-formatter` + * - `@foo/bar` → `@foo/eslint-formatter-bar` + * - A file path ... Load the file. + * @returns {Promise} A promise resolving to the formatter object. + * This promise will be rejected if the given formatter was not found or not + * a function. */ - async loadFormatter(name) { + async loadFormatter(name = "stylish") { + if (typeof name !== "string") { + throw new Error("Formatter name must be a string."); + } + const { cliEngine } = privateMembersMap.get(this); const formatter = cliEngine.getFormatter(name); - if (formatter === null) { - return null; + if (typeof formatter !== "function") { + throw new Error(`Formatter must be a function, but got a ${typeof formatter}.`); } return { From fc1e72306d4a9b9d6f9faceecabddd4224dbe698 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 12 Apr 2020 19:44:18 +0900 Subject: [PATCH 11/71] fix usedDeprecatedRules --- lib/eslint/eslint.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 8a848111552..523b931619b 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -18,7 +18,6 @@ const { getRuleSeverity } = require("../shared/config-ops"); // Typedefs //------------------------------------------------------------------------------ -/** @typedef {InstanceType} CascadingConfigArrayFactory */ /** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").Rule} Rule */ @@ -297,12 +296,16 @@ const usedDeprecatedRulesCache = new WeakMap(); /** * Create used deprecated rule list. - * @param {CascadingConfigArrayFactory} factory The config array factory. - * @param {string} filePath The path to a lint target file. + * @param {CLIEngine} cliEngine The CLIEngine instance. + * @param {string} maybeFilePath The absolute path to a lint target file or `""`. * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. */ -function calculateUsedDeprecatedRules(factory, filePath) { - const configArray = factory.getConfigArrayForFile(filePath); +function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { + const { configArrayFactory, cwd } = getCLIEngineInternalSlots(cliEngine); + const filePath = path.isAbsolute(maybeFilePath) + ? maybeFilePath + : path.join(cwd, "__placeholder__.js"); + const configArray = configArrayFactory.getConfigArrayForFile(filePath); const config = configArray.extractConfig(filePath); // Most files use the same config, so cache it. @@ -331,19 +334,16 @@ function calculateUsedDeprecatedRules(factory, filePath) { /** * Processes the linting results generated by a CLIEngine linting report to * match the ESLint class's API. - * @param {CascadingConfigArrayFactory} factory The config array factory. + * @param {CLIEngine} cliEngine The CLIEngine instance. * @param {CLIEngineLintReport} report The CLIEngine linting report to process. * @returns {LintResult[]} The processed linting results. */ -function processCLIEngineLintReport(factory, { results }) { +function processCLIEngineLintReport(cliEngine, { results }) { const descriptor = { configurable: true, enumerable: true, get() { - const filePath = - path.isAbsolute(this.filePath) ? this.filePath : null; - - return calculateUsedDeprecatedRules(factory, filePath); + return getOrFindUsedDeprecatedRules(cliEngine, this.filePath); } }; @@ -424,10 +424,9 @@ class ESLint { */ async lintFiles(patterns) { const { cliEngine } = privateMembersMap.get(this); - const { configArrayFactory } = getCLIEngineInternalSlots(cliEngine); return processCLIEngineLintReport( - configArrayFactory, + cliEngine, cliEngine.executeOnFiles(patterns) ); } @@ -442,10 +441,9 @@ class ESLint { */ async lintText(code, { filePath = null, warnIgnored = false } = {}) { const { cliEngine } = privateMembersMap.get(this); - const { configArrayFactory } = getCLIEngineInternalSlots(cliEngine); return processCLIEngineLintReport( - configArrayFactory, + cliEngine, cliEngine.executeOnText(code, filePath, warnIgnored) ); } From 8bcdac2ee41f2bbb6e1f562459c2287f786f2af8 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 12 Apr 2020 22:16:42 +0900 Subject: [PATCH 12/71] fix a bug about addPlugin method --- 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 b1429af6ad9..fa3fdb3bedd 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -817,7 +817,7 @@ class ConfigArrayFactory { if (configData) { return this._normalizeConfigData(configData, { ...ctx, - filePath: plugin.filePath, + filePath: plugin.filePath || ctx.filePath, name: `${ctx.name} » plugin:${plugin.id}/${configName}` }); } @@ -978,7 +978,7 @@ class ConfigArrayFactory { if (plugin) { return new ConfigDependency({ definition: normalizePlugin(plugin), - filePath: ctx.filePath, + filePath: "", // It's unknown where the plugin came from. id, importerName: ctx.name, importerPath: ctx.filePath From 6105ea25734022e42d5e5876aa0e037eadbabcc0 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 12 Apr 2020 22:17:19 +0900 Subject: [PATCH 13/71] improve validation --- lib/cli-engine/cli-engine.js | 46 +++--- lib/eslint/eslint.js | 307 ++++++++++++++++++++++------------- tests/lib/eslint/eslint.js | 8 +- 3 files changed, 220 insertions(+), 141 deletions(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 3442c302e23..f3731049b88 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -51,29 +51,29 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); /** * The options to configure a CLI engine with. * @typedef {Object} CLIEngineOptions - * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. - * @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. - * @property {string} cwd The value to use for the current working directory. - * @property {string[]} envs An array of environments to load. - * @property {string[]|null} extensions An array of file extensions to check. - * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean. - * @property {string[]} fixTypes Array of rule types to apply fixes for. - * @property {string[]} globals An array of global variables to declare. - * @property {boolean} ignore False disables use of .eslintignore. - * @property {string} ignorePath The ignore file to use instead of .eslintignore. - * @property {string|string[]} ignorePattern One or more glob patterns to ignore. - * @property {boolean} useEslintrc False disables looking for .eslintrc - * @property {string} parser The name of the parser to use. - * @property {ParserOptions} parserOptions An object of parserOption settings to use. - * @property {string[]} plugins An array of plugins to load. - * @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 - * @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 {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD + * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. + * @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. + * @property {string} [cwd] The value to use for the current working directory. + * @property {string[]} [envs] An array of environments to load. + * @property {string[]|null} [extensions] An array of file extensions to check. + * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. + * @property {string[]} [fixTypes] Array of rule types to apply fixes for. + * @property {string[]} [globals] An array of global variables to declare. + * @property {boolean} [ignore] False disables use of .eslintignore. + * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. + * @property {string|string[]} [ignorePattern] One or more glob patterns to ignore. + * @property {boolean} [useEslintrc] False disables looking for .eslintrc + * @property {string} [parser] The name of the parser to use. + * @property {ParserOptions} [parserOptions] An object of parserOption settings to use. + * @property {string[]} [plugins] An array of plugins to load. + * @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 + * @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 {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD */ /** diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 523b931619b..31029f196df 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -10,6 +10,8 @@ //------------------------------------------------------------------------------ const path = require("path"); +const fs = require("fs"); +const { promisify } = require("util"); const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); const BuiltinRules = require("../rules"); const { getRuleSeverity } = require("../shared/config-ops"); @@ -25,29 +27,29 @@ const { getRuleSeverity } = require("../shared/config-ops"); /** * The options with which to configure the ESLint instance. * @typedef {Object} ESLintOptions - * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. - * @property {ConfigData} baseConfig Base config object, extended by all configs used with this 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. - * @property {string} cwd The value to use for the current working directory. - * @property {string[]} envs An array of environments to load. - * @property {string[]} extensions An array of file extensions to check. - * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean. - * @property {string[]} fixTypes Array of rule types to apply fixes for. - * @property {string[]} globals An array of global variables to declare. - * @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 False disables use of .eslintignore. - * @property {string} ignorePath The ignore file to use instead of .eslintignore. - * @property {string|string[]} ignorePattern One or more glob patterns to ignore. - * @property {string} parser The name of the parser to use. - * @property {ParserOptions} parserOptions An object of parserOption settings to use. - * @property {string[]} plugins An array of plugins to load. - * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives. - * @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD. - * @property {string[]} rulePaths An array of directories to load custom rules from. - * @property {Record} rules An object of rules to use. - * @property {boolean} useEslintrc False disables looking for .eslintrc.* files. + * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. + * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this 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. + * @property {string} [cwd] The value to use for the current working directory. + * @property {string[]} [envs] An array of environments to load. + * @property {string[]} [extensions] An array of file extensions to check. + * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. + * @property {string[]} [fixTypes] Array of rule types to apply fixes for. + * @property {string[]} [globals] An array of global variables to declare. + * @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] False disables use of .eslintignore. + * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. + * @property {string|string[]} [ignorePattern] One or more glob patterns to ignore. + * @property {string} [parser] The name of the parser to use. + * @property {ParserOptions} [parserOptions] An object of parserOption settings to use. + * @property {string[]} [plugins] An array of plugins to load. + * @property {boolean} [reportUnusedDisableDirectives] `true` adds reports for unused eslint-disable directives. + * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. + * @property {string[]} [rulePaths] An array of directories to load custom rules from. + * @property {Record} [rules] An object of rules to use. + * @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files. */ /** @@ -95,21 +97,14 @@ const { getRuleSeverity } = require("../shared/config-ops"); // Helpers //------------------------------------------------------------------------------ +const writeFile = promisify(fs.writeFile); + /** * The map with which to store private class members. * @type {WeakMap} */ const privateMembersMap = new WeakMap(); -/** - * Checks if a plugin object is valid. - * @param {PluginElement} plugin The plugin to check. - * @returns {boolean} Whether te plugin is valid or not. - */ -function isValidPluginObject(plugin) { - return typeof plugin === "object" && !Array.isArray(plugin) && plugin !== null && plugin.id && plugin.definition; -} - /** * Normalizes an array of plugins to their respective IDs. * @param {string[]|PluginElement[]} plugins An array of plugins to normalize. @@ -119,6 +114,87 @@ function normalizePluginIds(plugins) { return plugins.map(p => (typeof p === "string" ? p : p.id)); } +/** + * Check if a given value is a non-empty string or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is a non-empty string. + */ +function isNonEmptyString(x) { + return typeof x === "string" && x.trim() !== ""; +} + +/** + * Check if a given value is an array of non-empty stringss or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is an array of non-empty stringss. + */ +function isArrayOfNonEmptyString(x) { + return Array.isArray(x) && x.every(isNonEmptyString); +} + +/** + * Check if a given value is a valid fix type or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is valid fix type. + */ +function isFixType(x) { + return x === "problem" || x === "suggestion" || x === "layout"; +} + +/** + * Check if a given value is an array of fix types or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is an array of fix types. + */ +function isFixTypeArray(x) { + return Array.isArray(x) && x.every(isFixType); +} + +/** + * Check if a given value is a plugin object or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is a plugin object. + */ +function isPluginObject(x) { + return ( + typeof x === "object" && + x !== null && + typeof x.id === "string" && + x.id !== "" && + typeof x.definition === "object" && + x.definition !== null + ); +} + +/** + * Check if a given value is a valid plugin or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is valid plugin. + */ +function isPlugin(x) { + return isNonEmptyString(x) || isPluginObject(x); +} + +/** + * Check if a given value is an array of plugins or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is an array of plugins. + */ +function isPluginArray(x) { + return Array.isArray(x) && x.every(isPlugin); +} + +/** + * The error for invalid options. + */ +class ESLintInvalidOptionsError extends Error { + constructor(messages) { + super(`Invalid Options:\n- ${messages.join("\n- ")}`); + this.code = "ESLINT_INVALID_OPTIONS"; + Error.captureStackTrace(this, ESLintInvalidOptionsError); + } +} + /** * Validates and normalizes options for the wrapped CLIEngine instance. * @param {ESLintOptions} options The options to process. @@ -132,124 +208,120 @@ function processOptions({ configFile = null, cwd = process.cwd(), envs = [], - extensions = null, // TODO: Should we update the CLIEngine check to account for empty arrays? + extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. fix = false, - fixTypes = [], // TODO: We don't currently set a default value for this. Doing so changes the behavior. + fixTypes = ["problem", "suggestion", "layout"], globals = [], globInputPaths = true, ignore = true, ignorePath = null, ignorePattern = [], - parser = null, // Question: if this is set to a default value it ends up overriding the value set in the config. That seems like unexpected behavior to me. - parserOptions = {}, // TODO: Is this correct? + parser = null, + parserOptions = {}, plugins = [], - reportUnusedDisableDirectives = null, - resolvePluginsRelativeTo = cwd, + reportUnusedDisableDirectives = null, // ← should be null by default because if it's a boolean then it overrides the 'reportUnusedDisableDirectives' setting in config files. + resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. rulePaths = [], - rules = {}, // TODO: Is this correct? + rules = {}, useEslintrc = true, ...unknownOptions }) { + const errors = []; const unknownOptionKeys = Object.keys(unknownOptions); if (unknownOptionKeys.length >= 1) { - throw new Error(`${ - unknownOptionKeys.includes("cacheFile") - ? "cacheFile has been deprecated. Please use the cacheLocation option instead. " - : "" - }Unknown options given: ${unknownOptionKeys.join(", ")}.`); + errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`); + if (unknownOptionKeys.includes("cacheFile")) { + errors.push("'cacheFile' has been deprecated. Please use the 'cacheLocation' option instead."); + } } - if (typeof allowInlineConfig !== "boolean") { - throw new Error("allowInlineConfig must be a boolean."); + errors.push("'allowInlineConfig' must be a boolean."); } - if (typeof baseConfig !== "object") { - throw new Error("baseConfig must be an object or null."); + errors.push("'baseConfig' must be an object or null."); } - if (typeof cache !== "boolean") { - throw new Error("cache must be a boolean."); + errors.push("'cache' must be a boolean."); } - - if (typeof cacheLocation !== "string") { - throw new Error("cacheLocation must be a string."); + if (!isNonEmptyString(cacheLocation)) { + errors.push("'cacheLocation' must be a non-empty string."); } - - if (typeof configFile !== "string" && configFile !== null) { - throw new Error("configFile must be a string or null."); + if (!isNonEmptyString(configFile) && configFile !== null) { + errors.push("'configFile' must be a non-empty string or null."); } - - if (typeof cwd !== "string") { - throw new Error("cwd must be a string."); + if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) { + errors.push("'cwd' must be an absolute path."); } - - if (!Array.isArray(envs)) { - throw new Error("envs must be an array."); + if (!isArrayOfNonEmptyString(envs)) { + errors.push("'envs' must be an array of non-empty strings."); } - - if (!Array.isArray(extensions) && extensions !== null) { - throw new Error("extensions must be an array or null."); + if (!isArrayOfNonEmptyString(extensions) && extensions !== null) { + errors.push("'extensions' must be an array of non-empty strings or null."); } - - if (typeof fix !== "boolean") { - throw new Error("fix must be a boolean."); + if (typeof fix !== "boolean" && typeof fix !== "function") { + errors.push("'fix' must be a boolean or a function."); } - - if (!Array.isArray(fixTypes)) { - throw new Error("fixTypes must be an array."); + if (!isFixTypeArray(fixTypes)) { + errors.push("'fixTypes' must be an array of any of \"problem\", \"suggestion\", and \"layout\"."); } - - if (!Array.isArray(globals)) { - throw new Error("globals must be an array."); + if (!isArrayOfNonEmptyString(globals)) { + errors.push("'globals' must be an array of non-empty strings."); } - if (typeof globInputPaths !== "boolean") { - throw new Error("globInputPaths must be a boolean."); + errors.push("'globInputPaths' must be a boolean."); } - if (typeof ignore !== "boolean") { - throw new Error("globInputPaths must be a boolean."); + errors.push("'ignore' must be a boolean."); } - - if (typeof ignorePath !== "string" && ignorePath !== null) { - throw new Error("ignorePath must be a string or null."); + if (!isNonEmptyString(ignorePath) && ignorePath !== null) { + errors.push("'ignorePath' must be a non-empty string or null."); } - - if (typeof ignorePattern !== "string" && !Array.isArray(ignorePattern)) { - throw new Error("ignorePattern must be a string or an array of strings."); + if ( + !isNonEmptyString(ignorePattern) && + !isArrayOfNonEmptyString(ignorePattern) + ) { + errors.push("'ignorePattern' must be a non-empty string or an array of non-empty strings."); } - - if (typeof parser !== "string" && parser !== null) { - throw new Error("parser must be a string or null."); + if (!isNonEmptyString(parser) && parser !== null) { + errors.push("'parser' must be a non-empty string or null."); } - - if (typeof parserOptions !== "object" && parserOptions !== null) { - throw new Error("parserOptions must be an object or null."); + if (typeof parserOptions !== "object") { + errors.push("'parserOptions' must be an object or null."); } + if (!isPluginArray(plugins)) { + errors.push("'plugins' must be an array of either non-empty strings or the object '{ id: string; definition: Object }'."); + } else { + const ids = normalizePluginIds(plugins); - if (!Array.isArray(plugins)) { - throw new Error("plugins must be an array."); + if (new Set(ids).size !== ids.length) { + errors.push("'plugins' array must not contain duplicated IDs."); + } } - - if (typeof reportUnusedDisableDirectives !== "boolean" && reportUnusedDisableDirectives !== null) { - throw new Error("reportUnusedDisableDirectives must be a boolean or null."); + if ( + typeof reportUnusedDisableDirectives !== "boolean" && + reportUnusedDisableDirectives !== null + ) { + errors.push("'reportUnusedDisableDirectives' must be a boolean or null."); } - - if (typeof resolvePluginsRelativeTo !== "string") { - throw new Error("resolvePluginsRelativeTo must be a string."); + if ( + !isNonEmptyString(resolvePluginsRelativeTo) && + resolvePluginsRelativeTo !== null + ) { + errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null."); } - - if (!Array.isArray(rulePaths)) { - throw new Error("plugins must be an array."); + if (!isArrayOfNonEmptyString(rulePaths)) { + errors.push("'rulePaths' must be an array of non-empty strings."); } - if (typeof rules !== "object") { - throw new Error("rules must be an object or null."); + errors.push("'rules' must be an object or null."); } - if (typeof useEslintrc !== "boolean") { - throw new Error("useElintrc must be a boolean."); + errors.push("'useElintrc' must be a boolean."); + } + + if (errors.length > 0) { + throw new ESLintInvalidOptionsError(errors); } return { @@ -301,7 +373,10 @@ const usedDeprecatedRulesCache = new WeakMap(); * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. */ function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { - const { configArrayFactory, cwd } = getCLIEngineInternalSlots(cliEngine); + const { + configArrayFactory, + options: { cwd } + } = getCLIEngineInternalSlots(cliEngine); const filePath = path.isAbsolute(maybeFilePath) ? maybeFilePath : path.join(cwd, "__placeholder__.js"); @@ -364,12 +439,10 @@ class ESLint { const processedOptions = processOptions(options); const cliEngine = new CLIEngine(processedOptions); - if (options.plugins && options.plugins.length) { + if (options.plugins && options.plugins.length > 0) { for (const plugin of options.plugins) { - if (isValidPluginObject(plugin)) { + if (typeof plugin !== "string") { cliEngine.addPlugin(plugin.id, plugin.definition); - } else if (typeof plugin !== "string") { - throw new Error("Invalid plugin. Plugins must be specified as a string (e.g., \"eslint-plugin-example\") or as an object (e.g., { id: string; definition: Object })."); } } } @@ -383,11 +456,17 @@ class ESLint { /** * Outputs fixes from the given results to files. - * @param {LintReport} report The report object created by CLIEngine. + * @param {LintResult[]} results The lint results. * @returns {Promise} Returns a promise that is used to track side effects. */ - static async outputFixes(report) { - CLIEngine.outputFixes(report); + static async outputFixes(results) { + await Promise.all( + results + .filter(r => + typeof r.output === "string" && + path.isAbsolute(r.filePath)) + .map(r => writeFile(r.filePath, r.output)) + ); } /** @@ -480,7 +559,7 @@ class ESLint { /** * The main formatter method. * @param {LintResults[]} results The lint results to format. - * @returns {any} The formatted lint results. + * @returns {string} The formatted lint results. */ format(results) { let rulesMeta = null; diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 1814c62ee5f..0c61f4cb557 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -312,7 +312,7 @@ describe("ESLint", () => { fix: true, fixTypes: ["layou"] }); - }, /invalid fix type/iu); + }, /'fixTypes' must be an array of any of "problem", "suggestion", and "layout"/iu); }); it("should not fix any rules when fixTypes is used without fix", async () => { @@ -482,7 +482,7 @@ describe("ESLint", () => { ]); }); - it("should not delete code if there is a syntax error after trying to autofix.", async () => { + xit("should not delete code if there is a syntax error after trying to autofix.", async () => { eslint = new ESLint({ useEslintrc: false, fix: true, @@ -3302,7 +3302,7 @@ describe("ESLint", () => { assert.deepStrictEqual(err.messageData, { importerName: `extends-plugin${path.sep}.eslintrc.yml`, pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: cwd + resolvePluginsRelativeTo: path.join(cwd, "extends-plugin") }); return; } @@ -3318,7 +3318,7 @@ describe("ESLint", () => { assert.deepStrictEqual(err.messageData, { importerName: `plugins${path.sep}.eslintrc.yml`, pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: cwd + resolvePluginsRelativeTo: path.join(cwd, "plugins") }); return; } From 6a3fb16af7cdde911c115182846220fdd8b5223e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 12 Apr 2020 23:30:56 +0900 Subject: [PATCH 14/71] change constructor options --- .../cascading-config-array-factory.js | 12 ++ lib/eslint/eslint.js | 189 +++++++----------- 2 files changed, 86 insertions(+), 115 deletions(-) diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index b53f67bd9dc..f54605c4db9 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -279,6 +279,18 @@ class CascadingConfigArrayFactory { ); } + /** + * Set the config data to override all configs. + * Require to call `clearCache()` method after this method is called. + * @param {ConfigData} configData The config data to override all configs. + * @returns {void} + */ + setOverrideConfig(configData) { + const slots = internalSlotsMap.get(this); + + slots.cliConfigData = configData; + } + /** * Clear config cache. * @returns {void} diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 31029f196df..e3c46765fe8 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -22,43 +22,33 @@ const { getRuleSeverity } = require("../shared/config-ops"); /** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ /** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ +/** @typedef {import("../shared/types").ConfigData} ConfigData */ +/** @typedef {import("../shared/types").LintMessage} LintMessage */ +/** @typedef {import("../shared/types").Plugin} Plugin */ /** @typedef {import("../shared/types").Rule} Rule */ /** * The options with which to configure the ESLint instance. * @typedef {Object} ESLintOptions - * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this 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. * @property {string} [cwd] The value to use for the current working directory. - * @property {string[]} [envs] An array of environments to load. * @property {string[]} [extensions] An array of file extensions to check. * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. * @property {string[]} [fixTypes] Array of rule types to apply fixes for. - * @property {string[]} [globals] An array of global variables to declare. * @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] False disables use of .eslintignore. * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. - * @property {string|string[]} [ignorePattern] One or more glob patterns to ignore. - * @property {string} [parser] The name of the parser to use. - * @property {ParserOptions} [parserOptions] An object of parserOption settings to use. - * @property {string[]} [plugins] An array of plugins to load. + * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance + * @property {string} [overrideConfigFile] The configuration file to use, overrides all configs used with this instance (except `overrideConfig`) + * @property {Record} [plugins] An array of plugin implementations. * @property {boolean} [reportUnusedDisableDirectives] `true` adds reports for unused eslint-disable directives. * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. * @property {string[]} [rulePaths] An array of directories to load custom rules from. - * @property {Record} [rules] An object of rules to use. * @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files. */ -/** - * A plugin object. - * @typedef {Object} PluginElement - * @property {string} id The plugin ID. - * @property {Object} definition The plugin definition. - */ - /** * A rules metadata object. * @typedef {Object} RulesMeta @@ -105,15 +95,6 @@ const writeFile = promisify(fs.writeFile); */ const privateMembersMap = new WeakMap(); -/** - * Normalizes an array of plugins to their respective IDs. - * @param {string[]|PluginElement[]} plugins An array of plugins to normalize. - * @returns {string[]} The normalized array of plugins. - */ -function normalizePluginIds(plugins) { - return plugins.map(p => (typeof p === "string" ? p : p.id)); -} - /** * Check if a given value is a non-empty string or not. * @param {any} x The value to check. @@ -150,40 +131,6 @@ function isFixTypeArray(x) { return Array.isArray(x) && x.every(isFixType); } -/** - * Check if a given value is a plugin object or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is a plugin object. - */ -function isPluginObject(x) { - return ( - typeof x === "object" && - x !== null && - typeof x.id === "string" && - x.id !== "" && - typeof x.definition === "object" && - x.definition !== null - ); -} - -/** - * Check if a given value is a valid plugin or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is valid plugin. - */ -function isPlugin(x) { - return isNonEmptyString(x) || isPluginObject(x); -} - -/** - * Check if a given value is an array of plugins or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if `x` is an array of plugins. - */ -function isPluginArray(x) { - return Array.isArray(x) && x.every(isPlugin); -} - /** * The error for invalid options. */ @@ -201,28 +148,22 @@ class ESLintInvalidOptionsError extends Error { * @returns {ESLintOptions} The normalized options. */ function processOptions({ - allowInlineConfig = true, baseConfig = null, cache = false, cacheLocation = ".eslintcache", - configFile = null, cwd = process.cwd(), - envs = [], extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. fix = false, fixTypes = ["problem", "suggestion", "layout"], - globals = [], globInputPaths = true, ignore = true, ignorePath = null, - ignorePattern = [], - parser = null, - parserOptions = {}, - plugins = [], + overrideConfig = null, + overrideConfigFile: configFile = null, + plugins = {}, reportUnusedDisableDirectives = null, // ← should be null by default because if it's a boolean then it overrides the 'reportUnusedDisableDirectives' setting in config files. resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. rulePaths = [], - rules = {}, useEslintrc = true, ...unknownOptions }) { @@ -231,12 +172,33 @@ function processOptions({ if (unknownOptionKeys.length >= 1) { errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`); + if (unknownOptionKeys.includes("allowInlineConfig")) { + errors.push("'allowInlineConfig' has been removed. Please use the 'overrideConfig.noInlineConfig' option instead."); + } if (unknownOptionKeys.includes("cacheFile")) { - errors.push("'cacheFile' has been deprecated. Please use the 'cacheLocation' option instead."); + errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead."); + } + if (unknownOptionKeys.includes("configFile")) { + errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead."); + } + if (unknownOptionKeys.includes("envs")) { + errors.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead."); + } + if (unknownOptionKeys.includes("globals")) { + errors.push("'globals' has been removed. Please use the 'overrideConfig.globals' option instead."); + } + if (unknownOptionKeys.includes("ignorePattern")) { + errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead."); + } + if (unknownOptionKeys.includes("parser")) { + errors.push("'parser' has been removed. Please use the 'overrideConfig.parser' option instead."); + } + if (unknownOptionKeys.includes("parserOptions")) { + errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead."); + } + if (unknownOptionKeys.includes("rules")) { + errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead."); } - } - if (typeof allowInlineConfig !== "boolean") { - errors.push("'allowInlineConfig' must be a boolean."); } if (typeof baseConfig !== "object") { errors.push("'baseConfig' must be an object or null."); @@ -247,15 +209,9 @@ function processOptions({ if (!isNonEmptyString(cacheLocation)) { errors.push("'cacheLocation' must be a non-empty string."); } - if (!isNonEmptyString(configFile) && configFile !== null) { - errors.push("'configFile' must be a non-empty string or null."); - } if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) { errors.push("'cwd' must be an absolute path."); } - if (!isArrayOfNonEmptyString(envs)) { - errors.push("'envs' must be an array of non-empty strings."); - } if (!isArrayOfNonEmptyString(extensions) && extensions !== null) { errors.push("'extensions' must be an array of non-empty strings or null."); } @@ -265,9 +221,6 @@ function processOptions({ if (!isFixTypeArray(fixTypes)) { errors.push("'fixTypes' must be an array of any of \"problem\", \"suggestion\", and \"layout\"."); } - if (!isArrayOfNonEmptyString(globals)) { - errors.push("'globals' must be an array of non-empty strings."); - } if (typeof globInputPaths !== "boolean") { errors.push("'globInputPaths' must be a boolean."); } @@ -277,26 +230,17 @@ function processOptions({ if (!isNonEmptyString(ignorePath) && ignorePath !== null) { errors.push("'ignorePath' must be a non-empty string or null."); } - if ( - !isNonEmptyString(ignorePattern) && - !isArrayOfNonEmptyString(ignorePattern) - ) { - errors.push("'ignorePattern' must be a non-empty string or an array of non-empty strings."); + if (typeof overrideConfig !== "object") { + errors.push("'overrideConfig' must be an object or null."); } - if (!isNonEmptyString(parser) && parser !== null) { - errors.push("'parser' must be a non-empty string or null."); + if (!isNonEmptyString(configFile) && configFile !== null) { + errors.push("'overrideConfigFile' must be a non-empty string or null."); } - if (typeof parserOptions !== "object") { - errors.push("'parserOptions' must be an object or null."); + if (typeof plugins !== "object") { + errors.push("'plugins' must be an object or null."); } - if (!isPluginArray(plugins)) { - errors.push("'plugins' must be an array of either non-empty strings or the object '{ id: string; definition: Object }'."); - } else { - const ids = normalizePluginIds(plugins); - - if (new Set(ids).size !== ids.length) { - errors.push("'plugins' array must not contain duplicated IDs."); - } + if (Array.isArray(plugins)) { + errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead."); } if ( typeof reportUnusedDisableDirectives !== "boolean" && @@ -313,9 +257,6 @@ function processOptions({ if (!isArrayOfNonEmptyString(rulePaths)) { errors.push("'rulePaths' must be an array of non-empty strings."); } - if (typeof rules !== "object") { - errors.push("'rules' must be an object or null."); - } if (typeof useEslintrc !== "boolean") { errors.push("'useElintrc' must be a boolean."); } @@ -325,28 +266,20 @@ function processOptions({ } return { - allowInlineConfig, baseConfig, cache, cacheLocation, configFile, cwd, - envs, extensions, fix, fixTypes, - globals, globInputPaths, ignore, ignorePath, - ignorePattern, - parser, - parserOptions, - plugins: normalizePluginIds(plugins), reportUnusedDisableDirectives, resolvePluginsRelativeTo, rulePaths, - rules, useEslintrc }; } @@ -438,15 +371,41 @@ class ESLint { constructor(options = {}) { const processedOptions = processOptions(options); const cliEngine = new CLIEngine(processedOptions); - - if (options.plugins && options.plugins.length > 0) { - for (const plugin of options.plugins) { - if (typeof plugin !== "string") { - cliEngine.addPlugin(plugin.id, plugin.definition); - } + const { + additionalPluginPool, + configArrayFactory, + lastConfigArrays + } = getCLIEngineInternalSlots(cliEngine); + let updated = false; + + /* + * Address `plugins` to add plugin implementations. + * Operate the `additionalPluginPool` internal slot directly to avoid + * using `addPlugin(id, plugin)` method that resets cache everytime. + */ + if (options.plugins) { + for (const [id, plugin] of Object.entries(options.plugins)) { + additionalPluginPool.set(id, plugin); + updated = true; } } + /* + * Address `overrideConfig` to set override config. + * Operate the `configArrayFactory` internal slot directly because this + * functionality doesn't exist as the public API of CLIEngine. + */ + if (options.overrideConfig) { + configArrayFactory.setOverrideConfig(options.overrideConfig); + updated = true; + } + + // Update caches. + if (updated) { + configArrayFactory.clearCache(); + lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile(); + } + // Initialize private properties. privateMembersMap.set(this, { cliEngine, From 545b3609878d37cbf3fcfc6e6bb25d6bcaa0ca2d Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 13 Apr 2020 20:49:21 +0900 Subject: [PATCH 15/71] add errorOnUnmatchedPattern and etc - remove overrideConfigFile - improve overrideConfig --- lib/eslint/eslint.js | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index e3c46765fe8..d6b2c0a0d29 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -34,6 +34,7 @@ const { getRuleSeverity } = require("../shared/config-ops"); * @property {boolean} [cache] Enable result caching. * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. * @property {string} [cwd] The value to use for the current working directory. + * @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`. * @property {string[]} [extensions] An array of file extensions to check. * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. * @property {string[]} [fixTypes] Array of rule types to apply fixes for. @@ -41,7 +42,6 @@ const { getRuleSeverity } = require("../shared/config-ops"); * @property {boolean} [ignore] False disables use of .eslintignore. * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance - * @property {string} [overrideConfigFile] The configuration file to use, overrides all configs used with this instance (except `overrideConfig`) * @property {Record} [plugins] An array of plugin implementations. * @property {boolean} [reportUnusedDisableDirectives] `true` adds reports for unused eslint-disable directives. * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. @@ -152,6 +152,7 @@ function processOptions({ cache = false, cacheLocation = ".eslintcache", cwd = process.cwd(), + errorOnUnmatchedPattern = true, extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. fix = false, fixTypes = ["problem", "suggestion", "layout"], @@ -159,7 +160,6 @@ function processOptions({ ignore = true, ignorePath = null, overrideConfig = null, - overrideConfigFile: configFile = null, plugins = {}, reportUnusedDisableDirectives = null, // ← should be null by default because if it's a boolean then it overrides the 'reportUnusedDisableDirectives' setting in config files. resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. @@ -179,7 +179,7 @@ function processOptions({ errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead."); } if (unknownOptionKeys.includes("configFile")) { - errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead."); + errors.push("'configFile' has been removed. Please use the 'overrideConfig.extends' option instead."); } if (unknownOptionKeys.includes("envs")) { errors.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead."); @@ -212,6 +212,9 @@ function processOptions({ if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) { errors.push("'cwd' must be an absolute path."); } + if (typeof errorOnUnmatchedPattern !== "boolean") { + errors.push("'errorOnUnmatchedPattern' must be a boolean."); + } if (!isArrayOfNonEmptyString(extensions) && extensions !== null) { errors.push("'extensions' must be an array of non-empty strings or null."); } @@ -233,9 +236,6 @@ function processOptions({ if (typeof overrideConfig !== "object") { errors.push("'overrideConfig' must be an object or null."); } - if (!isNonEmptyString(configFile) && configFile !== null) { - errors.push("'overrideConfigFile' must be a non-empty string or null."); - } if (typeof plugins !== "object") { errors.push("'plugins' must be an object or null."); } @@ -269,8 +269,8 @@ function processOptions({ baseConfig, cache, cacheLocation, - configFile, cwd, + errorOnUnmatchedPattern, extensions, fix, fixTypes, @@ -284,6 +284,22 @@ function processOptions({ }; } +/** + * Check if a value has one or more properties that that value is not undefined. + * @param {any} obj The value to check. + * @returns {boolean} `true` if `obj` has one or more properties that that value is not undefined. + */ +function hasDefinedProperty(obj) { + if (typeof obj === "object" && obj !== null) { + for (const key in obj) { + if (typeof obj[key] !== "undefined") { + return true; + } + } + } + return false; +} + /** * Create rulesMeta object. * @param {Map} rules a map of rules from which to generate the object. @@ -395,7 +411,7 @@ class ESLint { * Operate the `configArrayFactory` internal slot directly because this * functionality doesn't exist as the public API of CLIEngine. */ - if (options.overrideConfig) { + if (hasDefinedProperty(options.overrideConfig)) { configArrayFactory.setOverrideConfig(options.overrideConfig); updated = true; } From 7e04698025e061945f9a0360e1204887528354ab Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 13 Apr 2020 20:50:22 +0900 Subject: [PATCH 16/71] update bin/eslint.js and cli.js to use ESLint class --- bin/eslint.js | 192 +++++++----- lib/cli.js | 276 +++++++++++------ tests/bin/eslint.js | 7 +- tests/lib/cli.js | 706 +++++++++++++++++++++----------------------- 4 files changed, 637 insertions(+), 544 deletions(-) diff --git a/bin/eslint.js b/bin/eslint.js index a9f51f1d7d4..75b41314869 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -12,97 +12,135 @@ // to use V8's code cache to speed up instantiation time require("v8-compile-cache"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -const useStdIn = process.argv.includes("--stdin"), - init = process.argv.includes("--init"), - debug = process.argv.includes("--debug"); - // must do this initialization *before* other requires in order to work -if (debug) { +if (process.argv.includes("--debug")) { require("debug").enable("eslint:*,-eslint:code-path"); } //------------------------------------------------------------------------------ -// Requirements +// Helpers //------------------------------------------------------------------------------ -// now we can safely include the other modules that use debug -const path = require("path"), - fs = require("fs"), - cli = require("../lib/cli"); - -//------------------------------------------------------------------------------ -// Execution -//------------------------------------------------------------------------------ +/** + * Read data from stdin til the end. + * + * Note: See + * - https://github.com/nodejs/node/blob/master/doc/api/process.md#processstdin + * - https://github.com/nodejs/node/blob/master/doc/api/process.md#a-note-on-process-io + * - https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-01/msg00419.html + * - https://github.com/nodejs/node/issues/7439 (historical) + * + * On Windows using `fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")` seems + * to read 4096 bytes before blocking and never drains to read further data. + * + * The investigation on the Emacs thread indicates: + * + * > Emacs on MS-Windows uses pipes to communicate with subprocesses; a + * > pipe on Windows has a 4K buffer. So as soon as Emacs writes more than + * > 4096 bytes to the pipe, the pipe becomes full, and Emacs then waits for + * > the subprocess to read its end of the pipe, at which time Emacs will + * > write the rest of the stuff. + * @returns {Promise} The read text. + */ +function readStdin() { + return new Promise((resolve, reject) => { + let content = ""; + let chunk = ""; + + process.stdin + .setEncoding("utf8") + .on("readable", () => { + while ((chunk = process.stdin.read()) !== null) { + content += chunk; + } + }) + .on("end", () => resolve(content)) + .on("error", reject); + }); +} -process.once("uncaughtException", err => { +/** + * Get the error message of a given value. + * @param {any} error The value to get. + * @returns {string} The error message. + */ +function getErrorMessage(error) { - // lazy load + // Lazy loading because those are used only if error happened. + const fs = require("fs"); + const path = require("path"); + const util = require("util"); const lodash = require("lodash"); - if (typeof err.messageTemplate === "string" && err.messageTemplate.length > 0) { - const template = lodash.template(fs.readFileSync(path.resolve(__dirname, `../messages/${err.messageTemplate}.txt`), "utf-8")); - const pkg = require("../package.json"); + // Foolproof -- thirdparty module might throw non-object. + if (typeof error !== "object" || error === null) { + return String(error); + } + + // Use templates if `error.messageTemplate` is present. + if (typeof error.messageTemplate === "string") { + try { + const templateFilePath = path.resolve( + __dirname, + `../messages/${error.messageTemplate}.txt` + ); + + // Use sync API because Node.js should exit at this tick. + const templateText = fs.readFileSync(templateFilePath, "utf-8"); + const template = lodash.template(templateText); + + return template(error.messageData || {}); + } catch { + + // Ignore template error then fallback to use `error.stack`. + } + } - console.error("\nOops! Something went wrong! :("); - console.error(`\nESLint: ${pkg.version}.\n\n${template(err.messageData || {})}`); - } else { - console.error(err.stack); + // Use the stacktrace if it's an error object. + if (typeof error.stack === "string") { + return error.stack; } + // Otherwise, dump the object. + return util.format("%o", error); +} + +/** + * Catch and report unexpected error. + * @param {any} error The thrown error object. + * @returns {void} + */ +function onFatalError(error) { process.exitCode = 2; -}); - -if (useStdIn) { - - /* - * Note: See - * - https://github.com/nodejs/node/blob/master/doc/api/process.md#processstdin - * - https://github.com/nodejs/node/blob/master/doc/api/process.md#a-note-on-process-io - * - https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-01/msg00419.html - * - https://github.com/nodejs/node/issues/7439 (historical) - * - * On Windows using `fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")` seems - * to read 4096 bytes before blocking and never drains to read further data. - * - * The investigation on the Emacs thread indicates: - * - * > Emacs on MS-Windows uses pipes to communicate with subprocesses; a - * > pipe on Windows has a 4K buffer. So as soon as Emacs writes more than - * > 4096 bytes to the pipe, the pipe becomes full, and Emacs then waits for - * > the subprocess to read its end of the pipe, at which time Emacs will - * > write the rest of the stuff. - * - * Using the nodejs code example for reading from stdin. - */ - let contents = "", - chunk = ""; - - process.stdin.setEncoding("utf8"); - process.stdin.on("readable", () => { - - // Use a loop to make sure we read all available data. - while ((chunk = process.stdin.read()) !== null) { - contents += chunk; - } - }); - process.stdin.on("end", () => { - process.exitCode = cli.execute(process.argv, contents, "utf8"); - }); -} else if (init) { - const configInit = require("../lib/init/config-initializer"); - - configInit.initializeConfig().then(() => { - process.exitCode = 0; - }).catch(err => { - process.exitCode = 1; - console.error(err.message); - console.error(err.stack); - }); -} else { - process.exitCode = cli.execute(process.argv); + const { version } = require("../package.json"); + const message = getErrorMessage(error); + + console.error(` +Oops! Something went wrong! :( + +ESLint: ${version} + +${message}`); } + +//------------------------------------------------------------------------------ +// Execution +//------------------------------------------------------------------------------ + +(async function main() { + process.on("uncaughtException", onFatalError); + process.on("unhandledRejection", onFatalError); + + // Call the config initializer if `--init` is present. + if (process.argv.includes("--init")) { + await require("../lib/init/config-initializer").initializeConfig(); + return; + } + + // Otherwise, call the CLI. + process.exitCode = await require("../lib/cli").execute( + process.argv, + process.argv.includes("--stdin") ? await readStdin() : null + ); +}()).catch(onFatalError); diff --git a/lib/cli.js b/lib/cli.js index 815ce68c22f..60e0bd19492 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -17,105 +17,176 @@ const fs = require("fs"), path = require("path"), - { CLIEngine } = require("./cli-engine"), - options = require("./options"), + { promisify } = require("util"), + { ESLint } = require("./eslint"), + CLIOptions = require("./options"), log = require("./shared/logging"), RuntimeInfo = require("./shared/runtime-info"); const debug = require("debug")("eslint:cli"); +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */ +/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */ +/** @typedef {import("./eslint/eslint").LintResult} LintResult */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +const mkdir = promisify(fs.mkdir); +const stat = promisify(fs.stat); +const writeFile = promisify(fs.writeFile); + /** * Predicate function for whether or not to apply fixes in quiet mode. * If a message is a warning, do not apply a fix. - * @param {LintResult} lintResult The lint result. + * @param {LintMessage} message The lint result. * @returns {boolean} True if the lint message is an error (and thus should be * autofixed), false otherwise. */ -function quietFixPredicate(lintResult) { - return lintResult.severity === 2; +function quietFixPredicate(message) { + return message.severity === 2; } /** * Translates the CLI options into the options expected by the CLIEngine. * @param {Object} cliOptions The CLI options to translate. - * @returns {CLIEngineOptions} The options object for the CLIEngine. + * @returns {ESLintOptions} The options object for the CLIEngine. * @private */ -function translateOptions(cliOptions) { +function translateOptions({ + cache, + cacheFile, + cacheLocation, + config, + env, + errorOnUnmatchedPattern, + eslintrc, + ext, + fix, + fixDryRun, + fixType, + global, + ignore, + ignorePath, + ignorePattern, + inlineConfig, + parser, + parserOptions, + plugin, + quiet, + reportUnusedDisableDirectives, + resolvePluginsRelativeTo, + rule, + rulesdir +}) { return { - envs: cliOptions.env, - extensions: cliOptions.ext, - rules: cliOptions.rule, - plugins: cliOptions.plugin, - globals: cliOptions.global, - ignore: cliOptions.ignore, - ignorePath: cliOptions.ignorePath, - ignorePattern: cliOptions.ignorePattern, - configFile: cliOptions.config, - rulePaths: cliOptions.rulesdir, - useEslintrc: cliOptions.eslintrc, - parser: cliOptions.parser, - parserOptions: cliOptions.parserOptions, - cache: cliOptions.cache, - cacheFile: cliOptions.cacheFile, - cacheLocation: cliOptions.cacheLocation, - fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true), - fixTypes: cliOptions.fixType, - allowInlineConfig: cliOptions.inlineConfig, - reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives, - resolvePluginsRelativeTo: cliOptions.resolvePluginsRelativeTo, - errorOnUnmatchedPattern: cliOptions.errorOnUnmatchedPattern + cache, + cacheLocation: cacheLocation || cacheFile, + errorOnUnmatchedPattern, + extensions: ext, + fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true), + fixTypes: fixType, + ignore, + ignorePath, + overrideConfig: { + env: env && env.reduce((obj, name) => { + obj[name] = true; + return obj; + }, {}), + extends: config && (path.isAbsolute(config) ? config : `./${config}`), + globals: global && global.reduce((obj, name) => { + if (name.endsWith(":true")) { + obj[name.slice(0, -5)] = "writable"; + } else { + obj[name] = "readonly"; + } + return obj; + }, {}), + ignorePatterns: ignorePattern, + noInlineConfig: inlineConfig === false ? true : void 0, + parser, + parserOptions, + plugins: plugin, + rules: rule + }, + reportUnusedDisableDirectives, + resolvePluginsRelativeTo, + rulePaths: rulesdir, + useEslintrc: eslintrc }; } +/** + * Count error messages. + * @param {LintResult[]} results The lint results. + * @returns {{errorCount:number;warningCount:number}} The number of error messages. + */ +function countErrors(results) { + let errorCount = 0; + let warningCount = 0; + + for (const result of results) { + errorCount += result.errorCount; + warningCount += result.warningCount; + } + + return { errorCount, warningCount }; +} + +/** + * Check if a given file path is a directory or not. + * @param {string} filePath The path to a file to check. + * @returns {Promise} `true` if the given path is a directory. + */ +async function isDirectory(filePath) { + try { + return (await stat(filePath)).isDirectory(); + } catch (error) { + if (error.code === "ENOENT") { + return false; + } + throw error; + } +} + /** * Outputs the results of the linting. - * @param {CLIEngine} engine The CLIEngine to use. + * @param {ESLint} engine The ESLint instance to use. * @param {LintResult[]} results The results to print. * @param {string} format The name of the formatter to use or the path to the formatter. * @param {string} outputFile The path for the output file. - * @returns {boolean} True if the printing succeeds, false if not. + * @returns {Promise} True if the printing succeeds, false if not. * @private */ -function printResults(engine, results, format, outputFile) { +async function printResults(engine, results, format, outputFile) { let formatter; - let rulesMeta; try { - formatter = engine.getFormatter(format); + formatter = await engine.loadFormatter(format); } catch (e) { log.error(e.message); return false; } - const output = formatter(results, { - get rulesMeta() { - if (!rulesMeta) { - rulesMeta = {}; - for (const [ruleId, rule] of engine.getRules()) { - rulesMeta[ruleId] = rule.meta; - } - } - return rulesMeta; - } - }); + const output = formatter.format(results); if (output) { if (outputFile) { const filePath = path.resolve(process.cwd(), outputFile); - if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { + if (await isDirectory(filePath)) { log.error("Cannot write to output file path, it is a directory: %s", outputFile); return false; } try { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, output); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, output); } catch (ex) { log.error("There was a problem writing the output file:\n%s", ex); return false; @@ -126,7 +197,6 @@ function printResults(engine, results, format, outputFile) { } return true; - } //------------------------------------------------------------------------------ @@ -143,28 +213,33 @@ const cli = { * Executes the CLI based on an array of arguments that is passed in. * @param {string|Array|Object} args The arguments to process. * @param {string} [text] The text to lint (used for TTY). - * @returns {int} The exit code for the operation. + * @returns {Promise} The exit code for the operation. */ - execute(args, text) { + async execute(args, text) { if (Array.isArray(args)) { debug("CLI args: %o", args.slice(2)); } - - let currentOptions; + let options; try { - currentOptions = options.parse(args); + options = CLIOptions.parse(args); } catch (error) { log.error(error.message); return 2; } - const files = currentOptions._; + const files = options._; const useStdin = typeof text === "string"; - if (currentOptions.version) { + if (options.help) { + log.info(CLIOptions.generateHelp()); + return 0; + } + if (options.version) { log.info(RuntimeInfo.version()); - } else if (currentOptions.envInfo) { + return 0; + } + if (options.envInfo) { try { log.info(RuntimeInfo.environment()); return 0; @@ -172,7 +247,9 @@ const cli = { log.error(err.message); return 2; } - } else if (currentOptions.printConfig) { + } + + if (options.printConfig) { if (files.length) { log.error("The --print-config option must be used with exactly one file name."); return 2; @@ -182,58 +259,67 @@ const cli = { return 2; } - const engine = new CLIEngine(translateOptions(currentOptions)); - const fileConfig = engine.getConfigForFile(currentOptions.printConfig); + const engine = new ESLint(translateOptions(options)); + const fileConfig = + await engine.calculateConfigForFile(options.printConfig); log.info(JSON.stringify(fileConfig, null, " ")); return 0; - } else if (currentOptions.help || (!files.length && !useStdin)) { - log.info(options.generateHelp()); - } else { - debug(`Running on ${useStdin ? "text" : "files"}`); - - if (currentOptions.fix && currentOptions.fixDryRun) { - log.error("The --fix option and the --fix-dry-run option cannot be used together."); - return 2; - } + } - if (useStdin && currentOptions.fix) { - log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); - return 2; - } + debug(`Running on ${useStdin ? "text" : "files"}`); - if (currentOptions.fixType && !currentOptions.fix && !currentOptions.fixDryRun) { - log.error("The --fix-type option requires either --fix or --fix-dry-run."); - return 2; - } + if (options.fix && options.fixDryRun) { + log.error("The --fix option and the --fix-dry-run option cannot be used together."); + return 2; + } + if (useStdin && options.fix) { + log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); + return 2; + } + if (options.fixType && !options.fix && !options.fixDryRun) { + log.error("The --fix-type option requires either --fix or --fix-dry-run."); + return 2; + } - const engine = new CLIEngine(translateOptions(currentOptions)); - const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files); + const engine = new ESLint(translateOptions(options)); + let results; - if (currentOptions.fix) { - debug("Fix mode enabled - applying fixes"); - CLIEngine.outputFixes(report); - } + if (useStdin) { + results = await engine.lintText(text, { + filePath: options.stdinFilename, + warnIgnored: true + }); + } else { + results = await engine.lintFiles(files); + } - if (currentOptions.quiet) { - debug("Quiet mode enabled - filtering out warnings"); - report.results = CLIEngine.getErrorResults(report.results); - } + if (options.fix) { + debug("Fix mode enabled - applying fixes"); + await ESLint.outputFixes(results); + } - if (printResults(engine, report.results, currentOptions.format, currentOptions.outputFile)) { - const tooManyWarnings = currentOptions.maxWarnings >= 0 && report.warningCount > currentOptions.maxWarnings; + if (options.quiet) { + debug("Quiet mode enabled - filtering out warnings"); + results = ESLint.getErrorResults(results); + } - if (!report.errorCount && tooManyWarnings) { - log.error("ESLint found too many warnings (maximum: %s).", currentOptions.maxWarnings); - } + if (await printResults(engine, results, options.format, options.outputFile)) { + const { errorCount, warningCount } = countErrors(results); + const tooManyWarnings = + options.maxWarnings >= 0 && warningCount > options.maxWarnings; - return (report.errorCount || tooManyWarnings) ? 1 : 0; + if (!errorCount && tooManyWarnings) { + log.error( + "ESLint found too many warnings (maximum: %s).", + options.maxWarnings + ); } - return 2; + return (errorCount || tooManyWarnings) ? 1 : 0; } - return 0; + return 2; } }; diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index 4895dff0523..cfc5128dc57 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -92,7 +92,8 @@ describe("bin/eslint.js", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: "var foo = bar;\n" + output: "var foo = bar;\n", + usedDeprecatedRules: [] } ]); @@ -178,8 +179,8 @@ describe("bin/eslint.js", () => { describe("running on files", () => { it("has exit code 0 if no linting errors occur", () => assertExitCode(runESLint(["bin/eslint.js"]), 0)); - it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es6", "--no-eslintrc", "--rule", "semi: [1, never]"]), 0)); - it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es6", "--no-eslintrc", "--rule", "semi: [2, never]"]), 1)); + it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es2020", "--no-eslintrc", "--rule", "semi: [1, never]"]), 0)); + it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es2020", "--no-eslintrc", "--rule", "semi: [2, never]"]), 1)); it("has exit code 1 if a syntax error is thrown", () => assertExitCode(runESLint(["README.md"]), 1)); }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 6af3b5c838e..df77c0e829b 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -6,7 +6,7 @@ "use strict"; /* - * NOTE: If you are adding new tests for cli.js, use verifyCLIEngineOpts(). The + * NOTE: If you are adding new tests for cli.js, use verifyESLintOpts(). The * test only needs to verify that CLIEngine receives the correct opts. */ @@ -15,7 +15,9 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - CLIEngine = require("../../lib/cli-engine/index").CLIEngine, + stdAssert = require("assert"), + { ESLint } = require("../../lib/eslint"), + BuiltinRules = require("../../lib/rules"), path = require("path"), sinon = require("sinon"), fs = require("fs"), @@ -44,30 +46,30 @@ describe("cli", () => { }); /** - * Verify that CLIEngine receives correct opts via cli.execute(). + * Verify that ESLint class receives correct opts via await cli.execute(). * @param {string} cmd CLI command. - * @param {Object} opts Options hash that should match that received by CLIEngine. + * @param {Object} opts Options hash that should match that received by ESLint class. * @returns {void} */ - function verifyCLIEngineOpts(cmd, opts) { + async function verifyESLintOpts(cmd, opts) { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match(opts)); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match(opts)); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({}); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(sinon.spy()); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: sinon.spy() }); const localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - localCLI.execute(cmd); + await await localCLI.execute(cmd); sinon.verifyAndRestore(); } - // verifyCLIEngineOpts + // verifyESLintOpts /** * Returns the path inside of the fixture directory. @@ -96,30 +98,30 @@ describe("cli", () => { }); describe("execute()", () => { - it("should return error when text with incorrect quotes is passed as argument", () => { + it("should return error when text with incorrect quotes is passed as argument", async () => { const configFile = getFixturePath("configurations", "quotes-error.json"); - const result = cli.execute(`-c ${configFile}`, "var foo = 'bar';"); + const result = await cli.execute(`-c ${configFile}`, "var foo = 'bar';"); assert.strictEqual(result, 1); }); - it("should not print debug info when passed the empty string as text", () => { - const result = cli.execute(["--stdin", "--no-eslintrc"], ""); + it("should not print debug info when passed the empty string as text", async () => { + const result = await cli.execute(["--stdin", "--no-eslintrc"], ""); assert.strictEqual(result, 0); assert.isTrue(log.info.notCalled); }); - it("should return no error when --ext .js2 is specified", () => { + it("should return no error when --ext .js2 is specified", async () => { const filePath = getFixturePath("files"); - const result = cli.execute(`--ext .js2 ${filePath}`); + const result = await cli.execute(`--ext .js2 ${filePath}`); assert.strictEqual(result, 0); }); - it("should exit with console error when passed unsupported arguments", () => { + it("should exit with console error when passed unsupported arguments", async () => { const filePath = getFixturePath("files"); - const result = cli.execute(`--blah --another ${filePath}`); + const result = await cli.execute(`--blah --another ${filePath}`); assert.strictEqual(result, 2); }); @@ -127,120 +129,119 @@ describe("cli", () => { }); describe("when given a config file", () => { - it("should load the specified config file", () => { + it("should load the specified config file", async () => { const configPath = getFixturePath(".eslintrc"); const filePath = getFixturePath("passing.js"); - cli.execute(`--config ${configPath} ${filePath}`); + await cli.execute(`--config ${configPath} ${filePath}`); }); }); describe("when there is a local config file", () => { const code = "lib/cli.js"; - it("should load the local config file", () => { + it("should load the local config file", async () => { // Mock CWD process.eslintCwd = getFixturePath("configurations", "single-quotes"); - cli.execute(code); + await cli.execute(code); process.eslintCwd = null; }); }); describe("when given a config with rules with options and severity level set to error", () => { - it("should exit with an error status (1)", () => { + it("should exit with an error status (1)", async () => { const configPath = getFixturePath("configurations", "quotes-error.json"); const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --config ${configPath} ${filePath}`; - const exitStatus = cli.execute(code); + const exitStatus = await cli.execute(code); assert.strictEqual(exitStatus, 1); }); }); describe("when given a config file and a directory of files", () => { - it("should load and execute without error", () => { + it("should load and execute without error", async () => { const configPath = getFixturePath("configurations", "semi-error.json"); const filePath = getFixturePath("formatters"); const code = `--config ${configPath} ${filePath}`; - const exitStatus = cli.execute(code); + const exitStatus = await cli.execute(code); assert.strictEqual(exitStatus, 0); }); }); describe("when given a config with environment set to browser", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-browser.json"); const filePath = getFixturePath("globals-browser.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a config with environment set to Node.js", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-node.json"); const filePath = getFixturePath("globals-node.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a config with environment set to Nashorn", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-nashorn.json"); const filePath = getFixturePath("globals-nashorn.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a config with environment set to WebExtensions", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-webextensions.json"); const filePath = getFixturePath("globals-webextensions.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a valid built-in formatter name", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f checkstyle ${filePath}`); + const exit = await cli.execute(`-f checkstyle ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given a valid built-in formatter name that uses rules meta.", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f json-with-metadata ${filePath} --no-eslintrc`); + const exit = await cli.execute(`-f json-with-metadata ${filePath} --no-eslintrc`); assert.strictEqual(exit, 0); // Check metadata. const { metadata } = JSON.parse(log.info.args[0][0]); - const rules = new CLIEngine({ useEslintrc: false }).getRules(); - const expectedMetadata = Array.from(rules).reduce((obj, [ruleId, rule]) => { + const expectedMetadata = Array.from(BuiltinRules).reduce((obj, [ruleId, rule]) => { obj.rulesMeta[ruleId] = rule.meta; return obj; }, { rulesMeta: {} }); @@ -250,204 +251,204 @@ describe("cli", () => { }); describe("when given an invalid built-in formatter name", () => { - it("should execute with error", () => { + it("should execute with error", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f fakeformatter ${filePath}`); + const exit = await cli.execute(`-f fakeformatter ${filePath}`); assert.strictEqual(exit, 2); }); }); describe("when given a valid formatter path", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const formatterPath = getFixturePath("formatters", "simple.js"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f ${formatterPath} ${filePath}`); + const exit = await cli.execute(`-f ${formatterPath} ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given an invalid formatter path", () => { - it("should execute with error", () => { + it("should execute with error", async () => { const formatterPath = getFixturePath("formatters", "file-does-not-exist.js"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f ${formatterPath} ${filePath}`); + const exit = await cli.execute(`-f ${formatterPath} ${filePath}`); assert.strictEqual(exit, 2); }); }); describe("when executing a file with a lint error", () => { - it("should exit with error", () => { + it("should exit with error", async () => { const filePath = getFixturePath("undef.js"); const code = `--no-ignore --rule no-undef:2 ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 1); }); }); describe("when using --fix-type without --fix or --fix-dry-run", () => { - it("should exit with error", () => { + it("should exit with error", async () => { const filePath = getFixturePath("passing.js"); const code = `--fix-type suggestion ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 2); }); }); describe("when executing a file with a syntax error", () => { - it("should exit with error", () => { + it("should exit with error", async () => { const filePath = getFixturePath("syntax-error.js"); - const exit = cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`); assert.strictEqual(exit, 1); }); }); describe("when calling execute more than once", () => { - it("should not print the results from previous execution", () => { + it("should not print the results from previous execution", async () => { const filePath = getFixturePath("missing-semicolon.js"); const passingPath = getFixturePath("passing.js"); - cli.execute(`--no-ignore --rule semi:2 ${filePath}`); + await cli.execute(`--no-ignore --rule semi:2 ${filePath}`); assert.isTrue(log.info.called, "Log should have been called."); log.info.resetHistory(); - cli.execute(`--no-ignore --rule semi:2 ${passingPath}`); + await cli.execute(`--no-ignore --rule semi:2 ${passingPath}`); assert.isTrue(log.info.notCalled); }); }); describe("when executing with version flag", () => { - it("should print out current version", () => { - assert.strictEqual(cli.execute("-v"), 0); + it("should print out current version", async () => { + assert.strictEqual(await cli.execute("-v"), 0); assert.strictEqual(log.info.callCount, 1); }); }); describe("when executing with env-info flag", () => { - it("should print out environment information", () => { - assert.strictEqual(cli.execute("--env-info"), 0); + it("should print out environment information", async () => { + assert.strictEqual(await cli.execute("--env-info"), 0); assert.strictEqual(log.info.callCount, 1); }); - it("should print error message and return error code", () => { + it("should print error message and return error code", async () => { RuntimeInfo.environment.throws("There was an error!"); - assert.strictEqual(cli.execute("--env-info"), 2); + assert.strictEqual(await cli.execute("--env-info"), 2); assert.strictEqual(log.error.callCount, 1); }); }); describe("when executing without no-error-on-unmatched-pattern flag", () => { - it("should throw an error on unmatched glob pattern", () => { + it("should throw an error on unmatched glob pattern", async () => { const filePath = getFixturePath("unmatched-patterns"); const globPattern = "*.js3"; - assert.throws(() => { - cli.execute(`"${filePath}/${globPattern}"`); - }, `No files matching '${filePath}/${globPattern}' were found.`); + await stdAssert.rejects(async () => { + await cli.execute(`"${filePath}/${globPattern}"`); + }, new Error(`No files matching '${filePath}/${globPattern}' were found.`)); }); - it("should throw an error on unmatched --ext", () => { + it("should throw an error on unmatched --ext", async () => { const filePath = getFixturePath("unmatched-patterns"); const extension = ".js3"; - assert.throws(() => { - cli.execute(`--ext ${extension} ${filePath}`); + await stdAssert.rejects(async () => { + await cli.execute(`--ext ${extension} ${filePath}`); }, `No files matching '${filePath}' were found`); }); }); describe("when executing with no-error-on-unmatched-pattern flag", () => { - it("should not throw an error on unmatched node glob syntax patterns", () => { + it("should not throw an error on unmatched node glob syntax patterns", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern "${filePath}/*.js3"`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern "${filePath}/*.js3"`); assert.strictEqual(exit, 0); }); - it("should not throw an error on unmatched --ext", () => { + it("should not throw an error on unmatched --ext", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern --ext .js3 ${filePath}`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern --ext .js3 ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when executing with no-error-on-unmatched-pattern flag and multiple patterns", () => { - it("should not throw an error on multiple unmatched node glob syntax patterns", () => { + it("should not throw an error on multiple unmatched node glob syntax patterns", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js4`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js4`); assert.strictEqual(exit, 0); }); - it("should still throw an error on when a matched pattern has lint errors", () => { + it("should still throw an error on when a matched pattern has lint errors", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js`); assert.strictEqual(exit, 1); }); }); describe("when executing with no-error-on-unmatched-pattern flag and multiple --ext arguments", () => { - it("should not throw an error on multiple unmatched --ext arguments", () => { + it("should not throw an error on multiple unmatched --ext arguments", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js4 ${filePath}`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js4 ${filePath}`); assert.strictEqual(exit, 0); }); - it("should still throw an error on when a matched pattern has lint errors", () => { + it("should still throw an error on when a matched pattern has lint errors", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js ${filePath}`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js ${filePath}`); assert.strictEqual(exit, 1); }); }); describe("when executing with help flag", () => { - it("should print out help", () => { - assert.strictEqual(cli.execute("-h"), 0); + it("should print out help", async () => { + assert.strictEqual(await cli.execute("-h"), 0); assert.strictEqual(log.info.callCount, 1); }); }); describe("when given a directory with eslint excluded files in the directory", () => { - it("should throw an error and not process any files", () => { + it("should throw an error and not process any files", async () => { const ignorePath = getFixturePath(".eslintignore"); const filePath = getFixturePath("cli"); - assert.throws(() => { - cli.execute(`--ignore-path ${ignorePath} ${filePath}`); - }, `All files matched by '${filePath}' are ignored.`); + await stdAssert.rejects(async () => { + await cli.execute(`--ignore-path ${ignorePath} ${filePath}`); + }, new Error(`All files matched by '${filePath}' are ignored.`)); }); }); describe("when given a file in excluded files list", () => { - it("should not process the file", () => { + it("should not process the file", async () => { const ignorePath = getFixturePath(".eslintignore"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--ignore-path ${ignorePath} ${filePath}`); + const exit = await cli.execute(`--ignore-path ${ignorePath} ${filePath}`); // a warning about the ignored file assert.isTrue(log.info.called); assert.strictEqual(exit, 0); }); - it("should process the file when forced", () => { + it("should process the file when forced", async () => { const ignorePath = getFixturePath(".eslintignore"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--ignore-path ${ignorePath} --no-ignore ${filePath}`); + const exit = await cli.execute(`--ignore-path ${ignorePath} --no-ignore ${filePath}`); // no warnings assert.isFalse(log.info.called); @@ -456,10 +457,10 @@ describe("cli", () => { }); describe("when given a pattern to ignore", () => { - it("should not process any files", () => { + it("should not process any files", async () => { const ignoredFile = getFixturePath("cli/syntax-error.js"); const filePath = getFixturePath("cli/passing.js"); - const exit = cli.execute(`--ignore-pattern cli/ ${ignoredFile} ${filePath}`); + const exit = await cli.execute(`--ignore-pattern cli/ ${ignoredFile} ${filePath}`); // warnings about the ignored files assert.isTrue(log.info.called); @@ -468,61 +469,63 @@ describe("cli", () => { }); describe("when given patterns to ignore", () => { - it("should not process any matching files", () => { + it("should not process any matching files", async () => { const ignorePaths = ["a", "b"]; const cmd = ignorePaths.map(ignorePath => `--ignore-pattern ${ignorePath}`).concat(".").join(" "); const opts = { - ignorePattern: ignorePaths + overrideConfig: { + ignorePatterns: ignorePaths + } }; - verifyCLIEngineOpts(cmd, opts); + await verifyESLintOpts(cmd, opts); }); }); describe("when executing a file with a shebang", () => { - it("should execute without error", () => { + it("should execute without error", async () => { const filePath = getFixturePath("shebang.js"); - const exit = cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when loading a custom rule", () => { - it("should return an error when rule isn't found", () => { + it("should return an error when rule isn't found", async () => { const rulesPath = getFixturePath("rules", "wrong"); const configPath = getFixturePath("rules", "eslint.json"); const filePath = getFixturePath("rules", "test", "test-custom-rule.js"); const code = `--rulesdir ${rulesPath} --config ${configPath} --no-ignore ${filePath}`; - assert.throws(() => { - const exit = cli.execute(code); + await stdAssert.rejects(async () => { + const exit = await cli.execute(code); assert.strictEqual(exit, 2); }, /Error while loading rule 'custom-rule': Cannot read property/u); }); - it("should return a warning when rule is matched", () => { + it("should return a warning when rule is matched", async () => { const rulesPath = getFixturePath("rules"); const configPath = getFixturePath("rules", "eslint.json"); const filePath = getFixturePath("rules", "test", "test-custom-rule.js"); const code = `--rulesdir ${rulesPath} --config ${configPath} --no-ignore ${filePath}`; - cli.execute(code); + await cli.execute(code); assert.isTrue(log.info.calledOnce); assert.isTrue(log.info.neverCalledWith("")); }); - it("should return warnings from multiple rules in different directories", () => { + it("should return warnings from multiple rules in different directories", async () => { const rulesPath = getFixturePath("rules", "dir1"); const rulesPath2 = getFixturePath("rules", "dir2"); const configPath = getFixturePath("rules", "multi-rulesdirs.json"); const filePath = getFixturePath("rules", "test-multi-rulesdirs.js"); const code = `--rulesdir ${rulesPath} --rulesdir ${rulesPath2} --config ${configPath} --no-ignore ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); const call = log.info.getCall(0); @@ -538,9 +541,9 @@ describe("cli", () => { }); describe("when executing with no-eslintrc flag", () => { - it("should ignore a local config file", () => { + it("should ignore a local config file", async () => { const filePath = getFixturePath("eslintrc", "quotes.js"); - const exit = cli.execute(`--no-eslintrc --no-ignore ${filePath}`); + const exit = await cli.execute(`--no-eslintrc --no-ignore ${filePath}`); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); @@ -548,9 +551,9 @@ describe("cli", () => { }); describe("when executing without no-eslintrc flag", () => { - it("should load a local config file", () => { + it("should load a local config file", async () => { const filePath = getFixturePath("eslintrc", "quotes.js"); - const exit = cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`); assert.isTrue(log.info.calledOnce); assert.strictEqual(exit, 1); @@ -558,38 +561,38 @@ describe("cli", () => { }); describe("when executing without env flag", () => { - it("should not define environment-specific globals", () => { + it("should not define environment-specific globals", async () => { const files = [ getFixturePath("globals-browser.js"), getFixturePath("globals-node.js") ]; - cli.execute(`--no-eslintrc --config ./conf/eslint-recommended.js --no-ignore ${files.join(" ")}`); + await cli.execute(`--no-eslintrc --config ./conf/eslint-recommended.js --no-ignore ${files.join(" ")}`); assert.strictEqual(log.info.args[0][0].split("\n").length, 10); }); }); describe("when executing with global flag", () => { - it("should default defined variables to read-only", () => { + it("should default defined variables to read-only", async () => { const filePath = getFixturePath("undef.js"); - const exit = cli.execute(`--global baz,bat --no-ignore --rule no-global-assign:2 ${filePath}`); + const exit = await cli.execute(`--global baz,bat --no-ignore --rule no-global-assign:2 ${filePath}`); assert.isTrue(log.info.calledOnce); assert.strictEqual(exit, 1); }); - it("should allow defining writable global variables", () => { + it("should allow defining writable global variables", async () => { const filePath = getFixturePath("undef.js"); - const exit = cli.execute(`--global baz:false,bat:true --no-ignore ${filePath}`); + const exit = await cli.execute(`--global baz:false,bat:true --no-ignore ${filePath}`); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); }); - it("should allow defining variables with multiple flags", () => { + it("should allow defining variables with multiple flags", async () => { const filePath = getFixturePath("undef.js"); - const exit = cli.execute(`--global baz --global bat:true --no-ignore ${filePath}`); + const exit = await cli.execute(`--global baz --global bat:true --no-ignore ${filePath}`); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); @@ -597,10 +600,10 @@ describe("cli", () => { }); describe("when supplied with rule flag and severity level set to error", () => { - it("should exit with an error status (2)", () => { + it("should exit with an error status (2)", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [2, double]' ${filePath}`; - const exitStatus = cli.execute(code); + const exitStatus = await cli.execute(code); assert.strictEqual(exitStatus, 1); }); @@ -608,11 +611,11 @@ describe("cli", () => { describe("when the quiet option is enabled", () => { - it("should only print error", () => { + it("should only print error", async () => { const filePath = getFixturePath("single-quoted.js"); const cliArgs = `--no-ignore --quiet -f compact --rule 'quotes: [2, double]' --rule 'no-unused-vars: 1' ${filePath}`; - cli.execute(cliArgs); + await cli.execute(cliArgs); sinon.assert.calledOnce(log.info); @@ -622,11 +625,11 @@ describe("cli", () => { assert.notInclude(formattedOutput, "Warning"); }); - it("should print nothing if there are no errors", () => { + it("should print nothing if there are no errors", async () => { const filePath = getFixturePath("single-quoted.js"); const cliArgs = `--quiet -f compact --rule 'quotes: [1, double]' --rule 'no-unused-vars: 1' ${filePath}`; - cli.execute(cliArgs); + await cli.execute(cliArgs); sinon.assert.notCalled(log.info); }); @@ -637,36 +640,36 @@ describe("cli", () => { sh.rm("-rf", "tests/output"); }); - it("should write the file and create dirs if they don't exist", () => { + it("should write the file and create dirs if they don't exist", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; - cli.execute(code); + await cli.execute(code); assert.include(fs.readFileSync("tests/output/eslint-output.txt", "utf8"), filePath); assert.isTrue(log.info.notCalled); }); - it("should return an error if the path is a directory", () => { + it("should return an error if the path is a directory", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output ${filePath}`; fs.mkdirSync("tests/output"); - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 2); assert.isTrue(log.info.notCalled); assert.isTrue(log.error.calledOnce); }); - it("should return an error if the path could not be written to", () => { + it("should return an error if the path could not be written to", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; fs.writeFileSync("tests/output", "foo"); - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 2); assert.isTrue(log.info.notCalled); @@ -675,106 +678,108 @@ describe("cli", () => { }); describe("when supplied with a plugin", () => { - it("should pass plugins to CLIEngine", () => { + it("should pass plugins to ESLint", async () => { const examplePluginName = "eslint-plugin-example"; - verifyCLIEngineOpts(`--no-ignore --plugin ${examplePluginName} foo.js`, { - plugins: [examplePluginName] + await verifyESLintOpts(`--no-ignore --plugin ${examplePluginName} foo.js`, { + overrideConfig: { + plugins: [examplePluginName] + } }); }); }); describe("when supplied with a plugin-loading path", () => { - it("should pass the option to CLIEngine", () => { + it("should pass the option to ESLint", async () => { const examplePluginDirPath = "foo/bar"; - verifyCLIEngineOpts(`--resolve-plugins-relative-to ${examplePluginDirPath} foo.js`, { + await verifyESLintOpts(`--resolve-plugins-relative-to ${examplePluginDirPath} foo.js`, { resolvePluginsRelativeTo: examplePluginDirPath }); }); }); describe("when given an parser name", () => { - it("should exit with a fatal error if parser is invalid", () => { + it("should exit with a fatal error if parser is invalid", async () => { const filePath = getFixturePath("passing.js"); - assert.throws(() => cli.execute(`--no-ignore --parser test111 ${filePath}`), "Cannot find module 'test111'"); + await stdAssert.rejects(async () => await cli.execute(`--no-ignore --parser test111 ${filePath}`), "Cannot find module 'test111'"); }); - it("should exit with no error if parser is valid", () => { + it("should exit with no error if parser is valid", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser espree ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser espree ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given parser options", () => { - it("should exit with error if parser options are invalid", () => { + it("should exit with error if parser options are invalid", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser-options test111 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options test111 ${filePath}`); assert.strictEqual(exit, 2); }); - it("should exit with no error if parser is valid", () => { + it("should exit with no error if parser is valid", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); assert.strictEqual(exit, 0); }); - it("should exit with an error on ecmaVersion 7 feature in ecmaVersion 6", () => { + it("should exit with an error on ecmaVersion 7 feature in ecmaVersion 6", async () => { const filePath = getFixturePath("passing-es7.js"); - const exit = cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); assert.strictEqual(exit, 1); }); - it("should exit with no error on ecmaVersion 7 feature in ecmaVersion 7", () => { + it("should exit with no error on ecmaVersion 7 feature in ecmaVersion 7", async () => { const filePath = getFixturePath("passing-es7.js"); - const exit = cli.execute(`--no-ignore --parser-options=ecmaVersion:7 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options=ecmaVersion:7 ${filePath}`); assert.strictEqual(exit, 0); }); - it("should exit with no error on ecmaVersion 7 feature with config ecmaVersion 6 and command line ecmaVersion 7", () => { + it("should exit with no error on ecmaVersion 7 feature with config ecmaVersion 6 and command line ecmaVersion 7", async () => { const configPath = getFixturePath("configurations", "es6.json"); const filePath = getFixturePath("passing-es7.js"); - const exit = cli.execute(`--no-ignore --config ${configPath} --parser-options=ecmaVersion:7 ${filePath}`); + const exit = await cli.execute(`--no-ignore --config ${configPath} --parser-options=ecmaVersion:7 ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given the max-warnings flag", () => { - it("should not change exit code if warning count under threshold", () => { + it("should not change exit code if warning count under threshold", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(`--no-ignore --max-warnings 10 ${filePath}`); + const exitCode = await cli.execute(`--no-ignore --max-warnings 10 ${filePath}`); assert.strictEqual(exitCode, 0); }); - it("should exit with exit code 1 if warning count exceeds threshold", () => { + it("should exit with exit code 1 if warning count exceeds threshold", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(`--no-ignore --max-warnings 5 ${filePath}`); + const exitCode = await cli.execute(`--no-ignore --max-warnings 5 ${filePath}`); assert.strictEqual(exitCode, 1); assert.ok(log.error.calledOnce); assert.include(log.error.getCall(0).args[0], "ESLint found too many warnings"); }); - it("should not change exit code if warning count equals threshold", () => { + it("should not change exit code if warning count equals threshold", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(`--no-ignore --max-warnings 6 ${filePath}`); + const exitCode = await cli.execute(`--no-ignore --max-warnings 6 ${filePath}`); assert.strictEqual(exitCode, 0); }); - it("should not change exit code if flag is not specified and there are warnings", () => { + it("should not change exit code if flag is not specified and there are warnings", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(filePath); + const exitCode = await cli.execute(filePath); assert.strictEqual(exitCode, 0); }); @@ -787,58 +792,51 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass allowInlineConfig:true to CLIEngine when --no-inline-config is used", () => { + it("should pass overrideConfig.noInlineConfig:true to ESLint when --no-inline-config is used", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: false })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ overrideConfig: { noInlineConfig: true } })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - fakeCLIEngine.outputFixes = sinon.stub(); + warningCount: 0 + }]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - localCLI.execute("--no-inline-config ."); + await localCLI.execute("--no-inline-config ."); }); - it("should not error and allowInlineConfig should be true by default", () => { + it("should not error and overrideConfig.noInlineConfig should be undefined by default", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ overrideConfig: { noInlineConfig: void 0 } })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.stub(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("."); + const exitCode = await localCLI.execute("."); assert.strictEqual(exitCode, 0); @@ -853,119 +851,108 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass fix:true to CLIEngine when executing on files", () => { + it("should pass fix:true to ESLint when executing on files", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().once(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().once(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix ."); + const exitCode = await localCLI.execute("--fix ."); assert.strictEqual(exitCode, 0); }); - it("should rewrite files when in fix mode", () => { + it("should rewrite files when in fix mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }; + warningCount: 0 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().withExactArgs(report); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().withExactArgs(report); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix ."); + const exitCode = await localCLI.execute("--fix ."); assert.strictEqual(exitCode, 1); }); - it("should provide fix predicate and rewrite files when in fix mode and quiet mode", () => { + it("should provide fix predicate and rewrite files when in fix mode and quiet mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 1, + message: "Fake message" + } + ], errorCount: 0, - warningCount: 1, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 1, - message: "Fake message" - } - ] - }] - }; + warningCount: 1 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.getErrorResults = sinon.stub().returns([]); - fakeCLIEngine.outputFixes = sinon.mock().withExactArgs(report); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.getErrorResults = sinon.stub().returns([]); + fakeESLint.outputFixes = sinon.mock().withExactArgs(report); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix --quiet ."); + const exitCode = await localCLI.execute("--fix --quiet ."); assert.strictEqual(exitCode, 0); }); - it("should not call CLIEngine and return 1 when executing on text", () => { + it("should not call ESLint and return 2 when executing on text", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().never(); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix .", "foo = bar;"); + const exitCode = await localCLI.execute("--fix .", "foo = bar;"); assert.strictEqual(exitCode, 2); }); @@ -979,212 +966,193 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass fix:true to CLIEngine when executing on files", () => { + it("should pass fix:true to ESLint when executing on files", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run ."); + const exitCode = await localCLI.execute("--fix-dry-run ."); assert.strictEqual(exitCode, 0); }); - it("should pass fixTypes to CLIEngine when --fix-type is passed", () => { + it("should pass fixTypes to ESLint when --fix-type is passed", async () => { - const expectedCLIEngineOptions = { + const expectedESLintOptions = { fix: true, fixTypes: ["suggestion"] }; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match(expectedCLIEngineOptions)); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match(expectedESLintOptions)); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.stub(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run --fix-type suggestion ."); + const exitCode = await localCLI.execute("--fix-dry-run --fix-type suggestion ."); assert.strictEqual(exitCode, 0); }); - it("should not rewrite files when in fix-dry-run mode", () => { + it("should not rewrite files when in fix-dry-run mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }; + warningCount: 0 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run ."); + const exitCode = await localCLI.execute("--fix-dry-run ."); assert.strictEqual(exitCode, 1); }); - it("should provide fix predicate when in fix-dry-run mode and quiet mode", () => { + it("should provide fix predicate when in fix-dry-run mode and quiet mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 1, + message: "Fake message" + } + ], errorCount: 0, - warningCount: 1, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 1, - message: "Fake message" - } - ] - }] - }; + warningCount: 1 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.getErrorResults = sinon.stub().returns([]); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.getErrorResults = sinon.stub().returns([]); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run --quiet ."); + const exitCode = await localCLI.execute("--fix-dry-run --quiet ."); assert.strictEqual(exitCode, 0); }); - it("should allow executing on text", () => { + it("should allow executing on text", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }; + warningCount: 0 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnText").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintText").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run .", "foo = bar;"); + const exitCode = await localCLI.execute("--fix-dry-run .", "foo = bar;"); assert.strictEqual(exitCode, 1); }); - it("should not call CLIEngine and return 1 when used with --fix", () => { + it("should not call ESLint and return 2 when used with --fix", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().never(); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix --fix-dry-run .", "foo = bar;"); + const exitCode = await localCLI.execute("--fix --fix-dry-run .", "foo = bar;"); assert.strictEqual(exitCode, 2); }); }); describe("when passing --print-config", () => { - it("should print out the configuration", () => { + it("should print out the configuration", async () => { const filePath = getFixturePath("xxxx"); - const exitCode = cli.execute(`--print-config ${filePath}`); + const exitCode = await cli.execute(`--print-config ${filePath}`); assert.isTrue(log.info.calledOnce); assert.strictEqual(exitCode, 0); }); - it("should error if any positional file arguments are passed", () => { + it("should error if any positional file arguments are passed", async () => { const filePath1 = getFixturePath("files", "bar.js"); const filePath2 = getFixturePath("files", "foo.js"); - const exitCode = cli.execute(`--print-config ${filePath1} ${filePath2}`); + const exitCode = await cli.execute(`--print-config ${filePath1} ${filePath2}`); assert.isTrue(log.info.notCalled); assert.isTrue(log.error.calledOnce); assert.strictEqual(exitCode, 2); }); - it("should error out when executing on text", () => { - const exitCode = cli.execute("--print-config=myFile.js", "foo = bar;"); + it("should error out when executing on text", async () => { + const exitCode = await cli.execute("--print-config=myFile.js", "foo = bar;"); assert.isTrue(log.info.notCalled); assert.isTrue(log.error.calledOnce); From 5427043bc87614df69ff7b3f7a6f1f969b2594a5 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 15 Apr 2020 19:53:49 +0900 Subject: [PATCH 17/71] rewrite tests --- lib/cli.js | 6 +- lib/eslint/eslint.js | 104 +- package.json | 1 + tests/_utils/in-memory-fs.js | 6 +- .../fix-types/ignore-missing-meta.expected.js | 2 +- .../fixtures/fix-types/ignore-missing-meta.js | 2 +- tests/lib/eslint/eslint.js | 4016 +++++++++++++---- 7 files changed, 3265 insertions(+), 872 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 60e0bd19492..0ccf8f13e1e 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -85,6 +85,7 @@ function translateOptions({ rulesdir }) { return { + allowInlineConfig: inlineConfig, cache, cacheLocation: cacheLocation || cacheFile, errorOnUnmatchedPattern, @@ -98,7 +99,6 @@ function translateOptions({ obj[name] = true; return obj; }, {}), - extends: config && (path.isAbsolute(config) ? config : `./${config}`), globals: global && global.reduce((obj, name) => { if (name.endsWith(":true")) { obj[name.slice(0, -5)] = "writable"; @@ -108,13 +108,13 @@ function translateOptions({ return obj; }, {}), ignorePatterns: ignorePattern, - noInlineConfig: inlineConfig === false ? true : void 0, parser, parserOptions, plugins: plugin, rules: rule }, - reportUnusedDisableDirectives, + overrideConfigFile: config, + reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0, resolvePluginsRelativeTo, rulePaths: rulesdir, useEslintrc: eslintrc diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index d6b2c0a0d29..52588df5c9a 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -15,6 +15,7 @@ const { promisify } = require("util"); const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); const BuiltinRules = require("../rules"); const { getRuleSeverity } = require("../shared/config-ops"); +const { version } = require("../../package.json"); //------------------------------------------------------------------------------ // Typedefs @@ -26,10 +27,12 @@ const { getRuleSeverity } = require("../shared/config-ops"); /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").Plugin} Plugin */ /** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {import("./load-formatter").Formatter} Formatter */ /** * The options with which to configure the ESLint instance. * @typedef {Object} ESLintOptions + * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance * @property {boolean} [cache] Enable result caching. * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. @@ -42,8 +45,9 @@ const { getRuleSeverity } = require("../shared/config-ops"); * @property {boolean} [ignore] False disables use of .eslintignore. * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance + * @property {string} [overrideConfigFile] The configuration file to use. * @property {Record} [plugins] An array of plugin implementations. - * @property {boolean} [reportUnusedDisableDirectives] `true` adds reports for unused eslint-disable directives. + * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD. * @property {string[]} [rulePaths] An array of directories to load custom rules from. * @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files. @@ -70,12 +74,6 @@ const { getRuleSeverity } = require("../shared/config-ops"); * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. */ -/** - * A formatter object. - * @typedef {Object} Formatter - * @property {(results: LintResult[]) => string} format The main formatter method. - */ - /** * Private members for the `ESLint` instance. * @typedef {Object} ESLintPrivateMembers @@ -148,6 +146,7 @@ class ESLintInvalidOptionsError extends Error { * @returns {ESLintOptions} The normalized options. */ function processOptions({ + allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instaed because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored. baseConfig = null, cache = false, cacheLocation = ".eslintcache", @@ -155,13 +154,14 @@ function processOptions({ errorOnUnmatchedPattern = true, extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. fix = false, - fixTypes = ["problem", "suggestion", "layout"], + fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property. globInputPaths = true, ignore = true, - ignorePath = null, + ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT. overrideConfig = null, + overrideConfigFile = null, plugins = {}, - reportUnusedDisableDirectives = null, // ← should be null by default because if it's a boolean then it overrides the 'reportUnusedDisableDirectives' setting in config files. + reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instaed because we cannot configure the `error` severity with that. resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. rulePaths = [], useEslintrc = true, @@ -172,14 +172,11 @@ function processOptions({ if (unknownOptionKeys.length >= 1) { errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`); - if (unknownOptionKeys.includes("allowInlineConfig")) { - errors.push("'allowInlineConfig' has been removed. Please use the 'overrideConfig.noInlineConfig' option instead."); - } if (unknownOptionKeys.includes("cacheFile")) { errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead."); } if (unknownOptionKeys.includes("configFile")) { - errors.push("'configFile' has been removed. Please use the 'overrideConfig.extends' option instead."); + errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead."); } if (unknownOptionKeys.includes("envs")) { errors.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead."); @@ -200,6 +197,9 @@ function processOptions({ errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead."); } } + if (typeof allowInlineConfig !== "boolean") { + errors.push("'allowInlineConfig' must be a boolean."); + } if (typeof baseConfig !== "object") { errors.push("'baseConfig' must be an object or null."); } @@ -221,7 +221,7 @@ function processOptions({ if (typeof fix !== "boolean" && typeof fix !== "function") { errors.push("'fix' must be a boolean or a function."); } - if (!isFixTypeArray(fixTypes)) { + if (fixTypes !== null && !isFixTypeArray(fixTypes)) { errors.push("'fixTypes' must be an array of any of \"problem\", \"suggestion\", and \"layout\"."); } if (typeof globInputPaths !== "boolean") { @@ -236,17 +236,24 @@ function processOptions({ if (typeof overrideConfig !== "object") { errors.push("'overrideConfig' must be an object or null."); } + if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null) { + errors.push("'overrideConfigFile' must be a non-empty string or null."); + } if (typeof plugins !== "object") { errors.push("'plugins' must be an object or null."); + } else if (plugins !== null && Object.keys(plugins).includes("")) { + errors.push("'plugins' must not include the empty string in keys."); } if (Array.isArray(plugins)) { errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead."); } if ( - typeof reportUnusedDisableDirectives !== "boolean" && + reportUnusedDisableDirectives !== "error" && + reportUnusedDisableDirectives !== "warn" && + reportUnusedDisableDirectives !== "off" && reportUnusedDisableDirectives !== null ) { - errors.push("'reportUnusedDisableDirectives' must be a boolean or null."); + errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null."); } if ( !isNonEmptyString(resolvePluginsRelativeTo) && @@ -266,9 +273,11 @@ function processOptions({ } return { + allowInlineConfig, baseConfig, cache, cacheLocation, + configFile: overrideConfigFile, cwd, errorOnUnmatchedPattern, extensions, @@ -429,17 +438,35 @@ class ESLint { }); } + /** + * The version text. + * @type {string} + */ + static get version() { + return version; + } + /** * Outputs fixes from the given results to files. * @param {LintResult[]} results The lint results. * @returns {Promise} Returns a promise that is used to track side effects. */ static async outputFixes(results) { + if (!Array.isArray(results)) { + throw new Error("'results' must be an array"); + } + await Promise.all( results - .filter(r => - typeof r.output === "string" && - path.isAbsolute(r.filePath)) + .filter(result => { + if (typeof result !== "object" || result === null) { + throw new Error("'results' must include only objects"); + } + return ( + typeof result.output === "string" && + path.isAbsolute(result.filePath) + ); + }) .map(r => writeFile(r.filePath, r.output)) ); } @@ -477,6 +504,9 @@ class ESLint { * @returns {Promise} The results of linting the file patterns given. */ async lintFiles(patterns) { + if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) { + throw new Error("'patterns' must be a non-empty string or an array of non-empty strings"); + } const { cliEngine } = privateMembersMap.get(this); return processCLIEngineLintReport( @@ -493,7 +523,29 @@ class ESLint { * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. * @returns {Promise} The results of linting the string of code given. */ - async lintText(code, { filePath = null, warnIgnored = false } = {}) { + async lintText(code, options = {}) { + if (typeof code !== "string") { + throw new Error("'code' must be a string"); + } + if (typeof options !== "object") { + throw new Error("'options' must be an object, null, or undefined"); + } + const { + filePath, + warnIgnored = false, + ...unknownOptions + } = options || {}; + + for (const key of Object.keys(unknownOptions)) { + throw new Error(`'options' must not include the unknown option '${key}'`); + } + if (filePath !== void 0 && !isNonEmptyString(filePath)) { + throw new Error("'options.filePath' must be a non-empty string or undefined"); + } + if (typeof warnIgnored !== "boolean") { + throw new Error("'options.warnIgnored' must be a boolean or undefined"); + } + const { cliEngine } = privateMembersMap.get(this); return processCLIEngineLintReport( @@ -519,7 +571,7 @@ class ESLint { */ async loadFormatter(name = "stylish") { if (typeof name !== "string") { - throw new Error("Formatter name must be a string."); + throw new Error("'name' must be a string"); } const { cliEngine } = privateMembersMap.get(this); @@ -562,6 +614,9 @@ class ESLint { * @returns {Promise} A configuration object for the file. */ async calculateConfigForFile(filePath) { + if (!isNonEmptyString(filePath)) { + throw new Error("'filePath' must be a non-empty string"); + } const { cliEngine } = privateMembersMap.get(this); return cliEngine.getConfigForFile(filePath); @@ -570,9 +625,12 @@ class ESLint { /** * Checks if a given path is ignored by ESLint. * @param {string} filePath The path of the file to check. - * @returns {boolean} Whether or not the given path is ignored. + * @returns {Promise} Whether or not the given path is ignored. */ async isPathIgnored(filePath) { + if (!isNonEmptyString(filePath)) { + throw new Error("'filePath' must be a non-empty string"); + } const { cliEngine } = privateMembersMap.get(this); return cliEngine.isPathIgnored(filePath); diff --git a/package.json b/package.json index a4c16472460..7196b538c1f 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "core-js": "^3.1.3", "dateformat": "^3.0.3", "ejs": "^2.6.1", + "escape-string-regexp": "^3.0.0", "eslint": "file:.", "eslint-config-eslint": "file:packages/eslint-config-eslint", "eslint-plugin-eslint-plugin": "^2.2.1", diff --git a/tests/_utils/in-memory-fs.js b/tests/_utils/in-memory-fs.js index 5b6ecd6c8c3..5ef4f0453f3 100644 --- a/tests/_utils/in-memory-fs.js +++ b/tests/_utils/in-memory-fs.js @@ -507,7 +507,7 @@ function defineESLintWithInMemoryFileSystem({ CLIEngine, getCLIEngineInternalSlots } = defineCLIEngineWithInMemoryFileSystem({ cwd, files }); - const { ESLint, getESLintInternalSlots } = proxyquire(ESLintPath, { + const { ESLint, getESLintPrivateMembers } = proxyquire(ESLintPath, { "../cli-engine/cli-engine": { CLIEngine, getCLIEngineInternalSlots } }); @@ -518,6 +518,8 @@ function defineESLintWithInMemoryFileSystem({ ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator, + CLIEngine, + getCLIEngineInternalSlots, ESLint: cwd === process.cwd ? ESLint : class extends ESLint { @@ -525,7 +527,7 @@ function defineESLintWithInMemoryFileSystem({ super({ cwd: cwd(), ...options }); } }, - getESLintInternalSlots + getESLintPrivateMembers }; } diff --git a/tests/fixtures/fix-types/ignore-missing-meta.expected.js b/tests/fixtures/fix-types/ignore-missing-meta.expected.js index b6e3019aa01..dc4cb7223ec 100644 --- a/tests/fixtures/fix-types/ignore-missing-meta.expected.js +++ b/tests/fixtures/fix-types/ignore-missing-meta.expected.js @@ -1,5 +1,5 @@ /* eslint semi: "error" */ -/* eslint no-program: "error" */ +/* eslint test/no-program: "error" */ /* eslint prefer-arrow-callback: "error" */ "use strict"; diff --git a/tests/fixtures/fix-types/ignore-missing-meta.js b/tests/fixtures/fix-types/ignore-missing-meta.js index ee9b9d9d869..4e43de09dc6 100644 --- a/tests/fixtures/fix-types/ignore-missing-meta.js +++ b/tests/fixtures/fix-types/ignore-missing-meta.js @@ -1,5 +1,5 @@ /* eslint semi: "error" */ -/* eslint no-program: "error" */ +/* eslint test/no-program: "error" */ /* eslint prefer-arrow-callback: "error" */ "use strict"; diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 0c61f4cb557..5de4396909d 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -1,6 +1,7 @@ /** * @fileoverview Tests for the ESLint class. * @author Kai Cataldo + * @author Toru Nagashima */ "use strict"; @@ -9,14 +10,18 @@ // Requirements //------------------------------------------------------------------------------ -const path = require("path"); -const os = require("os"); +const assert = require("assert"); const fs = require("fs"); -const assert = require("chai").assert; +const os = require("os"); +const path = require("path"); +const escapeStringRegExp = require("escape-string-regexp"); +const fCache = require("file-entry-cache"); +const leche = require("leche"); const sinon = require("sinon"); +const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); const shell = require("shelljs"); +const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); const hash = require("../../../lib/cli-engine/hash"); -const fCache = require("file-entry-cache"); const { unIndent, defineESLintWithInMemoryFileSystem } = require("../../_utils"); //------------------------------------------------------------------------------ @@ -39,12 +44,6 @@ describe("ESLint", () => { /** @type {import("../../../lib/eslint")["ESLint"]} */ let ESLint; - /** @type {InstanceType} */ - let eslint; - - /** @type {import("../../../lib/eslint/eslint")["getESLintPrivateMembers"]} */ - let getESLintPrivateMembers; - /** * Returns the path inside of the fixture directory. * @param {...string} args file path segments. @@ -62,16 +61,29 @@ describe("ESLint", () => { } /** - * Create a plugins configuration array containing mocked plugins - * @returns {import("../../../lib/eslint/eslint")["PluginElement"][]} an array of plugins + * Create the ESLint object by mocking some of the plugins + * @param {Object} options options for ESLint + * @returns {ESLint} engine object * @private */ - function createMockedPluginsOption() { - return [ - { id: examplePluginName, definition: examplePlugin }, - { id: examplePluginNameWithNamespace, definition: examplePlugin }, - { id: examplePreprocessorName, definition: require("../../fixtures/processors/custom-processor") } - ]; + function eslintWithPlugins(options) { + return new ESLint({ + ...options, + plugins: { + [examplePluginName]: examplePlugin, + [examplePluginNameWithNamespace]: examplePlugin, + [examplePreprocessorName]: require("../../fixtures/processors/custom-processor") + } + }); + } + + /** + * Call the last argument. + * @param {any[]} args Arguments + * @returns {void} + */ + function callLastArgument(...args) { + process.nextTick(args[args.length - 1], null); } // copy into clean area so as not to get "infected" by this project's .eslintrc files @@ -81,7 +93,7 @@ describe("ESLint", () => { }); beforeEach(() => { - ({ ESLint, getESLintPrivateMembers } = require("../../../lib/eslint/eslint")); + ({ ESLint } = require("../../../lib/eslint/eslint")); }); after(() => { @@ -91,26 +103,36 @@ describe("ESLint", () => { describe("ESLint constructor function", () => { it("the default value of 'options.cwd' should be the current working directory.", async () => { process.chdir(__dirname); - try { - eslint = new ESLint(); - const { options } = getESLintPrivateMembers(eslint); + const engine = new ESLint(); + const results = await engine.lintFiles("eslint.js"); - assert.strictEqual(options.cwd, __dirname); + assert.strictEqual(path.dirname(results[0].filePath), __dirname); } finally { process.chdir(originalDir); } }); - it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", async () => { + it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", () => { assert.throws(() => { // eslint-disable-next-line no-new - new ESLint({ ignorePath: fixtureDir, ignore: true }); - }, `Cannot read .eslintignore file: ${fixtureDir}\nError: EISDIR: illegal operation on a directory, read`); + new ESLint({ ignorePath: fixtureDir }); + }, new RegExp(escapeStringRegExp(`Cannot read .eslintignore file: ${fixtureDir}\nError: EISDIR: illegal operation on a directory, read`), "u")); + }); + + // https://github.com/eslint/eslint/issues/2380 + it("should not modify baseConfig when format is specified", () => { + const customBaseConfig = { root: true }; + + new ESLint({ baseConfig: customBaseConfig }); // eslint-disable-line no-new + + assert.deepStrictEqual(customBaseConfig, { root: true }); }); }); describe("lintText()", () => { + let eslint; + it("should report the total and per file errors when using local cwd .eslintrc", async () => { eslint = new ESLint(); const results = await eslint.lintText("var foo = 'bar';"); @@ -129,12 +151,14 @@ describe("ESLint", () => { it("should report the total and per file warnings when using local cwd .eslintrc", async () => { eslint = new ESLint({ - rules: { - quotes: 1, - "no-var": 1, - "eol-last": 1, - strict: 1, - "no-unused-vars": 1 + overrideConfig: { + rules: { + quotes: 1, + "no-var": 1, + "eol-last": 1, + strict: 1, + "no-unused-vars": 1 + } } }); const results = await eslint.lintText("var foo = 'bar';"); @@ -153,7 +177,7 @@ describe("ESLint", () => { it("should report one message when using specific config file", async () => { eslint = new ESLint({ - configFile: "fixtures/configurations/quotes-error.json", + overrideConfigFile: "fixtures/configurations/quotes-error.json", useEslintrc: false, cwd: getFixturePath("..") }); @@ -162,7 +186,7 @@ describe("ESLint", () => { assert.strictEqual(results.length, 1); assert.strictEqual(results[0].messages.length, 1); assert.strictEqual(results[0].messages[0].ruleId, "quotes"); - assert.isUndefined(results[0].messages[0].output); + assert.strictEqual(results[0].messages[0].output, void 0); assert.strictEqual(results[0].errorCount, 1); assert.strictEqual(results[0].fixableErrorCount, 1); assert.strictEqual(results[0].warningCount, 0); @@ -188,10 +212,11 @@ describe("ESLint", () => { const options = { filePath: "fixtures/passing.js", warnIgnored: true }; const results = await eslint.lintText("var bar = foo;", options); + assert.strictEqual(results.length, 1); assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); assert.strictEqual(results[0].messages[0].severity, 1); assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); - assert.isUndefined(results[0].messages[0].output); + assert.strictEqual(results[0].messages[0].output, void 0); assert.strictEqual(results[0].errorCount, 0); assert.strictEqual(results[0].warningCount, 1); assert.strictEqual(results[0].fixableErrorCount, 0); @@ -234,8 +259,10 @@ describe("ESLint", () => { cwd: getFixturePath(".."), ignore: false, useEslintrc: false, - rules: { - "no-undef": 2 + overrideConfig: { + rules: { + "no-undef": 2 + } } }); const options = { filePath: "fixtures/passing.js" }; @@ -245,15 +272,17 @@ describe("ESLint", () => { assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); assert.strictEqual(results[0].messages[0].severity, 2); - assert.isUndefined(results[0].messages[0].output); + assert.strictEqual(results[0].messages[0].output, void 0); }); it("should return a message and fixed text when in fix mode", async () => { eslint = new ESLint({ useEslintrc: false, fix: true, - rules: { - semi: 2 + overrideConfig: { + rules: { + semi: 2 + } }, ignore: false, cwd: getFixturePath() @@ -304,7 +333,7 @@ describe("ESLint", () => { }); describe("Fix Types", () => { - it("should throw an error when an invalid fix type is specified", async () => { + it("should throw an error when an invalid fix type is specified", () => { assert.throws(() => { eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -312,7 +341,7 @@ describe("ESLint", () => { fix: true, fixTypes: ["layou"] }); - }, /'fixTypes' must be an array of any of "problem", "suggestion", and "layout"/iu); + }, /'fixTypes' must be an array of any of "problem", "suggestion", and "layout"\./iu); }); it("should not fix any rules when fixTypes is used without fix", async () => { @@ -322,11 +351,10 @@ describe("ESLint", () => { fix: false, fixTypes: ["layout"] }); - const inputPath = getFixturePath("fix-types/fix-only-semi.js"); const results = await eslint.lintFiles([inputPath]); - assert.isUndefined(results[0].output); + assert.strictEqual(results[0].output, void 0); }); it("should not fix non-style rules when fixTypes has only 'layout'", async () => { @@ -382,7 +410,6 @@ describe("ESLint", () => { fixTypes: ["layout"], rulePaths: [getFixturePath("rules", "fix-types-test")] }); - const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); const results = await eslint.lintFiles([inputPath]); @@ -391,55 +418,48 @@ describe("ESLint", () => { assert.strictEqual(results[0].output, expectedOutput); }); - describe("should not throw an error when a rule is loaded after initialization", () => { - - /** @type {import("../../../lib/cli-engine/cli-engine")["getCLIEngineInternalSlots"]} */ - let getCLIEngineInternalSlots; - - /** @type {InstanceType} */ - let linter; - - beforeEach(() => { - ({ getCLIEngineInternalSlots } = require("../../../lib/cli-engine/cli-engine")); - - eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), - useEslintrc: false, - fix: true, - fixTypes: ["layout"] - }); - const { cliEngine } = getESLintPrivateMembers(eslint); - - ({ linter } = getCLIEngineInternalSlots(cliEngine)); + it("should not throw an error when a rule is loaded after initialization with lintFiles()", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"], + plugins: { + test: { + rules: { + "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"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); - it("with executeOnFiles()", async () => { - 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"); - const results = await eslint.lintFiles([inputPath]); - const expectedOutput = fs.readFileSync(outputPath, "utf8"); + assert.strictEqual(results[0].output, expectedOutput); + }); - assert.strictEqual(results[0].output, expectedOutput); + it("should not throw an error when a rule is loaded after initialization with lintText()", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"], + plugins: { + test: { + rules: { + "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"); + const results = await eslint.lintText(fs.readFileSync(inputPath, { encoding: "utf8" }), { filePath: inputPath }); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); - it("with executeOnText()", async () => { - 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"); - const results = await eslint.lintText(fs.readFileSync(inputPath, { encoding: "utf8" }), inputPath); - const expectedOutput = fs.readFileSync(outputPath, "utf8"); - - assert.strictEqual(results[0].output, expectedOutput); - }); + assert.strictEqual(results[0].output, expectedOutput); }); }); @@ -447,8 +467,10 @@ describe("ESLint", () => { eslint = new ESLint({ useEslintrc: false, fix: true, - rules: { - "no-undef": 2 + overrideConfig: { + rules: { + "no-undef": 2 + } }, ignore: false, cwd: getFixturePath() @@ -482,13 +504,15 @@ describe("ESLint", () => { ]); }); - xit("should not delete code if there is a syntax error after trying to autofix.", async () => { - eslint = new ESLint({ + it("should not delete code if there is a syntax error after trying to autofix.", async () => { + eslint = eslintWithPlugins({ useEslintrc: false, fix: true, - plugins: createMockedPluginsOption(), - rules: { - "example/make-syntax-error": "error" + overrideConfig: { + plugins: ["example"], + rules: { + "example/make-syntax-error": "error" + } }, ignore: false, cwd: getFixturePath() @@ -523,8 +547,10 @@ describe("ESLint", () => { eslint = new ESLint({ useEslintrc: false, fix: true, - rules: { - "example/make-syntax-error": "error" + overrideConfig: { + rules: { + "example/make-syntax-error": "error" + } }, ignore: false, cwd: getFixturePath() @@ -558,7 +584,9 @@ describe("ESLint", () => { it("should return source code of file in `source` property when errors are present", async () => { eslint = new ESLint({ useEslintrc: false, - rules: { semi: 2 } + overrideConfig: { + rules: { semi: 2 } + } }); const results = await eslint.lintText("var foo = 'bar'"); @@ -568,7 +596,9 @@ describe("ESLint", () => { it("should return source code of file in `source` property when warnings are present", async () => { eslint = new ESLint({ useEslintrc: false, - rules: { semi: 1 } + overrideConfig: { + rules: { semi: 1 } + } }); const results = await eslint.lintText("var foo = 'bar'"); @@ -579,33 +609,39 @@ describe("ESLint", () => { it("should not return a `source` property when no errors or warnings are present", async () => { eslint = new ESLint({ useEslintrc: false, - rules: { semi: 2 } + overrideConfig: { + rules: { semi: 2 } + } }); const results = await eslint.lintText("var foo = 'bar';"); - assert.lengthOf(results[0].messages, 0); - assert.isUndefined(results[0].source); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].source, void 0); }); it("should not return a `source` property when fixes are applied", async () => { eslint = new ESLint({ useEslintrc: false, fix: true, - rules: { - semi: 2, - "no-unused-vars": 2 + overrideConfig: { + rules: { + semi: 2, + "no-unused-vars": 2 + } } }); const results = await eslint.lintText("var msg = 'hi' + foo\n"); - assert.isUndefined(results[0].source); + assert.strictEqual(results[0].source, void 0); assert.strictEqual(results[0].output, "var msg = 'hi' + foo;\n"); }); it("should return a `source` property when a parsing error has occurred", async () => { eslint = new ESLint({ useEslintrc: false, - rules: { semi: 2 } + overrideConfig: { + rules: { semi: 2 } + } }); const results = await eslint.lintText("var bar = foothis is a syntax error.\n return bar;"); @@ -660,7 +696,6 @@ describe("ESLint", () => { return originalFindPath.call(this, id, ...otherArgs); }; }); - after(() => { Module._findPath = originalFindPath; }); @@ -689,7 +724,7 @@ describe("ESLint", () => { eslint = new ESLint({ cwd: originalDir, useEslintrc: false, - configFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml" + overrideConfigFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml" }); const [result] = await eslint.lintText("foo"); @@ -701,27 +736,28 @@ describe("ESLint", () => { }); describe("lintFiles()", () => { + + /** @type {InstanceType} */ + let eslint; + it("should use correct parser when custom parser is specified", async () => { eslint = new ESLint({ cwd: originalDir, ignore: false }); - const filePath = path.resolve(__dirname, "../../fixtures/configurations/parser/custom.js"); const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].messages.length, 1); assert.strictEqual(results[0].messages[0].message, "Parsing error: Boom!"); - }); it("should report zero messages when given a config file and a valid file", async () => { eslint = new ESLint({ cwd: originalDir, - configFile: ".eslintrc.js" + overrideConfigFile: ".eslintrc.js" }); - const results = await eslint.lintFiles(["lib/**/cli*.js"]); assert.strictEqual(results.length, 2); @@ -732,9 +768,8 @@ describe("ESLint", () => { it("should handle multiple patterns with overlapping files", async () => { eslint = new ESLint({ cwd: originalDir, - configFile: ".eslintrc.js" + overrideConfigFile: ".eslintrc.js" }); - const results = await eslint.lintFiles(["lib/**/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine/cli-engine}.js"]); assert.strictEqual(results.length, 2); @@ -744,11 +779,14 @@ describe("ESLint", () => { it("should report zero messages when given a config file and a valid file and espree as parser", async () => { eslint = new ESLint({ - parser: "espree", - envs: ["es6"], + overrideConfig: { + parser: "espree", + parserOptions: { + ecmaVersion: 2020 + } + }, useEslintrc: false }); - const results = await eslint.lintFiles(["lib/cli.js"]); assert.strictEqual(results.length, 1); @@ -756,13 +794,14 @@ describe("ESLint", () => { }); it("should report zero messages when given a config file and a valid file and esprima as parser", async () => { - eslint = new ESLint({ - parser: "esprima", - useEslintrc: false + overrideConfig: { + parser: "esprima" + }, + useEslintrc: false, + ignore: false }); - - const results = await eslint.lintFiles(["lib/cli.js"]); + const results = await eslint.lintFiles(["tests/fixtures/passing.js"]); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].messages.length, 0); @@ -770,26 +809,20 @@ describe("ESLint", () => { it("should throw an error when given a config file and a valid file and invalid parser", async () => { eslint = new ESLint({ - parser: "test11", + overrideConfig: { + parser: "test11" + }, useEslintrc: false }); - try { - await eslint.lintFiles(["lib/cli.js"]); - } catch (e) { - assert.isTrue(/Failed to load parser 'test11'/u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + await assert.rejects(async () => await eslint.lintFiles(["lib/cli.js"]), /Cannot find module 'test11'/u); }); it("should report zero messages when given a directory with a .js2 file", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), extensions: [".js2"] }); - const results = await eslint.lintFiles([getFixturePath("files/foo.js2")]); assert.strictEqual(results.length, 1); @@ -797,10 +830,9 @@ describe("ESLint", () => { }); it("should fall back to defaults when extensions is set to an empty array", async () => { - eslint = new ESLint({ cwd: getFixturePath("configurations"), - configFile: getFixturePath("configurations", "quotes-error.json"), + overrideConfigFile: getFixturePath("configurations", "quotes-error.json"), extensions: [] }); const results = await eslint.lintFiles([getFixturePath("single-quoted.js")]); @@ -816,13 +848,11 @@ describe("ESLint", () => { }); it("should report zero messages when given a directory with a .js and a .js2 file", async () => { - eslint = new ESLint({ extensions: [".js", ".js2"], ignore: false, cwd: getFixturePath("..") }); - const results = await eslint.lintFiles(["fixtures/files/"]); assert.strictEqual(results.length, 2); @@ -831,13 +861,11 @@ describe("ESLint", () => { }); it("should report zero messages when given a '**' pattern with a .js and a .js2 file", async () => { - eslint = new ESLint({ extensions: [".js", ".js2"], ignore: false, cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles(["fixtures/files/*"]); assert.strictEqual(results.length, 2); @@ -851,7 +879,6 @@ describe("ESLint", () => { ignore: false, cwd: getFixturePath("..") }); - const results = await eslint.lintFiles(["fixtures/files/*"]); assert.strictEqual(results.length, 2); @@ -867,21 +894,15 @@ describe("ESLint", () => { globInputPaths: false }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["fixtures/files/*"]); - } catch (e) { - assert.strictEqual("No files matching 'fixtures/files/*' were found (glob was disabled).", e.message); - return; - } - assert.fail("Expected to throw an error"); + }, /No files matching 'fixtures\/files\/\*' were found \(glob was disabled\)\./u); }); it("should report on all files passed explicitly, even if ignored by default", async () => { - eslint = new ESLint({ cwd: getFixturePath("cli-engine") }); - const results = await eslint.lintFiles(["node_modules/foo.js"]); const expectedMsg = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; @@ -894,14 +915,14 @@ describe("ESLint", () => { }); it("should report on globs with explicit inclusion of dotfiles, even though ignored by default", async () => { - eslint = new ESLint({ cwd: getFixturePath("cli-engine"), - rules: { - quotes: [2, "single"] + overrideConfig: { + rules: { + quotes: [2, "single"] + } } }); - const results = await eslint.lintFiles(["hidden/.hiddenfolder/*.js"]); assert.strictEqual(results.length, 1); @@ -916,13 +937,9 @@ describe("ESLint", () => { cwd: getFixturePath("cli-engine") }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["node_modules"]); - } catch (e) { - assert.strictEqual("All files matched by 'node_modules' are ignored.", e.message); - return; - } - assert.fail("Expected to throw an error"); + }, /All files matched by 'node_modules' are ignored\./u); }); // https://github.com/eslint/eslint/issues/5547 @@ -932,25 +949,21 @@ describe("ESLint", () => { ignore: false }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["node_modules"]); - } catch (e) { - assert.strictEqual("All files matched by 'node_modules' are ignored.", e.message); - return; - } - assert.fail("Expected to throw an error"); + }, /All files matched by 'node_modules' are ignored\./u); }); it("should not check .hidden files if they are passed explicitly without --no-ignore flag", async () => { - eslint = new ESLint({ cwd: getFixturePath(".."), useEslintrc: false, - rules: { - quotes: [2, "single"] + overrideConfig: { + rules: { + quotes: [2, "single"] + } } }); - const results = await eslint.lintFiles(["fixtures/files/.bar.js"]); const expectedMsg = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!'\") to override."; @@ -962,17 +975,39 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages[0].message, expectedMsg); }); - it("should check .hidden files if they are passed explicitly with --no-ignore flag", async () => { + // https://github.com/eslint/eslint/issues/12873 + it("should not check files within a .hidden folder if they are passed explicitly without the --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + useEslintrc: false, + overrideConfig: { + rules: { + quotes: [2, "single"] + } + } + }); + const results = await eslint.lintFiles(["hidden/.hiddenfolder/double-quotes.js"]); + const expectedMsg = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!'\") to override."; + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + it("should check .hidden files if they are passed explicitly with --no-ignore flag", async () => { eslint = new ESLint({ cwd: getFixturePath(".."), ignore: false, useEslintrc: false, - rules: { - quotes: [2, "single"] + overrideConfig: { + rules: { + quotes: [2, "single"] + } } }); - const results = await eslint.lintFiles(["fixtures/files/.bar.js"]); assert.strictEqual(results.length, 1); @@ -984,17 +1019,17 @@ describe("ESLint", () => { }); it("should check .hidden files if they are unignored with an --ignore-pattern", async () => { - eslint = new ESLint({ cwd: getFixturePath("cli-engine"), ignore: true, useEslintrc: false, - ignorePattern: "!.hidden*", - rules: { - quotes: [2, "single"] + overrideConfig: { + ignorePatterns: "!.hidden*", + rules: { + quotes: [2, "single"] + } } }); - const results = await eslint.lintFiles(["hidden/"]); assert.strictEqual(results.length, 1); @@ -1006,13 +1041,11 @@ describe("ESLint", () => { }); it("should report zero messages when given a pattern with a .js and a .js2 file", async () => { - eslint = new ESLint({ extensions: [".js", ".js2"], ignore: false, cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles(["fixtures/files/*.?s*"]); assert.strictEqual(results.length, 2); @@ -1021,10 +1054,9 @@ describe("ESLint", () => { }); it("should return one error message when given a config with rules with options and severity level set to error", async () => { - eslint = new ESLint({ cwd: getFixturePath("configurations"), - configFile: getFixturePath("configurations", "quotes-error.json") + overrideConfigFile: getFixturePath("configurations", "quotes-error.json") }); const results = await eslint.lintFiles([getFixturePath("single-quoted.js")]); @@ -1039,12 +1071,10 @@ describe("ESLint", () => { }); it("should return 3 messages when given a config file and a directory of 3 valid files", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "semi-error.json") + overrideConfigFile: getFixturePath("configurations", "semi-error.json") }); - const results = await eslint.lintFiles([getFixturePath("formatters")]); assert.strictEqual(results.length, 3); @@ -1065,37 +1095,11 @@ describe("ESLint", () => { assert.strictEqual(results[2].fixableWarningCount, 0); }); - - it("should return the total number of errors when given multiple files", async () => { - - eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "single-quotes-error.json") - }); - - const results = await eslint.lintFiles([getFixturePath("formatters")]); - - assert.strictEqual(results[0].errorCount, 0); - assert.strictEqual(results[0].warningCount, 0); - assert.strictEqual(results[0].fixableErrorCount, 0); - assert.strictEqual(results[0].fixableWarningCount, 0); - assert.strictEqual(results[1].errorCount, 3); - assert.strictEqual(results[1].warningCount, 0); - assert.strictEqual(results[1].fixableErrorCount, 3); - assert.strictEqual(results[1].fixableWarningCount, 0); - assert.strictEqual(results[2].errorCount, 3); - assert.strictEqual(results[2].warningCount, 0); - assert.strictEqual(results[2].fixableErrorCount, 3); - assert.strictEqual(results[2].fixableWarningCount, 0); - }); - it("should process when file is given by not specifying extensions", async () => { - eslint = new ESLint({ ignore: false, cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles(["fixtures/files/foo.js2"]); assert.strictEqual(results.length, 1); @@ -1103,12 +1107,10 @@ describe("ESLint", () => { }); it("should return zero messages when given a config with environment set to browser", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "env-browser.json") + overrideConfigFile: getFixturePath("configurations", "env-browser.json") }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); assert.strictEqual(results.length, 1); @@ -1116,16 +1118,16 @@ describe("ESLint", () => { }); it("should return zero messages when given an option to set environment to browser", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - envs: ["browser"], - rules: { - "no-alert": 0, - "no-undef": 2 + overrideConfig: { + env: { browser: true }, + rules: { + "no-alert": 0, + "no-undef": 2 + } } }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); assert.strictEqual(results.length, 1); @@ -1133,12 +1135,10 @@ describe("ESLint", () => { }); it("should return zero messages when given a config with environment set to Node.js", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "env-node.json") + overrideConfigFile: getFixturePath("configurations", "env-node.json") }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-node.js"))]); assert.strictEqual(results.length, 1); @@ -1149,13 +1149,15 @@ describe("ESLint", () => { eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), ignore: false, - rules: { - semi: 2 + overrideConfig: { + rules: { + semi: 2 + } } }); - const failFilePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); const passFilePath = fs.realpathSync(getFixturePath("passing.js")); + let results = await eslint.lintFiles([failFilePath]); assert.strictEqual(results.length, 1); @@ -1168,7 +1170,6 @@ describe("ESLint", () => { assert.strictEqual(results.length, 1); assert.strictEqual(results[0].filePath, passFilePath); assert.strictEqual(results[0].messages.length, 0); - }); it("should throw an error when given a directory with all eslint excluded files in the directory", async () => { @@ -1176,27 +1177,15 @@ describe("ESLint", () => { ignorePath: getFixturePath(".eslintignore") }); - try { + await assert.rejects(async () => { await eslint.lintFiles([getFixturePath("./cli-engine/")]); - } catch (e) { - assert.strictEqual(`All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`, e.message); - return; - } - assert.fail("Expected to throw an error"); + }, new Error(`All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`)); }); it("should throw an error when all given files are ignored", async () => { - eslint = new ESLint({ - ignorePath: getFixturePath(".eslintignore") - }); - - try { + await assert.rejects(async () => { await eslint.lintFiles(["tests/fixtures/cli-engine/"]); - } catch (e) { - assert.isTrue(/All files matched by 'tests\/fixtures\/cli-engine\/' are ignored./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /All files matched by 'tests\/fixtures\/cli-engine\/' are ignored\./u); }); it("should throw an error when all given files are ignored even with a `./` prefix", async () => { @@ -1204,13 +1193,9 @@ describe("ESLint", () => { ignorePath: getFixturePath(".eslintignore") }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); - } catch (e) { - assert.isTrue(/All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored\./u); }); // https://github.com/eslint/eslint/issues/3788 @@ -1218,12 +1203,13 @@ describe("ESLint", () => { eslint = new ESLint({ ignorePath: getFixturePath("cli-engine", "nested_node_modules", ".eslintignore"), useEslintrc: false, - rules: { - quotes: [2, "double"] + overrideConfig: { + rules: { + quotes: [2, "double"] + } }, cwd: getFixturePath("cli-engine", "nested_node_modules") }); - const results = await eslint.lintFiles(["."]); assert.strictEqual(results.length, 1); @@ -1238,32 +1224,28 @@ describe("ESLint", () => { eslint = new ESLint({ ignorePath: getFixturePath("cli-engine/.eslintignore2"), useEslintrc: false, - rules: { - quotes: [2, "double"] + overrideConfig: { + rules: { + quotes: [2, "double"] + } } }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); - } catch (e) { - assert.isTrue(/All files matched by '.\/tests\/fixtures\/cli-engine\/' are ignored./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored\./u); }); it("should throw an error when all given files are ignored via ignore-pattern", async () => { eslint = new ESLint({ - ignorePattern: "tests/fixtures/single-quoted.js" + overrideConfig: { + ignorePatterns: "tests/fixtures/single-quoted.js" + } }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["tests/fixtures/*-quoted.js"]); - } catch (e) { - assert.isTrue(/All files matched by 'tests\/fixtures\/\*-quoted.js' are ignored./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /All files matched by 'tests\/fixtures\/\*-quoted\.js' are ignored\./u); }); it("should return a warning when an explicitly given file is ignored", async () => { @@ -1271,9 +1253,7 @@ describe("ESLint", () => { ignorePath: getFixturePath(".eslintignore"), cwd: getFixturePath() }); - const filePath = getFixturePath("passing.js"); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1287,17 +1267,16 @@ describe("ESLint", () => { }); it("should return two messages when given a file in excluded files list while ignore is off", async () => { - eslint = new ESLint({ ignorePath: getFixturePath(".eslintignore"), ignore: false, - rules: { - "no-undef": 2 + overrideConfig: { + rules: { + "no-undef": 2 + } } }); - const filePath = fs.realpathSync(getFixturePath("undef.js")); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1312,7 +1291,6 @@ describe("ESLint", () => { eslint = new ESLint({ ignore: false }); - const results = await eslint.lintFiles([getFixturePath("shebang.js")]); assert.strictEqual(results.length, 1); @@ -1323,7 +1301,7 @@ describe("ESLint", () => { eslint = new ESLint({ ignore: false, rulePaths: [getFixturePath("rules", "dir1")], - configFile: getFixturePath("rules", "missing-rule.json") + overrideConfigFile: getFixturePath("rules", "missing-rule.json") }); const results = await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); @@ -1338,29 +1316,23 @@ describe("ESLint", () => { eslint = new ESLint({ ignore: false, rulePaths: [getFixturePath("rules", "wrong")], - configFile: getFixturePath("rules", "eslint.json") + overrideConfigFile: getFixturePath("rules", "eslint.json") }); - try { + + await assert.rejects(async () => { await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); - } catch (e) { - assert.isTrue(/Error while loading rule 'custom-rule'/u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /Error while loading rule 'custom-rule'/u); }); it("should return one message when a custom rule matches a file", async () => { - eslint = new ESLint({ ignore: false, useEslintrc: false, rulePaths: [getFixturePath("rules/")], - configFile: getFixturePath("rules", "eslint.json") + overrideConfigFile: getFixturePath("rules", "eslint.json") }); - const filePath = fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js")); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1377,11 +1349,9 @@ describe("ESLint", () => { ignore: false, cwd, rulePaths: ["./"], - configFile: "eslint.json" + overrideConfigFile: "eslint.json" }); - const filePath = fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js")); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1392,18 +1362,15 @@ describe("ESLint", () => { }); it("should return messages when multiple custom rules match a file", async () => { - eslint = new ESLint({ ignore: false, rulePaths: [ getFixturePath("rules", "dir1"), getFixturePath("rules", "dir2") ], - configFile: getFixturePath("rules", "multi-rulesdirs.json") + overrideConfigFile: getFixturePath("rules", "multi-rulesdirs.json") }); - const filePath = fs.realpathSync(getFixturePath("rules", "test-multi-rulesdirs.js")); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1420,9 +1387,7 @@ describe("ESLint", () => { ignore: false, useEslintrc: false }); - const filePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1434,9 +1399,10 @@ describe("ESLint", () => { eslint = new ESLint({ ignore: false, useEslintrc: false, - envs: ["node"] + overrideConfig: { + env: { node: true } + } }); - const filePath = fs.realpathSync(getFixturePath("process-exit.js")); const results = await eslint.lintFiles([filePath]); @@ -1445,31 +1411,15 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages.length, 0); }); - it("should return zero messages when executing with base-config flag set to null", async () => { + it("should return zero messages and ignore .eslintrc files when executing with no-eslintrc flag", async () => { eslint = new ESLint({ ignore: false, - baseConfig: null, - useEslintrc: false + useEslintrc: false, + overrideConfig: { + env: { node: true } + } }); - - const filePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); - - const results = await eslint.lintFiles([filePath]); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].filePath, filePath); - assert.strictEqual(results[0].messages.length, 0); - }); - - it("should return zero messages and ignore .eslintrc files when executing with no-eslintrc flag", async () => { - eslint = new ESLint({ - ignore: false, - useEslintrc: false, - envs: ["node"] - }); - - const filePath = fs.realpathSync(getFixturePath("eslintrc", "quotes.js")); - + const filePath = fs.realpathSync(getFixturePath("eslintrc", "quotes.js")); const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1481,11 +1431,11 @@ describe("ESLint", () => { eslint = new ESLint({ ignore: false, useEslintrc: false, - envs: ["node"] + overrideConfig: { + env: { node: true } + } }); - const filePath = fs.realpathSync(getFixturePath("packagejson", "quotes.js")); - const results = await eslint.lintFiles([filePath]); assert.strictEqual(results.length, 1); @@ -1496,17 +1446,18 @@ describe("ESLint", () => { it("should warn when deprecated rules are configured", async () => { eslint = new ESLint({ cwd: originalDir, - configFile: ".eslintrc.js", - rules: { - "indent-legacy": 1, - "require-jsdoc": 1, - "valid-jsdoc": 1 + overrideConfigFile: ".eslintrc.js", + overrideConfig: { + rules: { + "indent-legacy": 1, + "require-jsdoc": 1, + "valid-jsdoc": 1 + } } }); - const results = await eslint.lintFiles(["lib/cli*.js"]); - assert.sameDeepMembers( + assert.deepStrictEqual( results[0].usedDeprecatedRules, [ { ruleId: "indent-legacy", replacedBy: ["indent"] }, @@ -1519,10 +1470,11 @@ describe("ESLint", () => { it("should not warn when deprecated rules are not configured", async () => { eslint = new ESLint({ cwd: originalDir, - configFile: ".eslintrc.js", - rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + overrideConfigFile: ".eslintrc.js", + overrideConfig: { + rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + } }); - const results = await eslint.lintFiles(["lib/cli*.js"]); assert.deepStrictEqual(results[0].usedDeprecatedRules, []); @@ -1531,10 +1483,9 @@ describe("ESLint", () => { it("should warn when deprecated rules are found in a config", async () => { eslint = new ESLint({ cwd: originalDir, - configFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml", + overrideConfigFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml", useEslintrc: false }); - const results = await eslint.lintFiles(["lib/cli*.js"]); assert.deepStrictEqual( @@ -1544,7 +1495,6 @@ describe("ESLint", () => { }); describe("Fix Mode", () => { - it("should return fixed text on multiple files when in fix mode", async () => { /** @@ -1563,15 +1513,16 @@ describe("ESLint", () => { cwd: path.join(fixtureDir, ".."), useEslintrc: false, fix: true, - rules: { - semi: 2, - quotes: [2, "double"], - eqeqeq: 2, - "no-undef": 2, - "space-infix-ops": 2 + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } } }); - const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); results.forEach(convertCRLF); @@ -1646,27 +1597,27 @@ describe("ESLint", () => { const baseOptions = { cwd: path.join(fixtureDir, ".."), useEslintrc: false, - rules: { - semi: 2, - quotes: [2, "double"], - eqeqeq: 2, - "no-undef": 2, - "space-infix-ops": 2 + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } } }; eslint = new ESLint(Object.assign({}, baseOptions, { cache: true, fix: false })); // Do initial lint run and populate the cache file - eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); eslint = new ESLint(Object.assign({}, baseOptions, { cache: true, fix: true })); - const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); - assert.ok(results.some(result => result.output)); + assert(results.some(result => result.output)); }); - }); // These tests have to do with https://github.com/eslint/eslint/issues/963 @@ -1679,7 +1630,6 @@ describe("ESLint", () => { cwd: path.join(fixtureDir, ".."), useEslintrc: false }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1692,7 +1642,6 @@ describe("ESLint", () => { cwd: path.join(fixtureDir, ".."), useEslintrc: false }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes-node.js`)]); assert.strictEqual(results.length, 1); @@ -1704,7 +1653,6 @@ describe("ESLint", () => { eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/process-exit.js`)]); assert.strictEqual(results.length, 1); @@ -1716,7 +1664,6 @@ describe("ESLint", () => { eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/process-exit.js`)]); assert.strictEqual(results.length, 1); @@ -1728,7 +1675,6 @@ describe("ESLint", () => { eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1739,11 +1685,9 @@ describe("ESLint", () => { // Project configuration - second level .eslintrc it("should return one message when executing with local .eslintrc that overrides parent .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1754,11 +1698,9 @@ describe("ESLint", () => { // Project configuration - third level .eslintrc it("should return one message when executing with local .eslintrc that overrides parent and grandparent .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/subsubbroken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1769,11 +1711,9 @@ describe("ESLint", () => { // Project configuration - first level package.json it("should return one message when executing with package.json", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1784,11 +1724,9 @@ describe("ESLint", () => { // Project configuration - second level package.json it("should return zero messages when executing with local package.json that overrides parent package.json", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/subsubdir/wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1797,11 +1735,9 @@ describe("ESLint", () => { // Project configuration - third level package.json it("should return one message when executing with local package.json that overrides parent and grandparent package.json", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/subsubdir/subsubsubdir/wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1812,11 +1748,9 @@ describe("ESLint", () => { // Project configuration - .eslintrc overrides package.json in same directory it("should return one message when executing with .eslintrc that overrides a package.json in the same directory", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1827,12 +1761,10 @@ describe("ESLint", () => { // Command line configuration - --config with first level .eslintrc it("should return two messages when executing with config file that adds to local .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1845,12 +1777,10 @@ describe("ESLint", () => { // Command line configuration - --config with first level .eslintrc it("should return no messages when executing with config file that overrides local .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1859,12 +1789,10 @@ describe("ESLint", () => { // Command line configuration - --config with second level .eslintrc it("should return two messages when executing with config file that adds to local and parent .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1877,12 +1805,10 @@ describe("ESLint", () => { // Command line configuration - --config with second level .eslintrc it("should return one message when executing with config file that overrides local and parent .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("config-hierarchy/broken/override-conf.yaml") + overrideConfigFile: getFixturePath("config-hierarchy/broken/override-conf.yaml") }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1893,12 +1819,10 @@ describe("ESLint", () => { // Command line configuration - --config with first level .eslintrc it("should return no messages when executing with config file that overrides local .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1907,15 +1831,15 @@ describe("ESLint", () => { // Command line configuration - --rule with --config and first level .eslintrc it("should return one message when executing with command line rule and config file that overrides local .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("config-hierarchy/broken/override-conf.yaml"), - rules: { - quotes: [1, "double"] + overrideConfigFile: getFixturePath("config-hierarchy/broken/override-conf.yaml"), + overrideConfig: { + rules: { + quotes: [1, "double"] + } } }); - const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); assert.strictEqual(results.length, 1); @@ -1926,15 +1850,15 @@ describe("ESLint", () => { // Command line configuration - --rule with --config and first level .eslintrc it("should return one message when executing with command line rule and config file that overrides local .eslintrc", async () => { - eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("/config-hierarchy/broken/override-conf.yaml"), - rules: { - quotes: [1, "double"] + overrideConfigFile: getFixturePath("/config-hierarchy/broken/override-conf.yaml"), + overrideConfig: { + rules: { + quotes: [1, "double"] + } } }); - const results = await eslint.lintFiles([getFixturePath("config-hierarchy/broken/console-wrong-quotes.js")]); assert.strictEqual(results.length, 1); @@ -1942,18 +1866,15 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages[0].ruleId, "quotes"); assert.strictEqual(results[0].messages[0].severity, 1); }); - }); describe("plugins", () => { it("should return two messages when executing with config file that specifies a plugin", async () => { - eslint = new ESLint({ + eslint = eslintWithPlugins({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "plugins-with-prefix.json"), - useEslintrc: false, - plugins: createMockedPluginsOption() + overrideConfigFile: getFixturePath("configurations", "plugins-with-prefix.json"), + useEslintrc: false }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test/test-custom-rule.js"))]); assert.strictEqual(results.length, 1); @@ -1962,13 +1883,11 @@ describe("ESLint", () => { }); it("should return two messages when executing with config file that specifies a plugin with namespace", async () => { - eslint = new ESLint({ + eslint = eslintWithPlugins({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "plugins-with-prefix-and-namespace.json"), - useEslintrc: false, - plugins: createMockedPluginsOption() + overrideConfigFile: getFixturePath("configurations", "plugins-with-prefix-and-namespace.json"), + useEslintrc: false }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); assert.strictEqual(results.length, 1); @@ -1977,13 +1896,11 @@ describe("ESLint", () => { }); it("should return two messages when executing with config file that specifies a plugin without prefix", async () => { - eslint = new ESLint({ + eslint = eslintWithPlugins({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "plugins-without-prefix.json"), - useEslintrc: false, - plugins: createMockedPluginsOption() + overrideConfigFile: getFixturePath("configurations", "plugins-without-prefix.json"), + useEslintrc: false }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); assert.strictEqual(results.length, 1); @@ -1992,13 +1909,11 @@ describe("ESLint", () => { }); it("should return two messages when executing with config file that specifies a plugin without prefix and with namespace", async () => { - eslint = new ESLint({ + eslint = eslintWithPlugins({ cwd: path.join(fixtureDir, ".."), - configFile: getFixturePath("configurations", "plugins-without-prefix-with-namespace.json"), - useEslintrc: false, - plugins: createMockedPluginsOption() + overrideConfigFile: getFixturePath("configurations", "plugins-without-prefix-with-namespace.json"), + useEslintrc: false }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); assert.strictEqual(results.length, 1); @@ -2007,13 +1922,14 @@ describe("ESLint", () => { }); it("should return two messages when executing with cli option that specifies a plugin", async () => { - eslint = new ESLint({ + eslint = eslintWithPlugins({ cwd: path.join(fixtureDir, ".."), useEslintrc: false, - plugins: ["example", ...createMockedPluginsOption()], - rules: { "example/example-rule": 1 } + overrideConfig: { + plugins: ["example"], + rules: { "example/example-rule": 1 } + } }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); assert.strictEqual(results.length, 1); @@ -2021,6 +1937,25 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); }); + it("should return two messages when executing with cli option that specifies preloaded plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + plugins: ["test"], + rules: { "test/example-rule": 1 } + }, + plugins: { + "eslint-plugin-test": { rules: { "example-rule": require("../../fixtures/rules/custom-rule") } } + } + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "test/example-rule"); + }); + it("should load plugins from the `loadPluginsRelativeTo` directory, if specified", async () => { eslint = new ESLint({ resolvePluginsRelativeTo: getFixturePath("plugins"), @@ -2030,7 +1965,6 @@ describe("ESLint", () => { }, useEslintrc: false }); - const results = await eslint.lintText("foo"); assert.strictEqual(results.length, 1); @@ -2077,8 +2011,61 @@ describe("ESLint", () => { deleteCache(); }); + describe("when the cacheFile is a directory or looks like a directory", () => { + + /** + * helper method to delete the cache files created during testing + * @returns {void} + */ + function deleteCacheDir() { + try { + fs.unlinkSync("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory"); + } catch (ex) { + + /* + * we don't care if the file didn't exist, since our + * intention was to remove the file + */ + } + } + beforeEach(() => { + deleteCacheDir(); + }); + + afterEach(() => { + deleteCacheDir(); + }); + + it("should create the cache file inside the provided directory", async () => { + assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir/", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + + sinon.restore(); + }); + }); + it("should create the cache file inside the provided directory using the cacheLocation option", async () => { - assert.isFalse(shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); eslint = new ESLint({ useEslintrc: false, @@ -2086,19 +2073,20 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, cacheLocation: "./tmp/.cacheFileDir/", - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"], ignore: false }); - const file = getFixturePath("cache/src", "test-file.js"); await eslint.lintFiles([file]); - assert.isTrue(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); sinon.restore(); }); @@ -2110,31 +2098,34 @@ describe("ESLint", () => { useEslintrc: false, cache: true, cwd, - rules: { - "no-console": 0 + overrideConfig: { + rules: { + "no-console": 0 + } }, extensions: ["js"], ignore: false }); - const file = getFixturePath("cli-engine", "console.js"); await eslint.lintFiles([file]); - assert.isTrue(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd"); + assert(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd"); }); it("should invalidate the cache if the configuration changed between executions", async () => { - assert.isFalse(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); eslint = new ESLint({ useEslintrc: false, // specifying cache true the cache will be created cache: true, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"], ignore: false @@ -2145,12 +2136,13 @@ describe("ESLint", () => { let file = getFixturePath("cache/src", "test-file.js"); file = fs.realpathSync(file); + const results = await eslint.lintFiles([file]); - const [result] = await eslint.lintFiles([file]); - - assert.strictEqual(result.errorCount + result.warningCount, 0, "the file passed without errors or warnings"); + for (const { errorCount, warningCount } of results) { + assert.strictEqual(errorCount + warningCount, 0, "the file passed without errors or warnings"); + } assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); - assert.isTrue(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); // destroy the spy sinon.restore(); @@ -2160,9 +2152,11 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - rules: { - "no-console": 2, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 2, + "no-unused-vars": 2 + } }, extensions: ["js"], ignore: false @@ -2175,21 +2169,22 @@ describe("ESLint", () => { assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed because the config changed"); assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used an one error was reported"); - assert.isTrue(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); }); it("should remember the files from a previous run and do not operate on them if not changed", async () => { - - assert.isFalse(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); eslint = new ESLint({ useEslintrc: false, // specifying cache true the cache will be created cache: true, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"], ignore: false @@ -2204,7 +2199,7 @@ describe("ESLint", () => { const result = await eslint.lintFiles([file]); assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); - assert.isTrue(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); // destroy the spy sinon.restore(); @@ -2214,9 +2209,11 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"], ignore: false @@ -2230,7 +2227,7 @@ describe("ESLint", () => { assert.deepStrictEqual(result, cachedResult, "the result is the same regardless of using cache or not"); // assert the file was not processed because the cache was used - assert.isFalse(spy.calledWith(file), "the file was not loaded because it used the cache"); + assert(!spy.calledWith(file), "the file was not loaded because it used the cache"); }); it("should remember the files from a previous run and do not operate on then if not changed", async () => { @@ -2241,15 +2238,17 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, cacheLocation, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"], cwd: path.join(fixtureDir, "..") }; - assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); eslint = new ESLint(eslintOptions); @@ -2259,21 +2258,20 @@ describe("ESLint", () => { await eslint.lintFiles([file]); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint was created"); + assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); eslintOptions.cache = false; eslint = new ESLint(eslintOptions); await eslint.lintFiles([file]); - assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint was deleted since last run did not used the cache"); + assert(!shell.test("-f", cacheLocation), "the cache for eslint was deleted since last run did not used the cache"); }); it("should store in the cache a file that failed the test", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2282,34 +2280,30 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, cacheLocation, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"] }); - const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); - const result = await eslint.lintFiles([badFile, goodFile]); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint was created"); - + assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); const fileCache = fCache.createFromFile(cacheLocation); const { cache } = fileCache; - assert.isTrue(typeof cache.getKey(goodFile) === "object", "the entry for the good file is in the cache"); - - assert.isTrue(typeof cache.getKey(badFile) === "object", "the entry for the bad file is in the cache"); - + assert.strictEqual(typeof cache.getKey(goodFile), "object", "the entry for the good file is in the cache"); + assert.strictEqual(typeof cache.getKey(badFile), "object", "the entry for the bad file is in the cache"); const cachedResult = await eslint.lintFiles([badFile, goodFile]); assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); }); it("should not contain in the cache a file that was deleted", async () => { - const cacheLocation = getFixturePath(".eslintcache"); doDelete(cacheLocation); @@ -2321,23 +2315,23 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, cacheLocation, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"] }); - const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); const toBeDeletedFile = fs.realpathSync(getFixturePath("cache/src", "file-to-delete.js")); await eslint.lintFiles([badFile, goodFile, toBeDeletedFile]); - const fileCache = fCache.createFromFile(cacheLocation); let { cache } = fileCache; - assert.isTrue(typeof cache.getKey(toBeDeletedFile) === "object", "the entry for the file to be deleted is in the cache"); + assert.strictEqual(typeof cache.getKey(toBeDeletedFile), "object", "the entry for the file to be deleted is in the cache"); // delete the file from the file system fs.unlinkSync(toBeDeletedFile); @@ -2350,11 +2344,10 @@ describe("ESLint", () => { cache = JSON.parse(fs.readFileSync(cacheLocation)); - assert.isTrue(typeof cache[toBeDeletedFile] === "undefined", "the entry for the file to be deleted is not in the cache"); + assert.strictEqual(typeof cache[toBeDeletedFile], "undefined", "the entry for the file to be deleted is not in the cache"); }); it("should contain files that were not visited in the cache provided they still exist", async () => { - const cacheLocation = getFixturePath(".eslintcache"); doDelete(cacheLocation); @@ -2366,13 +2359,14 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, cacheLocation, - rules: { - "no-console": 0, - "no-unused-vars": 2 + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, extensions: ["js"] }); - const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); const testFile2 = fs.realpathSync(getFixturePath("cache/src", "test-file2.js")); @@ -2382,7 +2376,7 @@ describe("ESLint", () => { let fileCache = fCache.createFromFile(cacheLocation); let { cache } = fileCache; - assert.isTrue(typeof cache.getKey(testFile2) === "object", "the entry for the test-file2 is in the cache"); + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); /* * we pass a different set of files minus test-file2 @@ -2394,359 +2388,377 @@ describe("ESLint", () => { fileCache = fCache.createFromFile(cacheLocation); cache = fileCache.cache; - assert.isTrue(typeof cache.getKey(testFile2) === "object", "the entry for the test-file2 is in the cache"); + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); }); - describe("cache deletion", () => { - beforeEach(async () => { - const cacheLocation = getFixturePath(".eslintcache"); - const file = getFixturePath("cli-engine", "console.js"); + it("should not delete cache when executing on text", async () => { + const cacheLocation = getFixturePath(".eslintcache"); - eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), - useEslintrc: false, - cacheLocation, - cache: true, + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + overrideConfig: { rules: { "no-console": 0, "no-unused-vars": 2 - }, - extensions: ["js"] - }); - - await eslint.lintFiles([file]); + } + }, + extensions: ["js"] }); - it("should not delete cache when executing on text", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); - eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), - useEslintrc: false, - cacheLocation, + await eslint.lintText("var foo = 'bar';"); + + assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should not delete cache when executing on text with a provided filename", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + overrideConfig: { rules: { "no-console": 0, "no-unused-vars": 2 - }, - extensions: ["js"] - }); + } + }, + extensions: ["js"] + }); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); - await eslint.lintText("var foo = 'bar';"); + await eslint.lintText("var bar = foo;", { filePath: "fixtures/passing.js" }); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint still exists"); - }); + assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); - it("should not delete cache when executing on text with a provided filename", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + it("should not delete cache when executing on files with --cache flag", async () => { + const cacheLocation = getFixturePath(".eslintcache"); - eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), - useEslintrc: false, - cacheLocation, + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cache: true, + cacheLocation, + overrideConfig: { rules: { "no-console": 0, "no-unused-vars": 2 - }, - extensions: ["js"] - }); - - await eslint.lintText("var bar = foo;", "fixtures/passing.js"); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); - - await eslint.lintText("var bar = foo;", "fixtures/passing.js"); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint still exists"); - }); - - it("should not delete cache when executing on files with --cache flag", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - const file = getFixturePath("cli-engine", "console.js"); + } + }, + extensions: ["js"] + }); + const file = getFixturePath("cli-engine", "console.js"); - eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), - useEslintrc: false, - cacheLocation, - cache: true, + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should delete cache when executing on files without --cache flag", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + overrideConfig: { rules: { "no-console": 0, "no-unused-vars": 2 - }, - extensions: ["js"] - }); + } + }, + extensions: ["js"] + }); + const file = getFixturePath("cli-engine", "console.js"); + + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + await eslint.lintFiles([file]); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(!shell.test("-f", cacheLocation), "the cache for eslint has been deleted"); + }); - await eslint.lintFiles([file]); - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint still exists"); - }); + describe("cacheFile", () => { + it("should use the specified cache file", async () => { + const customCacheFile = path.resolve(".cache/custom-cache"); - it("should delete cache when executing on files without --cache flag", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - const file = getFixturePath("cli-engine", "console.js"); + assert(!shell.test("-f", customCacheFile), "the cache for eslint does not exist"); eslint = new ESLint({ - cwd: path.join(fixtureDir, ".."), useEslintrc: false, - cacheLocation, - rules: { - "no-console": 0, - "no-unused-vars": 2 + + // specify a custom cache file + cacheLocation: customCacheFile, + + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } }, - extensions: ["js"] + extensions: ["js"], + cwd: path.join(fixtureDir, "..") }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const result = await eslint.lintFiles([badFile, goodFile]); + assert(shell.test("-f", customCacheFile), "the cache for eslint was created"); + const fileCache = fCache.createFromFile(customCacheFile); + const { cache } = fileCache; - assert.isTrue(shell.test("-f", cacheLocation), "the cache for eslint exists"); - await eslint.lintFiles([file]); - assert.isFalse(shell.test("-f", cacheLocation), "the cache for eslint has been deleted"); - }); + assert(typeof cache.getKey(goodFile) === "object", "the entry for the good file is in the cache"); + assert(typeof cache.getKey(badFile) === "object", "the entry for the bad file is in the cache"); + const cachedResult = await eslint.lintFiles([badFile, goodFile]); + + assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); + }); }); }); describe("processors", () => { it("should return two messages when executing with config file that specifies a processor", async () => { - eslint = new ESLint({ - configFile: getFixturePath("configurations", "processors.json"), + eslint = eslintWithPlugins({ + overrideConfigFile: getFixturePath("configurations", "processors.json"), useEslintrc: false, extensions: ["js", "txt"], - plugins: createMockedPluginsOption(), cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("processors", "test", "test-processor.txt"))]); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].messages.length, 2); }); - xit("should return two messages when executing with config file that specifies preloaded processor", async () => { + it("should return two messages when executing with config file that specifies preloaded processor", async () => { eslint = new ESLint({ useEslintrc: false, - plugins: ["test-processor"], - rules: { - "no-console": 2, - "no-unused-vars": 2 + overrideConfig: { + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } }, extensions: ["js", "txt"], - cwd: path.join(fixtureDir, "..") + cwd: path.join(fixtureDir, ".."), + plugins: { + "test-processor": { + processors: { + ".txt": { + preprocess(text) { + return [text]; + }, + postprocess(messages) { + return messages[0]; + } + } + } + } + } }); - - /* - * engine.addPlugin("test-processor", { - * processors: { - * ".txt": { - * preprocess(text) { - * return [text]; - * }, - * postprocess(messages) { - * return messages[0]; - * } - * } - * } - * }); - */ - const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("processors", "test", "test-processor.txt"))]); assert.strictEqual(results.length, 1); assert.strictEqual(results[0].messages.length, 2); }); - it("should run processors when calling executeOnFiles with config file that specifies a processor", async () => { - eslint = new ESLint({ - configFile: getFixturePath("configurations", "processors.json"), + it("should run processors when calling lintFiles with config file that specifies a processor", async () => { + eslint = eslintWithPlugins({ + overrideConfigFile: getFixturePath("configurations", "processors.json"), useEslintrc: false, extensions: ["js", "txt"], - plugins: createMockedPluginsOption(), cwd: path.join(fixtureDir, "..") }); - const results = await eslint.lintFiles([getFixturePath("processors", "test", "test-processor.txt")]); assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); }); - xit("should run processors when calling executeOnFiles with config file that specifies preloaded processor", async () => { + it("should run processors when calling lintFiles with config file that specifies preloaded processor", async () => { eslint = new ESLint({ useEslintrc: false, - plugins: ["test-processor"], - rules: { - "no-console": 2, - "no-unused-vars": 2 + overrideConfig: { + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } }, extensions: ["js", "txt"], - cwd: path.join(fixtureDir, "..") + cwd: path.join(fixtureDir, ".."), + plugins: { + "test-processor": { + processors: { + ".txt": { + preprocess(text) { + return [text.replace("a()", "b()")]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + } }); - - /* - * engine.addPlugin("test-processor", { - * processors: { - * ".txt": { - * preprocess(text) { - * return [text.replace("a()", "b()")]; - * }, - * postprocess(messages) { - * messages[0][0].ruleId = "post-processed"; - * return messages[0]; - * } - * } - * } - * }); - */ - const results = await eslint.lintFiles([getFixturePath("processors", "test", "test-processor.txt")]); assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); }); - it("should run processors when calling executeOnText with config file that specifies a processor", async () => { - eslint = new ESLint({ - configFile: getFixturePath("configurations", "processors.json"), + it("should run processors when calling lintText with config file that specifies a processor", async () => { + eslint = eslintWithPlugins({ + overrideConfigFile: getFixturePath("configurations", "processors.json"), useEslintrc: false, extensions: ["js", "txt"], - plugins: createMockedPluginsOption(), ignore: false }); - const results = await eslint.lintText("function a() {console.log(\"Test\");}", { filePath: "tests/fixtures/processors/test/test-processor.txt" }); assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); }); - xit("should run processors when calling executeOnText with config file that specifies preloaded processor", async () => { + it("should run processors when calling lintText with config file that specifies preloaded processor", async () => { eslint = new ESLint({ useEslintrc: false, - plugins: ["test-processor"], - rules: { - "no-console": 2, - "no-unused-vars": 2 + overrideConfig: { + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } }, extensions: ["js", "txt"], - ignore: false + ignore: false, + plugins: { + "test-processor": { + processors: { + ".txt": { + preprocess(text) { + return [text.replace("a()", "b()")]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + } }); - - /* - * engine.addPlugin("test-processor", { - * processors: { - * ".txt": { - * preprocess(text) { - * return [text.replace("a()", "b()")]; - * }, - * postprocess(messages) { - * messages[0][0].ruleId = "post-processed"; - * return messages[0]; - * } - * } - * } - * }); - */ - - const results = await eslint.lintText("function a() {console.log(\"Test\");}", "tests/fixtures/processors/test/test-processor.txt"); + const results = await eslint.lintText("function a() {console.log(\"Test\");}", { filePath: "tests/fixtures/processors/test/test-processor.txt" }); assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); }); - xdescribe("autofixing with processors", async () => { - - /* - * const HTML_PROCESSOR = Object.freeze({ - * preprocess(text) { - * return [text.replace(/^", "foo.html"); + const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 0); assert.strictEqual(results[0].output, ""); }); - xit("should not run in autofix mode when using a processor that does not support autofixing", async () => { + it("should not run in autofix mode when using a processor that does not support autofixing", async () => { eslint = new ESLint({ useEslintrc: false, - plugins: ["test-processor"], - rules: { - semi: 2 + overrideConfig: { + plugins: ["test-processor"], + rules: { + semi: 2 + } }, extensions: ["js", "txt"], ignore: false, - fix: true + fix: true, + plugins: { + "test-processor": { processors: { ".html": HTML_PROCESSOR } } + } }); - - // engine.addPlugin("test-processor", { processors: { ".html": HTML_PROCESSOR } }); - - const results = await eslint.lintText("", "foo.html"); + const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 1); - assert.isFalse(Object.prototype.hasOwnProperty.call(results[0], "output")); + assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); }); - xit("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", async () => { + it("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", async () => { eslint = new ESLint({ useEslintrc: false, - plugins: ["test-processor"], - rules: { - semi: 2 + overrideConfig: { + plugins: ["test-processor"], + rules: { + semi: 2 + } }, extensions: ["js", "txt"], - ignore: false + ignore: false, + plugins: { + "test-processor": { + processors: { + ".html": Object.assign({ supportsAutofix: true }, HTML_PROCESSOR) + } + } + } }); - - /* - * engine.addPlugin("test-processor", { - * processors: { - * ".html": Object.assign({ supportsAutofix: true }, HTML_PROCESSOR) - * } - * }); - */ - - const results = await eslint.lintText("", "foo.html"); + const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 1); - assert.isFalse(Object.prototype.hasOwnProperty.call(results[0], "output")); + assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); }); }); }); @@ -2760,74 +2772,57 @@ describe("ESLint", () => { }); it("one file", async () => { - try { + await assert.rejects(async () => { await eslint.lintFiles(["non-exist.js"]); - } catch (e) { - assert.isTrue(/No files matching 'non-exist.js' were found./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /No files matching 'non-exist\.js' were found\./u); }); it("should throw if the directory exists and is empty", async () => { - try { + await assert.rejects(async () => { await eslint.lintFiles(["empty"]); - } catch (e) { - assert.isTrue(/No files matching 'empty' were found./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /No files matching 'empty' were found\./u); }); it("one glob pattern", async () => { - try { + await assert.rejects(async () => { await eslint.lintFiles(["non-exist/**/*.js"]); - } catch (e) { - assert.isTrue(/No files matching 'non-exist\/\*\*\/\*.js' were found./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /No files matching 'non-exist\/\*\*\/\*\.js' were found\./u); }); it("two files", async () => { - try { + await assert.rejects(async () => { await eslint.lintFiles(["aaa.js", "bbb.js"]); - } catch (e) { - assert.isTrue(/No files matching 'aaa.js' were found./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /No files matching 'aaa\.js' were found\./u); }); it("a mix of an existing file and a non-existing file", async () => { - try { + await assert.rejects(async () => { await eslint.lintFiles(["console.js", "non-exist.js"]); - } catch (e) { - assert.isTrue(/No files matching 'non-exist.js' were found./u.test(e.message)); - return; - } - assert.fail("Expected to throw an error"); + }, /No files matching 'non-exist\.js' were found\./u); }); }); describe("overrides", () => { - it("should recognize dotfiles", async () => { + beforeEach(() => { eslint = new ESLint({ cwd: getFixturePath("cli-engine/overrides-with-dot"), ignore: false }); - const results = await eslint.lintFiles([".test-target.js"]); + }); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].messages[0].ruleId, "no-unused-vars"); + it("should recognize dotfiles", async () => { + const ret = await eslint.lintFiles([".test-target.js"]); + + assert.strictEqual(ret.length, 1); + assert.strictEqual(ret[0].messages.length, 1); + assert.strictEqual(ret[0].messages[0].ruleId, "no-unused-vars"); }); }); - describe("a config file setting should have higher priority than a shareable config file's settings always; https://github.com/eslint/eslint/issues/11510", async () => { + 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(() => { ({ ESLint } = defineESLintWithInMemoryFileSystem({ - cwd: () => path.join(os.tmpdir(), "cli-engine/11510"), + cwd: () => path.join(os.tmpdir(), "eslint/11510"), files: { "no-console-error-in-overrides.json": JSON.stringify({ overrides: [{ @@ -2853,10 +2848,10 @@ describe("ESLint", () => { }); }); - describe("configs of plugin rules should be validated even if 'plugins' key doesn't exist; https://github.com/eslint/eslint/issues/11559", async () => { + describe("configs of plugin rules should be validated even if 'plugins' key doesn't exist; https://github.com/eslint/eslint/issues/11559", () => { beforeEach(() => { ({ ESLint } = defineESLintWithInMemoryFileSystem({ - cwd: () => path.join(os.tmpdir(), "cli-engine/11559"), + cwd: () => path.join(os.tmpdir(), "eslint/11559"), files: { "node_modules/eslint-plugin-test/index.js": ` exports.configs = { @@ -2884,21 +2879,16 @@ describe("ESLint", () => { }); it("should throw fatal error.", async () => { - try { + await assert.rejects(async () => { await eslint.lintFiles("a.js"); - } catch (e) { - assert.isTrue(/invalid-option/u.test(e.message)); - return; - } - - assert.fail("Expected to throw an error"); + }, /invalid-option/u); }); }); - describe("'--fix-type' should not crash even if plugin rules exist; https://github.com/eslint/eslint/issues/11586", async () => { + describe("'--fix-type' should not crash even if plugin rules exist; https://github.com/eslint/eslint/issues/11586", () => { beforeEach(() => { ({ ESLint } = defineESLintWithInMemoryFileSystem({ - cwd: () => path.join(os.tmpdir(), "cli-engine/11586"), + cwd: () => path.join(os.tmpdir(), "eslint/11586"), files: { "node_modules/eslint-plugin-test/index.js": ` exports.rules = { @@ -2940,7 +2930,7 @@ describe("ESLint", () => { }); describe("multiple processors", () => { - const root = path.join(os.tmpdir(), "eslint/cli-engine/multiple-processors"); + const root = path.join(os.tmpdir(), "eslint/eslint/multiple-processors"); const commonFiles = { "node_modules/pattern-processor/index.js": fs.readFileSync( require.resolve("../../fixtures/processors/pattern-processor"), @@ -2981,7 +2971,7 @@ describe("ESLint", () => { }; it("should lint only JavaScript blocks if '--ext' was not given.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -2990,9 +2980,8 @@ describe("ESLint", () => { rules: { semi: "error" } }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3002,7 +2991,7 @@ describe("ESLint", () => { }); it("should fix only JavaScript blocks if '--ext' was not given.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3011,9 +3000,8 @@ describe("ESLint", () => { rules: { semi: "error" } }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root, fix: true }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3035,7 +3023,7 @@ describe("ESLint", () => { }); it("should lint HTML blocks as well with multiple processors if '--ext' option was given.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3044,9 +3032,8 @@ describe("ESLint", () => { rules: { semi: "error" } }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3058,7 +3045,7 @@ describe("ESLint", () => { }); it("should fix HTML blocks as well with multiple processors if '--ext' option was given.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3067,9 +3054,8 @@ describe("ESLint", () => { rules: { semi: "error" } }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root, extensions: ["js", "html"], fix: true }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3091,7 +3077,7 @@ describe("ESLint", () => { }); it("should use overriden processor; should report HTML blocks but not fix HTML blocks if the processor for '*.html' didn't support autofix.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3106,9 +3092,8 @@ describe("ESLint", () => { ] }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root, extensions: ["js", "html"], fix: true }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3133,7 +3118,7 @@ describe("ESLint", () => { }); it("should use the config '**/*.html/*.js' to lint JavaScript blocks in HTML.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3160,9 +3145,8 @@ describe("ESLint", () => { ] }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3174,7 +3158,7 @@ describe("ESLint", () => { }); it("should use the same config as one which has 'processor' property in order to lint blocks in HTML if the processor was legacy style.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3200,9 +3184,8 @@ describe("ESLint", () => { ] }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3216,7 +3199,7 @@ describe("ESLint", () => { }); it("should throw an error if invalid processor was specified.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3225,21 +3208,16 @@ describe("ESLint", () => { processor: "markdown/unknown" }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root }); - try { + await assert.rejects(async () => { await eslint.lintFiles(["test.md"]); - } catch (e) { - assert.isTrue(/ESLint configuration of processor in '\.eslintrc\.json' is invalid: 'markdown\/unknown' was not found\./u.test(e.message)); - return; - } - - assert.fail("Expected to throw an error"); + }, /ESLint configuration of processor in '\.eslintrc\.json' is invalid: 'markdown\/unknown' was not found\./u); }); it("should lint HTML blocks as well with multiple processors if 'overrides[].files' is present.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ...commonFiles, @@ -3258,9 +3236,8 @@ describe("ESLint", () => { ] }) } - })); + }).ESLint; eslint = new ESLint({ cwd: root }); - const results = await eslint.lintFiles(["test.md"]); assert.strictEqual(results.length, 1); @@ -3302,7 +3279,7 @@ describe("ESLint", () => { assert.deepStrictEqual(err.messageData, { importerName: `extends-plugin${path.sep}.eslintrc.yml`, pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: path.join(cwd, "extends-plugin") + resolvePluginsRelativeTo: path.join(cwd, "extends-plugin") // the directory of the config file. }); return; } @@ -3318,7 +3295,7 @@ describe("ESLint", () => { assert.deepStrictEqual(err.messageData, { importerName: `plugins${path.sep}.eslintrc.yml`, pluginName: "eslint-plugin-nonexistent-plugin", - resolvePluginsRelativeTo: path.join(cwd, "plugins") + resolvePluginsRelativeTo: path.join(cwd, "plugins") // the directory of the config file. }); return; } @@ -3370,11 +3347,10 @@ describe("ESLint", () => { }); }); - describe("with '--rulesdir' option", async () => { + describe("with '--rulesdir' option", () => { it("should use the configured rules which are defined by '--rulesdir' option.", async () => { const rootPath = getFixturePath("cli-engine/with-rulesdir"); - - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + const StubbedESLint = defineESLintWithInMemoryFileSystem({ cwd: () => rootPath, files: { "internal-rules/test.js": ` @@ -3390,8 +3366,9 @@ describe("ESLint", () => { }), "test.js": "console.log('hello')" } - })); - eslint = new ESLint({ + }).ESLint; + + eslint = new StubbedESLint({ rulePaths: ["internal-rules"] }); const results = await eslint.lintFiles(["test.js"]); @@ -3406,7 +3383,7 @@ describe("ESLint", () => { const root = getFixturePath("cli-engine/unmatched-glob"); it("should match '[ab].js' if existed.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "a.js": "", @@ -3415,9 +3392,8 @@ describe("ESLint", () => { "[ab].js": "", ".eslintrc.yml": "root: true" } - })); + }).ESLint; eslint = new ESLint(); - const results = await eslint.lintFiles(["[ab].js"]); const filenames = results.map(r => path.basename(r.filePath)); @@ -3425,7 +3401,7 @@ describe("ESLint", () => { }); it("should match 'a.js' and 'b.js' if '[ab].js' didn't existed.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "a.js": "", @@ -3433,9 +3409,8 @@ describe("ESLint", () => { "ab.js": "", ".eslintrc.yml": "root: true" } - })); + }).ESLint; eslint = new ESLint(); - const results = await eslint.lintFiles(["[ab].js"]); const filenames = results.map(r => path.basename(r.filePath)); @@ -3443,57 +3418,57 @@ describe("ESLint", () => { }); }); - describe("with 'noInlineConfig' setting", async () => { + describe("with 'noInlineConfig' setting", () => { const root = getFixturePath("cli-engine/noInlineConfig"); it("should warn directive comments if 'noInlineConfig' was given.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "test.js": "/* globals foo */", ".eslintrc.yml": "noInlineConfig: true" } - })); + }).ESLint; eslint = new ESLint(); - - const [{ messages }] = await eslint.lintFiles(["test.js"]); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; assert.strictEqual(messages.length, 1); assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config (.eslintrc.yml)."); }); it("should show the config file what the 'noInlineConfig' came from.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "node_modules/eslint-config-foo/index.js": "module.exports = {noInlineConfig: true}", "test.js": "/* globals foo */", ".eslintrc.yml": "extends: foo" } - })); + }).ESLint; eslint = new ESLint(); - - const [{ messages }] = await eslint.lintFiles(["test.js"]); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; assert.strictEqual(messages.length, 1); assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config (.eslintrc.yml » eslint-config-foo)."); }); }); - describe("with 'reportUnusedDisableDirectives' setting", async () => { + describe("with 'reportUnusedDisableDirectives' setting", () => { const root = getFixturePath("cli-engine/reportUnusedDisableDirectives"); it("should warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives' was given.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "test.js": "/* eslint-disable eqeqeq */", ".eslintrc.yml": "reportUnusedDisableDirectives: true" } - })); + }).ESLint; eslint = new ESLint(); - - const [{ messages }] = await eslint.lintFiles(["test.js"]); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; assert.strictEqual(messages.length, 1); assert.strictEqual(messages[0].severity, 1); @@ -3502,31 +3477,31 @@ describe("ESLint", () => { describe("the runtime option overrides config files.", () => { it("should not warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives=off' was given in runtime.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "test.js": "/* eslint-disable eqeqeq */", ".eslintrc.yml": "reportUnusedDisableDirectives: true" } - })); - eslint = new ESLint({ reportUnusedDisableDirectives: false }); - - const [{ messages }] = await eslint.lintFiles(["test.js"]); + }).ESLint; + eslint = new ESLint({ reportUnusedDisableDirectives: "off" }); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; assert.strictEqual(messages.length, 0); }); it("should warn unused 'eslint-disable' comments as error if 'reportUnusedDisableDirectives=error' was given in runtime.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "test.js": "/* eslint-disable eqeqeq */", ".eslintrc.yml": "reportUnusedDisableDirectives: true" } - })); - eslint = new ESLint({ reportUnusedDisableDirectives: true }); - - const [{ messages }] = await eslint.lintFiles(["test.js"]); + }).ESLint; + eslint = new ESLint({ reportUnusedDisableDirectives: "error" }); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; assert.strictEqual(messages.length, 1); assert.strictEqual(messages[0].severity, 2); @@ -3535,11 +3510,11 @@ describe("ESLint", () => { }); }); - describe("with 'overrides[*].extends' setting on deep locations", async () => { + describe("with 'overrides[*].extends' setting on deep locations", () => { const root = getFixturePath("cli-engine/deeply-overrides-i-extends"); it("should not throw.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ @@ -3554,10 +3529,10 @@ describe("ESLint", () => { "test.js": "console.log('hello')", ".eslintrc.yml": "extends: one" } - })); + }).ESLint; eslint = new ESLint(); - - const [{ messages }] = await eslint.lintFiles(["test.js"]); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; assert.strictEqual(messages.length, 1); assert.strictEqual(messages[0].ruleId, "no-console"); @@ -3567,49 +3542,2406 @@ describe("ESLint", () => { describe("don't ignore the entry directory.", () => { const root = getFixturePath("cli-engine/dont-ignore-entry-dir"); - it("'executeOnFiles(\".\")' should not load config files from outside of \".\".", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + it("'lintFiles(\".\")' should not load config files from outside of \".\".", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "../.eslintrc.json": "BROKEN FILE", ".eslintrc.json": JSON.stringify({ root: true }), "index.js": "console.log(\"hello\")" } - })); + }).ESLint; eslint = new ESLint(); // Don't throw "failed to load config file" error. - eslint.lintFiles("."); + await eslint.lintFiles("."); }); - it("'executeOnFiles(\".\")' should not ignore '.' even if 'ignorePatterns' contains it.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + it("'lintFiles(\".\")' should not ignore '.' even if 'ignorePatterns' contains it.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { "../.eslintrc.json": JSON.stringify({ ignorePatterns: ["/dont-ignore-entry-dir"] }), ".eslintrc.json": JSON.stringify({ root: true }), "index.js": "console.log(\"hello\")" } - })); + }).ESLint; eslint = new ESLint(); // Don't throw "file not found" error. - eslint.lintFiles("."); + await eslint.lintFiles("."); }); - it("'executeOnFiles(\"subdir\")' should not ignore './subdir' even if 'ignorePatterns' contains it.", async () => { - ({ ESLint } = defineESLintWithInMemoryFileSystem({ + it("'lintFiles(\"subdir\")' should not ignore './subdir' even if 'ignorePatterns' contains it.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ cwd: () => root, files: { ".eslintrc.json": JSON.stringify({ ignorePatterns: ["/subdir"] }), "subdir/.eslintrc.json": JSON.stringify({ root: true }), "subdir/index.js": "console.log(\"hello\")" } - })); + }).ESLint; eslint = new ESLint(); // Don't throw "file not found" error. - eslint.lintFiles("subdir"); + await eslint.lintFiles("subdir"); + }); + }); + }); + + describe("calculateConfigForFile", () => { + it("should return the info from Config#getConfig when called", async () => { + const options = { + overrideConfigFile: getFixturePath("configurations", "quotes-error.json") + }; + const engine = new ESLint(options); + const filePath = getFixturePath("single-quoted.js"); + const actualConfig = await engine.calculateConfigForFile(filePath); + const expectedConfig = new CascadingConfigArrayFactory({ specificConfigPath: options.overrideConfigFile }) + .getConfigArrayForFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(actualConfig, expectedConfig); + }); + + + it("should return the config when run from within a subdir", async () => { + const options = { + cwd: getFixturePath("config-hierarchy", "root-true", "parent", "root", "subdir") + }; + const engine = new ESLint(options); + const filePath = getFixturePath("config-hierarchy", "root-true", "parent", "root", ".eslintrc"); + const actualConfig = await engine.calculateConfigForFile("./.eslintrc"); + const expectedConfig = new CascadingConfigArrayFactory(options) + .getConfigArrayForFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(actualConfig, expectedConfig); + }); + + it("should throw an error if a directory path was given.", async () => { + const engine = new ESLint(); + + try { + await engine.calculateConfigForFile("."); + } catch (error) { + assert.strictEqual(error.messageTemplate, "print-config-with-directory-path"); + return; + } + assert.fail("should throw an error"); + }); + }); + + describe("isPathIgnored", () => { + it("should check if the given path is ignored", async () => { + const engine = new ESLint({ + ignorePath: getFixturePath(".eslintignore2"), + cwd: getFixturePath() + }); + + assert(await engine.isPathIgnored("undef.js")); + assert(!await engine.isPathIgnored("passing.js")); + }); + + it("should return false if ignoring is disabled", async () => { + const engine = new ESLint({ + ignore: false, + ignorePath: getFixturePath(".eslintignore2"), + cwd: getFixturePath() + }); + + assert(!await engine.isPathIgnored("undef.js")); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should return true for default ignores even if ignoring is disabled", async () => { + const engine = new ESLint({ + ignore: false, + cwd: getFixturePath("cli-engine") + }); + + assert(await engine.isPathIgnored("node_modules/foo.js")); + }); + + describe("about the default ignore patterns", () => { + it("should always apply defaultPatterns if ignore option is true", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/package/file.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/node_modules/package/file.js"))); + }); + + it("should still apply defaultPatterns if ignore option is is false", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/package/file.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/node_modules/package/file.js"))); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ + cwd, + overrideConfig: { + ignorePatterns: "!/node_modules/package" + } + }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePath", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd, ignorePath: getFixturePath("ignored-paths", ".eslintignoreWithUnignoredDefaults") }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should ignore dotfiles", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar"))); + }); + + it("should ignore directories beginning with a dot", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo/bar"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar/baz"))); + }); + + it("should still ignore dotfiles when ignore option disabled", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar"))); + }); + + it("should still ignore directories beginning with a dot when ignore option disabled", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo/bar"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar/baz"))); + }); + + it("should not ignore absolute paths containing '..'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(!await engine.isPathIgnored(`${getFixturePath("ignored-paths", "foo")}/../unignored.js`)); + }); + + it("should ignore /node_modules/ relative to .eslintignore when loaded", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignorePath: getFixturePath("ignored-paths", ".eslintignore"), cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "existing.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo", "node_modules", "existing.js"))); + }); + + it("should ignore /node_modules/ relative to cwd without an .eslintignore", async () => { + const cwd = getFixturePath("ignored-paths", "no-ignore-file"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "no-ignore-file", "node_modules", "existing.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "no-ignore-file", "foo", "node_modules", "existing.js"))); + }); + }); + + describe("with no .eslintignore file", () => { + it("should not travel to parent directories to find .eslintignore when it's missing and cwd is provided", async () => { + const cwd = getFixturePath("ignored-paths", "configurations"); + const engine = new ESLint({ cwd }); + + // a .eslintignore in parent directories includes `*.js`, but don't load it. + assert(!await engine.isPathIgnored("foo.js")); + assert(await engine.isPathIgnored("node_modules/foo.js")); + }); + + it("should return false for files outside of the cwd (with no ignore file provided)", async () => { + + // Default ignore patterns should not inadvertently ignore files in parent directories + const engine = new ESLint({ cwd: getFixturePath("ignored-paths", "no-ignore-file") }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + }); + + describe("with .eslintignore file or package.json file", () => { + it("should load .eslintignore from cwd when explicitly passed", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + // `${cwd}/.eslintignore` includes `sampleignorepattern`. + assert(await engine.isPathIgnored("sampleignorepattern")); + }); + + it("should use package.json's eslintIgnore files if no specified .eslintignore file", async () => { + const cwd = getFixturePath("ignored-paths", "package-json-ignore"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored("hello.js")); + assert(await engine.isPathIgnored("world.js")); + }); + + it("should use correct message template if failed to parse package.json", () => { + const cwd = getFixturePath("ignored-paths", "broken-package-json"); + + assert.throws(() => { + try { + // eslint-disable-next-line no-new + new ESLint({ cwd }); + } catch (error) { + assert.strictEqual(error.messageTemplate, "failed-to-read-json"); + throw error; + } + }); + }); + + it("should not use package.json's eslintIgnore files if specified .eslintignore file", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + /* + * package.json includes `hello.js` and `world.js`. + * .eslintignore includes `sampleignorepattern`. + */ + assert(!await engine.isPathIgnored("hello.js")); + assert(!await engine.isPathIgnored("world.js")); + assert(await engine.isPathIgnored("sampleignorepattern")); + }); + + it("should error if package.json's eslintIgnore is not an array of file paths", () => { + const cwd = getFixturePath("ignored-paths", "bad-package-json-ignore"); + + assert.throws(() => { + // eslint-disable-next-line no-new + new ESLint({ cwd }); + }, /Package\.json eslintIgnore property requires an array of paths/u); + }); + }); + + describe("with --ignore-pattern option", () => { + it("should accept a string for options.ignorePattern", async () => { + const cwd = getFixturePath("ignored-paths", "ignore-pattern"); + const engine = new ESLint({ + overrideConfig: { + ignorePatterns: "ignore-me.txt" + }, + cwd + }); + + assert(await engine.isPathIgnored("ignore-me.txt")); + }); + + it("should accept an array for options.ignorePattern", async () => { + const engine = new ESLint({ + overrideConfig: { + ignorePatterns: ["a", "b"] + }, + useEslintrc: false + }); + + assert(await engine.isPathIgnored("a")); + assert(await engine.isPathIgnored("b")); + assert(!await engine.isPathIgnored("c")); + }); + + it("should return true for files which match an ignorePattern even if they do not exist on the filesystem", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ + overrideConfig: { + ignorePatterns: "not-a-file" + }, + cwd + }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "not-a-file"))); + }); + + it("should return true for file matching an ignore pattern exactly", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "undef.js" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should return false for file matching an invalid ignore pattern with leading './'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "./undef.js" }, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should return false for file in subfolder of cwd matching an ignore pattern with leading '/'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "/undef.js" }, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir", "undef.js"))); + }); + + it("should return true for file matching a child of an ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "ignore-pattern" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "ignore-pattern", "ignore-me.txt"))); + }); + + it("should return true for file matching a grandchild of an ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "ignore-pattern" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "ignore-pattern", "subdir", "ignore-me.txt"))); + }); + + it("should return false for file not matching any ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "failing.js" }, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "unignored.js"))); + }); + + it("two globstar '**' ignore pattern should ignore files in nested directories", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "**/*.js" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.js"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.j2"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.j2"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.j2"))); + }); + }); + + describe("with --ignore-path option", () => { + it("initialization with ignorePath should work when cwd is a parent directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("custom-name/foo.js")); + }); + + it("initialization with ignorePath should work when the file is in the cwd", async () => { + const cwd = getFixturePath("ignored-paths", "custom-name"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("foo.js")); + }); + + it("initialization with ignorePath should work when cwd is a subdirectory", async () => { + const cwd = getFixturePath("ignored-paths", "custom-name", "subdirectory"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("../custom-name/foo.js")); + }); + + it("initialization with invalid file should throw error", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "not-a-directory", ".foobaz"); + + assert.throws(() => { + // eslint-disable-next-line no-new + new ESLint({ ignorePath, cwd }); + }, "Cannot read .eslintignore file"); + }); + + it("should return false for files outside of ignorePath's directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should resolve relative paths from CWD", async () => { + const cwd = getFixturePath("ignored-paths", "subdir"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should resolve relative paths from CWD when it's in a child directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/foo.js"))); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/bar.js"))); + }); + + it("should resolve relative paths from CWD when it contains negated globs", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("subdir/blah.txt")); + assert(await engine.isPathIgnored("blah.txt")); + assert(await engine.isPathIgnored("subdir/bar.txt")); + assert(!await engine.isPathIgnored("bar.txt")); + assert(!await engine.isPathIgnored("subdir/baz.txt")); + assert(!await engine.isPathIgnored("baz.txt")); + }); + + it("should resolve default ignore patterns from the CWD even when the ignorePath is in a subdirectory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("node_modules/blah.js")); + }); + + it("should resolve default ignore patterns from the CWD even when the ignorePath is in a parent directory", async () => { + const cwd = getFixturePath("ignored-paths", "subdir"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("node_modules/blah.js")); + }); + + it("should handle .eslintignore which contains CRLF correctly.", async () => { + const ignoreFileContent = fs.readFileSync(getFixturePath("ignored-paths", "crlf/.eslintignore"), "utf8"); + + assert(ignoreFileContent.includes("\r"), "crlf/.eslintignore should contains CR."); + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "crlf/.eslintignore"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide1/a.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide2/a.js"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide3/a.js"))); + }); + + it("should not include comments in ignore rules", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithComments"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored("# should be ignored")); + assert(await engine.isPathIgnored("this_one_not")); + }); + + it("should ignore a non-negated pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "negation", "ignore.js"))); + }); + + it("should not ignore a negated pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "negation", "unignore.js"))); + }); + }); + + describe("with --ignore-path option and --ignore-pattern option", () => { + it("should return false for ignored file when unignored with ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ + ignorePath: getFixturePath("ignored-paths", ".eslintignore"), + overrideConfig: { + ignorePatterns: "!sampleignorepattern" + }, + cwd + }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "sampleignorepattern"))); + }); + }); + }); + + describe("loadFormatter()", () => { + it("should return a formatter object when a bundled formatter is requested", async () => { + const engine = new ESLint(); + const formatter = await engine.loadFormatter("compact"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when no argument is passed", async () => { + const engine = new ESLint(); + const formatter = await engine.loadFormatter(); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a custom formatter is requested", async () => { + const engine = new ESLint(); + const formatter = await engine.loadFormatter(getFixturePath("formatters", "simple.js")); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a custom formatter is requested, also if the path has backslashes", async () => { + const engine = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const formatter = await engine.loadFormatter(".\\fixtures\\formatters\\simple.js"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter prefixed with eslint-formatter is requested", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("bar"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter is requested, also when the eslint-formatter prefix is included in the format argument", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("eslint-formatter-bar"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter is requested within a scoped npm package", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("@somenamespace/foo"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter is requested within a scoped npm package, also when the eslint-formatter prefix is included in the format argument", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("@somenamespace/eslint-formatter-foo"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should throw if a customer formatter doesn't exist", async () => { + const engine = new ESLint(); + const formatterPath = getFixturePath("formatters", "doesntexist.js"); + const fullFormatterPath = path.resolve(formatterPath); + + await assert.rejects(async () => { + await engine.loadFormatter(formatterPath); + }, `There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`); + }); + + it("should throw if a built-in formatter doesn't exist", async () => { + const engine = new ESLint(); + const fullFormatterPath = path.resolve(__dirname, "../../../lib/cli-engine/formatters/special"); + + await assert.rejects(async () => { + await engine.loadFormatter("special"); + }, `There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`); + }); + + it("should throw if the required formatter exists but has an error", async () => { + const engine = new ESLint(); + const formatterPath = getFixturePath("formatters", "broken.js"); + + await assert.rejects(async () => { + await engine.loadFormatter(formatterPath); + }, `There was a problem loading formatter: ${formatterPath}\nError: Cannot find module 'this-module-does-not-exist'`); + }); + + it("should throw if a non-string formatter name is passed", async () => { + const engine = new ESLint(); + + assert.rejects(async () => { + await engine.loadFormatter(5); + }, "'name' must be a string"); + }); + }); + + describe("getErrorResults()", () => { + it("should report 5 error messages when looking for errors only", async () => { + process.chdir(originalDir); + const engine = new ESLint(); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 5); + assert.strictEqual(errorResults[0].errorCount, 5); + assert.strictEqual(errorResults[0].fixableErrorCount, 3); + assert.strictEqual(errorResults[0].fixableWarningCount, 0); + assert.strictEqual(errorResults[0].messages[0].ruleId, "strict"); + assert.strictEqual(errorResults[0].messages[0].severity, 2); + assert.strictEqual(errorResults[0].messages[1].ruleId, "no-var"); + assert.strictEqual(errorResults[0].messages[1].severity, 2); + assert.strictEqual(errorResults[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(errorResults[0].messages[2].severity, 2); + assert.strictEqual(errorResults[0].messages[3].ruleId, "quotes"); + assert.strictEqual(errorResults[0].messages[3].severity, 2); + assert.strictEqual(errorResults[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(errorResults[0].messages[4].severity, 2); + }); + + it("should not mutate passed report parameter", async () => { + process.chdir(originalDir); + const engine = new ESLint({ + overrideConfig: { + rules: { quotes: [1, "double"] } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const reportResultsLength = results[0].messages.length; + + ESLint.getErrorResults(results); + + assert.strictEqual(results[0].messages.length, reportResultsLength); + }); + + it("should report a warningCount of 0 when looking for errors only", async () => { + process.chdir(originalDir); + const engine = new ESLint(); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].warningCount, 0); + assert.strictEqual(errorResults[0].fixableWarningCount, 0); + }); + + it("should return 0 error or warning messages even when the file has warnings", async () => { + const engine = new ESLint({ + ignorePath: path.join(fixtureDir, ".eslintignore"), + cwd: path.join(fixtureDir, "..") + }); + const options = { + filePath: "fixtures/passing.js", + warnIgnored: true + }; + const results = await engine.lintText("var bar = foo;", options); + const errorReport = ESLint.getErrorResults(results); + + assert.strictEqual(errorReport.length, 0); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + }); + + it("should return source code of file in the `source` property", async () => { + process.chdir(originalDir); + const engine = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { quotes: [2, "double"] } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 1); + assert.strictEqual(errorResults[0].source, "var foo = 'bar';"); + }); + + it("should contain `output` property after fixes", async () => { + process.chdir(originalDir); + const engine = new ESLint({ + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + semi: 2, + "no-console": 2 + } + } + }); + const results = await engine.lintText("console.log('foo')"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 1); + assert.strictEqual(errorResults[0].output, "console.log('foo');"); + }); + }); + + describe("outputFixes()", () => { + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("should call fs.writeFile() for each result with output", async () => { + const fakeFS = leche.fake(fs); + const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const localESLint = proxyquire("../../../lib/eslint/eslint", { + fs: fakeFS + }).ESLint; + const results = [ + { + filePath: path.resolve("foo.js"), + output: "bar" + }, + { + filePath: path.resolve("bar.js"), + output: "baz" + } + ]; + + await localESLint.outputFixes(results); + + assert.strictEqual(spy.callCount, 2); + assert(spy.firstCall.calledWithExactly(path.resolve("foo.js"), "bar", sinon.match.func), "First call was incorrect."); + assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz", sinon.match.func), "Second call was incorrect."); + }); + + it("should call fs.writeFile() for each result with output and not at all for a result without output", async () => { + const fakeFS = leche.fake(fs); + const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const localESLint = proxyquire("../../../lib/eslint/eslint", { + fs: fakeFS + }).ESLint; + const results = [ + { + filePath: path.resolve("foo.js"), + output: "bar" + }, + { + filePath: path.resolve("abc.js") + }, + { + filePath: path.resolve("bar.js"), + output: "baz" + } + ]; + + await localESLint.outputFixes(results); + + assert.strictEqual(spy.callCount, 2); + assert(spy.firstCall.calledWithExactly(path.resolve("foo.js"), "bar", sinon.match.func), "First call was incorrect."); + assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz", sinon.match.func), "Second call was incorrect."); + }); + }); + + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { + it("should report a violation for disabling rules", async () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + ignore: true, + allowInlineConfig: false, + overrideConfig: { + env: { browser: true }, + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + strict: 0, + quotes: 0 + } + } + }; + const eslintCLI = new ESLint(config); + const results = await eslintCLI.lintText(code); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + }); + + it("should not report a violation by default", async () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + ignore: true, + allowInlineConfig: true, + overrideConfig: { + env: { browser: true }, + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + strict: 0, + quotes: 0 + } + } + }; + const eslintCLI = new ESLint(config); + const results = await eslintCLI.lintText(code); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 0); + }); + }); + + describe("when evaluating code when reportUnusedDisableDirectives is enabled", () => { + it("should report problems for unused eslint-disable directives", async () => { + const eslint = new ESLint({ useEslintrc: false, reportUnusedDisableDirectives: "error" }); + + assert.deepStrictEqual( + await eslint.lintText("/* eslint-disable */"), + [ + { + filePath: "", + messages: [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + severity: 2, + nodeType: null + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "/* eslint-disable */", + usedDeprecatedRules: [] + } + ] + ); + }); + }); + + describe("when retreiving version number", () => { + it("should return current version number", () => { + const eslintCLI = require("../../../lib/eslint").ESLint; + const version = eslintCLI.version; + + assert.strictEqual(typeof version, "string"); + assert(parseInt(version[0], 10) >= 3); + }); + }); + + describe("mutability", () => { + describe("plugins", () => { + it("Loading plugin in one instance doesn't mutate to another instance", async () => { + const filePath = getFixturePath("single-quoted.js"); + const engine1 = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + plugins: ["example"], + rules: { "example/example-rule": 1 } + } + }); + const engine2 = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + const fileConfig1 = await engine1.calculateConfigForFile(filePath); + const fileConfig2 = await engine2.calculateConfigForFile(filePath); + + // plugin + assert.deepStrictEqual(fileConfig1.plugins, ["example"], "Plugin is present for engine 1"); + assert.deepStrictEqual(fileConfig2.plugins, [], "Plugin is not present for engine 2"); + }); + }); + + describe("rules", () => { + it("Loading rules in one instance doesn't mutate to another instance", async () => { + const filePath = getFixturePath("single-quoted.js"); + const engine1 = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { rules: { "example/example-rule": 1 } } + }); + const engine2 = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + const fileConfig1 = await engine1.calculateConfigForFile(filePath); + const fileConfig2 = await engine2.calculateConfigForFile(filePath); + + // plugin + assert.deepStrictEqual(fileConfig1.rules["example/example-rule"], [1], "example is present for engine 1"); + assert.strictEqual(fileConfig2.rules["example/example-rule"], void 0, "example is not present for engine 2"); + }); + }); + }); + + describe("with ignorePatterns config", () => { + const root = getFixturePath("cli-engine/ignore-patterns"); + + /** @type {typeof ESLint} */ + let InMemoryESLint; + + describe("ignorePatterns can add an ignore pattern ('foo.js').", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), false); + }); + + it("'lintFiles()' should not verify 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js"), + path.join(root, "subdir/bar.js") + ]); + }); + }); + + describe("ignorePatterns can add ignore patterns ('foo.js', '/bar.js').", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: ["foo.js", "/bar.js"] + }), + "foo.js": "", + "bar.js": "", + "baz.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "", + "subdir/baz.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for '/bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), false); + }); + + it("'lintFiles()' should not verify 'foo.js' and '/bar.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "baz.js"), + path.join(root, "subdir/bar.js"), + path.join(root, "subdir/baz.js") + ]); + }); + }); + + describe("ignorePatterns can unignore '/node_modules/foo'.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "!/node_modules/foo" + }), + "node_modules/foo/index.js": "", + "node_modules/foo/.dot.js": "", + "node_modules/bar/index.js": "", + "foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for 'node_modules/foo/index.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("node_modules/foo/index.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/foo/.dot.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("node_modules/foo/.dot.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/bar/index.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("node_modules/bar/index.js"), true); + }); + + it("'lintFiles()' should verify 'node_modules/foo/index.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js"), + path.join(root, "node_modules/foo/index.js") + ]); + }); + }); + + describe("ignorePatterns can unignore '.eslintrc.js'.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!.eslintrc.js" + })}`, + "foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for '.eslintrc.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored(".eslintrc.js"), false); + }); + + it("'lintFiles()' should verify '.eslintrc.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, ".eslintrc.js"), + path.join(root, "foo.js") + ]); + }); + }); + + describe(".eslintignore can re-ignore files that are unignored by ignorePatterns.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!.*" + })}`, + ".eslintignore": ".foo*", + ".foo.js": "", + ".bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for re-ignored '.foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored(".foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for unignored '.bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored(".bar.js"), false); + }); + + it("'lintFiles()' should not verify re-ignored '.foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, ".bar.js"), + path.join(root, ".eslintrc.js") + ]); + }); + }); + + describe(".eslintignore can unignore files that are ignored by ignorePatterns.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + ".eslintignore": "!foo.js", + "foo.js": "", + "bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), true); + }); + + it("'lintFiles()' should verify unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignorePatterns in the config file in a child directory affects to only in the directory.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "subdir/.eslintrc.json": JSON.stringify({ + ignorePatterns: "bar.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "", + "subdir/subsubdir/foo.js": "", + "subdir/subsubdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/subsubdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in 'subdir'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/subsubdir/bar.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the outside of 'subdir'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'lintFiles()' should verify 'bar.js' in the outside of 'subdir'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the config file in a child directory can unignore the ignored files in the parent directory's config.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "subdir/.eslintrc.json": JSON.stringify({ + ignorePatterns: "!foo.js" + }), + "foo.js": "", + "subdir/foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'lintFiles()' should verify 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe(".eslintignore can unignore files that are ignored by ignorePatterns in the config file in the child directory.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({}), + "subdir/.eslintrc.json": JSON.stringify({ + ignorePatterns: "*.js" + }), + ".eslintignore": "!foo.js", + "foo.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'lintFiles()' should verify unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js"), + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe("if the config in a child directory has 'root:true', ignorePatterns in the config file in the parent directory should not be used.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "subdir/.eslintrc.json": JSON.stringify({ + root: true, + ignorePatterns: "bar.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'lintFiles()' should verify 'bar.js' in the root directory and 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js"), + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe("even if the config in a child directory has 'root:true', .eslintignore should be used.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({}), + "subdir/.eslintrc.json": JSON.stringify({ + root: true, + ignorePatterns: "bar.js" + }), + ".eslintignore": "foo.js", + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'lintFiles()' should verify 'bar.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the shareable config should be used.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "foo.js" + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "one" + }), + "foo.js": "", + "bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'lintFiles()' should verify 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the shareable config should be relative to the entry config file.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "/foo.js" + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "one" + }), + "foo.js": "", + "subdir/foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'subdir/foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'lintFiles()' should verify 'subdir/foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe("ignorePatterns in a config file can unignore the files which are ignored by ignorePatterns in the shareable config.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "one", + ignorePatterns: "!bar.js" + }), + "foo.js": "", + "bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'lintFiles()' should verify 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in a config file should not be used if --no-ignore option was given.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "*.js" + }), + "foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js'.", async () => { + const engine = new InMemoryESLint({ ignore: false }); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + }); + + it("'lintFiles()' should verify 'foo.js'.", async () => { + const engine = new InMemoryESLint({ ignore: false }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignorePatterns in overrides section is not allowed.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + overrides: [ + { + files: "*.js", + ignorePatterns: "foo.js" + } + ] + })}`, + "foo.js": "" + } + }).ESLint; + }); + + it("should throw a configuration error.", async () => { + await assert.rejects(async () => { + const engine = new InMemoryESLint(); + + await engine.lintFiles("*.js"); + }, "Unexpected top-level property \"overrides[0].ignorePatterns\""); + }); + }); + }); + + describe("'overrides[].files' adds lint targets", () => { + const root = getFixturePath("cli-engine/additional-lint-targets"); + let InMemoryESLint; + + describe("if { files: 'foo/*.txt', excludedFiles: '**/ignore.txt' } is present,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/*.txt", + excludedFiles: "**/ignore.txt" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "foo/ignore.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "bar/ignore.txt": "", + "test.js": "", + "test.txt": "", + "ignore.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + + it("'lintFiles()' with a glob pattern '*.js' should not contain 'foo/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/**/*.txt" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*' } is present,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/**/*" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should NOT contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present in a shareable config,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-foo/index.js": `module.exports = ${JSON.stringify({ + overrides: [ + { + files: "foo/**/*.txt" + } + ] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "foo" + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present in a plugin config,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": `exports.configs = ${JSON.stringify({ + bar: { + overrides: [ + { + files: "foo/**/*.txt" + } + ] + } + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "plugin:foo/bar" + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + }); + + describe("'ignorePatterns', 'overrides[].files', and 'overrides[].excludedFiles' of the configuration that the '--config' option provided should be resolved from CWD.", () => { + const root = getFixturePath("cli-engine/config-and-overrides-files"); + + /** @type {ESLint} */ + let InMemoryESLint; + + describe("if { files: 'foo/*.txt', ... } is present by '--config node_modules/myconf/.eslintrc.json',", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/myconf/.eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/*.js", + rules: { + eqeqeq: "error" + } + } + ] + }), + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }).ESLint; + }); + + it("'lintFiles()' with 'foo/test.js' should use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("foo/test.js"); + + // Expected to be an 'eqeqeq' error because the file matches to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 1, + filePath: path.join(root, "foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [ + { + column: 3, + endColumn: 5, + endLine: 1, + line: 1, + message: "Expected '===' and instead saw '=='.", + messageId: "unexpected", + nodeType: "BinaryExpression", + ruleId: "eqeqeq", + severity: 2 + } + ], + source: "a == b", + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + + it("'lintFiles()' with 'node_modules/myconf/foo/test.js' should NOT use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("node_modules/myconf/foo/test.js"); + + // Expected to be no errors because the file doesn't match to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 0, + filePath: path.join(root, "node_modules/myconf/foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [], + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + }); + + describe("if { files: '*', excludedFiles: 'foo/*.txt', ... } is present by '--config node_modules/myconf/.eslintrc.json',", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/myconf/.eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "*", + excludedFiles: "foo/*.js", + rules: { + eqeqeq: "error" + } + } + ] + }), + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }).ESLint; + }); + + it("'lintFiles()' with 'foo/test.js' should NOT use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("foo/test.js"); + + // Expected to be no errors because the file matches to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 0, + filePath: path.join(root, "foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [], + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + + it("'lintFiles()' with 'node_modules/myconf/foo/test.js' should use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("node_modules/myconf/foo/test.js"); + + // Expected to be an 'eqeqeq' error because the file doesn't match to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 1, + filePath: path.join(root, "node_modules/myconf/foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [ + { + column: 3, + endColumn: 5, + endLine: 1, + line: 1, + message: "Expected '===' and instead saw '=='.", + messageId: "unexpected", + nodeType: "BinaryExpression", + ruleId: "eqeqeq", + severity: 2 + } + ], + source: "a == b", + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + }); + + describe("if { ignorePatterns: 'foo/*.txt', ... } is present by '--config node_modules/myconf/.eslintrc.json',", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/myconf/.eslintrc.json": JSON.stringify({ + ignorePatterns: ["!/node_modules/myconf", "foo/*.js"], + rules: { + eqeqeq: "error" + } + }), + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }).ESLint; + }); + + it("'lintFiles()' with '**/*.js' should iterate 'node_modules/myconf/foo/test.js' but not 'foo/test.js'.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + useEslintrc: false + }); + const files = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(files, [ + path.join(root, "node_modules/myconf/foo/test.js") + ]); + }); + }); + }); + + describe("plugin conflicts", () => { + let uid = 0; + let root = ""; + + beforeEach(() => { + root = getFixturePath(`eslint/plugin-conflicts-${++uid}`); + }); + + /** @type {typeof ESLint} */ + let InMemoryESLint; + + /** + * Verify thrown errors. + * @param {() => Promise} f The function to run and throw. + * @param {Record} props The properties to verify. + * @returns {Promise} void + */ + async function assertThrows(f, props) { + try { + await f(); + } catch (error) { + for (const [key, value] of Object.entries(props)) { + assert.deepStrictEqual(error[key], value, key); + } + return; + } + + assert.fail("Function should throw an error, but not."); + } + + describe("between a config file and linear extendees.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + extends: ["two"], + plugins: ["foo"] + })}`, + "node_modules/eslint-config-two/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: ["one"], + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await engine.lintFiles("test.js"); + }); + }); + + describe("between a config file and same-depth extendees.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + "node_modules/eslint-config-two/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: ["one", "two"], + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await engine.lintFiles("test.js"); + }); + }); + + describe("between two config files in different directories, with single node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files, but node_modules directory is unique.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await engine.lintFiles("subdir/test.js"); + }); + }); + + describe("between two config files in different directories, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await assertThrows( + () => engine.lintFiles("subdir/test.js"), + { + message: `Plugin "foo" was conflicted between "subdir${path.sep}.eslintrc.json" and ".eslintrc.json".`, + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "subdir/node_modules/eslint-plugin-foo/index.js"), + importerName: `subdir${path.sep}.eslintrc.json` + }, + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: ".eslintrc.json" + } + ] + } + } + ); + }); + }); + + describe("between '--config' option and a regular config file, with single node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files, but node_modules directory is unique.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfigFile: "node_modules/mine/.eslintrc.json" + }); + + await engine.lintFiles("test.js"); + }); + }); + + describe("between '--config' option and a regular config file, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfigFile: "node_modules/mine/.eslintrc.json" + }); + + await assertThrows( + () => engine.lintFiles("test.js"), + { + message: "Plugin \"foo\" was conflicted between \"--config\" and \".eslintrc.json\".", + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "node_modules/mine/node_modules/eslint-plugin-foo/index.js"), + importerName: "--config" + }, + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: ".eslintrc.json" + } + ] + } + } + ); + }); + }); + + describe("between '--plugin' option and a regular config file, with single node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from both CWD and the base directory of the entry config file, but node_modules directory is unique.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfig: { plugins: ["foo"] } + }); + + await engine.lintFiles("subdir/test.js"); + }); + }); + + describe("between '--plugin' option and a regular config file, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should throw plugin-conflict error. (Load the plugin from both CWD and the base directory of the entry config file.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfig: { plugins: ["foo"] } + }); + + await assertThrows( + () => engine.lintFiles("subdir/test.js"), + { + message: `Plugin "foo" was conflicted between "CLIOptions" and "subdir${path.sep}.eslintrc.json".`, + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: "CLIOptions" + }, + { + filePath: path.join(root, "subdir/node_modules/eslint-plugin-foo/index.js"), + importerName: `subdir${path.sep}.eslintrc.json` + } + ] + } + } + ); + }); + }); + + describe("'--resolve-plugins-relative-to' option overrides the location that ESLint load plugins from.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from '--resolve-plugins-relative-to'.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + resolvePluginsRelativeTo: root + }); + + await engine.lintFiles("subdir/test.js"); + }); + }); + + describe("between two config files with different target files.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "one/node_modules/eslint-plugin-foo/index.js": "", + "one/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "one/test.js": "", + "two/node_modules/eslint-plugin-foo/index.js": "", + "two/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "two/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file for each target file. Not related to each other.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + const results = await engine.lintFiles("*/test.js"); + + assert.strictEqual(results.length, 2); }); }); }); From 54e6fa96c766a8ad117043f8327d60013a89315e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 15 Apr 2020 20:44:51 +0900 Subject: [PATCH 18/71] fix tests --- tests/lib/cli-engine/cli-engine.js | 4 +++- tests/lib/cli.js | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 9425b89dbcd..5f183466ed6 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -821,7 +821,9 @@ describe("CLIEngine", () => { engine = new CLIEngine({ parser: "espree", - envs: ["es6"], + parserOptions: { + ecmaVersion: 2020 + }, useEslintrc: false }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index df77c0e829b..085cae3044e 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -792,10 +792,10 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass overrideConfig.noInlineConfig:true to ESLint when --no-inline-config is used", async () => { + it("should pass allowInlineConfig:false to ESLint when --no-inline-config is used", async () => { // create a fake ESLint class to test with - const fakeESLint = sinon.mock().withExactArgs(sinon.match({ overrideConfig: { noInlineConfig: true } })); + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: false })); Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([{ @@ -821,10 +821,10 @@ describe("cli", () => { await localCLI.execute("--no-inline-config ."); }); - it("should not error and overrideConfig.noInlineConfig should be undefined by default", async () => { + it("should not error and allowInlineConfig should be true by default", async () => { // create a fake ESLint class to test with - const fakeESLint = sinon.mock().withExactArgs(sinon.match({ overrideConfig: { noInlineConfig: void 0 } })); + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: true })); Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); From e5aea0afe9356f6a8b5a40af10047387e69d37d5 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 15 Apr 2020 20:49:13 +0900 Subject: [PATCH 19/71] remove compareResultsByFilePath --- lib/eslint/eslint.js | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 52588df5c9a..ca6a334a364 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -387,6 +387,24 @@ function processCLIEngineLintReport(cliEngine, { results }) { return results; } +/** + * An Array.prototype.sort() compatible compare function to order results by their file path. + * @param {LintResult} a The first lint result. + * @param {LintResult} b The second lint result. + * @returns {number} An integer representing the order in which the two results should occur. + */ +function compareResultsByFilePath(a, b) { + if (a.filePath < b.filePath) { + return -1; + } + + if (a.filePath > b.filePath) { + return 1; + } + + return 0; +} + class ESLint { /** @@ -471,24 +489,6 @@ class ESLint { ); } - /** - * An Array.prototype.sort() compatible compare function to order results by their file path. - * @param {LintResult} a The first lint result. - * @param {LintResult} b The second lint result. - * @returns {number} An integer representing the order in which the two results should occur. - */ - static compareResultsByFilePath(a, b) { - if (a.filePath < b.filePath) { - return -1; - } - - if (a.filePath > b.filePath) { - return 1; - } - - return 0; - } - /** * Returns results that only contains errors. * @param {LintResult[]} results The results to filter. @@ -591,7 +591,7 @@ class ESLint { format(results) { let rulesMeta = null; - results.sort(ESLint.compareResultsByFilePath); + results.sort(compareResultsByFilePath); return formatter(results, { get rulesMeta() { From ac17cec8cbe30413bd27218c284f5974e9d95989 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 15 Apr 2020 21:52:56 +0900 Subject: [PATCH 20/71] fix tests --- tests/lib/eslint/eslint.js | 157 +++++++++++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 8 deletions(-) diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 5de4396909d..c77b4dfe924 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -128,6 +128,99 @@ describe("ESLint", () => { assert.deepStrictEqual(customBaseConfig, { root: true }); }); + + it("should throw readable messages if removed options are present", () => { + assert.throws( + () => new ESLint({ + cacheFile: "", + configFile: "", + envs: [], + globals: [], + ignorePattern: [], + parser: "", + parserOptions: {}, + rules: {}, + plugins: [] + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- Unknown options: cacheFile, configFile, envs, globals, ignorePattern, parser, parserOptions, rules", + "- 'cacheFile' has been removed. Please use the 'cacheLocation' option instead.", + "- 'configFile' has been removed. Please use the 'overrideConfigFile' option instead.", + "- 'envs' has been removed. Please use the 'overrideConfig.env' option instead.", + "- 'globals' has been removed. Please use the 'overrideConfig.globals' option instead.", + "- 'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.", + "- 'parser' has been removed. Please use the 'overrideConfig.parser' option instead.", + "- 'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead.", + "- 'rules' has been removed. Please use the 'overrideConfig.rules' option instead.", + "- 'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead." + ].join("\n")), "u") + ); + }); + + it("should throw readable messages if wrong type values are given to options", () => { + assert.throws( + () => new ESLint({ + allowInlineConfig: "", + baseConfig: "", + cache: "", + cacheLocation: "", + cwd: "foo", + errorOnUnmatchedPattern: "", + extensions: "", + fix: "", + fixTypes: ["xyz"], + globInputPaths: "", + ignore: "", + ignorePath: "", + overrideConfig: "", + overrideConfigFile: "", + plugins: "", + reportUnusedDisableDirectives: "", + resolvePluginsRelativeTo: "", + rulePaths: "", + useEslintrc: "" + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- 'allowInlineConfig' must be a boolean.", + "- 'baseConfig' must be an object or null.", + "- 'cache' must be a boolean.", + "- 'cacheLocation' must be a non-empty string.", + "- 'cwd' must be an absolute path.", + "- 'errorOnUnmatchedPattern' must be a boolean.", + "- 'extensions' must be an array of non-empty strings or null.", + "- 'fix' must be a boolean or a function.", + "- 'fixTypes' must be an array of any of \"problem\", \"suggestion\", and \"layout\".", + "- 'globInputPaths' must be a boolean.", + "- 'ignore' must be a boolean.", + "- 'ignorePath' must be a non-empty string or null.", + "- 'overrideConfig' must be an object or null.", + "- 'overrideConfigFile' must be a non-empty string or null.", + "- 'plugins' must be an object or null.", + "- 'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.", + "- 'resolvePluginsRelativeTo' must be a non-empty string or null.", + "- 'rulePaths' must be an array of non-empty strings.", + "- 'useElintrc' must be a boolean." + ].join("\n")), "u") + ); + }); + + it("should throw readable messages if 'plugins' option contains empty key", () => { + assert.throws( + () => new ESLint({ + plugins: { + "eslint-plugin-foo": {}, + "eslint-plugin-bar": {}, + "": {} + } + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- 'plugins' must not include the empty string in keys." + ].join("\n")), "u") + ); + }); }); describe("lintText()", () => { @@ -733,6 +826,31 @@ describe("ESLint", () => { [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] ); }); + + it("should throw if non-string value is given to 'code' parameter", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText(100), /'code' must be a string/u); + }); + + it("should throw if non-object value is given to 'options' parameter", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", "foo.js"), /'options' must be an object, null, or undefined/u); + }); + + it("should throw if 'options' argument contains unknown key", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option 'filename'/u); + }); + + it("should throw if non-string value is given to 'options.filePath' option", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { filePath: "" }), /'options.filePath' must be a non-empty string or undefined/u); + }); + + it("should throw if non-boolean value is given to 'options.warnIgnored' option", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { warnIgnored: "" }), /'options.warnIgnored' must be a boolean or undefined/u); + }); }); describe("lintFiles()", () => { @@ -1179,7 +1297,7 @@ describe("ESLint", () => { await assert.rejects(async () => { await eslint.lintFiles([getFixturePath("./cli-engine/")]); - }, new Error(`All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`)); + }, new RegExp(escapeStringRegExp(`All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`), "u")); }); it("should throw an error when all given files are ignored", async () => { @@ -3587,6 +3705,12 @@ describe("ESLint", () => { await eslint.lintFiles("subdir"); }); }); + + it("should throw if non-boolean value is given to 'options.warnIgnored' option", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintFiles(777), /'patterns' must be a non-empty string or an array of non-empty strings/u); + await assert.rejects(() => eslint.lintFiles([null]), /'patterns' must be a non-empty string or an array of non-empty strings/u); + }); }); describe("calculateConfigForFile", () => { @@ -3632,6 +3756,12 @@ describe("ESLint", () => { } assert.fail("should throw an error"); }); + + it("should throw if non-string value is given to 'filePath' parameter", async () => { + const eslint = new ESLint(); + + await assert.rejects(() => eslint.calculateConfigForFile(null), /'filePath' must be a non-empty string/u); + }); }); describe("isPathIgnored", () => { @@ -3955,7 +4085,7 @@ describe("ESLint", () => { assert.throws(() => { // eslint-disable-next-line no-new new ESLint({ ignorePath, cwd }); - }, "Cannot read .eslintignore file"); + }, /Cannot read \.eslintignore file/u); }); it("should return false for files outside of ignorePath's directory", async () => { @@ -4070,6 +4200,12 @@ describe("ESLint", () => { assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "sampleignorepattern"))); }); }); + + it("should throw if non-string value is given to 'filePath' parameter", async () => { + const eslint = new ESLint(); + + await assert.rejects(() => eslint.isPathIgnored(null), /'filePath' must be a non-empty string/u); + }); }); describe("loadFormatter()", () => { @@ -4154,7 +4290,7 @@ describe("ESLint", () => { await assert.rejects(async () => { await engine.loadFormatter(formatterPath); - }, `There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`); + }, new RegExp(escapeStringRegExp(`There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`), "u")); }); it("should throw if a built-in formatter doesn't exist", async () => { @@ -4163,7 +4299,7 @@ describe("ESLint", () => { await assert.rejects(async () => { await engine.loadFormatter("special"); - }, `There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`); + }, new RegExp(escapeStringRegExp(`There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`), "u")); }); it("should throw if the required formatter exists but has an error", async () => { @@ -4172,15 +4308,15 @@ describe("ESLint", () => { await assert.rejects(async () => { await engine.loadFormatter(formatterPath); - }, `There was a problem loading formatter: ${formatterPath}\nError: Cannot find module 'this-module-does-not-exist'`); + }, new RegExp(escapeStringRegExp(`There was a problem loading formatter: ${formatterPath}\nError: Cannot find module 'this-module-does-not-exist'`), "u")); }); it("should throw if a non-string formatter name is passed", async () => { const engine = new ESLint(); - assert.rejects(async () => { + await assert.rejects(async () => { await engine.loadFormatter(5); - }, "'name' must be a string"); + }, /'name' must be a string/u); }); }); @@ -4340,6 +4476,11 @@ describe("ESLint", () => { assert(spy.firstCall.calledWithExactly(path.resolve("foo.js"), "bar", sinon.match.func), "First call was incorrect."); assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz", sinon.match.func), "Second call was incorrect."); }); + + it("should throw if non object array is given to 'results' parameter", async () => { + await assert.rejects(() => ESLint.outputFixes(null), /'results' must be an array/u); + await assert.rejects(() => ESLint.outputFixes([null]), /'results' must include only objects/u); + }); }); describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { @@ -5159,7 +5300,7 @@ describe("ESLint", () => { const engine = new InMemoryESLint(); await engine.lintFiles("*.js"); - }, "Unexpected top-level property \"overrides[0].ignorePatterns\""); + }, /Unexpected top-level property "overrides\[0\]\.ignorePatterns"/u); }); }); }); From 49f302feadb832b64d2db617024fff02a4cfd5d5 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 15 Apr 2020 22:00:02 +0900 Subject: [PATCH 21/71] fix tests --- lib/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli.js b/lib/cli.js index 0ccf8f13e1e..ce11878008f 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -147,7 +147,7 @@ async function isDirectory(filePath) { try { return (await stat(filePath)).isDirectory(); } catch (error) { - if (error.code === "ENOENT") { + if (error.code === "ENOENT" || error.code === "ENOTDIR") { return false; } throw error; From 8421fb3b401087b7036bedb3057b9f5bb8fbcf79 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sat, 18 Apr 2020 22:53:30 +0900 Subject: [PATCH 22/71] remove deprecated source property --- lib/eslint/eslint.js | 34 ++++++++++++++++++++++------------ tests/lib/eslint/eslint.js | 31 +++++++++++++++++-------------- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index ca6a334a364..a3859055caa 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -367,24 +367,34 @@ function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { /** * Processes the linting results generated by a CLIEngine linting report to * match the ESLint class's API. + * + * - Remove the deprecated `source` property from each result. + * - Add the `usedDeprecatedRules` proeprty to each result. * @param {CLIEngine} cliEngine The CLIEngine instance. * @param {CLIEngineLintReport} report The CLIEngine linting report to process. * @returns {LintResult[]} The processed linting results. */ function processCLIEngineLintReport(cliEngine, { results }) { - const descriptor = { - configurable: true, - enumerable: true, - get() { - return getOrFindUsedDeprecatedRules(cliEngine, this.filePath); + return results.map(({ + errorCount, + filePath, + fixableErrorCount, + fixableWarningCount, + messages, + output, + warningCount + }) => ({ + filePath, + messages, + errorCount, + warningCount, + fixableErrorCount, + fixableWarningCount, + output, + get usedDeprecatedRules() { + return getOrFindUsedDeprecatedRules(cliEngine, filePath); } - }; - - for (const result of results) { - Object.defineProperty(result, "usedDeprecatedRules", descriptor); - } - - return results; + })); } /** diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index c77b4dfe924..d845d431f83 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -591,7 +591,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - source: "var bar = foo", + output: void 0, usedDeprecatedRules: [] } ]); @@ -668,13 +668,13 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - source: "var bar =", + output: void 0, usedDeprecatedRules: [] } ]); }); - it("should return source code of file in `source` property when errors are present", async () => { + it("should not contain `source` property when errors are present", async () => { eslint = new ESLint({ useEslintrc: false, overrideConfig: { @@ -683,10 +683,10 @@ describe("ESLint", () => { }); const results = await eslint.lintText("var foo = 'bar'"); - assert.strictEqual(results[0].source, "var foo = 'bar'"); + assert.strictEqual(results[0].source, void 0); }); - it("should return source code of file in `source` property when warnings are present", async () => { + it("should not contain `source` property when warnings are present", async () => { eslint = new ESLint({ useEslintrc: false, overrideConfig: { @@ -695,7 +695,7 @@ describe("ESLint", () => { }); const results = await eslint.lintText("var foo = 'bar'"); - assert.strictEqual(results[0].source, "var foo = 'bar'"); + assert.strictEqual(results[0].source, void 0); }); @@ -755,7 +755,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - source: "var bar = foothis is a syntax error.\n return bar;", + output: void 0, usedDeprecatedRules: [] } ]); @@ -1662,6 +1662,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, + output: void 0, usedDeprecatedRules: [] }, { @@ -2851,7 +2852,7 @@ describe("ESLint", () => { const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 1); - assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); + assert.strictEqual(results[0].output, void 0); }); it("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", async () => { @@ -2876,7 +2877,7 @@ describe("ESLint", () => { const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 1); - assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); + assert.strictEqual(results[0].output, void 0); }); }); }); @@ -4386,7 +4387,7 @@ describe("ESLint", () => { assert.strictEqual(results[0].warningCount, 1); }); - it("should return source code of file in the `source` property", async () => { + it("should not contain `source` property", async () => { process.chdir(originalDir); const engine = new ESLint({ useEslintrc: false, @@ -4398,7 +4399,7 @@ describe("ESLint", () => { const errorResults = ESLint.getErrorResults(results); assert.strictEqual(errorResults[0].messages.length, 1); - assert.strictEqual(errorResults[0].source, "var foo = 'bar';"); + assert.strictEqual(errorResults[0].source, void 0); }); it("should contain `output` property after fixes", async () => { @@ -4559,7 +4560,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - source: "/* eslint-disable */", + output: void 0, usedDeprecatedRules: [] } ] @@ -5583,7 +5584,7 @@ describe("ESLint", () => { severity: 2 } ], - source: "a == b", + output: void 0, usedDeprecatedRules: [], warningCount: 0 } @@ -5607,6 +5608,7 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, messages: [], + output: void 0, usedDeprecatedRules: [], warningCount: 0 } @@ -5653,6 +5655,7 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, messages: [], + output: void 0, usedDeprecatedRules: [], warningCount: 0 } @@ -5688,7 +5691,7 @@ describe("ESLint", () => { severity: 2 } ], - source: "a == b", + output: void 0, usedDeprecatedRules: [], warningCount: 0 } From 8322e01f860037ac1ff1dc9f29a4695453924bc1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 19 Apr 2020 03:12:36 +0900 Subject: [PATCH 23/71] expose the new ESLint class --- lib/api.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/api.js b/lib/api.js index 40a5cc9fa5c..e4b6643b447 100644 --- a/lib/api.js +++ b/lib/api.js @@ -6,6 +6,7 @@ "use strict"; const { CLIEngine } = require("./cli-engine"); +const { ESLint } = require("./eslint"); const { Linter } = require("./linter"); const { RuleTester } = require("./rule-tester"); const { SourceCode } = require("./source-code"); @@ -13,6 +14,7 @@ const { SourceCode } = require("./source-code"); module.exports = { Linter, CLIEngine, + ESLint, RuleTester, SourceCode }; From dbc286c71f598a7747833b34b877246a90dbfc79 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 19 Apr 2020 03:48:54 +0900 Subject: [PATCH 24/71] update nodejs-api.md --- docs/developer-guide/nodejs-api.md | 408 ++++++++++++++++++++++++++++- 1 file changed, 396 insertions(+), 12 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index ee1a7a44970..2234940421f 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -6,6 +6,20 @@ While ESLint is designed to be run on the command line, it's possible to use ESL ## Table of Contents +* [ESLint] + * [constructor()][eslint-constructor] + * [lintFiles()][eslint-lintFiles] + * [lintText()][eslint-lintText] + * [calculateConfigForFile()][eslint-calculateConfigForFile] + * [isPathIgnored()][eslint-isPathIgnored] + * [loadFormatter()][eslint-loadFormatter] + * [static version][eslint-version] + * [static outputFixes][eslint-outputFixes] + * [static getErrorResults][eslint-getErrorResults] + * [LintResult type](#LintResult-type) + * [LintMessage type](#LintMessage-type) + * [EditInfo type](#EditInfo-type) + * [Formatter type](#Formatter-type) * [SourceCode](#sourcecode) * [splitLines()](#sourcecode-splitlines) * [Linter](#linter) @@ -17,22 +31,361 @@ While ESLint is designed to be run on the command line, it's possible to use ESL * [defineParser()](#linter-defineparser) * [version](#linter-version) * [linter (deprecated)](#linter-1) -* [CLIEngine](#cliengine) - * [executeOnFiles()](#cliengine-executeonfiles) - * [resolveFileGlobPatterns()](#cliengine-resolvefileglobpatterns) - * [getConfigForFile()](#cliengine-getconfigforfile) - * [executeOnText()](#cliengine-executeontext) - * [addPlugin()](#cliengine-addplugin) - * [isPathIgnored()](#cliengine-ispathignored) - * [getFormatter()](#cliengine-getformatter) - * [getErrorResults()](#cliengine-geterrorresults) - * [outputFixes()](#cliengine-outputfixes) - * [getRules()](#cliengine-getrules) - * [version](#cliengine-version) +* [CLIEngine (deprecated)](#cliengine) * [RuleTester](#ruletester) * [Customizing RuleTester](#customizing-ruletester) * [Deprecated APIs](#deprecated-apis) +--- + +## ESLint class + +The `ESLint` class is the facade class to use ESLint from Node.js application. + +This class pretty depends on the file system, so you cannot use it on browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. + +For a simple example: + +```js +const { ESLint } = require("eslint"); + +(async function main() { + // 1. Create an instance. + const eslint = new ESLint(); + + // 2. Lint files. + const results = await eslint.lintFiles(["lib/**/*.js"]); + + // 3. Format the results. + const formatter = await eslint.loadFormatter("stylish"); + const resultText = formatter.format(results); + + // 4. Output it. + console.log(resultText); +})().catch((error) => { + process.exitCode = 1; + console.error(error); +}); +``` + +For a simple example with autofix: + +```js +const { ESLint } = require("eslint"); + +(async function main() { + // 1. Create an instance with the `fix` option. + const eslint = new ESLint({ fix: true }); + + // 2. Lint files. This doesn't modify target files. + const results = await eslint.lintFiles(["lib/**/*.js"]); + + // 3. Modify the files with the fixed code. + await ESLint.outputFixes(results); + + // 4. Format the results. + const formatter = await eslint.loadFormatter("stylish"); + const resultText = formatter.format(results); + + // 5. Output it. + console.log(resultText); +})().catch((error) => { + process.exitCode = 1; + console.error(error); +}); +``` + +### 💠 new ESLint(options) + +```js +const eslint = new ESLint(options); +``` + +Create a new `ESLint` instance. + +#### Parameters + +The `ESLint` constructor takes an `options` object. If you omit the `options` object then it uses default values for all options. The `options` object has the following properties. + +##### File Enumeration + +* `options.cwd` (`string`)
+ Default is `process.cwd()`. The working directory. This must be an absolute path. +* `options.errorOnUnmatchedPattern` (`boolean`)
+ Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't throw even if no target files found. +* `options.extensions` (`string[] | null`)
+ Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
Mind this option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. +* `options.globInputPaths` (`boolean`)
+ Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interprit glob patterns. +* `options.ignore` (`boolean`)
+ Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't use the `.eslintignore` file and `ignorePatterns` in your configuration. +* `options.ignorePath` (`string | null`)
+ Default is `null`. The path to a file ESLint uses instead of `$CWD/.eslintignore`. If a path is present and the file doesn't exist, this constructor will throw an error. + +##### Linting + +* `options.allowInlineConfig` (`boolean`)
+ Default is `true`. If `false` is present, ESLint suppresses directive comments in source code. If this option is `false`, it overrides the `noInlineConfig` setting in your configurations. +* `options.baseConfig` (`ConfigData | null`)
+ Default is `null`. [Configuration object], extended by all configurations used with this instance. You can use this option to define the default settings that will be used if your configuration files don't configure it. +* `options.overrideConfig` (`ConfigData | null`)
+ Default is `null`. [Configuration object], overrides all configurations used with this instance. You can use this option to define the settings that will be used even if your configuration files configure it. +* `options.overrideConfigFile` (`string | null`)
+ Default is `null`. The path to a configuration file, overrides all configurations used with this instance. The `options.overrideConfig` option is applied after this option is applied. +* `options.plugins` (`Record | null`)
+ Default is `null`. The plugin implementations that ESLint uses for the `plugins` setting of your configuration. This is a map-like object. Those keys are plugin IDs and each value is implementation. +* `options.reportUnusedDisableDirectives` (`"error" | "warn" | "off" | null`)
+ Default is `null`. The severity to report unused eslint-disable directives. If this option is a severity, it overrides the `reportUnusedDisableDirectives` setting in your configurations. +* `options.resolvePluginsRelativeTo` (`string` | `null`)
+ Default is `null`. The path to a directory where plugins should be resolved from. If `null` is present, ESLint loads plugins from the location of the configuration file that contains the plugin setting. If a path is present, ESLint loads all plugins from there. +* `options.rulePaths` (`string[]`)
+ Default is `[]`. An array of paths to directories to load custom rules from. +* `options.useEslintrc` (`boolean`)
+ Default is `true`. If `false` is present, ESLint doesn't load configuration files (`.eslintrc.*` files). Only the configuration of the constructor options is valid. + +##### Autofix + +* `options.fix` (`boolean | (message: LintMessage) => boolean`)
+ Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods work in autofix mode. If a function is present, the methods pass each lint message to the function, then use only the lint messages that the function returned `true`. +* `options.fixTypes` (`("problem" | "suggestion" | "layout")[] | null`)
+ Default is `null`. The types of the rules that the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods use for autofix. + +##### Misc + +* `options.cache` (`boolean`)
+ Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method caches lint results and uses it if each target file is not changed. Please mind that ESLint doesn't clear the cache when you upgrade ESLint plugins. In that case, you have to remove the cache file manually. The [`eslint.lintText()`][eslint-linttext] method doesn't use caches even if you pass the `options.filePath` to the method. +* `options.cacheLocation` (`string`)
+ Default is `.eslintcache`. The [`eslint.lintFiles()`][eslint-lintfiles] method writes caches into this file. + +### 💠 eslint.lintFiles(patterns) + +```js +const results = await eslint.lintFiles(patterns); +``` + +This method checks the given files then returns the results. + +#### Parameters + +* `patterns` (`string | string[]`)
+ The lint target files. This can contain any of file paths, directory paths, and glob patterns. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with an array of [LintResult] objects. + +### 💠 eslint.lintText(code, options) + +```js +const results = await eslint.lintText(code, options); +``` + +This method checks the given source code text then returns the results. + +If you want to use suitable configuration files to lint the text, passing the `options.filePath` option is important. This method loads configuration files with the same manner as the [`eslint.lintFiles()`][eslint-lintfiles] method checks the file the `options.filePath` option pointers. If the `options.filePath` option is not present, this method uses the configuration file at the current working directory (the `cwd` constructor option). + +If the given `options.filePath` option value is an ignored path by your configurations, this method returns an empty array. If the `options.warnIgnored` option is set along with the `options.filePath` option, this method returns a [LintResult] object at least. In that case, the result may contain a warning that indicates the file was ignored. + +#### Parameters + +The second parameter `options` is omittable. + +* `code` (`string`)
+ The source code text to check. +* `options.filePath` (`string`)
+ Optional. The path to the file of the source code text. If omitted, the `result.filePath` becomes the string `""`. +* `options.warnIgnored` (`boolean`)
+ Optional. If `true` is present and the `options.filePath` is a file ESLint should ignore, this method returns a lint result contains a warning message. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with an array of [LintResult] objects. The length of this array is `1` at most, but it's an array to because of aligning to the [`eslint.lintFiles()`][eslint-lintfiles] method. + +### 💠 eslint.calculateConfigForFile(filePath) + +```js +const config = await eslint.calculateConfigForFile(filePath); +``` + +This method calculates the configuration for a given file. + +* It resolves and merges the `extends` and `overrides` settings, so the result object doesn't contain the two. +* It resolves the `parser` setting to absolute paths. +* It normalizes the `plugins` setting to align short names. (e.g., `eslint-plugin-foo` → `foo`) +* It adds the `processor` setting if a legacy file extension processor is matched. +* It doesn't interpret the `env` setting to the `globals` and `parserOotions` settings, so the result object contains the `env` setting as is. + +You can use the calculated configuration object for the debugging purpose. + +#### Parameters + +* `filePath` (`string`)
+ The path to the file you want to check the actual configuration. Directory paths are forbidden because ESLint cannot handle the `overrides` setting. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with a configuration object. + +### 💠 eslint.isPathIgnored(filePath) + +```js +const isPathIgnored = await eslint.isPathIgnored(filePath); +``` + +This method checks if a given file is ignored by your configurations. + +#### Parameters + +* `filePath` (`string`)
+ The path to the file you want to check. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with whether the file is ignored or not. If the file is ignored then `true`. + +### 💠 eslint.loadFormatter(nameOrPath) + +```js +const formatter = await eslint.loadFormatter(nameOrPath); +``` + +This method loads a formatter. + +#### Parameters + +* `nameOrPath` (`string | undefined`)
+ The path to the file you want to check. The following values are allowed: + * `undefined`. In this case, loads the `"stylish"` built-in formatter. + * One of [built-in formatter names][builtin-formatters]. + * A third-party formatter name. For examples: + * `"foo"` will load `eslint-formatter-foo`. + * `"@foo"` will load `@foo/eslint-formatter`. + * `"@foo/bar"` will load `@foo/eslint-formatter-bar`. + * A path to the file that defines a formatter. The path must contain one or more path separators (`/`) in order to distinguish if it's a path or not. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with a [Formatter] object. + +### 💠 ESLint.version + +```js +const version = ESLint.version; +``` + +The version string of ESLint. E.g. `"7.0.0"`. + +This is not instance property. + +### 💠 ESLint.outputFixes(results) + +```js +await ESLint.outputFixes(results); +``` + +This method writes the modified code by the autofix feature into each file. If any modified files don't exist, this method does nothing. + +This is not instance method. + +#### Parameters + +* `results` (`LintResult[]`)
+ The [LintResult] objects to write. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled after all files are written. + +### 💠 ESLint.getErrorResults(results) + +```js +const filteredResults = ESLint.getErrorResults(results); +``` + +This method copies the given results and removes warnings. The returned value contains only errors. + +This is not instance method. + +#### Parameters + +* `results` (`LintResult[]`)
+ The [LintResult] objects to filter. + +#### Return Value + +* (`LintResult[]`)
+ The filtered [LintResult] objects. + +### 💠 LintResult type + +The `LintResult` value is the information of the linting result of each file. The [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods return it. It has following properties: + +* `filePath` (`string`)
+ The absolute path to the file of this result. This may be the string `""` rather than a file path if the [`eslint.lintText()`][eslint-linttext] method returned it. +* `messages` (`LintMessage[]`)
+ The array of [LintMessage] objects. +* `fixableErrorCount` (`number`)
+ The number of errors that can be fixed automatically by the `fix` constructor option. +* `fixableWarningCount` (`number`)
+ The number of warnings that can be fixed automatically by the `fix` constructor option. +* `errorCount` (`number`)
+ The number of errors. This includes fixable errors. +* `warningCount` (`number`)
+ The number of warnings. This includes fixable warnings. +* `output` (`string | undefined`)
+ The modified source code text. This property is undefined if any fixable messages didn't exist. +* `usedDeprecatedRules` (`{ ruleId: string; replacedBy: string[] }[]`)
+ The information about the deprecated rules that were used to check this file. + +### 💠 LintMessage type + +The `LintMessage` value is the information of each linting error. The `messages` property of the [LintResult] type contains it. It has following properties: + +* `ruleId` (`string` | `null`)
+ The rule name that generates this lint message. If this message is generated by the ESLint core rather than rules, this is `null`. +* `severity` (`1 | 2`)
+ The severity of this message. `1` means warning and `2` means error. +* `message` (`string`)
+ The error message. +* `line` (`number`)
+ The 1-based line number of the begin point of this message. +* `column` (`number`)
+ The 1-based column number of the begin point of this message. +* `endLine` (`number | undefined`)
+ The 1-based line number of the end point of this message. This property is undefined if this message is not a range. +* `endColumn` (`number | undefined`)
+ The 1-based column number of the end point of this message. This property is undefined if this message is not a range. +* `fix` (`EditInfo | undefined`)
+ The [EditInfo] object of autofix. This property is undefined if this message is not fixable. +* `suggestions` (`{ desc: string; fix: EditInfo }[] | undefined`)
+ The list of suggestions. Each suggestion is the pair of a description and an [EditInfo] object to fix code. API users such as editor integrations can choose one of them to fix the problem of this message. This property is undefined if this message doesn't have any suggestions. + +### 💠 EditInfo type + +The `EditInfo` value is information to edit text. The `fix` and `suggestions` properties of [LintMessage] type contain it. It has following properties: + +* `range` (`[number, number]`)
+ The pair of 0-based indices in source code text to remove. +* `text` (`string`)
+ The text to add. + +This edit information means replacing the range of the `range` property by the `text` property value. It's like `sourceCodeText.slice(0, edit.range[0]) + edit.text + sourceCodeText.slice(edit.range[1])`. Therefore, it's an add if the `range[0]` and `range[1]` property values are the same value, and it's removal if the `text` property value is empty string. + +### 💠 Formatter type + +The `Formatter` value is the object to convert the [LintResult] objects to text. The [eslint.loadFormatter()][eslint-loadformatter] method returns it. It has the following method: + +* `format` (`(results: LintResult[]) => string`)
+ The method to convert the [LintResult] objects to text. + +--- + ## SourceCode The `SourceCode` type represents the parsed source code that ESLint executes on. It's used internally in ESLint and is also available so that already-parsed code can be used. You can create a new instance of `SourceCode` by passing in the text string representing the code and an abstract syntax tree (AST) in [ESTree](https://github.com/estree/estree) format (including location information, range information, comments, and tokens): @@ -78,6 +431,8 @@ const codeLines = SourceCode.splitLines(code); */ ``` +--- + ## Linter The `Linter` object does the actual evaluation of the JavaScript code. It doesn't do any filesystem operations, it simply parses and reports on the code. In particular, the `Linter` object does not process configuration objects or files. @@ -336,8 +691,12 @@ const messages = linter.verify("var foo;", { Note: This API is deprecated as of 4.0.0. +--- + ## CLIEngine +⚠️ The `CLIEngine` class has been deprecated in favor of the `ESLint` class as of v7.0.0. + The primary Node.js API is `CLIEngine`, which is the underlying utility that runs the ESLint command line interface. This object will read the filesystem for configuration and file information but will not output any results. Instead, it allows you direct access to the important information so you can deal with the output yourself. You can get a reference to the `CLIEngine` by doing the following: @@ -819,6 +1178,8 @@ Map { require("eslint").CLIEngine.version; // '4.5.0' ``` +--- + ## RuleTester `eslint.RuleTester` is a utility to write tests for ESLint rules. It is used internally for the bundled rules that come with ESLint, and it can also be used by plugins. @@ -1007,7 +1368,30 @@ ruleTester.run("my-rule", myRule, { }) ``` +--- + ## Deprecated APIs * `cli` - the `cli` object has been deprecated in favor of `CLIEngine`. As of v1.0.0, `cli` is no longer exported and should not be used by external tools. * `linter` - the `linter` object has been deprecated in favor of `Linter` as of v4.0.0. +* `CLIEngine` - the `CLIEngine` class has been deprecated in favor of the `ESLint` class as of v7.0.0. + +--- + +[configuration object]: ../user-guide/configuring.md +[builtin-formatters]: https://eslint.org/docs/user-guide/formatters/ +[eslint]: #eslint-class +[eslint-constructor]: #new-eslint-options +[eslint-lintfiles]: #eslint-lintFiles-patterns +[eslint-linttext]: #eslint-lintText-code-options +[eslint-calculateconfigforfile]: #eslint-calculateConfigForFile-filePath +[eslint-ispathignored]: #eslint-isPathIgnored-filePath +[eslint-loadformatter]: #eslint-loadFormatter-nameOrPath +[eslint-version]: #eslint-version +[eslint-outputfixes]: #eslint-outputFixes-results +[eslint-geterrorresults]: #eslint-getErrorResults-results +[lintresult]: #lintresult-type +[lintmessage]: #lintmessage-type +[editinfo]: #editinfo-type +[formatter]: #formatter-type +[linter]: #linter From 4b7a22081652e8ad99af99ace39808c33bff3f94 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 19 Apr 2020 03:53:46 +0900 Subject: [PATCH 25/71] fix links --- docs/developer-guide/nodejs-api.md | 44 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 2234940421f..6cb1b0290b9 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -95,7 +95,7 @@ const { ESLint } = require("eslint"); }); ``` -### 💠 new ESLint(options) +### ◆ new ESLint(options) ```js const eslint = new ESLint(options); @@ -157,7 +157,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.cacheLocation` (`string`)
Default is `.eslintcache`. The [`eslint.lintFiles()`][eslint-lintfiles] method writes caches into this file. -### 💠 eslint.lintFiles(patterns) +### ◆ eslint.lintFiles(patterns) ```js const results = await eslint.lintFiles(patterns); @@ -175,7 +175,7 @@ This method checks the given files then returns the results. * (`Promise`)
The promise that will be fulfilled with an array of [LintResult] objects. -### 💠 eslint.lintText(code, options) +### ◆ eslint.lintText(code, options) ```js const results = await eslint.lintText(code, options); @@ -203,7 +203,7 @@ The second parameter `options` is omittable. * (`Promise`)
The promise that will be fulfilled with an array of [LintResult] objects. The length of this array is `1` at most, but it's an array to because of aligning to the [`eslint.lintFiles()`][eslint-lintfiles] method. -### 💠 eslint.calculateConfigForFile(filePath) +### ◆ eslint.calculateConfigForFile(filePath) ```js const config = await eslint.calculateConfigForFile(filePath); @@ -229,7 +229,7 @@ You can use the calculated configuration object for the debugging purpose. * (`Promise`)
The promise that will be fulfilled with a configuration object. -### 💠 eslint.isPathIgnored(filePath) +### ◆ eslint.isPathIgnored(filePath) ```js const isPathIgnored = await eslint.isPathIgnored(filePath); @@ -247,7 +247,7 @@ This method checks if a given file is ignored by your configurations. * (`Promise`)
The promise that will be fulfilled with whether the file is ignored or not. If the file is ignored then `true`. -### 💠 eslint.loadFormatter(nameOrPath) +### ◆ eslint.loadFormatter(nameOrPath) ```js const formatter = await eslint.loadFormatter(nameOrPath); @@ -272,7 +272,7 @@ This method loads a formatter. * (`Promise`)
The promise that will be fulfilled with a [Formatter] object. -### 💠 ESLint.version +### ◆ ESLint.version ```js const version = ESLint.version; @@ -282,7 +282,7 @@ The version string of ESLint. E.g. `"7.0.0"`. This is not instance property. -### 💠 ESLint.outputFixes(results) +### ◆ ESLint.outputFixes(results) ```js await ESLint.outputFixes(results); @@ -302,7 +302,7 @@ This is not instance method. * (`Promise`)
The promise that will be fulfilled after all files are written. -### 💠 ESLint.getErrorResults(results) +### ◆ ESLint.getErrorResults(results) ```js const filteredResults = ESLint.getErrorResults(results); @@ -322,7 +322,7 @@ This is not instance method. * (`LintResult[]`)
The filtered [LintResult] objects. -### 💠 LintResult type +### ◆ LintResult type The `LintResult` value is the information of the linting result of each file. The [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods return it. It has following properties: @@ -343,7 +343,7 @@ The `LintResult` value is the information of the linting result of each file. Th * `usedDeprecatedRules` (`{ ruleId: string; replacedBy: string[] }[]`)
The information about the deprecated rules that were used to check this file. -### 💠 LintMessage type +### ◆ LintMessage type The `LintMessage` value is the information of each linting error. The `messages` property of the [LintResult] type contains it. It has following properties: @@ -366,7 +366,7 @@ The `LintMessage` value is the information of each linting error. The `messages` * `suggestions` (`{ desc: string; fix: EditInfo }[] | undefined`)
The list of suggestions. Each suggestion is the pair of a description and an [EditInfo] object to fix code. API users such as editor integrations can choose one of them to fix the problem of this message. This property is undefined if this message doesn't have any suggestions. -### 💠 EditInfo type +### ◆ EditInfo type The `EditInfo` value is information to edit text. The `fix` and `suggestions` properties of [LintMessage] type contain it. It has following properties: @@ -377,7 +377,7 @@ The `EditInfo` value is information to edit text. The `fix` and `suggestions` pr This edit information means replacing the range of the `range` property by the `text` property value. It's like `sourceCodeText.slice(0, edit.range[0]) + edit.text + sourceCodeText.slice(edit.range[1])`. Therefore, it's an add if the `range[0]` and `range[1]` property values are the same value, and it's removal if the `text` property value is empty string. -### 💠 Formatter type +### ◆ Formatter type The `Formatter` value is the object to convert the [LintResult] objects to text. The [eslint.loadFormatter()][eslint-loadformatter] method returns it. It has the following method: @@ -1381,15 +1381,15 @@ ruleTester.run("my-rule", myRule, { [configuration object]: ../user-guide/configuring.md [builtin-formatters]: https://eslint.org/docs/user-guide/formatters/ [eslint]: #eslint-class -[eslint-constructor]: #new-eslint-options -[eslint-lintfiles]: #eslint-lintFiles-patterns -[eslint-linttext]: #eslint-lintText-code-options -[eslint-calculateconfigforfile]: #eslint-calculateConfigForFile-filePath -[eslint-ispathignored]: #eslint-isPathIgnored-filePath -[eslint-loadformatter]: #eslint-loadFormatter-nameOrPath -[eslint-version]: #eslint-version -[eslint-outputfixes]: #eslint-outputFixes-results -[eslint-geterrorresults]: #eslint-getErrorResults-results +[eslint-constructor]: #-new-eslintoptions +[eslint-lintfiles]: #-eslintlintFilespatterns +[eslint-linttext]: #-eslintlintTextcode-options +[eslint-calculateconfigforfile]: #-eslintcalculateConfigForFilefilePath +[eslint-ispathignored]: #-eslintisPathIgnoredfilePath +[eslint-loadformatter]: #-eslintloadFormatternameOrPath +[eslint-version]: #-eslintversion +[eslint-outputfixes]: #-eslintoutputFixesresults +[eslint-geterrorresults]: #-eslintgetErrorResultsresults [lintresult]: #lintresult-type [lintmessage]: #lintmessage-type [editinfo]: #editinfo-type From 72c42d0554281feaee2f7bc143ca8514402948cf Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sun, 19 Apr 2020 03:56:30 +0900 Subject: [PATCH 26/71] fix links --- docs/developer-guide/nodejs-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 6cb1b0290b9..282233a6467 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -1390,8 +1390,8 @@ ruleTester.run("my-rule", myRule, { [eslint-version]: #-eslintversion [eslint-outputfixes]: #-eslintoutputFixesresults [eslint-geterrorresults]: #-eslintgetErrorResultsresults -[lintresult]: #lintresult-type -[lintmessage]: #lintmessage-type -[editinfo]: #editinfo-type -[formatter]: #formatter-type +[lintresult]: #-lintresult-type +[lintmessage]: #-lintmessage-type +[editinfo]: #-editinfo-type +[formatter]: #-formatter-type [linter]: #linter From 499b8d7a1154241ef1eb217e700c31f6b9b16191 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 20 Apr 2020 14:09:15 +0900 Subject: [PATCH 27/71] Revert "remove deprecated source property" This reverts commit 8421fb3b401087b7036bedb3057b9f5bb8fbcf79. --- lib/eslint/eslint.js | 34 ++++++++++++---------------------- tests/lib/eslint/eslint.js | 31 ++++++++++++++----------------- 2 files changed, 26 insertions(+), 39 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index a3859055caa..ca6a334a364 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -367,34 +367,24 @@ function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { /** * Processes the linting results generated by a CLIEngine linting report to * match the ESLint class's API. - * - * - Remove the deprecated `source` property from each result. - * - Add the `usedDeprecatedRules` proeprty to each result. * @param {CLIEngine} cliEngine The CLIEngine instance. * @param {CLIEngineLintReport} report The CLIEngine linting report to process. * @returns {LintResult[]} The processed linting results. */ function processCLIEngineLintReport(cliEngine, { results }) { - return results.map(({ - errorCount, - filePath, - fixableErrorCount, - fixableWarningCount, - messages, - output, - warningCount - }) => ({ - filePath, - messages, - errorCount, - warningCount, - fixableErrorCount, - fixableWarningCount, - output, - get usedDeprecatedRules() { - return getOrFindUsedDeprecatedRules(cliEngine, filePath); + const descriptor = { + configurable: true, + enumerable: true, + get() { + return getOrFindUsedDeprecatedRules(cliEngine, this.filePath); } - })); + }; + + for (const result of results) { + Object.defineProperty(result, "usedDeprecatedRules", descriptor); + } + + return results; } /** diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index d845d431f83..c77b4dfe924 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -591,7 +591,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: void 0, + source: "var bar = foo", usedDeprecatedRules: [] } ]); @@ -668,13 +668,13 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: void 0, + source: "var bar =", usedDeprecatedRules: [] } ]); }); - it("should not contain `source` property when errors are present", async () => { + it("should return source code of file in `source` property when errors are present", async () => { eslint = new ESLint({ useEslintrc: false, overrideConfig: { @@ -683,10 +683,10 @@ describe("ESLint", () => { }); const results = await eslint.lintText("var foo = 'bar'"); - assert.strictEqual(results[0].source, void 0); + assert.strictEqual(results[0].source, "var foo = 'bar'"); }); - it("should not contain `source` property when warnings are present", async () => { + it("should return source code of file in `source` property when warnings are present", async () => { eslint = new ESLint({ useEslintrc: false, overrideConfig: { @@ -695,7 +695,7 @@ describe("ESLint", () => { }); const results = await eslint.lintText("var foo = 'bar'"); - assert.strictEqual(results[0].source, void 0); + assert.strictEqual(results[0].source, "var foo = 'bar'"); }); @@ -755,7 +755,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: void 0, + source: "var bar = foothis is a syntax error.\n return bar;", usedDeprecatedRules: [] } ]); @@ -1662,7 +1662,6 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: void 0, usedDeprecatedRules: [] }, { @@ -2852,7 +2851,7 @@ describe("ESLint", () => { const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].output, void 0); + assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); }); it("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", async () => { @@ -2877,7 +2876,7 @@ describe("ESLint", () => { const results = await eslint.lintText("", { filePath: "foo.html" }); assert.strictEqual(results[0].messages.length, 1); - assert.strictEqual(results[0].output, void 0); + assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); }); }); }); @@ -4387,7 +4386,7 @@ describe("ESLint", () => { assert.strictEqual(results[0].warningCount, 1); }); - it("should not contain `source` property", async () => { + it("should return source code of file in the `source` property", async () => { process.chdir(originalDir); const engine = new ESLint({ useEslintrc: false, @@ -4399,7 +4398,7 @@ describe("ESLint", () => { const errorResults = ESLint.getErrorResults(results); assert.strictEqual(errorResults[0].messages.length, 1); - assert.strictEqual(errorResults[0].source, void 0); + assert.strictEqual(errorResults[0].source, "var foo = 'bar';"); }); it("should contain `output` property after fixes", async () => { @@ -4560,7 +4559,7 @@ describe("ESLint", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: void 0, + source: "/* eslint-disable */", usedDeprecatedRules: [] } ] @@ -5584,7 +5583,7 @@ describe("ESLint", () => { severity: 2 } ], - output: void 0, + source: "a == b", usedDeprecatedRules: [], warningCount: 0 } @@ -5608,7 +5607,6 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, messages: [], - output: void 0, usedDeprecatedRules: [], warningCount: 0 } @@ -5655,7 +5653,6 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, messages: [], - output: void 0, usedDeprecatedRules: [], warningCount: 0 } @@ -5691,7 +5688,7 @@ describe("ESLint", () => { severity: 2 } ], - output: void 0, + source: "a == b", usedDeprecatedRules: [], warningCount: 0 } From 35889b93687dcd66ba663d94f085edc85e0e28f9 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 20 Apr 2020 14:11:48 +0900 Subject: [PATCH 28/71] add author --- lib/eslint/eslint.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index ca6a334a364..e2fd0dfcb03 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -1,6 +1,7 @@ /** * @fileoverview Main API Class * @author Kai Cataldo + * @author Toru Nagashima */ "use strict"; From 2131de571d0b6d3da8e36e935ea2fca02fb534df Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 20 Apr 2020 14:13:23 +0900 Subject: [PATCH 29/71] fix typo Co-Authored-By: Kai Cataldo --- lib/eslint/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index e2fd0dfcb03..62e47bde733 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -147,7 +147,7 @@ class ESLintInvalidOptionsError extends Error { * @returns {ESLintOptions} The normalized options. */ function processOptions({ - allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instaed because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored. + allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored. baseConfig = null, cache = false, cacheLocation = ".eslintcache", From 630c65e9482244a2526167d2bbab8c0860bd0866 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Mon, 20 Apr 2020 14:19:04 +0900 Subject: [PATCH 30/71] add `LintResult#source` to docs --- docs/developer-guide/nodejs-api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 282233a6467..9117027bdbd 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -340,6 +340,8 @@ The `LintResult` value is the information of the linting result of each file. Th The number of warnings. This includes fixable warnings. * `output` (`string | undefined`)
The modified source code text. This property is undefined if any fixable messages didn't exist. +* `source` (`string | undefined`)
+ The original source code text. This property is undefined if any messages didn't exist or the `output` property exists. * `usedDeprecatedRules` (`{ ruleId: string; replacedBy: string[] }[]`)
The information about the deprecated rules that were used to check this file. From 8e9f961823c52311cc2cb0cc904767cef88bb3f1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:20:19 +0900 Subject: [PATCH 31/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 9117027bdbd..17c964223d6 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -40,7 +40,7 @@ While ESLint is designed to be run on the command line, it's possible to use ESL ## ESLint class -The `ESLint` class is the facade class to use ESLint from Node.js application. +The `ESLint` class is primary class to use in Node.js applications. This class pretty depends on the file system, so you cannot use it on browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. From 29262c8fddb2f5346c2df0faab1bba34e110b48b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:20:38 +0900 Subject: [PATCH 32/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 17c964223d6..f795695973e 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -42,7 +42,7 @@ While ESLint is designed to be run on the command line, it's possible to use ESL The `ESLint` class is primary class to use in Node.js applications. -This class pretty depends on the file system, so you cannot use it on browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. +This class depends on the Node.js `fs` module and the file system, so you cannot use it in browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. For a simple example: From 73a3d3978cbcb566b7236cf20b473583b80507b6 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:21:01 +0900 Subject: [PATCH 33/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f795695973e..2c2761eb417 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -150,7 +150,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.fixTypes` (`("problem" | "suggestion" | "layout")[] | null`)
Default is `null`. The types of the rules that the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods use for autofix. -##### Misc +##### Cache-related * `options.cache` (`boolean`)
Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method caches lint results and uses it if each target file is not changed. Please mind that ESLint doesn't clear the cache when you upgrade ESLint plugins. In that case, you have to remove the cache file manually. The [`eslint.lintText()`][eslint-linttext] method doesn't use caches even if you pass the `options.filePath` to the method. From 47d18d3818fcc43107ec30baf816c6b440584616 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:21:25 +0900 Subject: [PATCH 34/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 2c2761eb417..5ea80bfb404 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -181,7 +181,7 @@ This method checks the given files then returns the results. const results = await eslint.lintText(code, options); ``` -This method checks the given source code text then returns the results. +This method lints the given source code text and then returns the results. If you want to use suitable configuration files to lint the text, passing the `options.filePath` option is important. This method loads configuration files with the same manner as the [`eslint.lintFiles()`][eslint-lintfiles] method checks the file the `options.filePath` option pointers. If the `options.filePath` option is not present, this method uses the configuration file at the current working directory (the `cwd` constructor option). From 844bc8d359dd5b7247390edacc6cd0cc749701c6 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:23:29 +0900 Subject: [PATCH 35/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 5ea80bfb404..38899651708 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -185,7 +185,7 @@ This method lints the given source code text and then returns the results. If you want to use suitable configuration files to lint the text, passing the `options.filePath` option is important. This method loads configuration files with the same manner as the [`eslint.lintFiles()`][eslint-lintfiles] method checks the file the `options.filePath` option pointers. If the `options.filePath` option is not present, this method uses the configuration file at the current working directory (the `cwd` constructor option). -If the given `options.filePath` option value is an ignored path by your configurations, this method returns an empty array. If the `options.warnIgnored` option is set along with the `options.filePath` option, this method returns a [LintResult] object at least. In that case, the result may contain a warning that indicates the file was ignored. +If the `options.filePath` value is configured to be ignored, this method returns an empty array. If the `options.warnIgnored` option is set along with the `options.filePath` option, this method returns a [LintResult] object. In that case, the result may contain a warning that indicates the file was ignored. #### Parameters From ce0b4be46a5e86a6c8e5b238ef72a517dacfe718 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:24:02 +0900 Subject: [PATCH 36/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 38899651708..ab5f0325012 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -112,7 +112,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.cwd` (`string`)
Default is `process.cwd()`. The working directory. This must be an absolute path. * `options.errorOnUnmatchedPattern` (`boolean`)
- Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't throw even if no target files found. + Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method will not throw when no target files are found. * `options.extensions` (`string[] | null`)
Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
Mind this option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. * `options.globInputPaths` (`boolean`)
From e2d79e5772a1ea227dc20545a81876247ac3ffd1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:24:21 +0900 Subject: [PATCH 37/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index ab5f0325012..968ee4bb9b7 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -114,7 +114,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.errorOnUnmatchedPattern` (`boolean`)
Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method will not throw when no target files are found. * `options.extensions` (`string[] | null`)
- Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
Mind this option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. + Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
**Note:** This option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. * `options.globInputPaths` (`boolean`)
Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interprit glob patterns. * `options.ignore` (`boolean`)
From 8f44116dabd0f73b0f27028b7faae917624b645b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:24:55 +0900 Subject: [PATCH 38/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 968ee4bb9b7..78f774ff9c8 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -118,7 +118,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.globInputPaths` (`boolean`)
Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interprit glob patterns. * `options.ignore` (`boolean`)
- Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't use the `.eslintignore` file and `ignorePatterns` in your configuration. + Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't respect `.eslintignore` files or `ignorePatterns` in your configuration. * `options.ignorePath` (`string | null`)
Default is `null`. The path to a file ESLint uses instead of `$CWD/.eslintignore`. If a path is present and the file doesn't exist, this constructor will throw an error. From 78a1989efe4d068ca6d96a333e09e2fc154900cd Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:25:19 +0900 Subject: [PATCH 39/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 78f774ff9c8..a6478d398ec 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -116,7 +116,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.extensions` (`string[] | null`)
Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
**Note:** This option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. * `options.globInputPaths` (`boolean`)
- Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interprit glob patterns. + Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interpret glob patterns. * `options.ignore` (`boolean`)
Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't respect `.eslintignore` files or `ignorePatterns` in your configuration. * `options.ignorePath` (`string | null`)
From 72d323776611e50ecdc85859bd03939b89744dcb Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:25:52 +0900 Subject: [PATCH 40/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index a6478d398ec..f9a868e8cf5 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -201,7 +201,7 @@ The second parameter `options` is omittable. #### Return Value * (`Promise`)
- The promise that will be fulfilled with an array of [LintResult] objects. The length of this array is `1` at most, but it's an array to because of aligning to the [`eslint.lintFiles()`][eslint-lintfiles] method. + The promise that will be fulfilled with an array of [LintResult] objects. This is an array (despite there being only one lint result) in order to keep the interfaces between this and the [`eslint.lintFiles()`][eslint-lintfiles] method similar. ### ◆ eslint.calculateConfigForFile(filePath) From bb89cde7e5b64d943d14492320a847355f366711 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:26:38 +0900 Subject: [PATCH 41/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f9a868e8cf5..e864ae0bd20 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -211,7 +211,7 @@ const config = await eslint.calculateConfigForFile(filePath); This method calculates the configuration for a given file. -* It resolves and merges the `extends` and `overrides` settings, so the result object doesn't contain the two. +* It resolves and merges `extends` and `overrides` settings into the top level configuration. * It resolves the `parser` setting to absolute paths. * It normalizes the `plugins` setting to align short names. (e.g., `eslint-plugin-foo` → `foo`) * It adds the `processor` setting if a legacy file extension processor is matched. From eec219e524ecccc3ebafc3b7707efe35b50bdd17 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:27:03 +0900 Subject: [PATCH 42/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index e864ae0bd20..50d6867a679 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -215,7 +215,7 @@ This method calculates the configuration for a given file. * It resolves the `parser` setting to absolute paths. * It normalizes the `plugins` setting to align short names. (e.g., `eslint-plugin-foo` → `foo`) * It adds the `processor` setting if a legacy file extension processor is matched. -* It doesn't interpret the `env` setting to the `globals` and `parserOotions` settings, so the result object contains the `env` setting as is. +* It doesn't interpret the `env` setting to the `globals` and `parserOptions` settings, so the result object contains the `env` setting as is. You can use the calculated configuration object for the debugging purpose. From 3a3e00ca67d681123a1efe4938e34a09cf45f784 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:29:10 +0900 Subject: [PATCH 43/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 50d6867a679..1625d78e163 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -222,7 +222,7 @@ You can use the calculated configuration object for the debugging purpose. #### Parameters * `filePath` (`string`)
- The path to the file you want to check the actual configuration. Directory paths are forbidden because ESLint cannot handle the `overrides` setting. + The path to the file whose configuration you would like to calculate. Directory paths are forbidden because ESLint cannot handle the `overrides` setting. #### Return Value From 046b66adf599a1bf424c85295935ebb723724178 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:30:30 +0900 Subject: [PATCH 44/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 1625d78e163..f69a96fb4d6 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -235,7 +235,7 @@ You can use the calculated configuration object for the debugging purpose. const isPathIgnored = await eslint.isPathIgnored(filePath); ``` -This method checks if a given file is ignored by your configurations. +This method checks if a given file is ignored by your configuration. #### Parameters From 818438b7cb41fa2c7605cb634f6ee10194511206 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:31:14 +0900 Subject: [PATCH 45/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f69a96fb4d6..7a52282e52a 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -245,7 +245,7 @@ This method checks if a given file is ignored by your configuration. #### Return Value * (`Promise`)
- The promise that will be fulfilled with whether the file is ignored or not. If the file is ignored then `true`. + The promise that will be fulfilled with whether the file is ignored or not. If the file is ignored, then it will return `true`. ### ◆ eslint.loadFormatter(nameOrPath) From eff35d4eebde0c7e9bf520cac92f3c4a9d451042 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:32:25 +0900 Subject: [PATCH 46/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 7a52282e52a..22c4c63239d 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -280,7 +280,7 @@ const version = ESLint.version; The version string of ESLint. E.g. `"7.0.0"`. -This is not instance property. +This is a static property. ### ◆ ESLint.outputFixes(results) From c6d66a85c5b8128ee2f28188a7ebf381380df70e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:32:57 +0900 Subject: [PATCH 47/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 22c4c63239d..75859ea0f54 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -290,7 +290,7 @@ await ESLint.outputFixes(results); This method writes the modified code by the autofix feature into each file. If any modified files don't exist, this method does nothing. -This is not instance method. +This is a static method. #### Parameters From f0aeafbb51f935950d69a28db9e4dabcf3ee8f87 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:34:18 +0900 Subject: [PATCH 48/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 75859ea0f54..fd8db3b749a 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -288,7 +288,7 @@ This is a static property. await ESLint.outputFixes(results); ``` -This method writes the modified code by the autofix feature into each file. If any modified files don't exist, this method does nothing. +This method writes code modified by ESLint's autofix feature into its respective file. If any of the modified files don't exist, this method does nothing. This is a static method. From 7b445ca7d42053b6a830938f5cace30a0a32be85 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:34:43 +0900 Subject: [PATCH 49/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index fd8db3b749a..2bb21e5e94d 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -310,7 +310,7 @@ const filteredResults = ESLint.getErrorResults(results); This method copies the given results and removes warnings. The returned value contains only errors. -This is not instance method. +This is a static method. #### Parameters From da252e60779f60a0a23a845762692ae015c1049b Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:35:19 +0900 Subject: [PATCH 50/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 2bb21e5e94d..de6b96806ef 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -324,7 +324,7 @@ This is a static method. ### ◆ LintResult type -The `LintResult` value is the information of the linting result of each file. The [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods return it. It has following properties: +The `LintResult` value is the information of the linting result of each file. The [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods return it. It has the following properties: * `filePath` (`string`)
The absolute path to the file of this result. This may be the string `""` rather than a file path if the [`eslint.lintText()`][eslint-linttext] method returned it. From db8600f0634516f4bae5b5bcadaf554583f6a8e1 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:36:54 +0900 Subject: [PATCH 51/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index de6b96806ef..f0d60a156b5 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -347,7 +347,7 @@ The `LintResult` value is the information of the linting result of each file. Th ### ◆ LintMessage type -The `LintMessage` value is the information of each linting error. The `messages` property of the [LintResult] type contains it. It has following properties: +The `LintMessage` value is the information of each linting error. The `messages` property of the [LintResult] type contains it. It has the following properties: * `ruleId` (`string` | `null`)
The rule name that generates this lint message. If this message is generated by the ESLint core rather than rules, this is `null`. From f244a0410924df4b6dd4a5059e32ab66175dfa23 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:37:35 +0900 Subject: [PATCH 52/71] Update lib/eslint/eslint.js Co-Authored-By: Kai Cataldo --- lib/eslint/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 62e47bde733..66533ccb66a 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -162,7 +162,7 @@ function processOptions({ overrideConfig = null, overrideConfigFile = null, plugins = {}, - reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instaed because we cannot configure the `error` severity with that. + reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that. resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. rulePaths = [], useEslintrc = true, From fc91aea4f52a7fcd5cf43316bb3b9552a3fdb645 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:38:19 +0900 Subject: [PATCH 53/71] Update lib/eslint/eslint.js Co-Authored-By: Kai Cataldo --- lib/eslint/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 66533ccb66a..660fc35edfd 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -295,7 +295,7 @@ function processOptions({ } /** - * Check if a value has one or more properties that that value is not undefined. + * Check if a value has one or more properties and that value is not undefined. * @param {any} obj The value to check. * @returns {boolean} `true` if `obj` has one or more properties that that value is not undefined. */ From 9daca9d8939d6f717ab55c5edc52dc540c64d535 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Wed, 22 Apr 2020 23:38:45 +0900 Subject: [PATCH 54/71] Update tests/lib/cli.js Co-Authored-By: Kai Cataldo --- tests/lib/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 085cae3044e..0d8fc776b8f 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -7,7 +7,7 @@ /* * NOTE: If you are adding new tests for cli.js, use verifyESLintOpts(). The - * test only needs to verify that CLIEngine receives the correct opts. + * test only needs to verify that ESLint receives the correct opts. */ //------------------------------------------------------------------------------ From 186eebd014d34035172988c721d43cbb044b2eb2 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:12:38 +0900 Subject: [PATCH 55/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f0d60a156b5..382ef2a2034 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -44,7 +44,7 @@ The `ESLint` class is primary class to use in Node.js applications. This class depends on the Node.js `fs` module and the file system, so you cannot use it in browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. -For a simple example: +Here's a simple example of using the `ESLint` class: ```js const { ESLint } = require("eslint"); From 13d1de77205b555261f629fde3d8e023787f018d Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:12:52 +0900 Subject: [PATCH 56/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 382ef2a2034..83ce0314d0d 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -68,7 +68,7 @@ const { ESLint } = require("eslint"); }); ``` -For a simple example with autofix: +And here is an example that autofixes lint problems: ```js const { ESLint } = require("eslint"); From d2cb761894ee481f06b15a059a669e48f57bcf5e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:13:19 +0900 Subject: [PATCH 57/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Nicholas C. Zakas --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 83ce0314d0d..f6df6a69cea 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -163,7 +163,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob const results = await eslint.lintFiles(patterns); ``` -This method checks the given files then returns the results. +This method lints the files that match the glob patterns and then returns the results. #### Parameters From 00399d25a1b07fcf97c32c628f78608925186f10 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:22:02 +0900 Subject: [PATCH 58/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Kai Cataldo --- docs/developer-guide/nodejs-api.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f6df6a69cea..b1ebab13ac6 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -209,7 +209,7 @@ The second parameter `options` is omittable. const config = await eslint.calculateConfigForFile(filePath); ``` -This method calculates the configuration for a given file. +This method calculates the configuration for a given file, which can be useful for debugging purposes. * It resolves and merges `extends` and `overrides` settings into the top level configuration. * It resolves the `parser` setting to absolute paths. @@ -217,8 +217,6 @@ This method calculates the configuration for a given file. * It adds the `processor` setting if a legacy file extension processor is matched. * It doesn't interpret the `env` setting to the `globals` and `parserOptions` settings, so the result object contains the `env` setting as is. -You can use the calculated configuration object for the debugging purpose. - #### Parameters * `filePath` (`string`)
From b83ee7f88353e1e1708a11126052bc762eb019de Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:42:21 +0900 Subject: [PATCH 59/71] add loadFormatter description --- docs/developer-guide/nodejs-api.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index b1ebab13ac6..c39dd77b35a 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -251,19 +251,19 @@ This method checks if a given file is ignored by your configuration. const formatter = await eslint.loadFormatter(nameOrPath); ``` -This method loads a formatter. +This method loads a formatter. The formatters are what convert lint results to a formated string, human-readable or machine-readable. #### Parameters * `nameOrPath` (`string | undefined`)
The path to the file you want to check. The following values are allowed: * `undefined`. In this case, loads the `"stylish"` built-in formatter. - * One of [built-in formatter names][builtin-formatters]. - * A third-party formatter name. For examples: + * A name of [built-in formatters][builtin-formatters]. + * A name of [third-party formatters][thirdparty-formatters]. For examples: * `"foo"` will load `eslint-formatter-foo`. * `"@foo"` will load `@foo/eslint-formatter`. * `"@foo/bar"` will load `@foo/eslint-formatter-bar`. - * A path to the file that defines a formatter. The path must contain one or more path separators (`/`) in order to distinguish if it's a path or not. + * A path to the file that defines a formatter. The path must contain one or more path separators (`/`) in order to distinguish if it's a path or not. For example, start with `./`. #### Return Value @@ -1380,6 +1380,7 @@ ruleTester.run("my-rule", myRule, { [configuration object]: ../user-guide/configuring.md [builtin-formatters]: https://eslint.org/docs/user-guide/formatters/ +[thirdparty-formatters]: https://www.npmjs.com/search?q=eslintformatter [eslint]: #eslint-class [eslint-constructor]: #-new-eslintoptions [eslint-lintfiles]: #-eslintlintFilespatterns From 6c047f298f3ab0f3b12e0f62389a8c8cdf651354 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:49:00 +0900 Subject: [PATCH 60/71] add about --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index c39dd77b35a..dcc5eaa7d07 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -325,7 +325,7 @@ This is a static method. The `LintResult` value is the information of the linting result of each file. The [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods return it. It has the following properties: * `filePath` (`string`)
- The absolute path to the file of this result. This may be the string `""` rather than a file path if the [`eslint.lintText()`][eslint-linttext] method returned it. + The absolute path to the file of this result. This is the string `""` if the file path is unknown (when you didn't pass the `options.filePath` option to the [`eslint.lintText()`][eslint-linttext] method). * `messages` (`LintMessage[]`)
The array of [LintMessage] objects. * `fixableErrorCount` (`number`)
From d91fa566dc153deb3ee5e611c915e3f7df8c6762 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 04:58:56 +0900 Subject: [PATCH 61/71] fix error message --- lib/eslint/eslint.js | 2 +- tests/lib/eslint/eslint.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 660fc35edfd..d195aab09f1 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -243,7 +243,7 @@ function processOptions({ if (typeof plugins !== "object") { errors.push("'plugins' must be an object or null."); } else if (plugins !== null && Object.keys(plugins).includes("")) { - errors.push("'plugins' must not include the empty string in keys."); + errors.push("'plugins' must not include an empty string."); } if (Array.isArray(plugins)) { errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead."); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index c77b4dfe924..fc86a781d0d 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -217,7 +217,7 @@ describe("ESLint", () => { }), new RegExp(escapeStringRegExp([ "Invalid Options:", - "- 'plugins' must not include the empty string in keys." + "- 'plugins' must not include an empty string." ].join("\n")), "u") ); }); From 21c2300db4bc04b1fa39928bc45a16e02b0dcb10 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:11:30 +0900 Subject: [PATCH 62/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index dcc5eaa7d07..3c6c0f0ff35 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -183,7 +183,7 @@ const results = await eslint.lintText(code, options); This method lints the given source code text and then returns the results. -If you want to use suitable configuration files to lint the text, passing the `options.filePath` option is important. This method loads configuration files with the same manner as the [`eslint.lintFiles()`][eslint-lintfiles] method checks the file the `options.filePath` option pointers. If the `options.filePath` option is not present, this method uses the configuration file at the current working directory (the `cwd` constructor option). +By default, this method uses the configuration that applies to files in the current working directory (the `cwd` constructor option). If you want to use a different configuration, pass `options.filePath`, and ESLint will load the same configuration that [`eslint.lintFiles()`][eslint-lintfiles] would use for a file at `options.filePath`. If the `options.filePath` value is configured to be ignored, this method returns an empty array. If the `options.warnIgnored` option is set along with the `options.filePath` option, this method returns a [LintResult] object. In that case, the result may contain a warning that indicates the file was ignored. From fdabe84d92754920ec842eacf84d6774d90c3b59 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:12:58 +0900 Subject: [PATCH 63/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 3c6c0f0ff35..98a8851c543 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -16,10 +16,10 @@ While ESLint is designed to be run on the command line, it's possible to use ESL * [static version][eslint-version] * [static outputFixes][eslint-outputFixes] * [static getErrorResults][eslint-getErrorResults] - * [LintResult type](#LintResult-type) - * [LintMessage type](#LintMessage-type) - * [EditInfo type](#EditInfo-type) - * [Formatter type](#Formatter-type) + * [LintResult type](lintresult) + * [LintMessage type](lintmessage) + * [EditInfo type](editinfo) + * [Formatter type](formatter) * [SourceCode](#sourcecode) * [splitLines()](#sourcecode-splitlines) * [Linter](#linter) From 8e315cd8c1187d8f59f4fcb670d4c0f109598974 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:13:43 +0900 Subject: [PATCH 64/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 98a8851c543..2f827a5065a 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -40,7 +40,7 @@ While ESLint is designed to be run on the command line, it's possible to use ESL ## ESLint class -The `ESLint` class is primary class to use in Node.js applications. +The `ESLint` class is the primary class to use in Node.js applications. This class depends on the Node.js `fs` module and the file system, so you cannot use it in browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. From e1a94a4ec5eab5197df713243df322404dae05d4 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:14:17 +0900 Subject: [PATCH 65/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 2f827a5065a..a32a6eb057b 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -112,7 +112,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.cwd` (`string`)
Default is `process.cwd()`. The working directory. This must be an absolute path. * `options.errorOnUnmatchedPattern` (`boolean`)
- Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method will not throw when no target files are found. + Default is `true`. Unless set to `false`, the [`eslint.lintFiles()`][eslint-lintfiles] method will throw an error when no target files are found. * `options.extensions` (`string[] | null`)
Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
**Note:** This option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. * `options.globInputPaths` (`boolean`)
From d43d12c9cebd84c9524eb4656e89b5c61870ce5e Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:16:44 +0900 Subject: [PATCH 66/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index a32a6eb057b..97838d7023f 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -114,7 +114,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.errorOnUnmatchedPattern` (`boolean`)
Default is `true`. Unless set to `false`, the [`eslint.lintFiles()`][eslint-lintfiles] method will throw an error when no target files are found. * `options.extensions` (`string[] | null`)
- Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files which have the given extensions in the directories. If `null` is present, ESLint checks `*.js` files and the files which matches `overrides[].files` in your configuration.
**Note:** This option works only if you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint doesn't restrict file kinds to check. + Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files in those directories that have the given extensions. For example, when passing the `src/` directory and `extensions` is `[".js", ".ts"]`, ESLint will lint `*.js` and `*.ts` files in `src/`. If `extensions` is `null`, ESLint checks `*.js` files and files that match `overrides[].files` patterns in your configuration.
**Note:** This option only applies when you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint will lint all files matching the glob pattern regardless of extension. * `options.globInputPaths` (`boolean`)
Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interpret glob patterns. * `options.ignore` (`boolean`)
From ac957d22eafcd8658de5d4f502afdcdde2b25207 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:17:40 +0900 Subject: [PATCH 67/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 97838d7023f..341c4fc2e6f 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -146,7 +146,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob ##### Autofix * `options.fix` (`boolean | (message: LintMessage) => boolean`)
- Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods work in autofix mode. If a function is present, the methods pass each lint message to the function, then use only the lint messages that the function returned `true`. + Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods work in autofix mode. If a predicate function is present, the methods pass each lint message to the function, then use only the lint messages for which the function returned `true`. * `options.fixTypes` (`("problem" | "suggestion" | "layout")[] | null`)
Default is `null`. The types of the rules that the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods use for autofix. From de9e71bbbdf789b4adbe3655c7acdfc7f920a643 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:18:42 +0900 Subject: [PATCH 68/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 341c4fc2e6f..4d51fba66dd 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -251,7 +251,7 @@ This method checks if a given file is ignored by your configuration. const formatter = await eslint.loadFormatter(nameOrPath); ``` -This method loads a formatter. The formatters are what convert lint results to a formated string, human-readable or machine-readable. +This method loads a formatter. Formatters convert lint results to a human- or machine-readable string. #### Parameters From 98d483da786d158b24c7ba9568cd1805092c3c61 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:19:24 +0900 Subject: [PATCH 69/71] Update docs/developer-guide/nodejs-api.md Co-Authored-By: Brandon Mills --- docs/developer-guide/nodejs-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 4d51fba66dd..e980445447c 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -14,8 +14,8 @@ While ESLint is designed to be run on the command line, it's possible to use ESL * [isPathIgnored()][eslint-isPathIgnored] * [loadFormatter()][eslint-loadFormatter] * [static version][eslint-version] - * [static outputFixes][eslint-outputFixes] - * [static getErrorResults][eslint-getErrorResults] + * [static outputFixes()][eslint-outputFixes] + * [static getErrorResults()][eslint-getErrorResults] * [LintResult type](lintresult) * [LintMessage type](lintmessage) * [EditInfo type](editinfo) From 20dc74dd3804a8824c2734434edcf7f12c53ff55 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:21:09 +0900 Subject: [PATCH 70/71] Update lib/cli-engine/cli-engine.js Co-Authored-By: Brandon Mills --- lib/cli-engine/cli-engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index f3731049b88..b6aa995beef 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -964,7 +964,7 @@ class CLIEngine { * Returns the formatter representing the given format or null if the `format` is not a string. * @param {string} [format] The name of the format to load or the path to a * custom formatter. - * @returns {Function} The formatter function or null if the `format` is not a string. + * @returns {(Function|null)} The formatter function or null if the `format` is not a string. */ getFormatter(format) { From 27cc9342ce301274cbc3ea21440afc88c042a49a Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 24 Apr 2020 11:21:53 +0900 Subject: [PATCH 71/71] Update tests/lib/cli.js Co-Authored-By: Brandon Mills --- tests/lib/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 0d8fc776b8f..a1d9a23e491 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -65,7 +65,7 @@ describe("cli", () => { "./shared/logging": log }); - await await localCLI.execute(cmd); + await localCLI.execute(cmd); sinon.verifyAndRestore(); }