diff --git a/Makefile.js b/Makefile.js index 64db70365d0..1b241c1ae1d 100644 --- a/Makefile.js +++ b/Makefile.js @@ -988,7 +988,7 @@ function createConfigForPerformanceTest() { function time(cmd, runs, runNumber, results, cb) { const start = process.hrtime(); - exec(cmd, { silent: true }, (code, stdout, stderr) => { + exec(cmd, { maxBuffer: 64 * 1024 * 1024, silent: true }, (code, stdout, stderr) => { const diff = process.hrtime(start), actual = (diff[0] * 1e3 + diff[1] / 1e6); // ms diff --git a/conf/config-schema.js b/conf/config-schema.js index 164f0b4219f..83addff8781 100644 --- a/conf/config-schema.js +++ b/conf/config-schema.js @@ -55,6 +55,7 @@ const configSchema = { type: "object", properties: { root: { type: "boolean" }, + ignorePatterns: { $ref: "#/definitions/stringOrStrings" }, ...baseConfigProperties }, additionalProperties: false diff --git a/conf/default-cli-options.js b/conf/default-cli-options.js index abbd9184e21..0f7b377fb36 100644 --- a/conf/default-cli-options.js +++ b/conf/default-cli-options.js @@ -14,7 +14,7 @@ module.exports = { globals: [], extensions: [".js"], ignore: true, - ignorePath: null, + ignorePath: void 0, cache: false, /* diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index 8938bc91f24..c8871b07d2f 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -266,7 +266,7 @@ Example: #### `--no-ignore` -Disables excluding of files from `.eslintignore`, `--ignore-path` and `--ignore-pattern`. +Disables excluding of files from `.eslintignore`, `--ignore-path`, `--ignore-pattern`, and `ignorePatterns` property in config files. Example: diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 00d4cfe93f0..5022fc44d8e 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -915,8 +915,8 @@ module.exports = { * The patterns are applied against the file path relative to the directory of the config file. For example, if your config file has the path `/Users/john/workspace/any-project/.eslintrc.js` and the file you want to lint has the path `/Users/john/workspace/any-project/lib/util.js`, then the pattern provided in `.eslintrc.js` will be executed against the relative path `lib/util.js`. * Glob pattern overrides have higher precedence than the regular configuration in the same config file. Multiple overrides within the same config are applied in order. That is, the last override block in a config file always has the highest precedence. -* A glob specific configuration works almost the same as any other ESLint config. Override blocks can contain any configuration options that are valid in a regular config, with the exception of `root`. - * A glob specific configuration can have `extends` setting, but the `root` property in the extended configs is ignored. +* A glob specific configuration works almost the same as any other ESLint config. Override blocks can contain any configuration options that are valid in a regular config, with the exception of `root` and `ignorePatterns`. + * A glob specific configuration can have `extends` setting, but the `root` property in the extended configs is ignored. The `ignorePatterns` property in the extended configs is used only for the files the glob specific configuration matched. * Nested `overrides` setting will be applied only if the glob patterns of both of the parent config and the child config matched. This is the same when the extended configs have `overrides` setting. * Multiple glob patterns can be provided within a single override block. A file must match at least one of the supplied patterns for the configuration to apply. * Override blocks can also specify patterns to exclude from matches. If a file matches any of the excluded patterns, the configuration won't apply. @@ -986,6 +986,23 @@ Currently the sole method for telling ESLint which file extensions to lint is by ## Ignoring Files and Directories +### `ignorePatterns` in config files + +You can tell ESLint to ignore specific files and directories by `ignorePatterns` in your config files. Each value of `ignorePatterns` is the same pattern as each line of `.eslintignore` in the next section. + +```json +{ + "ignorePatterns": ["temp.js", "node_modules/"], + "rules": { + //... + } +} +``` + +* The `ignorePatterns` property affects only the directory that the config file placed. +* You cannot write `ignorePatterns` property under `overrides` property. +* `.eslintignore` can override `ignorePatterns` property of config files. + ### `.eslintignore` You can tell ESLint to ignore specific files and directories by creating an `.eslintignore` file in your project's root directory. The `.eslintignore` file is a plain text file where each line is a glob pattern indicating which paths should be omitted from linting. For example, the following will omit all JavaScript files: diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 6c914ea491c..52703a873bb 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -27,7 +27,7 @@ const os = require("os"); const path = require("path"); const { validateConfigArray } = require("../shared/config-validator"); const { ConfigArrayFactory } = require("./config-array-factory"); -const { ConfigArray, ConfigDependency } = require("./config-array"); +const { ConfigArray, ConfigDependency, IgnorePattern } = require("./config-array"); const loadRules = require("./load-rules"); const debug = require("debug")("eslint:cascading-config-array-factory"); @@ -45,8 +45,9 @@ const debug = require("debug")("eslint:cascading-config-array-factory"); * @typedef {Object} CascadingConfigArrayFactoryOptions * @property {Map} [additionalPluginPool] The map for additional plugins. * @property {ConfigData} [baseConfig] The config by `baseConfig` option. - * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files. + * @property {ConfigData} [cliConfig] The config by CLI options (`--env`, `--global`, `--ignore-pattern`, `--parser`, `--parser-options`, `--plugin`, and `--rule`). CLI options overwrite the setting in config files. * @property {string} [cwd] The base directory to start lookup. + * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`. * @property {string[]} [rulePaths] The value of `--rulesdir` option. * @property {string} [specificConfigPath] The value of `--config` option. * @property {boolean} [useEslintrc] if `false` then it doesn't load config files. @@ -62,6 +63,7 @@ const debug = require("debug")("eslint:cascading-config-array-factory"); * @property {Map} configCache The cache from directory paths to config arrays. * @property {string} cwd The base directory to start lookup. * @property {WeakMap} finalizeCache The cache from config arrays to finalized config arrays. + * @property {string} [ignorePath] The path to the alternative file of `.eslintignore`. * @property {string[]|null} rulePaths The value of `--rulesdir` option. This is used to reset `baseConfigArray`. * @property {string|null} specificConfigPath The value of `--config` option. This is used to reset `cliConfigArray`. * @property {boolean} useEslintrc if `false` then it doesn't load config files. @@ -86,14 +88,22 @@ function createBaseConfigArray({ { name: "BaseConfig" } ); + /* + * Create the config array element for the default ignore patterns. + * This element has `ignorePattern` property that ignores the default + * patterns in the current working directory. + */ + baseConfigArray.unshift(configArrayFactory.create( + { ignorePatterns: IgnorePattern.DefaultPatterns }, + { name: "DefaultIgnorePattern" } + )[0]); + + /* + * Load rules `--rulesdir` option as a pseudo plugin. + * Use a pseudo plugin to define rules of `--rulesdir`, so we can validate + * the rule's options with only information in the config array. + */ if (rulePaths && rulePaths.length > 0) { - - /* - * Load rules `--rulesdir` option as a pseudo plugin. - * Use a pseudo plugin to define rules of `--rulesdir`, so we can - * validate the rule's options with only information in the config - * array. - */ baseConfigArray.push({ name: "--rulesdir", filePath: "", @@ -128,6 +138,7 @@ function createBaseConfigArray({ function createCLIConfigArray({ cliConfigData, configArrayFactory, + ignorePath, specificConfigPath }) { const cliConfigArray = configArrayFactory.create( @@ -135,6 +146,12 @@ function createCLIConfigArray({ { name: "CLIOptions" } ); + cliConfigArray.unshift( + ...(ignorePath + ? configArrayFactory.loadESLintIgnore(ignorePath) + : configArrayFactory.loadDefaultESLintIgnore()) + ); + if (specificConfigPath) { cliConfigArray.unshift( ...configArrayFactory.loadFile( @@ -178,6 +195,7 @@ class CascadingConfigArrayFactory { baseConfig: baseConfigData = null, cliConfig: cliConfigData = null, cwd = process.cwd(), + ignorePath, resolvePluginsRelativeTo = cwd, rulePaths = [], specificConfigPath = null, @@ -200,6 +218,7 @@ class CascadingConfigArrayFactory { cliConfigArray: createCLIConfigArray({ cliConfigData, configArrayFactory, + ignorePath, specificConfigPath }), cliConfigData, @@ -207,6 +226,7 @@ class CascadingConfigArrayFactory { configCache: new Map(), cwd, finalizeCache: new WeakMap(), + ignorePath, rulePaths, specificConfigPath, useEslintrc @@ -229,9 +249,11 @@ class CascadingConfigArrayFactory { * If `filePath` was not given, it returns the config which contains only * `baseConfigData` and `cliConfigData`. * @param {string} [filePath] The file path to a file. + * @param {Object} [options] The options. + * @param {boolean} [options.ignoreNotFoundError] If `true` then it doesn't throw `ConfigurationNotFoundError`. * @returns {ConfigArray} The config array of the file. */ - getConfigArrayForFile(filePath) { + getConfigArrayForFile(filePath, { ignoreNotFoundError = false } = {}) { const { baseConfigArray, cliConfigArray, @@ -248,7 +270,8 @@ class CascadingConfigArrayFactory { return this._finalizeConfigArray( this._loadConfigInAncestors(directoryPath), - directoryPath + directoryPath, + ignoreNotFoundError ); } @@ -354,10 +377,11 @@ class CascadingConfigArrayFactory { * Concatenate `--config` and other CLI options. * @param {ConfigArray} configArray The parent config array. * @param {string} directoryPath The path to the leaf directory to find config files. + * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`. * @returns {ConfigArray} The loaded config. * @private */ - _finalizeConfigArray(configArray, directoryPath) { + _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) { const { cliConfigArray, configArrayFactory, @@ -403,7 +427,8 @@ class CascadingConfigArrayFactory { ); } - if (useEslintrc && finalConfigArray.length === 0) { + // At least one element (the default ignore patterns) exists. + if (!ignoreNotFoundError && useEslintrc && finalConfigArray.length <= 1) { throw new ConfigurationNotFoundError(directoryPath); } diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 8afd262708f..0b1c76bac68 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -25,10 +25,9 @@ const ModuleResolver = require("../shared/relative-module-resolver"); const { Linter } = require("../linter"); const builtInRules = require("../rules"); const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory"); -const { getUsedExtractedConfigs } = require("./config-array"); +const { IgnorePattern, getUsedExtractedConfigs } = require("./config-array"); const { FileEnumerator } = require("./file-enumerator"); const hash = require("./hash"); -const { IgnoredPaths } = require("./ignored-paths"); const LintResultCache = require("./lint-result-cache"); const debug = require("debug")("eslint:cli-engine"); @@ -64,7 +63,7 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @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} ignorePattern A glob pattern of files to ignore. + * @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. @@ -113,8 +112,8 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @property {Map} additionalPluginPool The map for additional plugins. * @property {string} cacheFilePath The path to the cache of lint results. * @property {CascadingConfigArrayFactory} configArrayFactory The factory of configs. + * @property {(filePath: string) => boolean} defaultIgnores The default predicate function to check if a file ignored or not. * @property {FileEnumerator} fileEnumerator The file enumerator. - * @property {IgnoredPaths} ignoredPaths The ignored paths. * @property {ConfigArray[]} lastConfigArrays The list of config arrays that the last `executeOnFiles` or `executeOnText` used. * @property {LintResultCache|null} lintResultCache The cache of lint results. * @property {Linter} linter The linter instance which has loaded rules. @@ -486,13 +485,20 @@ function toBooleanMap(keys, defaultValue, displayName) { * @returns {ConfigData|null} The created config data. */ function createConfigDataFromOptions(options) { - const { parser, parserOptions, plugins, rules } = options; + const { + ignorePattern, + parser, + parserOptions, + plugins, + rules + } = options; const env = toBooleanMap(options.envs, true, "envs"); const globals = toBooleanMap(options.globals, false, "globals"); if ( env === void 0 && globals === void 0 && + (ignorePattern === void 0 || ignorePattern.length === 0) && parser === void 0 && parserOptions === void 0 && plugins === void 0 && @@ -500,7 +506,15 @@ function createConfigDataFromOptions(options) { ) { return null; } - return { env, globals, parser, parserOptions, plugins, rules }; + return { + env, + globals, + ignorePatterns: ignorePattern, + parser, + parserOptions, + plugins, + rules + }; } /** @@ -551,19 +565,18 @@ class CLIEngine { baseConfig: options.baseConfig || null, cliConfig: createConfigDataFromOptions(options), cwd: options.cwd, + ignorePath: options.ignorePath, resolvePluginsRelativeTo: options.resolvePluginsRelativeTo, rulePaths: options.rulePaths, specificConfigPath: options.configFile, useEslintrc: options.useEslintrc }); - const ignoredPaths = new IgnoredPaths(options); const fileEnumerator = new FileEnumerator({ configArrayFactory, cwd: options.cwd, extensions: options.extensions, globInputPaths: options.globInputPaths, - ignore: options.ignore, - ignoredPaths + ignore: options.ignore }); const lintResultCache = options.cache ? new LintResultCache(cacheFilePath) : null; @@ -577,8 +590,8 @@ class CLIEngine { additionalPluginPool, cacheFilePath, configArrayFactory, + defaultIgnores: IgnorePattern.createDefaultIgnore(options.cwd), fileEnumerator, - ignoredPaths, lastConfigArrays, lintResultCache, linter, @@ -835,7 +848,6 @@ class CLIEngine { const { configArrayFactory, fileEnumerator, - ignoredPaths, lastConfigArrays, linter, options: { @@ -852,7 +864,7 @@ class CLIEngine { // Clear the last used config arrays. lastConfigArrays.length = 0; - if (resolvedFilename && ignoredPaths.contains(resolvedFilename)) { + if (resolvedFilename && this.isPathIgnored(resolvedFilename)) { if (warnIgnored) { results.push(createIgnoreResult(resolvedFilename, cwd)); } @@ -926,9 +938,23 @@ class CLIEngine { * @returns {boolean} Whether or not the given path is ignored. */ isPathIgnored(filePath) { - const { ignoredPaths } = internalSlotsMap.get(this); + const { + configArrayFactory, + defaultIgnores, + options: { cwd, ignore } + } = internalSlotsMap.get(this); + const absolutePath = path.resolve(cwd, filePath); + + if (ignore) { + const config = configArrayFactory + .getConfigArrayForFile(absolutePath) + .extractConfig(absolutePath); + const ignores = config.ignores || defaultIgnores; + + return ignores(absolutePath); + } - return ignoredPaths.contains(filePath); + return defaultIgnores(absolutePath); } /** diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index cf529b6ee63..c444031bcb0 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -17,6 +17,12 @@ * Create a `ConfigArray` instance from a config file which is on a given * directory. This tries to load `.eslintrc.*` or `package.json`. If not * found, returns an empty `ConfigArray`. + * - `loadESLintIgnore(filePath)` + * Create a `ConfigArray` instance from a config file that is `.eslintignore` + * format. This is to handle `--ignore-path` option. + * - `loadDefaultESLintIgnore()` + * Create a `ConfigArray` instance from `.eslintignore` or `package.json` in + * the current working directory. * * `ConfigArrayFactory` class has the responsibility that loads configuration * files, including loading `extends`, `parser`, and `plugins`. The created @@ -40,7 +46,12 @@ const stripComments = require("strip-json-comments"); const { validateConfigSchema } = require("../shared/config-validator"); const naming = require("../shared/naming"); const ModuleResolver = require("../shared/relative-module-resolver"); -const { ConfigArray, ConfigDependency, OverrideTester } = require("./config-array"); +const { + ConfigArray, + ConfigDependency, + IgnorePattern, + OverrideTester +} = require("./config-array"); const debug = require("debug")("eslint:config-array-factory"); //------------------------------------------------------------------------------ @@ -221,6 +232,26 @@ function loadPackageJSONConfigFile(filePath) { } } +/** + * Loads a `.eslintignore` from a file. + * @param {string} filePath The filename to load. + * @returns {string[]} The ignore patterns from the file. + * @private + */ +function loadESLintIgnoreFile(filePath) { + debug(`Loading .eslintignore file: ${filePath}`); + + try { + return readFile(filePath) + .split(/\r?\n/gu) + .filter(line => line.trim() !== "" && !line.startsWith("#")); + } catch (e) { + debug(`Error reading .eslintignore file: ${filePath}`); + e.message = `Cannot read .eslintignore file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + /** * Creates an error to notify about a missing config to extend from. * @param {string} configName The name of the missing config. @@ -403,6 +434,54 @@ class ConfigArrayFactory { ); } + /** + * Load `.eslintignore` file. + * @param {string} filePath The path to a `.eslintignore` file to load. + * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. + */ + loadESLintIgnore(filePath) { + const { cwd } = internalSlotsMap.get(this); + const absolutePath = path.resolve(cwd, filePath); + const name = path.relative(cwd, absolutePath); + const ignorePatterns = loadESLintIgnoreFile(absolutePath); + + return createConfigArray( + this._normalizeESLintIgnoreData(ignorePatterns, absolutePath, name) + ); + } + + /** + * Load `.eslintignore` file in the current working directory. + * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. + */ + loadDefaultESLintIgnore() { + const { cwd } = internalSlotsMap.get(this); + const eslintIgnorePath = path.resolve(cwd, ".eslintignore"); + const packageJsonPath = path.resolve(cwd, "package.json"); + + if (fs.existsSync(eslintIgnorePath)) { + return this.loadESLintIgnore(eslintIgnorePath); + } + if (fs.existsSync(packageJsonPath)) { + const data = loadJSONConfigFile(packageJsonPath); + + if (Object.hasOwnProperty.call(data, "eslintIgnore")) { + if (!Array.isArray(data.eslintIgnore)) { + throw new Error("Package.json eslintIgnore property requires an array of paths"); + } + return createConfigArray( + this._normalizeESLintIgnoreData( + data.eslintIgnore, + packageJsonPath, + "eslintIgnore in package.json" + ) + ); + } + } + + return new ConfigArray(); + } + /** * Load a given config file. * @param {string} filePath The path to a config file. @@ -451,6 +530,30 @@ class ConfigArrayFactory { return null; } + /** + * Normalize a given `.eslintignore` data to config array elements. + * @param {string[]} ignorePatterns The patterns to ignore files. + * @param {string|undefined} filePath The file path of this config. + * @param {string|undefined} name The name of this config. + * @returns {IterableIterator} The normalized config. + * @private + */ + *_normalizeESLintIgnoreData(ignorePatterns, filePath, name) { + const elements = this._normalizeObjectConfigData( + { ignorePatterns }, + filePath, + name + ); + + // Set `ignorePattern.loose` flag for backward compatibility. + for (const element of elements) { + if (element.ignorePattern) { + element.ignorePattern.loose = true; + } + yield element; + } + } + /** * Normalize a given config to an array. * @param {ConfigData} configData The config data to normalize. @@ -494,6 +597,9 @@ class ConfigArrayFactory { if (element.criteria) { element.criteria.basePath = basePath; } + if (element.ignorePattern) { + element.ignorePattern.basePath = basePath; + } /* * Merge the criteria; this is for only file extension processors in @@ -526,6 +632,7 @@ class ConfigArrayFactory { env, extends: extend, globals, + ignorePatterns, noInlineConfig, parser: parserName, parserOptions, @@ -541,6 +648,10 @@ class ConfigArrayFactory { name ) { const extendList = Array.isArray(extend) ? extend : [extend]; + const ignorePattern = ignorePatterns && new IgnorePattern( + Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns], + filePath ? path.dirname(filePath) : internalSlotsMap.get(this).cwd + ); // Flatten `extends`. for (const extendName of extendList.filter(Boolean)) { @@ -569,6 +680,7 @@ class ConfigArrayFactory { criteria: null, env, globals, + ignorePattern, noInlineConfig, parser, parserOptions, diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index 089ff305a2b..4fae8deaca1 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -31,6 +31,7 @@ //------------------------------------------------------------------------------ const { ExtractedConfig } = require("./extracted-config"); +const { IgnorePattern } = require("./ignore-pattern"); //------------------------------------------------------------------------------ // Helpers @@ -54,6 +55,7 @@ const { ExtractedConfig } = require("./extracted-config"); * @property {InstanceType|null} criteria The tester for the `files` and `excludedFiles` of this config element. * @property {Record|undefined} env The environment settings. * @property {Record|undefined} globals The global variable settings. + * @property {IgnorePattern|undefined} ignorePattern The ignore patterns. * @property {boolean|undefined} noInlineConfig The flag that disables directive comments. * @property {DependentParser|undefined} parser The parser loader. * @property {Object|undefined} parserOptions The parser options. @@ -231,6 +233,7 @@ function mergeRuleConfigs(target, source) { */ function createConfig(instance, indices) { const config = new ExtractedConfig(); + const ignorePatterns = []; // Merge elements. for (const index of indices) { @@ -260,6 +263,11 @@ function createConfig(instance, indices) { config.reportUnusedDisableDirectives = element.reportUnusedDisableDirectives; } + // Collect ignorePatterns + if (element.ignorePattern) { + ignorePatterns.push(element.ignorePattern); + } + // Merge others. mergeWithoutOverwrite(config.env, element.env); mergeWithoutOverwrite(config.globals, element.globals); @@ -269,6 +277,11 @@ function createConfig(instance, indices) { mergeRuleConfigs(config.rules, element.rules); } + // Create the predicate function for ignore patterns. + if (ignorePatterns.length > 0) { + config.ignores = IgnorePattern.createIgnore(ignorePatterns.reverse()); + } + return config; } diff --git a/lib/cli-engine/config-array/extracted-config.js b/lib/cli-engine/config-array/extracted-config.js index 66858313ba6..b27d6ffb188 100644 --- a/lib/cli-engine/config-array/extracted-config.js +++ b/lib/cli-engine/config-array/extracted-config.js @@ -16,6 +16,8 @@ */ "use strict"; +const { IgnorePattern } = require("./ignore-pattern"); + // For VSCode intellisense /** @typedef {import("../../shared/types").ConfigData} ConfigData */ /** @typedef {import("../../shared/types").GlobalConf} GlobalConf */ @@ -23,6 +25,17 @@ /** @typedef {import("./config-dependency").DependentParser} DependentParser */ /** @typedef {import("./config-dependency").DependentPlugin} DependentPlugin */ +/** + * Check if `xs` starts with `ys`. + * @template T + * @param {T[]} xs The array to check. + * @param {T[]} ys The array that may be the first part of `xs`. + * @returns {boolean} `true` if `xs` starts with `ys`. + */ +function startsWith(xs, ys) { + return xs.length >= ys.length && ys.every((y, i) => y === xs[i]); +} + /** * The class for extracted config data. */ @@ -47,6 +60,12 @@ class ExtractedConfig { */ this.globals = {}; + /** + * The glob patterns that ignore to lint. + * @type {(((filePath:string, dot?:boolean) => boolean) & { basePath:string; patterns:string[] }) | undefined} + */ + this.ignores = void 0; + /** * The flag that disables directive comments. * @type {boolean|undefined} @@ -106,11 +125,19 @@ class ExtractedConfig { configNameOfNoInlineConfig: _ignore1, processor: _ignore2, /* eslint-enable no-unused-vars */ + ignores, ...config } = this; config.parser = config.parser && config.parser.filePath; config.plugins = Object.keys(config.plugins).filter(Boolean).reverse(); + config.ignorePatterns = ignores ? ignores.patterns : []; + + // Strip the default patterns from `ignorePatterns`. + if (startsWith(config.ignorePatterns, IgnorePattern.DefaultPatterns)) { + config.ignorePatterns = + config.ignorePatterns.slice(IgnorePattern.DefaultPatterns.length); + } return config; } diff --git a/lib/cli-engine/config-array/ignore-pattern.js b/lib/cli-engine/config-array/ignore-pattern.js new file mode 100644 index 00000000000..6140194433e --- /dev/null +++ b/lib/cli-engine/config-array/ignore-pattern.js @@ -0,0 +1,231 @@ +/** + * @fileoverview `IgnorePattern` class. + * + * `IgnorePattern` class has the set of glob patterns and the base path. + * + * It provides two static methods. + * + * - `IgnorePattern.createDefaultIgnore(cwd)` + * Create the default predicate function. + * - `IgnorePattern.createIgnore(ignorePatterns)` + * Create the predicate function from multiple `IgnorePattern` objects. + * + * It provides two properties and a method. + * + * - `patterns` + * The glob patterns that ignore to lint. + * - `basePath` + * The base path of the glob patterns. If absolute paths existed in the + * glob patterns, those are handled as relative paths to the base path. + * - `getPatternsRelativeTo(basePath)` + * Get `patterns` as modified for a given base path. It modifies the + * absolute paths in the patterns as prepending the difference of two base + * paths. + * + * `ConfigArrayFactory` creates `IgnorePattern` objects when it processes + * `ignorePatterns` properties. + * + * @author Toru Nagashima + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("assert"); +const path = require("path"); +const ignore = require("ignore"); +const debug = require("debug")("eslint:ignore-pattern"); + +/** @typedef {ReturnType} Ignore */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Get the path to the common ancestor directory of given paths. + * @param {string[]} sourcePaths The paths to calculate the common ancestor. + * @returns {string} The path to the common ancestor directory. + */ +function getCommonAncestorPath(sourcePaths) { + let result = sourcePaths[0]; + + for (let i = 1; i < sourcePaths.length; ++i) { + const a = result; + const b = sourcePaths[i]; + + // Set the shorter one (it's the common ancestor if one includes the other). + result = a.length < b.length ? a : b; + + // Set the common ancestor. + for (let j = 0, lastSepPos = 0; j < a.length && j < b.length; ++j) { + if (a[j] !== b[j]) { + result = a.slice(0, lastSepPos); + break; + } + if (a[j] === path.sep) { + lastSepPos = j; + } + } + } + + return result || path.sep; +} + +/** + * Make relative path. + * @param {string} from The source path to get relative path. + * @param {string} to The destination path to get relative path. + * @returns {string} The relative path. + */ +function relative(from, to) { + const relPath = path.relative(from, to); + + if (path.sep === "/") { + return relPath; + } + return relPath.split(path.sep).join("/"); +} + +/** + * Get the trailing slash if existed. + * @param {string} filePath The path to check. + * @returns {string} The trailing slash if existed. + */ +function dirSuffix(filePath) { + const isDir = ( + filePath.endsWith(path.sep) || + (process.platform === "win32" && filePath.endsWith("/")) + ); + + return isDir ? "/" : ""; +} + +const DefaultPatterns = Object.freeze(["/node_modules/*", "/bower_components/*"]); +const DotPatterns = Object.freeze([".*", "!../"]); + +//------------------------------------------------------------------------------ +// Public +//------------------------------------------------------------------------------ + +class IgnorePattern { + + /** + * The default patterns. + * @type {string[]} + */ + static get DefaultPatterns() { + return DefaultPatterns; + } + + /** + * Create the default predicate function. + * @param {string} cwd The current working directory. + * @returns {((filePath:string, dot:boolean) => boolean) & {basePath:string; patterns:string[]}} + * The preficate function. + * The first argument is an absolute path that is checked. + * The second argument is the flag to not ignore dotfiles. + * If the predicate function returned `true`, it means the path should be ignored. + */ + static createDefaultIgnore(cwd) { + return this.createIgnore([new IgnorePattern(DefaultPatterns, cwd)]); + } + + /** + * Create the predicate function from multiple `IgnorePattern` objects. + * @param {IgnorePattern[]} ignorePatterns The list of ignore patterns. + * @returns {((filePath:string, dot?:boolean) => boolean) & {basePath:string; patterns:string[]}} + * The preficate function. + * The first argument is an absolute path that is checked. + * The second argument is the flag to not ignore dotfiles. + * If the predicate function returned `true`, it means the path should be ignored. + */ + static createIgnore(ignorePatterns) { + debug("Create with: %o", ignorePatterns); + + const basePath = getCommonAncestorPath(ignorePatterns.map(p => p.basePath)); + const patterns = [].concat( + ...ignorePatterns.map(p => p.getPatternsRelativeTo(basePath)) + ); + const ig = ignore().add([...DotPatterns, ...patterns]); + const dotIg = ignore().add(patterns); + + debug(" processed: %o", { basePath, patterns }); + + return Object.assign( + (filePath, dot = false) => { + assert(path.isAbsolute(filePath), "'filePath' should be an absolute path."); + const relPathRaw = relative(basePath, filePath); + const relPath = relPathRaw && (relPathRaw + dirSuffix(filePath)); + const adoptedIg = dot ? dotIg : ig; + const result = relPath !== "" && adoptedIg.ignores(relPath); + + debug("Check", { filePath, dot, relativePath: relPath, result }); + return result; + }, + { basePath, patterns } + ); + } + + /** + * Initialize a new `IgnorePattern` instance. + * @param {string[]} patterns The glob patterns that ignore to lint. + * @param {string} basePath The base path of `patterns`. + */ + constructor(patterns, basePath) { + assert(path.isAbsolute(basePath), "'basePath' should be an absolute path."); + + /** + * The glob patterns that ignore to lint. + * @type {string[]} + */ + this.patterns = patterns; + + /** + * The base path of `patterns`. + * @type {string} + */ + this.basePath = basePath; + + /** + * If `true` then patterns which don't start with `/` will match the paths to the outside of `basePath`. Defaults to `false`. + * + * It's set `true` for `.eslintignore`, `package.json`, and `--ignore-path` for backward compatibility. + * It's `false` as-is for `ignorePatterns` property in config files. + * @type {boolean} + */ + this.loose = false; + } + + /** + * Get `patterns` as modified for a given base path. It modifies the + * absolute paths in the patterns as prepending the difference of two base + * paths. + * @param {string} newBasePath The base path. + * @returns {string[]} Modifired patterns. + */ + getPatternsRelativeTo(newBasePath) { + assert(path.isAbsolute(newBasePath), "'newBasePath' should be an absolute path."); + const { basePath, loose, patterns } = this; + + if (newBasePath === basePath) { + return patterns; + } + const prefix = `/${relative(newBasePath, basePath)}`; + + return patterns.map(pattern => { + const negative = pattern.startsWith("!"); + const head = negative ? "!" : ""; + const body = negative ? pattern.slice(1) : pattern; + + if (body.startsWith("/") || body.startsWith("../")) { + return `${head}${prefix}${body}`; + } + return loose ? pattern : `${head}${prefix}/**/${body}`; + }); + } +} + +module.exports = { IgnorePattern }; diff --git a/lib/cli-engine/config-array/index.js b/lib/cli-engine/config-array/index.js index de8831906fd..928d76c83ab 100644 --- a/lib/cli-engine/config-array/index.js +++ b/lib/cli-engine/config-array/index.js @@ -7,12 +7,14 @@ const { ConfigArray, getUsedExtractedConfigs } = require("./config-array"); const { ConfigDependency } = require("./config-dependency"); const { ExtractedConfig } = require("./extracted-config"); +const { IgnorePattern } = require("./ignore-pattern"); const { OverrideTester } = require("./override-tester"); module.exports = { ConfigArray, ConfigDependency, ExtractedConfig, + IgnorePattern, OverrideTester, getUsedExtractedConfigs }; diff --git a/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js index 38f55de039d..700f8009cf8 100644 --- a/lib/cli-engine/file-enumerator.js +++ b/lib/cli-engine/file-enumerator.js @@ -40,8 +40,8 @@ const getGlobParent = require("glob-parent"); const isGlob = require("is-glob"); const { escapeRegExp } = require("lodash"); const { Minimatch } = require("minimatch"); +const { IgnorePattern } = require("./config-array"); const { CascadingConfigArrayFactory } = require("./cascading-config-array-factory"); -const { IgnoredPaths } = require("./ignored-paths"); const debug = require("debug")("eslint:file-enumerator"); //------------------------------------------------------------------------------ @@ -64,7 +64,6 @@ const IGNORED = 2; * @property {string[]} [extensions] The extensions to match files for directory patterns. * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. * @property {boolean} [ignore] The flag to check ignored files. - * @property {IgnoredPaths} [ignoredPaths] The ignored paths. * @property {string[]} [rulePaths] The value of `--rulesdir` option. */ @@ -92,8 +91,7 @@ const IGNORED = 2; * @property {RegExp} extensionRegExp The RegExp to test if a string ends with specific file extensions. * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. * @property {boolean} ignoreFlag The flag to check ignored files. - * @property {IgnoredPaths} ignoredPathsWithDotfiles The ignored paths but don't include dot files. - * @property {IgnoredPaths} ignoredPaths The ignored paths. + * @property {(filePath:string, dot:boolean) => boolean} defaultIgnores The default predicate function to ignore files. */ /** @type {WeakMap} */ @@ -192,12 +190,12 @@ class FileEnumerator { configArrayFactory = new CascadingConfigArrayFactory({ cwd }), extensions = [".js"], globInputPaths = true, - ignore = true, - ignoredPaths = new IgnoredPaths({ cwd, ignore }) + ignore = true } = {}) { internalSlotsMap.set(this, { configArrayFactory, cwd, + defaultIgnores: IgnorePattern.createDefaultIgnore(cwd), extensionRegExp: new RegExp( `.\\.(?:${extensions .map(ext => escapeRegExp( @@ -210,12 +208,7 @@ class FileEnumerator { "u" ), globInputPaths, - ignoreFlag: ignore, - ignoredPaths, - ignoredPathsWithDotfiles: new IgnoredPaths({ - ...ignoredPaths.options, - dotfiles: true - }) + ignoreFlag: ignore }); } @@ -321,7 +314,7 @@ class FileEnumerator { const { configArrayFactory } = internalSlotsMap.get(this); const config = configArrayFactory.getConfigArrayForFile(filePath); - const ignored = this._isIgnoredFile(filePath, { direct: true }); + const ignored = this._isIgnoredFile(filePath, { config, direct: true }); const flag = ignored ? IGNORED : NONE; return [{ config, filePath, flag }]; @@ -353,7 +346,7 @@ class FileEnumerator { _iterateFilesWithGlob(pattern, dotfiles) { debug(`Glob: ${pattern}`); - const directoryPath = getGlobParent(pattern); + const directoryPath = path.resolve(getGlobParent(pattern)); const globPart = pattern.slice(directoryPath.length + 1); /* @@ -399,9 +392,18 @@ class FileEnumerator { // Check if the file is matched. if (stat && stat.isFile()) { if (!config) { - config = configArrayFactory.getConfigArrayForFile(filePath); + config = configArrayFactory.getConfigArrayForFile( + filePath, + + /* + * We must ignore `ConfigurationNotFoundError` at this + * point because we don't know if target files exist in + * this directory. + */ + { ignoreNotFoundError: true } + ); } - const ignored = this._isIgnoredFile(filePath, options); + const ignored = this._isIgnoredFile(filePath, { ...options, config }); const flag = ignored ? IGNORED_SILENTLY : NONE; const matched = options.selector @@ -413,7 +415,11 @@ class FileEnumerator { if (matched) { debug(`Yield: ${filename}${ignored ? " but ignored" : ""}`); - yield { config, filePath, flag }; + yield { + config: configArrayFactory.getConfigArrayForFile(filePath), + filePath, + flag + }; } else { debug(`Didn't match: ${filename}`); } @@ -431,24 +437,37 @@ class FileEnumerator { * Check if a given file should be ignored. * @param {string} filePath The path to a file to check. * @param {Object} options Options + * @param {ConfigArray} [options.config] The config for this file. * @param {boolean} [options.dotfiles] If `true` then this is not ignore dot files by default. * @param {boolean} [options.direct] If `true` then this is a direct specified file. * @returns {boolean} `true` if the file should be ignored. * @private */ - _isIgnoredFile(filePath, { dotfiles = false, direct = false }) { + _isIgnoredFile(filePath, { + config: providedConfig, + dotfiles = false, + direct = false + }) { const { - ignoreFlag, - ignoredPaths, - ignoredPathsWithDotfiles + configArrayFactory, + defaultIgnores, + ignoreFlag } = internalSlotsMap.get(this); - const adoptedIgnoredPaths = dotfiles - ? ignoredPathsWithDotfiles - : ignoredPaths; - return ignoreFlag - ? adoptedIgnoredPaths.contains(filePath) - : (!direct && adoptedIgnoredPaths.contains(filePath, "default")); + if (ignoreFlag) { + const config = + providedConfig || + configArrayFactory.getConfigArrayForFile( + filePath, + { ignoreNotFoundError: true } + ); + const ignores = + config.extractConfig(filePath).ignores || defaultIgnores; + + return ignores(filePath, dotfiles); + } + + return !direct && defaultIgnores(filePath, dotfiles); } } diff --git a/lib/cli-engine/ignored-paths.js b/lib/cli-engine/ignored-paths.js deleted file mode 100644 index dec8e186042..00000000000 --- a/lib/cli-engine/ignored-paths.js +++ /dev/null @@ -1,363 +0,0 @@ -/** - * @fileoverview Responsible for loading ignore config files and managing ignore patterns - * @author Jonathan Rajavuori - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const fs = require("fs"), - path = require("path"), - ignore = require("ignore"); - -const debug = require("debug")("eslint:ignored-paths"); - -//------------------------------------------------------------------------------ -// Constants -//------------------------------------------------------------------------------ - -const ESLINT_IGNORE_FILENAME = ".eslintignore"; - -/** - * Adds `"*"` at the end of `"node_modules/"`, - * so that subtle directories could be re-included by .gitignore patterns - * such as `"!node_modules/should_not_ignored"` - */ -const DEFAULT_IGNORE_DIRS = [ - "/node_modules/*", - "/bower_components/*" -]; -const DEFAULT_OPTIONS = { - dotfiles: false, - cwd: process.cwd() -}; - -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Find a file in the current directory. - * @param {string} cwd Current working directory - * @param {string} name File name - * @returns {string} Path of ignore file or an empty string. - */ -function findFile(cwd, name) { - const ignoreFilePath = path.resolve(cwd, name); - - return fs.existsSync(ignoreFilePath) && fs.statSync(ignoreFilePath).isFile() ? ignoreFilePath : ""; -} - -/** - * Find an ignore file in the current directory. - * @param {string} cwd Current working directory - * @returns {string} Path of ignore file or an empty string. - */ -function findIgnoreFile(cwd) { - return findFile(cwd, ESLINT_IGNORE_FILENAME); -} - -/** - * Find an package.json file in the current directory. - * @param {string} cwd Current working directory - * @returns {string} Path of package.json file or an empty string. - */ -function findPackageJSONFile(cwd) { - return findFile(cwd, "package.json"); -} - -/** - * Merge options with defaults - * @param {Object} options Options to merge with DEFAULT_OPTIONS constant - * @returns {Object} Merged options - */ -function mergeDefaultOptions(options) { - return Object.assign({}, DEFAULT_OPTIONS, options); -} - -/* eslint-disable jsdoc/check-param-names, jsdoc/require-param */ -/** - * Normalize the path separators in a given string. - * On Windows environment, this replaces `\` by `/`. - * Otherwrise, this does nothing. - * @param {string} str The path string to normalize. - * @returns {string} The normalized path. - */ -const normalizePathSeps = path.sep === "/" - ? (str => str) - : ((seps, str) => str.replace(seps, "/")).bind(null, new RegExp(`\\${path.sep}`, "gu")); -/* eslint-enable jsdoc/check-param-names, jsdoc/require-param */ - -/** - * Converts a glob pattern to a new glob pattern relative to a different directory - * @param {string} globPattern The glob pattern, relative the the old base directory - * @param {string} relativePathToOldBaseDir A relative path from the new base directory to the old one - * @returns {string} A glob pattern relative to the new base directory - */ -function relativize(globPattern, relativePathToOldBaseDir) { - if (relativePathToOldBaseDir === "") { - return globPattern; - } - - const prefix = globPattern.startsWith("!") ? "!" : ""; - const globWithoutPrefix = globPattern.replace(/^!/u, ""); - - if (globWithoutPrefix.startsWith("/")) { - return `${prefix}/${normalizePathSeps(relativePathToOldBaseDir)}${globWithoutPrefix}`; - } - - return globPattern; -} - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -/** - * IgnoredPaths class - */ -class IgnoredPaths { - - // eslint-disable-next-line jsdoc/require-description - /** - * @param {Object} providedOptions object containing 'ignore', 'ignorePath' and 'patterns' properties - */ - constructor(providedOptions) { - const options = mergeDefaultOptions(providedOptions); - - this.cache = {}; - - this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []); - - this.ignoreFileDir = options.ignore !== false && options.ignorePath - ? path.dirname(path.resolve(options.cwd, options.ignorePath)) - : options.cwd; - this.options = options; - this._baseDir = null; - - this.ig = { - custom: ignore(), - default: ignore() - }; - - this.defaultPatterns.forEach(pattern => this.addPatternRelativeToCwd(this.ig.default, pattern)); - if (options.dotfiles !== true) { - - /* - * ignore files beginning with a dot, but not files in a parent or - * ancestor directory (which in relative format will begin with `../`). - */ - this.addPatternRelativeToCwd(this.ig.default, ".*"); - this.addPatternRelativeToCwd(this.ig.default, "!../"); - } - - /* - * Add a way to keep track of ignored files. This was present in node-ignore - * 2.x, but dropped for now as of 3.0.10. - */ - this.ig.custom.ignoreFiles = []; - this.ig.default.ignoreFiles = []; - - if (options.ignore !== false) { - let ignorePath; - - if (options.ignorePath) { - debug("Using specific ignore file"); - - try { - const stat = fs.statSync(options.ignorePath); - - if (!stat.isFile()) { - throw new Error(`${options.ignorePath} is not a file`); - } - ignorePath = options.ignorePath; - } catch (e) { - e.message = `Cannot read ignore file: ${options.ignorePath}\nError: ${e.message}`; - throw e; - } - } else { - debug(`Looking for ignore file in ${options.cwd}`); - ignorePath = findIgnoreFile(options.cwd); - - try { - fs.statSync(ignorePath); - debug(`Loaded ignore file ${ignorePath}`); - } catch (e) { - debug("Could not find ignore file in cwd"); - } - } - - if (ignorePath) { - debug(`Adding ${ignorePath}`); - this.addIgnoreFile(this.ig.custom, ignorePath); - this.addIgnoreFile(this.ig.default, ignorePath); - } else { - try { - - // if the ignoreFile does not exist, check package.json for eslintIgnore - const packageJSONPath = findPackageJSONFile(options.cwd); - - if (packageJSONPath) { - let packageJSONOptions; - - try { - packageJSONOptions = JSON.parse(fs.readFileSync(packageJSONPath, "utf8")); - } catch (e) { - debug("Could not read package.json file to check eslintIgnore property"); - e.messageTemplate = "failed-to-read-json"; - e.messageData = { - path: packageJSONPath, - message: e.message - }; - throw e; - } - - if (packageJSONOptions.eslintIgnore) { - if (Array.isArray(packageJSONOptions.eslintIgnore)) { - packageJSONOptions.eslintIgnore.forEach(pattern => { - this.addPatternRelativeToIgnoreFile(this.ig.custom, pattern); - this.addPatternRelativeToIgnoreFile(this.ig.default, pattern); - }); - } else { - throw new TypeError("Package.json eslintIgnore property requires an array of paths"); - } - } - } - } catch (e) { - debug("Could not find package.json to check eslintIgnore property"); - throw e; - } - } - - if (options.ignorePattern) { - this.addPatternRelativeToCwd(this.ig.custom, options.ignorePattern); - this.addPatternRelativeToCwd(this.ig.default, options.ignorePattern); - } - } - } - - /* - * If `ignoreFileDir` is a subdirectory of `cwd`, all paths will be normalized to be relative to `cwd`. - * Otherwise, all paths will be normalized to be relative to `ignoreFileDir`. - * This ensures that the final normalized ignore rule will not contain `..`, which is forbidden in - * ignore rules. - */ - - addPatternRelativeToCwd(ig, pattern) { - const baseDir = this.getBaseDir(); - const cookedPattern = baseDir === this.options.cwd - ? pattern - : relativize(pattern, path.relative(baseDir, this.options.cwd)); - - ig.addPattern(cookedPattern); - debug("addPatternRelativeToCwd:\n original = %j\n cooked = %j", pattern, cookedPattern); - } - - addPatternRelativeToIgnoreFile(ig, pattern) { - const baseDir = this.getBaseDir(); - const cookedPattern = baseDir === this.ignoreFileDir - ? pattern - : relativize(pattern, path.relative(baseDir, this.ignoreFileDir)); - - ig.addPattern(cookedPattern); - debug("addPatternRelativeToIgnoreFile:\n original = %j\n cooked = %j", pattern, cookedPattern); - } - - // Detect the common ancestor - getBaseDir() { - if (!this._baseDir) { - const a = path.resolve(this.options.cwd); - const b = path.resolve(this.ignoreFileDir); - let lastSepPos = 0; - - // Set the shorter one (it's the common ancestor if one includes the other). - this._baseDir = a.length < b.length ? a : b; - - // Set the common ancestor. - for (let i = 0; i < a.length && i < b.length; ++i) { - if (a[i] !== b[i]) { - this._baseDir = a.slice(0, lastSepPos); - break; - } - if (a[i] === path.sep) { - lastSepPos = i; - } - } - - // If it's only Windows drive letter, it needs \ - if (/^[A-Z]:$/u.test(this._baseDir)) { - this._baseDir += "\\"; - } - - debug("baseDir = %j", this._baseDir); - } - return this._baseDir; - } - - /** - * read ignore filepath - * @param {string} filePath file to add to ig - * @returns {Array} raw ignore rules - */ - readIgnoreFile(filePath) { - if (typeof this.cache[filePath] === "undefined") { - this.cache[filePath] = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu).filter(Boolean); - } - return this.cache[filePath]; - } - - /** - * add ignore file to node-ignore instance - * @param {Object} ig instance of node-ignore - * @param {string} filePath file to add to ig - * @returns {void} - */ - addIgnoreFile(ig, filePath) { - ig.ignoreFiles.push(filePath); - this - .readIgnoreFile(filePath) - .forEach(ignoreRule => this.addPatternRelativeToIgnoreFile(ig, ignoreRule)); - } - - /** - * Determine whether a file path is included in the default or custom ignore patterns - * @param {string} filepath Path to check - * @param {string} [category=undefined] check 'default', 'custom' or both (undefined) - * @returns {boolean} true if the file path matches one or more patterns, false otherwise - */ - contains(filepath, category) { - const isDir = filepath.endsWith(path.sep) || - (path.sep === "\\" && filepath.endsWith("/")); - let result = false; - const basePath = this.getBaseDir(); - const absolutePath = path.resolve(this.options.cwd, filepath); - let relativePath = path.relative(basePath, absolutePath); - - if (relativePath) { - if (isDir) { - relativePath += path.sep; - } - if (typeof category === "undefined") { - result = - (this.ig.default.filter([relativePath]).length === 0) || - (this.ig.custom.filter([relativePath]).length === 0); - } else { - result = - (this.ig[category].filter([relativePath]).length === 0); - } - } - debug("contains:"); - debug(" target = %j", filepath); - debug(" base = %j", basePath); - debug(" relative = %j", relativePath); - debug(" result = %j", result); - - return result; - - } -} - -module.exports = { IgnoredPaths }; diff --git a/lib/shared/types.js b/lib/shared/types.js index 12bd0aed8c6..57740b6c951 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -30,6 +30,7 @@ module.exports = {}; * @property {Record} [env] The environment settings. * @property {string | string[]} [extends] The path to other config files or the package name of shareable configs. * @property {Record} [globals] The global variable settings. + * @property {string | string[]} [ignorePatterns] The glob patterns that ignore to lint. * @property {boolean} [noInlineConfig] The flag that disables directive comments. * @property {OverrideConfigData[]} [overrides] The override settings per kind of files. * @property {string} [parser] The path to a parser or the package name of a parser. diff --git a/tests/lib/cli-engine/_utils.js b/tests/lib/cli-engine/_utils.js index bff1801f342..1d1bcd275b0 100644 --- a/tests/lib/cli-engine/_utils.js +++ b/tests/lib/cli-engine/_utils.js @@ -66,8 +66,6 @@ const ConfigArrayFactoryPath = require.resolve("../../../lib/cli-engine/config-array-factory"); const FileEnumeratorPath = require.resolve("../../../lib/cli-engine/file-enumerator"); -const IgnoredPathsPath = - require.resolve("../../../lib/cli-engine/ignored-paths"); const LoadRulesPath = require.resolve("../../../lib/cli-engine/load-rules"); const ESLintAllPath = @@ -80,7 +78,6 @@ require(CascadingConfigArrayFactoryPath); require(CLIEnginePath); require(ConfigArrayFactoryPath); require(FileEnumeratorPath); -require(IgnoredPathsPath); require(LoadRulesPath); require("js-yaml"); require("espree"); @@ -363,7 +360,7 @@ function defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ * @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"], IgnoredPaths: import("../../../lib/cli-engine/ignored-paths")["IgnoredPaths"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"] }} The stubbed `FileEnumerator` class. + * @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"] }} The stubbed `FileEnumerator` class. */ function defineFileEnumeratorWithInMemoryFileSystem({ cwd = process.cwd, @@ -376,11 +373,9 @@ function defineFileEnumeratorWithInMemoryFileSystem({ CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files }); - const { IgnoredPaths } = proxyquire(IgnoredPathsPath, { fs }); const { FileEnumerator } = proxyquire(FileEnumeratorPath, { fs, - "./cascading-config-array-factory": { CascadingConfigArrayFactory }, - "./ignored-paths": { IgnoredPaths } + "./cascading-config-array-factory": { CascadingConfigArrayFactory } }); // Override the default cwd. @@ -389,7 +384,6 @@ function defineFileEnumeratorWithInMemoryFileSystem({ RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, - IgnoredPaths, FileEnumerator: cwd === process.cwd ? FileEnumerator : class extends FileEnumerator { @@ -405,7 +399,7 @@ function defineFileEnumeratorWithInMemoryFileSystem({ * @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"], IgnoredPaths: import("../../../lib/cli-engine/ignored-paths")["IgnoredPaths"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"], CLIEngine: import("../../../lib/cli-engine/cli-engine")["CLIEngine"], getCLIEngineInternalSlots: import("../../../lib/cli-engine/cli-engine")["getCLIEngineInternalSlots"] }} The stubbed `CLIEngine` class. + * @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"], CLIEngine: import("../../../lib/cli-engine/cli-engine")["CLIEngine"], getCLIEngineInternalSlots: import("../../../lib/cli-engine/cli-engine")["getCLIEngineInternalSlots"] }} The stubbed `CLIEngine` class. */ function defineCLIEngineWithInMemoryFileSystem({ cwd = process.cwd, @@ -416,7 +410,6 @@ function defineCLIEngineWithInMemoryFileSystem({ RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, - IgnoredPaths, FileEnumerator } = defineFileEnumeratorWithInMemoryFileSystem({ cwd, files }); @@ -424,7 +417,6 @@ function defineCLIEngineWithInMemoryFileSystem({ fs, "./cascading-config-array-factory": { CascadingConfigArrayFactory }, "./file-enumerator": { FileEnumerator }, - "./ignored-paths": { IgnoredPaths }, "../shared/relative-module-resolver": RelativeModuleResolver }); @@ -434,7 +426,6 @@ function defineCLIEngineWithInMemoryFileSystem({ RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, - IgnoredPaths, FileEnumerator, CLIEngine: cwd === process.cwd ? CLIEngine diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index f7fbd702f62..2748aa608a4 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -14,6 +14,11 @@ const { ConfigArrayFactory } = require("../../../lib/cli-engine/config-array-fac const { ExtractedConfig } = require("../../../lib/cli-engine/config-array/extracted-config"); const { defineCascadingConfigArrayFactoryWithInMemoryFileSystem } = require("./_utils"); +const cwdIgnorePatterns = new ConfigArrayFactory() + .loadDefaultESLintIgnore()[0] + .ignorePattern + .patterns; + describe("CascadingConfigArrayFactory", () => { describe("'getConfigArrayForFile(filePath)' method should retrieve the proper configuration.", () => { describe("with three directories ('lib', 'lib/nested', 'test') that contains 'one.js' and 'two.js'", () => { @@ -56,23 +61,29 @@ describe("CascadingConfigArrayFactory", () => { it("should retrieve the config '.eslintrc.json' if 'lib/one.js' was given.", () => { const config = factory.getConfigArrayForFile("lib/one.js"); - assert.strictEqual(config.length, 1); - assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(config.length, 3); + assert.strictEqual(config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(config[2].filePath, path.join(root, ".eslintignore")); }); it("should retrieve the merged config of '.eslintrc.json' and 'lib/nested/.eslintrc.yml' if 'lib/nested/one.js' was given.", () => { const config = factory.getConfigArrayForFile("lib/nested/one.js"); - assert.strictEqual(config.length, 2); - assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); - assert.strictEqual(config[1].filePath, path.join(root, "lib/nested/.eslintrc.yml")); + assert.strictEqual(config.length, 4); + assert.strictEqual(config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(config[2].filePath, path.join(root, "lib/nested/.eslintrc.yml")); + assert.strictEqual(config[3].filePath, path.join(root, ".eslintignore")); }); it("should retrieve the config '.eslintrc.json' if 'lib/non-exist.js' was given.", () => { const config = factory.getConfigArrayForFile("lib/non-exist.js"); - assert.strictEqual(config.length, 1); - assert.strictEqual(config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(config.length, 3); + assert.strictEqual(config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(config[2].filePath, path.join(root, ".eslintignore")); }); }); @@ -282,7 +293,8 @@ describe("CascadingConfigArrayFactory", () => { env: { browser: true, node: false - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -296,7 +308,8 @@ describe("CascadingConfigArrayFactory", () => { const expected = { rules: {}, globals: {}, - env: {} + env: {}, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -309,7 +322,8 @@ describe("CascadingConfigArrayFactory", () => { const expected = { rules: {}, globals: {}, - env: {} + env: {}, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -322,7 +336,7 @@ describe("CascadingConfigArrayFactory", () => { const file = getFixturePath("broken", "console-wrong-quotes.js"); const actual = getConfig(factory, file); - assertConfigsEqual(actual, {}); + assertConfigsEqual(actual, { ignorePatterns: cwdIgnorePatterns }); }); it("should return a modified config when baseConfig is set to an object and no .eslintrc", () => { @@ -344,7 +358,8 @@ describe("CascadingConfigArrayFactory", () => { }, rules: { quotes: [2, "single"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -391,7 +406,8 @@ describe("CascadingConfigArrayFactory", () => { rules: { "no-console": [1], quotes: [2, "single"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -409,7 +425,8 @@ describe("CascadingConfigArrayFactory", () => { rules: { "no-console": [0], quotes: [1, "double"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -423,7 +440,8 @@ describe("CascadingConfigArrayFactory", () => { const expected = { rules: { semi: [2, "never"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -457,7 +475,8 @@ describe("CascadingConfigArrayFactory", () => { rules: { quotes: [2, "double"], semi: [1, "never"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -476,7 +495,8 @@ describe("CascadingConfigArrayFactory", () => { }, rules: { quotes: [0, "double"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -497,7 +517,8 @@ describe("CascadingConfigArrayFactory", () => { quotes: [2, "single"], "no-console": [1], semi: [1, "never"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -517,7 +538,8 @@ describe("CascadingConfigArrayFactory", () => { rules: { quotes: [0, "single"], "no-console": [1] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -541,7 +563,8 @@ describe("CascadingConfigArrayFactory", () => { }, rules: { quotes: [1, "double"] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -585,7 +608,8 @@ describe("CascadingConfigArrayFactory", () => { rules: { semi: [2, "always"], eqeqeq: [2] - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, file); @@ -599,7 +623,8 @@ describe("CascadingConfigArrayFactory", () => { const expected = { globals: { foo: true - } + }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, configPath); @@ -638,7 +663,8 @@ describe("CascadingConfigArrayFactory", () => { const factory = new CascadingConfigArrayFactory({ useEslintrc: false, specificConfigPath: configPath }); const expected = { rules: { "no-empty": [1], "comma-dangle": [2], "no-console": [2] }, - env: { browser: false, node: true, es6: true } + env: { browser: false, node: true, es6: true }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, configPath); @@ -652,7 +678,8 @@ describe("CascadingConfigArrayFactory", () => { const expected = { rules: {}, env: { commonjs: true }, - parserOptions: { ecmaFeatures: { globalReturn: false } } + parserOptions: { ecmaFeatures: { globalReturn: false } }, + ignorePatterns: cwdIgnorePatterns }; const actual = getConfig(factory, targetPath); @@ -1185,7 +1212,7 @@ describe("CascadingConfigArrayFactory", () => { it("should have a loading error in CLI config.", () => { const config = factory.getConfigArrayForFile("a.js"); - assert.strictEqual(config[1].plugins.test.definition, null); + assert.strictEqual(config[2].plugins.test.definition, null); }); it("should not have a loading error in CLI config after adding 'test' plugin to the additional plugin pool then calling 'clearCache()'.", () => { @@ -1198,7 +1225,7 @@ describe("CascadingConfigArrayFactory", () => { const config = factory.getConfigArrayForFile("a.js"); assert.deepStrictEqual( - config[1].plugins.test.definition, + config[2].plugins.test.definition, { configs: { name: "test" }, environments: {}, diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 2cdc81301bf..1d9bef0905f 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -112,7 +112,7 @@ describe("CLIEngine", () => { assert.throws(() => { // eslint-disable-next-line no-new new CLIEngine({ ignorePath: fixtureDir }); - }, `Cannot read ignore file: ${fixtureDir}\nError: ${fixtureDir} is not a file`); + }, `Cannot read .eslintignore file: ${fixtureDir}\nError: EISDIR: illegal operation on a directory, read`); }); // https://github.com/eslint/eslint/issues/2380 @@ -3679,14 +3679,6 @@ describe("CLIEngine", () => { }); describe("isPathIgnored", () => { - beforeEach(() => { - sinon.stub(console, "info").returns(void 0); - }); - - afterEach(() => { - sinon.restore(); - }); - it("should check if the given path is ignored", () => { const engine = new CLIEngine({ ignorePath: getFixturePath(".eslintignore2"), @@ -3717,6 +3709,416 @@ describe("CLIEngine", () => { assert.isTrue(engine.isPathIgnored("node_modules/foo.js")); }); + describe("about the default ignore patterns", () => { + it("should always apply defaultPatterns if ignore option is true", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "bower_components/package/file.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/package/file.js"))); + }); + + it("should still apply defaultPatterns if ignore option is is false", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignore: false, cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "bower_components/package/file.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/package/file.js"))); + }); + + it("should not ignore files in defaultPatterns within a subdirectory", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/bower_components/package/file.js"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/node_modules/package/file.js"))); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePattern", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd, ignorePattern: "!/node_modules/package" }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePath", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd, ignorePath: getFixturePath("ignored-paths", ".eslintignoreWithUnignoredDefaults") }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should ignore dotfiles", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", ".foo"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar"))); + }); + + it("should ignore directories beginning with a dot", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", ".foo/bar"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar/baz"))); + }); + + it("should still ignore dotfiles when ignore option disabled", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignore: false, cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", ".foo"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar"))); + }); + + it("should still ignore directories beginning with a dot when ignore option disabled", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignore: false, cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", ".foo/bar"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar/baz"))); + }); + + it("should not ignore absolute paths containing '..'", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + assert(!engine.isPathIgnored(`${getFixturePath("ignored-paths", "foo")}/../unignored.js`)); + }); + + it("should ignore /node_modules/ at top level relative to .eslintignore when loaded", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePath: getFixturePath("ignored-paths", ".eslintignore"), cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "existing.js"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "foo", "node_modules", "existing.js"))); + }); + + it("should ignore /node_modules/ at top level relative to cwd without an .eslintignore", () => { + const cwd = getFixturePath("ignored-paths", "no-ignore-file"); + const engine = new CLIEngine({ cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "no-ignore-file", "node_modules", "existing.js"))); + assert(!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", () => { + const cwd = getFixturePath("ignored-paths", "configurations"); + const engine = new CLIEngine({ cwd }); + + // a .eslintignore in parent directories includes `*.js`, but don't load it. + assert(!engine.isPathIgnored("foo.js")); + assert(engine.isPathIgnored("node_modules/foo.js")); + }); + + it("should return false for files outside of the cwd (with no ignore file provided)", () => { + + // Default ignore patterns should not inadvertantly ignore files in parent directories + const engine = new CLIEngine({ cwd: getFixturePath("ignored-paths", "no-ignore-file") }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + }); + + describe("with .eslintignore file or package.json file", () => { + it("should load .eslintignore from cwd when explicitly passed", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + // `${cwd}/.eslintignore` includes `sampleignorepattern`. + assert(engine.isPathIgnored("sampleignorepattern")); + }); + + it("should use package.json's eslintIgnore files if no specified .eslintignore file", () => { + const cwd = getFixturePath("ignored-paths", "package-json-ignore"); + const engine = new CLIEngine({ cwd }); + + assert(engine.isPathIgnored("hello.js")); + assert(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.throw(() => { + try { + // eslint-disable-next-line no-new + new CLIEngine({ 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", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ cwd }); + + /* + * package.json includes `hello.js` and `world.js`. + * .eslintignore includes `sampleignorepattern`. + */ + assert(!engine.isPathIgnored("hello.js")); + assert(!engine.isPathIgnored("world.js")); + assert(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 CLIEngine({ cwd }); + }, "Package.json eslintIgnore property requires an array of paths"); + }); + }); + + describe("with --ignore-pattern option", () => { + it("should accept a string for options.ignorePattern", () => { + const cwd = getFixturePath("ignored-paths", "ignore-pattern"); + const engine = new CLIEngine({ + ignorePattern: "ignore-me.txt", + cwd + }); + + assert(engine.isPathIgnored("ignore-me.txt")); + }); + + it("should accept an array for options.ignorePattern", () => { + const engine = new CLIEngine({ + ignorePattern: ["a", "b"], + useEslintrc: false + }); + + assert(engine.isPathIgnored("a")); + assert(engine.isPathIgnored("b")); + assert(!engine.isPathIgnored("c")); + }); + + it("should return true for files which match an ignorePattern even if they do not exist on the filesystem", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ + ignorePattern: "not-a-file", + cwd + }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "not-a-file"))); + }); + + it("should return true for file matching an ignore pattern exactly", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "undef.js", cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should return false for file matching an invalid ignore pattern with leading './'", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "./undef.js", cwd }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should return false for file in subfolder of cwd matching an ignore pattern with leading '/'", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "/undef.js", cwd }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "subdir", "undef.js"))); + }); + + it("should return true for file matching a child of an ignore pattern", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "ignore-pattern", cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "ignore-pattern", "ignore-me.txt"))); + }); + + it("should return true for file matching a grandchild of an ignore pattern", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "ignore-pattern", cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "ignore-pattern", "subdir", "ignore-me.txt"))); + }); + + it("should return false for file not matching any ignore pattern", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "failing.js", cwd }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "unignored.js"))); + }); + + it("two globstar '**' ignore pattern should ignore files in nested directories", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ ignorePattern: "**/*.js", cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.js"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "foo.j2"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.j2"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.j2"))); + }); + }); + + describe("with --ignore-path option", () => { + it("should load empty array with ignorePath set to false", () => { + const cwd = getFixturePath("ignored-paths", "no-ignore-file"); + const engine = new CLIEngine({ ignorePath: false, cwd }); + + // a .eslintignore in parent directories includes `*.js`, but don't load it. + assert(!engine.isPathIgnored("foo.js")); + assert(engine.isPathIgnored("node_modules/foo.js")); + }); + + it("initialization with ignorePath should work when cwd is a parent directory", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored("custom-name/foo.js")); + }); + + it("initialization with ignorePath should work when the file is in the cwd", () => { + const cwd = getFixturePath("ignored-paths", "custom-name"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored("foo.js")); + }); + + it("initialization with ignorePath should work when cwd is a subdirectory", () => { + const cwd = getFixturePath("ignored-paths", "custom-name", "subdirectory"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(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 CLIEngine({ ignorePath, cwd }); + }, "Cannot read .eslintignore file"); + }); + + it("should return false for files outside of ignorePath's directory", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should resolve relative paths from the ignorePath, not cwd", () => { + const cwd = getFixturePath("ignored-paths", "subdir"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should resolve relative paths from the ignorePath when it's in a child directory", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/foo.js"))); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/bar.js"))); + }); + + it("should resolve relative paths from the ignorePath when it contains negated globs", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored("subdir/blah.txt")); + assert(engine.isPathIgnored("blah.txt")); + assert(!engine.isPathIgnored("subdir/bar.txt")); + assert(engine.isPathIgnored("bar.txt")); + assert(!engine.isPathIgnored("subdir/baz.txt")); + assert(!engine.isPathIgnored("baz.txt")); + }); + + it("should resolve default ignore patterns from the CWD even when the ignorePath is in a subdirectory", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored("node_modules/blah.js")); + }); + + it("should resolve default ignore patterns from the CWD even when the ignorePath is in a parent directory", () => { + const cwd = getFixturePath("ignored-paths", "subdir"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored("node_modules/blah.js")); + }); + + it("should handle .eslintignore which contains CRLF correctly.", () => { + 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 CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide1/a.js"))); + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide2/a.js"))); + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide3/a.js"))); + }); + + it("should not include comments in ignore rules", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithComments"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(!engine.isPathIgnored("# should be ignored")); + assert(engine.isPathIgnored("this_one_not")); + }); + + it("should ignore a non-negated pattern", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(engine.isPathIgnored(getFixturePath("ignored-paths", "negation", "ignore.js"))); + }); + + it("should not ignore a negated pattern", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new CLIEngine({ ignorePath, cwd }); + + assert(!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", () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new CLIEngine({ + ignorePath: getFixturePath("ignored-paths", ".eslintignore"), + ignorePattern: "!sampleignorepattern", + cwd + }); + + assert(!engine.isPathIgnored(getFixturePath("ignored-paths", "sampleignorepattern"))); + }); + }); }); describe("getFormatter()", () => { @@ -4303,4 +4705,654 @@ describe("CLIEngine", () => { }); }); }); + + describe("with ignorePatterns config", () => { + const root = getFixturePath("cli-engine/ignore-patterns"); + let InMemoryCLIEngine; + + describe("ignorePatterns can add an ignore pattern ('foo.js').", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), false); + assert.strictEqual(engine.isPathIgnored("subdir/bar.js"), false); + }); + + it("'executeOnFiles()' should not verify 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + 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": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for '/bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/bar.js"), false); + }); + + it("'executeOnFiles()' should not verify 'foo.js' and '/bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + 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": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'false' for 'node_modules/foo/index.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("node_modules/foo/index.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/foo/.dot.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("node_modules/foo/.dot.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/bar/index.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("node_modules/bar/index.js"), true); + }); + + it("'executeOnFiles()' should verify 'node_modules/foo/index.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "node_modules/foo/index.js"), + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignorePatterns can unignore '.eslintrc.js'.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!.eslintrc.js" + })}`, + "foo.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'false' for '.eslintrc.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored(".eslintrc.js"), false); + }); + + it("'executeOnFiles()' should verify '.eslintrc.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!.*" + })}`, + ".eslintignore": ".foo*", + ".foo.js": "", + ".bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for re-ignored '.foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored(".foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for unignored '.bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored(".bar.js"), false); + }); + + it("'executeOnFiles()' should not verify re-ignored '.foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, ".eslintrc.js"), + path.join(root, ".bar.js") + ]); + }); + }); + + describe(".eslintignore can unignore files that are ignored by ignorePatterns.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + ".eslintignore": "!foo.js", + "foo.js": "", + "bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), true); + }); + + it("'executeOnFiles()' should verify unignored 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "foo.js" + })}`, + "subdir/.eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "bar.js" + })}`, + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "", + "subdir/subsubdir/foo.js": "", + "subdir/subsubdir/bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/subsubdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in 'subdir'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/bar.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/subsubdir/bar.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the outside of 'subdir'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), false); + }); + + it("'executeOnFiles()' should verify 'bar.js' in the outside of 'subdir'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "foo.js" + })}`, + "subdir/.eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!foo.js" + })}`, + "foo.js": "", + "subdir/foo.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js' in the root directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js' in the child directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'executeOnFiles()' should verify 'foo.js' in the child directory.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + 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": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), false); + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'executeOnFiles()' should verify unignored 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "subdir/foo.js"), + path.join(root, "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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "foo.js" + })}`, + "subdir/.eslintrc.js": `module.exports = ${JSON.stringify({ + root: true, + ignorePatterns: "bar.js" + })}`, + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js' in the root directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the root directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), false); + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js' in the child directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in the child directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'executeOnFiles()' should verify 'bar.js' in the root directory and 'foo.js' in the child directory.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "subdir/foo.js"), + path.join(root, "bar.js") + ]); + }); + }); + + describe("even if the config in a child directory has 'root:true', .eslintignore should be used.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({})}`, + "subdir/.eslintrc.js": `module.exports = ${JSON.stringify({ + root: true, + ignorePatterns: "bar.js" + })}`, + ".eslintignore": "foo.js", + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the root directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in the child directory.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'executeOnFiles()' should verify 'bar.js' in the root directory.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the shareable config should be used.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "foo.js" + })}`, + ".eslintrc.js": `module.exports = ${JSON.stringify({ + extends: "one" + })}`, + "foo.js": "", + "bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), false); + }); + + it("'executeOnFiles()' should verify 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the shareable config should be relative to the entry config file.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "/foo.js" + })}`, + ".eslintrc.js": `module.exports = ${JSON.stringify({ + extends: "one" + })}`, + "foo.js": "", + "subdir/foo.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'subdir/foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'executeOnFiles()' should verify 'subdir/foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + ".eslintrc.js": `module.exports = ${JSON.stringify({ + extends: "one", + ignorePatterns: "!bar.js" + })}`, + "foo.js": "", + "bar.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + + assert.strictEqual(engine.isPathIgnored("bar.js"), false); + }); + + it("'executeOnFiles()' should verify 'bar.js'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + 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(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + "foo.js": "" + } + }).CLIEngine; + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js'.", () => { + const engine = new InMemoryCLIEngine({ ignore: false }); + + assert.strictEqual(engine.isPathIgnored("foo.js"), false); + }); + + it("'executeOnFiles()' should verify 'foo.js'.", () => { + const engine = new InMemoryCLIEngine({ ignore: false }); + const filePaths = engine.executeOnFiles("**/*.js").results.map(r => r.filePath); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignorePatterns in overrides section is not allowed.", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + overrides: [ + { + files: "*.js", + ignorePatterns: "foo.js" + } + ] + })}`, + "foo.js": "" + } + }).CLIEngine; + }); + + it("should throw a configuration error.", () => { + assert.throws(() => { + const engine = new InMemoryCLIEngine(); + + engine.executeOnFiles("*.js"); + }, "Unexpected top-level property \"overrides[0].ignorePatterns\""); + }); + }); + + }); }); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 8b86b0f84ff..4f3ade1e60a 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -30,6 +30,7 @@ function assertConfigArrayElement(actual, providedExpected) { criteria: null, env: void 0, globals: void 0, + ignorePattern: void 0, noInlineConfig: void 0, parser: void 0, parserOptions: void 0, @@ -55,6 +56,7 @@ function assertConfig(actual, providedExpected) { const expected = { env: {}, globals: {}, + ignorePatterns: [], noInlineConfig: void 0, parser: null, parserOptions: {}, diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index 1d4c34708d1..fafb688d768 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -425,6 +425,7 @@ describe("ConfigArray", () => { configNameOfNoInlineConfig: "", env: {}, globals: {}, + ignores: void 0, noInlineConfig: void 0, parser: null, parserOptions: { @@ -457,6 +458,7 @@ describe("ConfigArray", () => { configNameOfNoInlineConfig: "", env: {}, globals: {}, + ignores: void 0, noInlineConfig: void 0, parser: null, parserOptions: { @@ -609,7 +611,8 @@ describe("ConfigArray", () => { settings: {}, processor: null, noInlineConfig: void 0, - reportUnusedDisableDirectives: void 0 + reportUnusedDisableDirectives: void 0, + ignores: void 0 }); assert.deepStrictEqual(config[0], { rules: { diff --git a/tests/lib/cli-engine/config-array/ignore-pattern.js b/tests/lib/cli-engine/config-array/ignore-pattern.js new file mode 100644 index 00000000000..443d350a2ed --- /dev/null +++ b/tests/lib/cli-engine/config-array/ignore-pattern.js @@ -0,0 +1,125 @@ +/** + * @fileoverview Tests for IgnorePattern class. + * @author Toru Nagashima + */ +"use strict"; + +const assert = require("assert"); +const path = require("path"); +const { IgnorePattern } = require("../../../../lib/cli-engine/config-array/ignore-pattern"); + +describe("IgnorePattern", () => { + describe("constructor(patterns, basePath)", () => { + it("should bind the first argument to 'patterns' property.", () => { + const p = new IgnorePattern(["a.js"], process.cwd()); + + assert.deepStrictEqual(p.patterns, ["a.js"]); + }); + + it("should bind the second argument to 'basePath' property.", () => { + const p = new IgnorePattern(["a.js"], process.cwd()); + + assert.strictEqual(p.basePath, process.cwd()); + }); + + it("should throw an error if the second argument was not an absolute path.", () => { + assert.throws(() => new IgnorePattern([], "a.js"), ""); + }); + }); + + describe("getPatternsRelativeTo(newBasePath)", () => { + it("should return 'patterns' as-is if the argument is the same as 'basePath'.", () => { + const basePath1 = path.join(process.cwd(), "foo/bar"); + const p = new IgnorePattern(["a.js", "/b.js", "!c.js", "!/d.js"], basePath1); + + assert.deepStrictEqual( + p.getPatternsRelativeTo(basePath1), + ["a.js", "/b.js", "!c.js", "!/d.js"] + ); + }); + + it("should return modified 'patterns' if the argument is different from 'basePath'.", () => { + const basePath1 = path.join(process.cwd(), "foo/bar"); + const basePath2 = process.cwd(); + const p = new IgnorePattern(["a.js", "/b.js", "!c.js", "!/d.js"], basePath1); + + assert.deepStrictEqual( + p.getPatternsRelativeTo(basePath2), + ["/foo/bar/**/a.js", "/foo/bar/b.js", "!/foo/bar/**/c.js", "!/foo/bar/d.js"] + ); + }); + }); + + describe("static createIgnore(ignorePatterns)", () => { + describe("with two patterns should return a function, and the function", () => { + const cwd = process.cwd(); + const basePath1 = path.join(cwd, "foo/bar"); + const basePath2 = path.join(cwd, "abc/"); + const ignores = IgnorePattern.createIgnore([ + new IgnorePattern(["*.js", "/*.ts", "!a.*", "!/b.*"], basePath1), + new IgnorePattern(["*.js", "/*.ts", "!a.*", "!/b.*"], basePath2) + ]); + const patterns = [ + ["a.js", false], + ["a.ts", false], + ["b.js", false], + ["b.ts", false], + ["c.js", false], + ["c.ts", false], + ["dir/a.js", false], + ["dir/a.ts", false], + ["dir/b.js", false], + ["dir/b.ts", false], + ["dir/c.js", false], + ["dir/c.ts", false], + ["foo/bar/a.js", false], + ["foo/bar/a.ts", false], + ["foo/bar/b.js", false], + ["foo/bar/b.ts", false], + ["foo/bar/c.js", true], + ["foo/bar/c.ts", true], + ["foo/bar/dir/a.js", false], + ["foo/bar/dir/a.ts", false], + ["foo/bar/dir/b.js", true], + ["foo/bar/dir/b.ts", false], + ["foo/bar/dir/c.js", true], + ["foo/bar/dir/c.ts", false], + ["abc/a.js", false], + ["abc/a.ts", false], + ["abc/b.js", false], + ["abc/b.ts", false], + ["abc/c.js", true], + ["abc/c.ts", true], + ["abc/dir/a.js", false], + ["abc/dir/a.ts", false], + ["abc/dir/b.js", true], + ["abc/dir/b.ts", false], + ["abc/dir/c.js", true], + ["abc/dir/c.ts", false] + ]; + + for (const [filename, expected] of patterns) { + it(`should return ${expected} if '${filename}' was given.`, () => { + assert.strictEqual(ignores(path.join(cwd, filename)), expected); + }); + } + + it("should return false if '.dot.js' and false was given.", () => { + assert.strictEqual(ignores(path.join(cwd, ".dot.js"), false), true); + }); + + it("should return true if '.dot.js' and true were given.", () => { + assert.strictEqual(ignores(path.join(cwd, ".dot.js"), true), false); + }); + + + it("should return false if '.dot/foo.js' and false was given.", () => { + assert.strictEqual(ignores(path.join(cwd, ".dot/foo.js"), false), true); + }); + + it("should return true if '.dot/foo.js' and true were given.", () => { + assert.strictEqual(ignores(path.join(cwd, ".dot/foo.js"), true), false); + }); + }); + }); +}); diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index d379ac520d6..3d05f63ab07 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -11,7 +11,6 @@ const { assert } = require("chai"); const sh = require("shelljs"); const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); -const { IgnoredPaths } = require("../../../lib/cli-engine/ignored-paths"); const { defineFileEnumeratorWithInMemoryFileSystem } = require("./_utils"); describe("FileEnumerator", () => { @@ -82,8 +81,10 @@ describe("FileEnumerator", () => { it("should use the config '.eslintrc.json' for both files.", () => { assert.strictEqual(list[0].config, list[1].config); - assert.strictEqual(list[0].config.length, 1); - assert.strictEqual(list[0].config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[0].config.length, 3); + assert.strictEqual(list[0].config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(list[0].config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[0].config[2].filePath, path.join(root, ".eslintignore")); }); }); @@ -114,15 +115,19 @@ describe("FileEnumerator", () => { it("should use the merged config of '.eslintrc.json' and 'lib/nested/.eslintrc.yml' for 'lib/nested/one.js' and 'lib/nested/two.js'.", () => { assert.strictEqual(list[0].config, list[1].config); - assert.strictEqual(list[0].config.length, 2); - assert.strictEqual(list[0].config[0].filePath, path.join(root, ".eslintrc.json")); - assert.strictEqual(list[0].config[1].filePath, path.join(root, "lib/nested/.eslintrc.yml")); + assert.strictEqual(list[0].config.length, 4); + assert.strictEqual(list[0].config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(list[0].config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[0].config[2].filePath, path.join(root, "lib/nested/.eslintrc.yml")); + assert.strictEqual(list[0].config[3].filePath, path.join(root, ".eslintignore")); }); it("should use the config '.eslintrc.json' for 'lib/one.js' and 'lib/two.js'.", () => { assert.strictEqual(list[2].config, list[3].config); - assert.strictEqual(list[2].config.length, 1); - assert.strictEqual(list[2].config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[2].config.length, 3); + assert.strictEqual(list[2].config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(list[2].config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[2].config[2].filePath, path.join(root, ".eslintignore")); }); }); @@ -153,15 +158,19 @@ describe("FileEnumerator", () => { it("should use the config '.eslintrc.json' for 'lib/one.js' and 'lib/two.js'.", () => { assert.strictEqual(list[0].config, list[1].config); - assert.strictEqual(list[0].config.length, 1); - assert.strictEqual(list[0].config[0].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[0].config.length, 3); + assert.strictEqual(list[0].config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(list[0].config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[0].config[2].filePath, path.join(root, ".eslintignore")); }); it("should use the merged config of '.eslintrc.json' and 'test/.eslintrc.yml' for 'test/one.js' and 'test/two.js'.", () => { assert.strictEqual(list[2].config, list[3].config); - assert.strictEqual(list[2].config.length, 2); - assert.strictEqual(list[2].config[0].filePath, path.join(root, ".eslintrc.json")); - assert.strictEqual(list[2].config[1].filePath, path.join(root, "test/.eslintrc.yml")); + assert.strictEqual(list[2].config.length, 4); + assert.strictEqual(list[2].config[0].name, "DefaultIgnorePattern"); + assert.strictEqual(list[2].config[1].filePath, path.join(root, ".eslintrc.json")); + assert.strictEqual(list[2].config[2].filePath, path.join(root, "test/.eslintrc.yml")); + assert.strictEqual(list[2].config[3].filePath, path.join(root, ".eslintignore")); }); }); }); @@ -196,8 +205,7 @@ describe("FileEnumerator", () => { // Disable "No Configuration Found" error. useEslintrc: false - }), - ignoredPaths: new IgnoredPaths(options) + }) }).iterateFiles(patterns), ({ filePath, ignored }) => ({ filename: filePath, ignored }) ); @@ -393,7 +401,7 @@ describe("FileEnumerator", () => { }); it("should ignore a file from a glob if matching a specified ignore pattern", () => { - const options = { ignore: true, ignorePattern: "foo.js", cwd: getFixturePath() }; + const options = { ignore: true, cliConfig: { ignorePatterns: ["foo.js"] }, cwd: getFixturePath() }; const patterns = [getFixturePath("glob-util", "ignored", "**/*.js")]; assert.throws(() => { @@ -419,7 +427,7 @@ describe("FileEnumerator", () => { }); it("should set 'ignored: true' for files that are explicitly specified but ignored", () => { - const options = { ignore: true, ignorePattern: "foo.js", cwd: getFixturePath() }; + const options = { ignore: true, cliConfig: { ignorePatterns: ["foo.js"] }, cwd: getFixturePath() }; const filename = getFixturePath("glob-util", "ignored", "foo.js"); const patterns = [filename]; const result = listFiles(patterns, options); @@ -441,7 +449,7 @@ describe("FileEnumerator", () => { }); it("should return unignored files from default ignored folders", () => { - const options = { ignorePattern: "!/node_modules/dependency.js", cwd: getFixturePath("glob-util") }; + const options = { cliConfig: { ignorePatterns: ["!/node_modules/dependency.js"] }, cwd: getFixturePath("glob-util") }; const glob = getFixturePath("glob-util", "**/*.js"); const patterns = [glob]; const result = listFiles(patterns, options); diff --git a/tests/lib/cli-engine/ignored-paths.js b/tests/lib/cli-engine/ignored-paths.js deleted file mode 100644 index 7fd8e8408ca..00000000000 --- a/tests/lib/cli-engine/ignored-paths.js +++ /dev/null @@ -1,659 +0,0 @@ -/** - * @fileoverview Tests for IgnoredPaths object. - * @author Jonathan Rajavuori - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const assert = require("chai").assert, - path = require("path"), - os = require("os"), - { IgnoredPaths } = require("../../../lib/cli-engine/ignored-paths.js"), - sinon = require("sinon"), - fs = require("fs"), - includes = require("lodash").includes; - -require("shelljs/global"); - -/* global mkdir, rm, cp */ - -//------------------------------------------------------------------------------ -// Helper -//------------------------------------------------------------------------------ - -let fixtureDir; - -/** - * get raw rules from IgnorePaths instance - * @param {IgnoredPaths} ignoredPaths instance of IgnoredPaths - * @returns {string[]} raw ignore rules - */ -function getIgnoreRules(ignoredPaths) { - const ignoreRulesProperty = "_rules"; - let ignoreRules = []; - - Object.keys(ignoredPaths.ig).forEach(key => { - const rules = ignoredPaths.ig[key][ignoreRulesProperty]; - - rules.forEach(rule => { - const ruleOrigins = ignoreRules.map(ruleObj => ruleObj.origin); - - /* - * Don't include duplicate ignore rules. - * (Duplicates occur because we add custom ignore patterns to the - * defaults as well, to allow unignoring default ignores) - */ - if (!includes(ruleOrigins, rule.origin)) { - ignoreRules = ignoreRules.concat(rule); - } - }); - }); - - return ignoreRules; -} - -/** - * Get a list of paths of loaded ignore files (e.g. .eslintignore) from IgnorePaths instance - * @param {IgnoredPaths} ignoredPaths Instance of IgnoredPaths - * @returns {string[]} loaded ignore files - */ -function getIgnoreFiles(ignoredPaths) { - return ignoredPaths.ig.custom.ignoreFiles; -} - -/** - * Get a list of ignore patterns that are loaded - * @param {Object[]} ignoredPaths Instance of IgnoredPaths - * @returns {string[]} Ignore patterns - */ -function getIgnorePatterns(ignoredPaths) { - const ignoreRules = getIgnoreRules(ignoredPaths); - - return ignoreRules.map(rule => rule.origin); -} - -/** - * count the number of default patterns applied to IgnoredPaths instance - * @param {IgnoredPaths} ignoredPaths instance of IgnoredPaths - * @returns {integer} count of default patterns - */ -function countDefaultPatterns(ignoredPaths) { - let count = ignoredPaths.defaultPatterns.length; - - if (!ignoredPaths.options || (ignoredPaths.options.dotfiles !== true)) { - count += 2; // Two patterns for ignoring dotfiles - } - return count; -} - -/** - * 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) { - return path.join(fs.realpathSync(fixtureDir), ...args); -} - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("IgnoredPaths", () => { - - // copy into clean area so as not to get "infected" by this project's .eslintrc files - before(() => { - fixtureDir = path.join(os.tmpdir(), "/eslint/fixtures/ignored-paths/"); - mkdir("-p", fixtureDir); - cp("-r", "./tests/fixtures/ignored-paths/.", fixtureDir); - }); - - after(() => { - rm("-r", fixtureDir); - }); - - describe("initialization", () => { - - it("should load .eslintignore from cwd when explicitly passed", () => { - const expectedIgnoreFile = getFixturePath(".eslintignore"); - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - const ignorePatterns = getIgnorePatterns(ignoredPaths); - - assert.isNotNull(ignoredPaths.ignoreFileDir); - assert.deepStrictEqual(getIgnoreFiles(ignoredPaths), [expectedIgnoreFile]); - assert.include(ignorePatterns, "sampleignorepattern"); - }); - - it("should set baseDir to cwd when no ignore file was loaded", () => { - const ignoredPaths = new IgnoredPaths({ cwd: getFixturePath("no-ignore-file") }); - - assert.strictEqual(ignoredPaths.ignoreFileDir, getFixturePath("no-ignore-file")); - }); - - it("should not travel to parent directories to find .eslintignore when it's missing and cwd is provided", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("configurations") }); - - assert.lengthOf(getIgnoreRules(ignoredPaths), 4); - assert.lengthOf(getIgnoreFiles(ignoredPaths), 0); - }); - - it("should load empty array with ignorePath set to false", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: false, cwd: getFixturePath("no-ignore-file") }); - - assert.isArray(getIgnoreRules(ignoredPaths)); - assert.lengthOf(getIgnoreRules(ignoredPaths), countDefaultPatterns(ignoredPaths)); - }); - - it("should accept an array for options.ignorePattern", () => { - const ignorePattern = ["a", "b"]; - - const ignoredPaths = new IgnoredPaths({ - ignorePattern - }); - - assert.ok( - ignorePattern.every(pattern => ( - getIgnoreRules(ignoredPaths).some(rule => rule.pattern === pattern) - )) - ); - }); - - it("should use package.json's eslintIgnore files if no specified .eslintignore file", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("package-json-ignore") }); - - assert.isTrue(ignoredPaths.contains("hello.js")); - assert.isTrue(ignoredPaths.contains("world.js")); - }); - - it("should use correct message template if failed to parse package.json", () => { - assert.throw(() => { - try { - // eslint-disable-next-line no-new - new IgnoredPaths({ ignore: true, cwd: getFixturePath("broken-package-json") }); - } 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", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains("hello.js")); - assert.isFalse(ignoredPaths.contains("world.js")); - assert.isTrue(ignoredPaths.contains("sampleignorepattern")); - }); - - it("should error if package.json's eslintIgnore is not an array of file paths", () => { - assert.throws(() => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("bad-package-json-ignore") }); - - assert.ok(ignoredPaths); - }, "Package.json eslintIgnore property requires an array of paths"); - }); - }); - - describe("caching file reads", () => { - - let readFileSyncCount; - - before(() => { - readFileSyncCount = sinon.spy(fs, "readFileSync"); - }); - - after(() => { - readFileSyncCount.restore(); - }); - - it("should cache readFileSync on same file paths", () => { - const ignoreFilePath = getFixturePath(".eslintignore"); - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - ignoredPaths.readIgnoreFile(ignoreFilePath); - assert.isTrue(ignoredPaths.contains(ignoreFilePath)); - sinon.assert.calledOnce(readFileSyncCount); - }); - }); - - describe("initialization with ignorePattern", () => { - - it("should ignore a normal pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "ignore-me.txt", cwd: getFixturePath("ignore-pattern") }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("ignore-pattern", "ignore-me.txt"))); - }); - - }); - - describe("initialization with ignorePath", () => { - - let ignoreFilePath; - - before(() => { - ignoreFilePath = getFixturePath(".eslintignore"); - }); - - it("should set baseDir to directory containing ignorePath if provided", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath() }); - - assert.strictEqual(ignoredPaths.ignoreFileDir, path.dirname(ignoreFilePath)); - }); - - it("should set the common ancestor directory of cwd and ignorePath to baseDir (in the case that 'ignoreFilePath' and 'cwd' are siblings)", () => { - const baseDir = path.dirname(ignoreFilePath); - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: path.resolve(baseDir, "testcwd") }); - - assert.strictEqual(ignoredPaths.getBaseDir(), baseDir); - }); - - it("should set the common ancestor directory of cwd and ignorePath to baseDir", () => { - const baseDir = path.resolve(ignoreFilePath, "../../.."); - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: path.resolve(baseDir, "fix/testcwd") }); - - assert.strictEqual(ignoredPaths.getBaseDir(), baseDir); - }); - - }); - - describe("initialization with ignorePath file not named .eslintignore", () => { - - let ignoreFilePath; - - before(() => { - ignoreFilePath = getFixturePath("custom-name", "ignore-file"); - }); - - it("should work when cwd is a parent directory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath() }); - - assert.notStrictEqual(getIgnoreRules(ignoredPaths).length, countDefaultPatterns(ignoredPaths)); - }); - - it("should work when the file is in the cwd", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath("custom-name") }); - - assert.notStrictEqual(getIgnoreRules(ignoredPaths).length, countDefaultPatterns(ignoredPaths)); - }); - - it("should work when cwd is a subdirectory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath("custom-name", "subdirectory") }); - - assert.notStrictEqual(getIgnoreRules(ignoredPaths).length, countDefaultPatterns(ignoredPaths)); - }); - - }); - - describe("initialization without ignorePath", () => { - - it("should not load an ignore file if none is in cwd", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("no-ignore-file") }); - - assert.lengthOf(getIgnoreFiles(ignoredPaths), 0); - assert.lengthOf(getIgnoreRules(ignoredPaths), countDefaultPatterns(ignoredPaths)); - }); - - }); - - describe("initialization with invalid file", () => { - - let invalidFilepath; - - before(() => { - invalidFilepath = getFixturePath("not-a-directory", ".foobaz"); - }); - - it("should throw error", () => { - assert.throws(() => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: invalidFilepath, cwd: getFixturePath() }); - - assert.ok(ignoredPaths); - }, "Cannot read ignore file"); - }); - - }); - - describe("contains", () => { - - it("should throw if initialized with invalid options", () => { - const ignoredPaths = new IgnoredPaths(null); - - assert.throw(ignoredPaths.contains, Error); - }); - - it("should not throw if given a relative filename", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "undef.js", cwd: getFixturePath() }); - - ignoredPaths.contains("undef.js"); - }); - - it("should return true for files which match an ignorePattern even if they do not exist on the filesystem", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "not-a-file", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("not-a-file"))); - }); - - it("should return false for files outside of the cwd (with no ignore file provided)", () => { - - // Default ignore patterns should not inadvertantly ignore files in parent directories - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("no-ignore-file") }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("undef.js"))); - }); - - it("should return false for files outside of ignorePath's directory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("custom-name", "ignore-file"), cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("undef.js"))); - }); - - it("should return true for file matching an ignore pattern exactly", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "undef.js", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("undef.js"))); - }); - - it("should return false for file matching an invalid ignore pattern with leading './'", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "./undef.js", cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("undef.js"))); - }); - - it("should return false for file in subfolder of cwd matching an ignore pattern with leading '/'", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "/undef.js", cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("subdir", "undef.js"))); - }); - - it("should return true for file matching a child of an ignore pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "ignore-pattern", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("ignore-pattern", "ignore-me.txt"))); - }); - - it("should return true for file matching a grandchild of an ignore pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "ignore-pattern", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("ignore-pattern", "subdir", "ignore-me.txt"))); - }); - - it("should return true for file matching a child of an ignore pattern with windows line termination", () => { - sinon.stub(fs, "readFileSync") - .withArgs(".eslintignore") - .returns("subdir\r\n"); - sinon.stub(fs, "statSync") - .withArgs(".eslintignore") - .returns({ - isFile() { - return true; - } - }); - - try { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ".eslintignore", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/undef.js"))); - } finally { - fs.readFileSync.restore(); - fs.statSync.restore(); - } - }); - - it("should return false for file not matching any ignore pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "failing.js", cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("unignored.js"))); - }); - - it("should return false for ignored file when unignored with ignore pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignore"), ignorePattern: "!sampleignorepattern", cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("sampleignorepattern"))); - - }); - - it("should resolve relative paths from the ignorePath, not cwd", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignoreForDifferentCwd"), cwd: getFixturePath("subdir") }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("subdir/undef.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("undef.js"))); - }); - - it("should resolve relative paths from the ignorePath when it's in a child directory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("subdir/.eslintignoreInChildDir"), cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/undef.js"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("undef.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/foo.js"))); - - assert.isTrue(ignoredPaths.contains(getFixturePath("node_modules/bar.js"))); - }); - - it("should resolve relative paths from the ignorePath when it contains negated globs", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("subdir/.eslintignoreInChildDir"), cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains("subdir/blah.txt")); - assert.isTrue(ignoredPaths.contains("blah.txt")); - assert.isFalse(ignoredPaths.contains("subdir/bar.txt")); - assert.isTrue(ignoredPaths.contains("bar.txt")); - assert.isFalse(ignoredPaths.contains("subdir/baz.txt")); - assert.isFalse(ignoredPaths.contains("baz.txt")); - }); - - it("should resolve default ignore patterns from the CWD even when the ignorePath is in a subdirectory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("subdir/.eslintignoreInChildDir"), cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains("node_modules/blah.js")); - }); - - it("should resolve default ignore patterns from the CWD even when the ignorePath is in a parent directory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignoreForDifferentCwd"), cwd: getFixturePath("subdir") }); - - assert.isTrue(ignoredPaths.contains("node_modules/blah.js")); - }); - - it("should handle .eslintignore which contains CRLF correctly.", () => { - const ignoreFileContent = fs.readFileSync(getFixturePath("crlf/.eslintignore"), "utf8"); - - assert.isTrue(ignoreFileContent.includes("\r"), "crlf/.eslintignore should contains CR."); - - const ignoredPaths = new IgnoredPaths({ - ignore: true, - ignorePath: getFixturePath("crlf/.eslintignore"), - cwd: getFixturePath() - }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("crlf/hide1/a.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("crlf/hide2/a.js"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("crlf/hide3/a.js"))); - }); - }); - - describe("initialization with ignorePath containing commented lines", () => { - - let ignoreFilePath; - - before(() => { - ignoreFilePath = getFixturePath(".eslintignoreWithComments"); - }); - - it("should not include comments in ignore rules", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath() }); - const ignorePatterns = getIgnorePatterns(ignoredPaths); - - assert.strictEqual(getIgnoreRules(ignoredPaths).length, countDefaultPatterns(ignoredPaths) + 1); - assert.include(ignorePatterns, "this_one_not"); - }); - - }); - - describe("initialization with ignorePath containing negations", () => { - let ignoreFilePath; - - before(() => { - ignoreFilePath = getFixturePath(".eslintignoreWithNegation"); - }); - - it("should ignore a non-negated pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("negation", "ignore.js"))); - }); - - it("should not ignore a negated pattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("negation", "unignore.js"))); - }); - - }); - - describe("default ignores", () => { - - it("should contain /bower_components/*", () => { - const ignoredPaths = new IgnoredPaths(); - - assert.include(ignoredPaths.defaultPatterns, "/bower_components/*"); - }); - - it("should contain /node_modules/*", () => { - const ignoredPaths = new IgnoredPaths(); - - assert.include(ignoredPaths.defaultPatterns, "/node_modules/*"); - }); - - it("should always apply defaultPatterns if ignore option is true", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("bower_components/package/file.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("node_modules/package/file.js"))); - }); - - it("should still apply defaultPatterns if ignore option is is false", () => { - const ignoredPaths = new IgnoredPaths({ ignore: false, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("bower_components/package/file.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("node_modules/package/file.js"))); - }); - - it("should not ignore files in defaultPatterns within a subdirectory", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("subdir/bower_components/package/file.js"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("subdir/node_modules/package/file.js"))); - }); - - it("should allow subfolders of defaultPatterns to be unignored by ignorePattern", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath(), ignorePattern: "!/node_modules/package" }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("node_modules", "package", "file.js"))); - }); - - it("should allow subfolders of defaultPatterns to be unignored by ignorePath", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath(), ignorePath: getFixturePath(".eslintignoreWithUnignoredDefaults") }); - - assert.isFalse(ignoredPaths.contains(getFixturePath("node_modules", "package", "file.js"))); - }); - - it("should ignore dotfiles", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath(".foo"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo/.bar"))); - }); - - it("should ignore directories beginning with a dot", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath(".foo/bar"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo/.bar/baz"))); - }); - - it("should still ignore dotfiles when ignore option disabled", () => { - const ignoredPaths = new IgnoredPaths({ ignore: false, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath(".foo"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo/.bar"))); - }); - - it("should still ignore directories beginning with a dot when ignore option disabled", () => { - const ignoredPaths = new IgnoredPaths({ ignore: false, cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath(".foo/bar"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo/.bar/baz"))); - }); - - it("should not ignore absolute paths containing '..'", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(`${getFixturePath("foo")}/../unignored.js`)); - }); - - it("should ignore /node_modules/ at top level relative to .eslintignore when loaded", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignore"), cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("node_modules", "existing.js"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("foo", "node_modules", "existing.js"))); - }); - - it("should ignore /node_modules/ at top level relative to cwd without an .eslintignore", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath("no-ignore-file") }); - - assert.isTrue(ignoredPaths.contains(getFixturePath("no-ignore-file", "node_modules", "existing.js"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("no-ignore-file", "foo", "node_modules", "existing.js"))); - }); - - }); - - describe("two globstar '**' ignore pattern", () => { - - it("should ignore files in nested directories", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePattern: "**/*.js", cwd: getFixturePath() }); - - assert.isTrue(ignoredPaths instanceof IgnoredPaths); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo/bar.js"))); - assert.isTrue(ignoredPaths.contains(getFixturePath("foo/bar/baz.js"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("foo.j2"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("foo/bar.j2"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("foo/bar/baz.j2"))); - }); - }); - - describe("dotfiles option", () => { - - it("should add at least one pattern when false", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, dotfiles: false, cwd: getFixturePath("no-ignore-file") }); - - assert(getIgnoreRules(ignoredPaths).length > ignoredPaths.defaultPatterns.length); - }); - - it("should add no patterns when true", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, dotfiles: true, cwd: getFixturePath("no-ignore-file") }); - - assert.lengthOf(getIgnoreRules(ignoredPaths), ignoredPaths.defaultPatterns.length); - }); - - it("should not ignore dotfiles when true", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, dotfiles: true, cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath(".foo"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("foo/.bar"))); - }); - - it("should not ignore directories beginning with a dot when true", () => { - const ignoredPaths = new IgnoredPaths({ ignore: true, dotfiles: true, cwd: getFixturePath() }); - - assert.isFalse(ignoredPaths.contains(getFixturePath(".foo/bar"))); - assert.isFalse(ignoredPaths.contains(getFixturePath("foo/.bar/baz"))); - }); - - }); - -});