diff --git a/.eslintrc.js b/.eslintrc.js index 780368769f68..ed1354d7f45c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,6 +52,13 @@ module.exports = { message: "`assert.doesNotThrow()` should be replaced with a comment next to the code." }] } + }, + { + files: ["tests/lib/linter.js"], + env: { + node: false, + "shared-node-browser": true + } } ] }; diff --git a/Makefile.js b/Makefile.js index fc1d5b40c2b6..9c8de5a1f830 100644 --- a/Makefile.js +++ b/Makefile.js @@ -597,6 +597,8 @@ target.test = function() { target.browserify(); + exec(`${getBinFile("browserify")} tests/fixtures/parsers/linter-test-parsers.js -o ${BUILD_DIR}/linter-test-parsers.js -s LinterTestParsers --global-transform [ babelify --presets [ es2015 ] ]`); + lastReturn = exec(`${getBinFile("karma")} start karma.conf.js`); if (lastReturn.code !== 0) { errors++; @@ -811,26 +813,16 @@ target.gensite = function(prereleaseVersion) { target.browserify = function() { - // 1. create temp and build directory - if (!test("-d", TEMP_DIR)) { - mkdir(TEMP_DIR); - } - + // 1. create build directory if (!test("-d", BUILD_DIR)) { mkdir(BUILD_DIR); } - // 2. browserify the temp directory - exec(`${getBinFile("browserify")} -x espree lib/linter.js -o ${BUILD_DIR}eslint.js -s eslint --global-transform [ babelify --presets [ es2015 ] ]`); - - // 3. Browserify espree - exec(`${getBinFile("browserify")} -r espree -o ${TEMP_DIR}espree.js --global-transform [ babelify --presets [ es2015 ] ]`); + // 2. browserify the linter file + exec(`${getBinFile("browserify")} lib/linter.js -o ${BUILD_DIR}eslint.js -s eslint --global-transform [ babelify --presets [ es2015 ] ]`); - // 4. Concatenate Babel polyfill, Espree, and ESLint files together - cat("./node_modules/babel-polyfill/dist/polyfill.js", `${TEMP_DIR}espree.js`, `${BUILD_DIR}eslint.js`).to(`${BUILD_DIR}eslint.js`); - - // 5. remove temp directory - rm("-rf", TEMP_DIR); + // 3. Concatenate Babel polyfill and ESLint files together + cat("./node_modules/babel-polyfill/dist/polyfill.js", `${BUILD_DIR}eslint.js`).to(`${BUILD_DIR}eslint.js`); }; target.checkRuleFiles = function() { diff --git a/README.md b/README.md index e1855ec3f49b..565ba0790478 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,7 @@ ESLint is a tool for identifying and reporting on patterns found in ECMAScript/J Prerequisites: [Node.js](https://nodejs.org/en/) (>=6.14), npm version 3+. -There are two ways to install ESLint: globally and locally. - -### Local Installation and Usage - -If you want to include ESLint as part of your project's build system, we recommend installing it locally. You can do so using npm: +You can install ESLint using npm: ``` $ npm install eslint --save-dev @@ -50,31 +46,7 @@ After that, you can run ESLint on any file or directory like this: $ ./node_modules/.bin/eslint yourfile.js ``` -Any plugins or shareable configs that you use must also be installed locally to work with a locally-installed ESLint. - -### Global Installation and Usage - -If you want to make ESLint available to tools that run across all of your projects, we recommend installing ESLint globally. You can do so using npm: - -``` -$ npm install -g eslint -``` - -You should then set up a configuration file: - -``` -$ eslint --init -``` - -After that, you can run ESLint on any file or directory like this: - -``` -$ eslint yourfile.js -``` - -Any plugins or shareable configs that you use must also be installed globally to work with a globally-installed ESLint. - -**Note:** `eslint --init` is intended for setting up and configuring ESLint on a per-project basis and will perform a local installation of ESLint and its plugins in the directory in which it is run. If you prefer using a global installation of ESLint, any plugins used in your configuration must also be installed globally. +It is also possible to install ESLint globally rather than locally (using `npm install eslint --global`). However, any plugins or shareable configs that you use must be installed locally in either case. ## Configuration @@ -124,16 +96,9 @@ No, ESLint does both traditional linting (looking for problematic patterns) and ### Why can't ESLint find my plugins? -ESLint can be [globally or locally installed](#installation-and-usage). If you install ESLint globally, your plugins must also be installed globally; if you install ESLint locally, your plugins must also be installed locally. - -If you are trying to run globally, make sure your plugins are installed globally (use `npm ls -g`). - -If you are trying to run locally: - * Make sure your plugins (and ESLint) are both in your project's `package.json` as devDependencies (or dependencies, if your project uses ESLint at runtime). * Make sure you have run `npm install` and all your dependencies are installed. - -In all cases, make sure your plugins' peerDependencies have been installed as well. You can use `npm view eslint-plugin-myplugin peerDependencies` to see what peer dependencies `eslint-plugin-myplugin` has. +* Make sure your plugins' peerDependencies have been installed as well. You can use `npm view eslint-plugin-myplugin peerDependencies` to see what peer dependencies `eslint-plugin-myplugin` has. ### Does ESLint support JSX? diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index 7b9ecb6ecfb1..5d9252a45b9a 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -273,10 +273,8 @@ Map { ### Linter#defineParser -Each instance of `Linter` holds a map of custom parsers. If you want to define a parser programmatically you can add this function -with the name of the parser as first argument and the [parser object](/docs/developer-guide/working-with-plugins.md#working-with-custom-parsers) as second argument. - -If during linting the parser is not found, it will fallback to `require(parserId)`. +Each instance of `Linter` holds a map of custom parsers. If you want to define a parser programmatically, you can add this function +with the name of the parser as first argument and the [parser object](/docs/developer-guide/working-with-plugins.md#working-with-custom-parsers) as second argument. The default `"espree"` parser will already be loaded for every `Linter` instance. ```js const Linter = require("eslint").Linter; diff --git a/docs/developer-guide/shareable-configs.md b/docs/developer-guide/shareable-configs.md index 41d8b8f35df3..16d5ed6348ef 100644 --- a/docs/developer-guide/shareable-configs.md +++ b/docs/developer-guide/shareable-configs.md @@ -38,6 +38,8 @@ You should declare your dependency on ESLint in `package.json` using the [peerDe } ``` +If your shareable config depends on a plugin, you should also specify it as a `peerDependency` (plugins are be loaded relative to the end user's project, so the end user is required to install the plugins they need). However, if your shareable config depends on a third-party parser or another shareable config, you can specify these packages as `dependencies`. + You can also test your shareable config on your computer before publishing by linking your module globally. Type: ```bash diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 1eed790a2e8a..8e93f213ffb7 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -60,9 +60,8 @@ Setting parser options helps ESLint determine what is a parsing error. All langu By default, ESLint uses [Espree](https://github.com/eslint/espree) as its parser. You can optionally specify that a different parser should be used in your configuration file so long as the parser meets the following requirements: -1. It must be an npm module installed locally. -1. It must have an Esprima-compatible interface (it must export a `parse()` method). -1. It must produce Esprima-compatible AST and token objects. +1. It must be a Node module loadable from the config file where it appears. Usually, this means you should install the parser package separately from npm. +1. It must conform to the [parser interface](/docs/developer-guide/working-with-plugins.md#working-with-custom-parsers). Note that even with these compatibilities, there are no guarantees that an external parser will work correctly with ESLint and ESLint will not fix bugs related to incompatibilities with other parsers. @@ -255,7 +254,7 @@ For historical reasons, the boolean values `false` and `true` can also be used t ## Configuring Plugins -ESLint supports the use of third-party plugins. Before using the plugin you have to install it using npm. +ESLint supports the use of third-party plugins. Before using the plugin, you have to install it using npm. To configure plugins inside of a configuration file, use the `plugins` key, which contains a list of plugin names. The `eslint-plugin-` prefix can be omitted from the plugin name. @@ -277,7 +276,7 @@ And in YAML: - eslint-plugin-plugin2 ``` -**Note:** Due to the behavior of Node's `require` function, a globally-installed instance of ESLint can only use globally-installed ESLint plugins, and locally-installed version can only use *locally-installed* plugins. Mixing local and global plugins is not supported. +**Note:** Plugins are resolved relative to the current working directory of the ESLint process. In other words, ESLint will load the same plugin as a user would obtain by running `require('eslint-plugin-pluginname')` in a Node REPL from their project root. ## Configuring Rules @@ -625,10 +624,10 @@ A configuration file can extend the set of enabled rules from base configuration The `extends` property value is either: -* a string that specifies a configuration +* a string that specifies a configuration (either a path to a config file, the name of a shareable config, `eslint:recommended`, or `eslint:all`) * an array of strings: each additional configuration extends the preceding configurations -ESLint extends configurations recursively so a base configuration can also have an `extends` property. +ESLint extends configurations recursively, so a base configuration can also have an `extends` property. Relative paths and shareable config names in an `extends` property are resolved from the location of the config file where they appear. The `rules` property can do any of the following to extend (or override) the set of rules: @@ -723,9 +722,7 @@ Example of a configuration file in JSON format: ### Using a configuration file -The `extends` property value can be an absolute or relative path to a base [configuration file](#using-configuration-files). - -ESLint resolves a relative path to a base configuration file relative to the configuration file that uses it **unless** that file is in your home directory or a directory that isn't an ancestor to the directory in which ESLint is installed (either locally or globally). In those cases, ESLint resolves the relative path to the base file relative to the linted **project** directory (typically the current working directory). +The `extends` property value can be an absolute or relative path to a base [configuration file](#using-configuration-files). ESLint resolves a relative path to a base configuration file relative to the configuration file that uses it. Example of a configuration file in JSON format: diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index 2e6e6617a517..c35471ea51e0 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -15,61 +15,25 @@ ESLint is a tool for identifying and reporting on patterns found in ECMAScript/J Prerequisites: [Node.js](https://nodejs.org/en/) (>=6.14), npm version 3+. -There are two ways to install ESLint: globally and locally. - -### Local Installation and Usage - -If you want to include ESLint as part of your project's build system, we recommend installing it locally. You can do so using npm: +You can install ESLint using npm: ``` $ npm install eslint --save-dev ``` -You should then setup a configuration file: +You should then set up a configuration file: ``` $ ./node_modules/.bin/eslint --init ``` -After that, you can run ESLint in your project's root directory like this: - -``` -$ ./node_modules/.bin/eslint yourfile.js -``` - -Instead of navigating to `./node_modules/.bin/` you may also use `npx` to run `eslint`: - -``` -$ npx eslint -``` - -**Note:** If ESLint wasn't manually installed (via `npm`), `npx` will install `eslint` to a temporary directory and execute it. - -Any plugins or shareable configs that you use must also be installed locally to work with a locally-installed ESLint. - -### Global Installation and Usage - -If you want to make ESLint available to tools that run across all of your projects, we recommend installing ESLint globally. You can do so using npm: - -``` -$ npm install -g eslint -``` - -You should then setup a configuration file: - -``` -$ eslint --init -``` - After that, you can run ESLint on any file or directory like this: ``` -$ eslint yourfile.js +$ ./node_modules/.bin/eslint yourfile.js ``` -Any plugins or shareable configs that you use must also be installed globally to work with a globally-installed ESLint. - -**Note:** `eslint --init` is intended for setting up and configuring ESLint on a per-project basis and will perform a local installation of ESLint and its plugins in the directory in which it is run. If you prefer using a global installation of ESLint, any plugins used in your configuration must also be installed globally. +It is also possible to install ESLint globally rather than locally (using `npm install eslint --global`). However, any plugins or shareable configs that you use must be installed locally in either case. ## Configuration diff --git a/karma.conf.js b/karma.conf.js index e8a027e540e2..8e5579b71240 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -16,9 +16,10 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ - "node_modules/mocha/mocha.js", "node_modules/chai/chai.js", + "node_modules/esprima/dist/esprima.js", "node_modules/sinon/pkg/sinon.js", + "build/linter-test-parsers.js", "build/eslint.js", "tests/lib/linter.js" ], diff --git a/lib/cli-engine.js b/lib/cli-engine.js index d7de2592a16d..4324f002cc3c 100644 --- a/lib/cli-engine.js +++ b/lib/cli-engine.js @@ -27,13 +27,12 @@ const fs = require("fs"), globUtils = require("./util/glob-utils"), validator = require("./config/config-validator"), hash = require("./util/hash"), - ModuleResolver = require("./util/module-resolver"), + relativeModuleResolver = require("./util/relative-module-resolver"), naming = require("./util/naming"), pkg = require("../package.json"), loadRules = require("./load-rules"); const debug = require("debug")("eslint:cli-engine"); -const resolver = new ModuleResolver(); const validFixTypes = new Set(["problem", "suggestion", "layout"]); //------------------------------------------------------------------------------ @@ -184,6 +183,13 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, repor configHelper.plugins.loadAll(config.plugins); } + if (config.parser) { + if (!path.isAbsolute(config.parser)) { + throw new Error(`Expected parser to be an absolute path but found ${config.parser}. This is a bug.`); + } + linter.defineParser(config.parser, require(config.parser)); + } + const loadedPlugins = configHelper.plugins.getAll(); for (const plugin in loadedPlugins) { @@ -460,7 +466,23 @@ class CLIEngine { }); } - this.config = new Config(this.options, this.linter); + this.config = new Config( + { + cwd: this.options.cwd, + baseConfig: this.options.baseConfig, + rules: this.options.rules, + ignore: this.options.ignore, + ignorePath: this.options.ignorePath, + parser: this.options.parser, + parserOptions: this.options.parserOptions, + useEslintrc: this.options.useEslintrc, + envs: this.options.envs, + globals: this.options.globals, + configFile: this.options.configFile, + plugins: this.options.plugins + }, + this.linter + ); if (this.options.cache) { const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd); @@ -761,16 +783,16 @@ class CLIEngine { let formatterPath; - // if there's a slash, then it's a file + // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages) if (!namespace && normalizedFormatName.indexOf("/") > -1) { formatterPath = path.resolve(cwd, normalizedFormatName); } else { try { const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); - formatterPath = resolver.resolve(npmFormat, `${cwd}/node_modules`); + formatterPath = relativeModuleResolver(npmFormat, path.join(cwd, "placeholder.js")); } catch (e) { - formatterPath = `./formatters/${normalizedFormatName}`; + formatterPath = path.resolve(__dirname, "formatters", normalizedFormatName); } } diff --git a/lib/config.js b/lib/config.js index 9c8438859ea5..161a2a3b1622 100644 --- a/lib/config.js +++ b/lib/config.js @@ -9,13 +9,15 @@ // Requirements //------------------------------------------------------------------------------ -const path = require("path"), +const fs = require("fs"), + path = require("path"), os = require("os"), ConfigOps = require("./config/config-ops"), ConfigFile = require("./config/config-file"), ConfigCache = require("./config/config-cache"), Plugins = require("./config/plugins"), - FileFinder = require("./util/file-finder"); + FileFinder = require("./util/file-finder"), + relativeModuleResolver = require("./util/relative-module-resolver"); const debug = require("debug")("eslint:config"); @@ -40,20 +42,6 @@ function hasRules(options) { return options.rules && Object.keys(options.rules).length > 0; } -/** - * Determines if a module is can be resolved. - * @param {string} moduleId The ID (name) of the module - * @returns {boolean} True if it is resolvable; False otherwise. - */ -function isResolvable(moduleId) { - try { - require.resolve(moduleId); - return true; - } catch (err) { - return false; - } -} - //------------------------------------------------------------------------------ // API //------------------------------------------------------------------------------ @@ -71,18 +59,30 @@ class Config { const options = providedOptions || {}; this.linterContext = linterContext; - this.plugins = new Plugins(linterContext.environments, linterContext.defineRule.bind(linterContext)); - this.options = options; + + /* + * `Linter#getRules` is slow, but a rule map is needed for every config file processed. + * Cache the rule map and keep it up to date. + */ + this.ruleMap = new Map(linterContext.getRules()); + this.plugins = new Plugins(linterContext.environments, { + defineRule: (ruleId, rule) => { + linterContext.defineRule(ruleId, rule); + this.ruleMap.set(ruleId, rule); + }, + pluginRootPath: this.options.cwd + }); this.ignore = options.ignore; this.ignorePath = options.ignorePath; this.parser = options.parser; this.parserOptions = options.parserOptions || {}; this.configCache = new ConfigCache(); + this._rootConfigReferencePath = path.join(this.options.cwd, "placeholder.js"); this.baseConfig = options.baseConfig - ? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this)) + ? ConfigFile.loadObject(options.baseConfig, this, this._rootConfigReferencePath, "(Provided base config)") : { rules: {} }; this.baseConfig.filePath = ""; this.baseConfig.baseDirectory = this.options.cwd; @@ -132,22 +132,24 @@ class Config { }); } + getPluginRootPath() { + return this.options.cwd; + } + /** - * Loads the config options from a config specified on the command line. + * Loads the config options from a config specified on the command line, relative to the CWD. * @param {string} [config] A shareable named config or path to a config file. * @returns {void} */ loadSpecificConfig(config) { if (config) { debug(`Using command line config ${config}`); - const isNamedConfig = - isResolvable(config) || - isResolvable(`eslint-config-${config}`) || - config.charAt(0) === "@"; + const isLocalPath = fs.existsSync(path.resolve(this.options.cwd, config)) && !config.startsWith("@"); this.specificConfig = ConfigFile.load( - isNamedConfig ? config : path.resolve(this.options.cwd, config), - this + isLocalPath ? path.resolve(this.options.cwd, config) : config, + this, + this._rootConfigReferencePath ); } } @@ -164,7 +166,7 @@ class Config { if (filename) { debug("Using personal config"); - config = ConfigFile.load(filename, this); + config = ConfigFile.load(filename, this, this._rootConfigReferencePath); } this.personalConfig = config || null; @@ -237,7 +239,7 @@ class Config { } debug(`Loading ${localConfigFile}`); - const localConfig = ConfigFile.load(localConfigFile, this); + const localConfig = ConfigFile.load(localConfigFile, this, this._rootConfigReferencePath); // Ignore empty config files if (!localConfig) { @@ -359,10 +361,9 @@ class Config { /* * Step 3: Override parser only if it is passed explicitly through the command line - * or if it's not defined yet (because the final object will at least have the parser key) */ - if (this.parser || !config.parser) { - config = ConfigOps.merge(config, { parser: this.parser }); + if (this.parser) { + config = ConfigOps.merge(config, { parser: relativeModuleResolver(this.parser, this._rootConfigReferencePath) }); } // Step 4: Apply environments to the config diff --git a/lib/config/config-file.js b/lib/config/config-file.js index dde79cb40c87..12330f06158d 100644 --- a/lib/config/config-file.js +++ b/lib/config/config-file.js @@ -13,9 +13,8 @@ const fs = require("fs"), path = require("path"), ConfigOps = require("./config-ops"), validator = require("./config-validator"), - ModuleResolver = require("../util/module-resolver"), + relativeModuleResolver = require("../util/relative-module-resolver"), naming = require("../util/naming"), - pathIsInside = require("path-is-inside"), stripComments = require("strip-json-comments"), stringify = require("json-stable-stringify-without-jsonify"), importFresh = require("import-fresh"); @@ -52,8 +51,6 @@ const CONFIG_FILES = [ "package.json" ]; -const resolver = new ModuleResolver(); - /** * Convenience wrapper for synchronously reading file contents. * @param {string} filePath The filename to read. @@ -319,45 +316,6 @@ function write(config, filePath) { } } -/** - * Determines the base directory for node packages referenced in a config file. - * This does not include node_modules in the path so it can be used for all - * references relative to a config file. - * @param {string} configFilePath The config file referencing the file. - * @returns {string} The base directory for the file path. - * @private - */ -function getBaseDir(configFilePath) { - - // calculates the path of the project including ESLint as dependency - const projectPath = path.resolve(__dirname, "../../../"); - - if (configFilePath && pathIsInside(configFilePath, projectPath)) { - - // be careful of https://github.com/substack/node-resolve/issues/78 - return path.join(path.resolve(configFilePath)); - } - - /* - * default to ESLint project path since it's unlikely that plugins will be - * in this directory - */ - return path.join(projectPath); -} - -/** - * Determines the lookup path, including node_modules, for package - * references relative to a config file. - * @param {string} configFilePath The config file referencing the file. - * @returns {string} The lookup path for the file path. - * @private - */ -function getLookupPath(configFilePath) { - const basedir = getBaseDir(configFilePath); - - return path.join(basedir, "node_modules"); -} - /** * Resolves a eslint core config path * @param {string} name The eslint config name. @@ -391,43 +349,20 @@ function getEslintCoreConfigPath(name) { * @param {Config} configContext Plugin context for the config instance * @param {string} filePath The file path from which the configuration information * was loaded. - * @param {string} [relativeTo] The path to resolve relative to. * @returns {Object} A new configuration object with all of the "extends" fields * loaded and merged. * @private */ -function applyExtends(config, configContext, filePath, relativeTo) { - let configExtends = config.extends; - - // normalize into an array for easier handling - if (!Array.isArray(config.extends)) { - configExtends = [config.extends]; - } +function applyExtends(config, configContext, filePath) { + const extendsList = Array.isArray(config.extends) ? config.extends : [config.extends]; // Make the last element in an array take the highest precedence - return configExtends.reduceRight((previousValue, parentPath) => { + return extendsList.reduceRight((previousValue, extendedConfigReference) => { try { - let extensionPath; - - if (parentPath.startsWith("eslint:")) { - extensionPath = getEslintCoreConfigPath(parentPath); - } else if (isFilePath(parentPath)) { - - /* - * If the `extends` path is relative, use the directory of the current configuration - * file as the reference point. Otherwise, use as-is. - */ - extensionPath = (path.isAbsolute(parentPath) - ? parentPath - : path.join(relativeTo || path.dirname(filePath), parentPath) - ); - } else { - extensionPath = parentPath; - } - debug(`Loading ${extensionPath}`); + debug(`Loading ${extendedConfigReference}`); // eslint-disable-next-line no-use-before-define - return ConfigOps.merge(load(extensionPath, configContext, relativeTo), previousValue); + return ConfigOps.merge(load(extendedConfigReference, configContext, filePath), previousValue); } catch (e) { /* @@ -446,114 +381,124 @@ function applyExtends(config, configContext, filePath, relativeTo) { /** * Resolves a configuration file path into the fully-formed path, whether filename * or package name. - * @param {string} filePath The filepath to resolve. - * @param {string} [relativeTo] The path to resolve relative to. + * @param {string} extendedConfigReference The config to extend, as it appears in a config file + * @param {string} referencedFromPath The path to the config file that contains `extendedConfigReference` + * @param {string} pluginRootPath The absolute path to the directory where plugins should be resolved from * @returns {Object} An object containing 3 properties: * - 'filePath' (required) the resolved path that can be used directly to load the configuration. - * - 'configName' the name of the configuration inside the plugin. + * - 'configName' the name of the configuration inside the plugin, if this is a plugin config. * - 'configFullName' (required) the name of the configuration as used in the eslint config(e.g. 'plugin:node/recommended'), - * or the absolute path to a config file. This should uniquely identify a config. + * or the absolute path to a config file. * @private */ -function resolve(filePath, relativeTo) { - if (isFilePath(filePath)) { - const fullPath = path.resolve(relativeTo || "", filePath); - - return { filePath: fullPath, configFullName: fullPath }; +function resolve(extendedConfigReference, referencedFromPath, pluginRootPath) { + if (extendedConfigReference.startsWith("eslint:")) { + return { + filePath: getEslintCoreConfigPath(extendedConfigReference), + configFullName: extendedConfigReference + }; } - let normalizedPackageName; - - if (filePath.startsWith("plugin:")) { - const configFullName = filePath; - const pluginName = filePath.slice(7, filePath.lastIndexOf("/")); - const configName = filePath.slice(filePath.lastIndexOf("/") + 1); - normalizedPackageName = naming.normalizePackageName(pluginName, "eslint-plugin"); - debug(`Attempting to resolve ${normalizedPackageName}`); + if (extendedConfigReference.startsWith("plugin:")) { + const configFullName = extendedConfigReference; + const pluginName = extendedConfigReference.slice(7, extendedConfigReference.lastIndexOf("/")); + const configName = extendedConfigReference.slice(extendedConfigReference.lastIndexOf("/") + 1); + const normalizedPluginName = naming.normalizePackageName(pluginName, "eslint-plugin"); return { - filePath: require.resolve(normalizedPackageName), + filePath: relativeModuleResolver(normalizedPluginName, path.join(pluginRootPath, "placeholder.js")), configName, configFullName }; } - normalizedPackageName = naming.normalizePackageName(filePath, "eslint-config"); - debug(`Attempting to resolve ${normalizedPackageName}`); - return { - filePath: resolver.resolve(normalizedPackageName, getLookupPath(relativeTo)), - configFullName: filePath - }; + if (isFilePath(extendedConfigReference)) { + const resolvedConfigPath = path.resolve(path.dirname(referencedFromPath), extendedConfigReference); + + return { + filePath: resolvedConfigPath, + configFullName: resolvedConfigPath + }; + } + const normalizedConfigName = naming.normalizePackageName(extendedConfigReference, "eslint-config"); + return { + filePath: relativeModuleResolver(normalizedConfigName, referencedFromPath), + configFullName: extendedConfigReference + }; } + /** - * Loads a configuration file from the given file path. - * @param {Object} resolvedPath The value from calling resolve() on a filename or package name. - * @param {Config} configContext Plugins context - * @returns {Object} The configuration information. + * Loads a config object, applying extends/parser/plugins if present. + * @param {Object} configObject a config object to load + * @param {Config} configContext Context for the config instance + * @param {string} configPath The path where `extends` and parsers should be resolved from (usually the path where the config was located from) + * @param {string} configDisplayName The display name of the config, for use in error messages + * @returns {Object} the config object with extends applied if present, or the passed config if not + * @private */ -function loadFromDisk(resolvedPath, configContext) { - const dirname = path.dirname(resolvedPath.filePath), - lookupPath = getLookupPath(dirname); - let config = loadConfigFile(resolvedPath); +function loadObject(configObject, configContext, configPath, configDisplayName) { + const config = Object.assign({}, configObject); - if (config) { + // ensure plugins are properly loaded first + if (config.plugins) { + configContext.plugins.loadAll(config.plugins); + } - // ensure plugins are properly loaded first - if (config.plugins) { - configContext.plugins.loadAll(config.plugins); - } + // include full path of parser if present + if (config.parser) { + config.parser = relativeModuleResolver(config.parser, configPath); - // include full path of parser if present - if (config.parser) { - if (isFilePath(config.parser)) { - config.parser = path.resolve(dirname || "", config.parser); - } else { - config.parser = resolver.resolve(config.parser, lookupPath); - } - } + // FIXME: add exception for "espree", add appropriate error message + } - const ruleMap = configContext.linterContext.getRules(); + const ruleMap = configContext.ruleMap; - // validate the configuration before continuing - validator.validate(config, resolvedPath.configFullName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments); + // validate the configuration before continuing + validator.validate(config, configDisplayName, ruleMap.get.bind(ruleMap), configContext.linterContext.environments); - /* - * If an `extends` property is defined, it represents a configuration file to use as - * a "parent". Load the referenced file and merge the configuration recursively. - */ - if (config.extends) { - config = applyExtends(config, configContext, resolvedPath.filePath, dirname); - } + /* + * If an `extends` property is defined, it represents a configuration file to use as + * a "parent". Load the referenced file and merge the configuration recursively. + */ + if (config.extends) { + return applyExtends(config, configContext, configPath); } return config; } /** - * Loads a config object, applying extends if present. - * @param {Object} configObject a config object to load - * @param {Config} configContext Context for the config instance - * @returns {Object} the config object with extends applied if present, or the passed config if not - * @private + * Loads a configuration file from the given file path. + * @param {Object} configInfo The value from calling resolve() on a filename or package name. + * @param {Config} configContext Config context + * @returns {Object} The configuration information. */ -function loadObject(configObject, configContext) { - return configObject.extends ? applyExtends(configObject, configContext, "") : configObject; +function loadFromDisk(configInfo, configContext) { + const config = loadConfigFile(configInfo); + + // loadConfigFile will return null for a `package.json` file that does not have an `eslintConfig` property. + if (config) { + return loadObject(config, configContext, configInfo.filePath, configInfo.configFullName); + } + + return null; } /** * Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet * cached. - * @param {string} filePath the path to the config file + * @param {string} extendedConfigReference The config to extend, as it appears in a config file * @param {Config} configContext Context for the config instance - * @param {string} [relativeTo] The path to resolve relative to. + * @param {string} referencedFromPath The path to the config file that contains `extendedConfigReference` (or where + * the `extendedConfigReference` should be resolved from, if it doesn't appear in a config file) * @returns {Object} the parsed config object (empty object if there was a parse error) * @private */ -function load(filePath, configContext, relativeTo) { - const resolvedPath = resolve(filePath, relativeTo); +function load(extendedConfigReference, configContext, referencedFromPath) { + const resolvedPath = resolve(extendedConfigReference, referencedFromPath, configContext.getPluginRootPath()); const cachedConfig = configContext.configCache.getConfig(resolvedPath.configFullName); @@ -565,6 +510,8 @@ function load(filePath, configContext, relativeTo) { if (config) { config.filePath = resolvedPath.filePath; + + // FIXME: remove baseDirectory property config.baseDirectory = path.dirname(resolvedPath.filePath); configContext.configCache.setConfig(resolvedPath.configFullName, config); } @@ -594,9 +541,6 @@ function isExistingFile(filename) { //------------------------------------------------------------------------------ module.exports = { - - getBaseDir, - getLookupPath, load, loadObject, resolve, diff --git a/lib/config/config-initializer.js b/lib/config/config-initializer.js index a6791ba85307..2aae40b7be79 100644 --- a/lib/config/config-initializer.js +++ b/lib/config/config-initializer.js @@ -17,7 +17,6 @@ const util = require("util"), ConfigFile = require("./config-file"), ConfigOps = require("./config-ops"), getSourceCodeOfFiles = require("../util/source-code-utils").getSourceCodeOfFiles, - ModuleResolver = require("../util/module-resolver"), npmUtils = require("../util/npm-utils"), recConfig = require("../../conf/eslint-recommended"), log = require("../util/logging"); @@ -303,19 +302,11 @@ function getConfigForStyleGuide(guide) { } /** - * Get the version of the local ESLint. - * @returns {string|null} The version. If the local ESLint was not found, returns null. + * Get the version ESLint currently being used. + * @returns {string|null} The version. If ESLint was not found, returns null. */ -function getLocalESLintVersion() { - try { - const resolver = new ModuleResolver(); - const eslintPath = resolver.resolve("eslint", process.cwd()); - const eslint = require(eslintPath); - - return eslint.linter.version || null; - } catch (_err) { - return null; - } +function getESLintVersion() { + return require("../linter").version; } /** @@ -331,18 +322,17 @@ function getStyleGuideName(answers) { } /** - * Check whether the local ESLint version conflicts with the required version of the chosen shareable config. + * Check whether the ESLint version conflicts with the required version of the chosen shareable config. * @param {Object} answers The answers object. - * @returns {boolean} `true` if the local ESLint is found then it conflicts with the required version of the chosen shareable config. + * @returns {boolean} `true` if the ESLint is found then it conflicts with the required version of the chosen shareable config. */ function hasESLintVersionConflict(answers) { - // Get the local ESLint version. - const localESLintVersion = getLocalESLintVersion(); - - if (!localESLintVersion) { - return false; - } + /* + * Get the version of ESLint currently being used. This is intentionally called via + * `module.exports` to make it easier to stub a different ESLint version in tests. + */ + const eslintVersion = module.exports.getESLintVersion(); // Get the required range of ESLint version. const configName = getStyleGuideName(answers); @@ -354,11 +344,11 @@ function hasESLintVersionConflict(answers) { return false; } - answers.localESLintVersion = localESLintVersion; + answers.eslintVersion = eslintVersion; answers.requiredESLintVersionRange = requiredESLintVersionRange; // Check the version. - if (semver.satisfies(localESLintVersion, requiredESLintVersionRange)) { + if (semver.satisfies(eslintVersion, requiredESLintVersionRange)) { answers.installESLint = false; return false; } @@ -479,11 +469,11 @@ function promptUser() { type: "confirm", name: "installESLint", message(answers) { - const verb = semver.ltr(answers.localESLintVersion, answers.requiredESLintVersionRange) + const verb = semver.ltr(answers.eslintVersion, answers.requiredESLintVersionRange) ? "upgrade" : "downgrade"; - return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.localESLintVersion}.\n Do you want to ${verb}?`; + return `The style guide "${answers.styleguide}" requires eslint@${answers.requiredESLintVersionRange}. You are currently using eslint@${answers.eslintVersion}.\n Do you want to ${verb}?`; }, default: true, when(answers) { @@ -498,7 +488,7 @@ function promptUser() { log.info("A package.json is necessary to install plugins such as style guides. Run `npm init` to create a package.json file and try again."); return void 0; } - if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.localESLintVersion, earlyAnswers.requiredESLintVersionRange)) { + if (earlyAnswers.installESLint === false && !semver.satisfies(earlyAnswers.eslintVersion, earlyAnswers.requiredESLintVersionRange)) { log.info(`Note: it might not work since ESLint's version is mismatched with the ${earlyAnswers.styleguide} config.`); } if (earlyAnswers.styleguide === "airbnb" && !earlyAnswers.airbnbReact) { @@ -633,9 +623,10 @@ function promptUser() { // Public Interface //------------------------------------------------------------------------------ -const init = { +module.exports = { getConfigForStyleGuide, getModulesList, + getESLintVersion, hasESLintVersionConflict, installModules, processAnswers, @@ -643,5 +634,3 @@ const init = { return promptUser(); } }; - -module.exports = init; diff --git a/lib/config/plugins.js b/lib/config/plugins.js index f32cc26c33f8..751587e961c5 100644 --- a/lib/config/plugins.js +++ b/lib/config/plugins.js @@ -8,9 +8,10 @@ // Requirements //------------------------------------------------------------------------------ +const path = require("path"); const debug = require("debug")("eslint:plugins"); const naming = require("../util/naming"); -const path = require("path"); +const relativeModuleResolver = require("../util/relative-module-resolver"); //------------------------------------------------------------------------------ // Public Interface @@ -24,12 +25,14 @@ class Plugins { /** * Creates the plugins context * @param {Environments} envContext - env context - * @param {function(string, Rule): void} defineRule - Callback for when a plugin is defined which introduces rules + * @param {function(string, Rule): void} options.defineRule - Callback for when a plugin is defined which introduces rules + * @param {string} options.pluginRootPath The path from which all plugins should be resolved */ - constructor(envContext, defineRule) { + constructor(envContext, { defineRule, pluginRootPath }) { this._plugins = Object.create(null); this._environments = envContext; this._defineRule = defineRule; + this._pluginRootPath = pluginRootPath; } /** @@ -82,7 +85,6 @@ class Plugins { load(pluginName) { const longName = naming.normalizePackageName(pluginName, "eslint-plugin"); const shortName = naming.getShorthandName(longName, "eslint-plugin"); - let plugin = null; if (pluginName.match(/\s+/)) { const whitespaceError = new Error(`Whitespace found in plugin name '${pluginName}'`); @@ -95,38 +97,31 @@ class Plugins { } if (!this._plugins[shortName]) { - try { - plugin = require(longName); - } catch (pluginLoadErr) { - try { + const resolveRelativeToPath = path.join(this._pluginRootPath, "placeholder.js"); + let pluginPath; - // Check whether the plugin exists - require.resolve(longName); - } catch (missingPluginErr) { - - // If the plugin can't be resolved, display the missing plugin error (usually a config or install error) - debug(`Failed to load plugin ${longName}.`); - missingPluginErr.message = `Failed to load plugin ${pluginName}: ${missingPluginErr.message}`; - missingPluginErr.messageTemplate = "plugin-missing"; - missingPluginErr.messageData = { - pluginName: longName, - eslintPath: path.resolve(__dirname, "../..") - }; - throw missingPluginErr; - } - - // Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace. - throw pluginLoadErr; + try { + pluginPath = relativeModuleResolver(longName, resolveRelativeToPath); + } catch (missingPluginErr) { + + // If the plugin can't be resolved, display the missing plugin error (usually a config or install error) + debug(`Failed to load plugin ${longName} from ${this._pluginRootPath}.`); + missingPluginErr.message = `Failed to load plugin ${pluginName} from ${this._pluginRootPath}: ${missingPluginErr.message}`; + missingPluginErr.messageTemplate = "plugin-missing"; + missingPluginErr.messageData = { + pluginName: longName + }; + throw missingPluginErr; } + const plugin = require(pluginPath); + // This step is costly, so skip if debug is disabled if (debug.enabled) { - const resolvedPath = require.resolve(longName); - let version = null; try { - version = require(`${longName}/package.json`).version; + version = require(relativeModuleResolver(`${longName}/package.json`, this._pluginRootPath)).version; } catch (e) { // Do nothing @@ -136,7 +131,7 @@ class Plugins { ? `${longName}@${version}` : `${longName}, version unknown`; - debug(`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`); + debug(`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${pluginPath})`); } this.define(pluginName, plugin); diff --git a/lib/linter.js b/lib/linter.js index 29505e9ac022..27e4d9a427fa 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -11,6 +11,7 @@ const eslintScope = require("eslint-scope"), evk = require("eslint-visitor-keys"), + espree = require("espree"), lodash = require("lodash"), CodePathAnalyzer = require("./code-path-analysis/code-path-analyzer"), ConfigOps = require("./config/config-ops"), @@ -444,24 +445,22 @@ function parse(text, providedParserOptions, parserName, parserMap, filePath) { filePath }); - let parser; - - try { - parser = parserMap.get(parserName) || require(parserName); - } catch (ex) { + if (!parserMap.has(parserName)) { return { success: false, error: { ruleId: null, fatal: true, severity: 2, - message: ex.message, + message: `Configured parser '${parserName}' was not found.`, line: 0, column: 0 } }; } + const parser = parserMap.get(parserName); + /* * Check for parsing errors first. If there's a parsing error, nothing * else can happen. However, a parsing error does not throw an error @@ -776,6 +775,8 @@ module.exports = class Linter { ruleMaps.set(this, new Rules()); this.version = pkg.version; this.environments = new Environments(); + + this.defineParser("espree", espree); } /** diff --git a/lib/testers/rule-tester.js b/lib/testers/rule-tester.js index 6d1bba989fde..5923d4df4948 100644 --- a/lib/testers/rule-tester.js +++ b/lib/testers/rule-tester.js @@ -42,6 +42,7 @@ const lodash = require("lodash"), assert = require("assert"), + path = require("path"), util = require("util"), validator = require("../config/config-validator"), ajv = require("../util/ajv"), @@ -365,6 +366,11 @@ class RuleTester { } })); + if (typeof config.parser === "string") { + assert(path.isAbsolute(config.parser), "Parsers provided as strings to RuleTester must be absolute paths"); + linter.defineParser(config.parser, require(config.parser)); + } + if (schema) { ajv.validateSchema(schema); diff --git a/lib/util/module-resolver.js b/lib/util/module-resolver.js deleted file mode 100644 index 1e9b663304f8..000000000000 --- a/lib/util/module-resolver.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @fileoverview Implements the Node.js require.resolve algorithm - * @author Nicholas C. Zakas - */ - -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const Module = require("module"); - -//------------------------------------------------------------------------------ -// Private -//------------------------------------------------------------------------------ - -const DEFAULT_OPTIONS = { - - /* - * module.paths is an array of paths to search for resolving things relative - * to this file. Module.globalPaths contains all of the special Node.js - * directories that can also be searched for modules. - * - * Need to check for existence of module.paths because Jest seems not to - * include it. See https://github.com/eslint/eslint/issues/5791. - */ - lookupPaths: module.paths ? module.paths.concat(Module.globalPaths) : Module.globalPaths.concat() -}; - -/** - * Resolves modules based on a set of options. - */ -class ModuleResolver { - - /** - * Resolves modules based on a set of options. - * @param {Object} options The options for resolving modules. - * @param {string[]} options.lookupPaths An array of paths to include in the - * lookup with the highest priority paths coming first. - */ - constructor(options) { - this.options = Object.assign({}, DEFAULT_OPTIONS, options || {}); - } - - /** - * Resolves the file location of a given module relative to the configured - * lookup paths. - * @param {string} name The module name to resolve. - * @param {string} extraLookupPath An extra path to look into for the module. - * This path is used with the highest priority. - * @returns {string} The resolved file path for the module. - * @throws {Error} If the module cannot be resolved. - */ - resolve(name, extraLookupPath) { - - /* - * First, clone the lookup paths so we're not messing things up for - * subsequent calls to this function. Then, move the extraLookupPath to the - * top of the lookup paths list so it will be searched first. - */ - const lookupPaths = [extraLookupPath, ...this.options.lookupPaths]; - - /** - * Module._findPath is an internal method to Node.js, then one they use to - * lookup file paths when require() is called. So, we are hooking into the - * exact same logic that Node.js uses. - */ - const result = Module._findPath(name, lookupPaths); // eslint-disable-line no-underscore-dangle - - if (!result) { - throw new Error(`Cannot find module '${name}'`); - } - - return result; - } -} - -//------------------------------------------------------------------------------ -// Public API -//------------------------------------------------------------------------------ - -module.exports = ModuleResolver; diff --git a/lib/util/relative-module-resolver.js b/lib/util/relative-module-resolver.js new file mode 100644 index 000000000000..9e86909c5ded --- /dev/null +++ b/lib/util/relative-module-resolver.js @@ -0,0 +1,30 @@ +/** + * Utility for resolving a module relative to another module + * @author Teddy Katz + */ + +"use strict"; + +const Module = require("module"); +const path = require("path"); + +// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) +const createRequireFromPath = Module.createRequireFromPath || (filename => { + const mod = new Module(filename, null); + + mod.filename = filename; + mod.paths = Module._nodeModulePaths(path.dirname(filename)); // eslint-disable-line no-underscore-dangle + mod._compile("module.exports = require;", filename); // eslint-disable-line no-underscore-dangle + return mod.exports; +}); + +/** + * Resolves a Node module relative to another module + * @param {string} moduleName The name of a Node module, or a path to a Node module. + * + * @param {string} relativeToPath An absolute path indicating the module that `moduleName` should be resolved relative to. This must be + * a file rather than a directory, but the file need not actually exist. + * @returns {string} The absolute path that would result from calling `require.resolve(moduleName)` in a file located at `relativeToPath` + */ +module.exports = (moduleName, relativeToPath) => + createRequireFromPath(relativeToPath).resolve(moduleName); diff --git a/messages/plugin-missing.txt b/messages/plugin-missing.txt index 766020b2b8f6..a71bdf108b9e 100644 --- a/messages/plugin-missing.txt +++ b/messages/plugin-missing.txt @@ -1,11 +1,7 @@ -ESLint couldn't find the plugin "<%- pluginName %>". This can happen for a couple different reasons: +ESLint couldn't find the plugin "<%- pluginName %>". -1. If ESLint is installed globally, then make sure <%- pluginName %> is also installed globally. A globally-installed ESLint cannot find a locally-installed plugin. - -2. If ESLint is installed locally, then it's likely that the plugin isn't installed correctly. Try reinstalling by running the following: +It's likely that the plugin isn't installed correctly. Try reinstalling by running the following: npm i <%- pluginName %>@latest --save-dev -Path to ESLint package: <%- eslintPath %> - If you still can't figure out the problem, please stop by https://gitter.im/eslint/eslint to chat with the team. diff --git a/package.json b/package.json index c3ad0008a189..389c965b80f6 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", "progress": "^2.0.0", "regexpp": "^2.0.1", "semver": "^5.5.1", diff --git a/tests/fixtures/config-file/extends-chain-2/parser.js b/tests/fixtures/config-file/extends-chain-2/parser.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-file/node_modules/eslint-config-enable-browser-env.js b/tests/fixtures/config-file/node_modules/eslint-config-enable-browser-env.js new file mode 100644 index 000000000000..536f601c8fa1 --- /dev/null +++ b/tests/fixtures/config-file/node_modules/eslint-config-enable-browser-env.js @@ -0,0 +1 @@ +module.exports = { env: { browser: true } }; diff --git a/tests/fixtures/config-file/node_modules/eslint-config-recursive-dependent/index.js b/tests/fixtures/config-file/node_modules/eslint-config-recursive-dependent/index.js new file mode 100644 index 000000000000..3c73f2e4e1f7 --- /dev/null +++ b/tests/fixtures/config-file/node_modules/eslint-config-recursive-dependent/index.js @@ -0,0 +1,6 @@ +module.exports = { + env: { + browser: true + }, + extends: 'recursive-dependency' +}; diff --git a/tests/fixtures/config-file/node_modules/eslint-config-recursive-dependent/node_modules/eslint-config-recursive-dependency.js b/tests/fixtures/config-file/node_modules/eslint-config-recursive-dependent/node_modules/eslint-config-recursive-dependency.js new file mode 100644 index 000000000000..403945a31703 --- /dev/null +++ b/tests/fixtures/config-file/node_modules/eslint-config-recursive-dependent/node_modules/eslint-config-recursive-dependency.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + bar: 2 + } +}; diff --git a/tests/fixtures/config-file/node_modules/eslint-plugin-enable-nonexistent-parser.js b/tests/fixtures/config-file/node_modules/eslint-plugin-enable-nonexistent-parser.js new file mode 100644 index 000000000000..e09b347f1703 --- /dev/null +++ b/tests/fixtures/config-file/node_modules/eslint-plugin-enable-nonexistent-parser.js @@ -0,0 +1,5 @@ +exports.configs = { + bar: { + parser: "nonexistent-parser" + } +}; diff --git a/tests/fixtures/config-file/node_modules/eslint-plugin-foo.js b/tests/fixtures/config-file/node_modules/eslint-plugin-foo.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-file/node_modules/eslint-plugin-with-environment.js b/tests/fixtures/config-file/node_modules/eslint-plugin-with-environment.js new file mode 100644 index 000000000000..2b7d695d5f84 --- /dev/null +++ b/tests/fixtures/config-file/node_modules/eslint-plugin-with-environment.js @@ -0,0 +1,5 @@ +module.exports = { + environments: { + bar: { globals: { bar: true } } + } +}; diff --git a/tests/fixtures/config-file/node_modules/eslint-plugin-with-two-configs.js b/tests/fixtures/config-file/node_modules/eslint-plugin-with-two-configs.js new file mode 100644 index 000000000000..016a35c85247 --- /dev/null +++ b/tests/fixtures/config-file/node_modules/eslint-plugin-with-two-configs.js @@ -0,0 +1,6 @@ +module.exports = { + configs: { + foo: { rules: { semi: 2, quotes: 1 } }, + bar: { rules: { quotes: 2, yoda: 2 } } + } +}; diff --git a/tests/fixtures/config-file/plugins/.eslintrc.yml b/tests/fixtures/config-file/plugins/.eslintrc.yml index 361805225676..18addbd49128 100644 --- a/tests/fixtures/config-file/plugins/.eslintrc.yml +++ b/tests/fixtures/config-file/plugins/.eslintrc.yml @@ -1,8 +1,8 @@ plugins: - - test + - with-environment rules: - test/foo: 2 + with-environment/foo: 2 env: - test/bar: true + with-environment/bar: true diff --git a/tests/fixtures/config-file/plugins/.eslintrc2.yml b/tests/fixtures/config-file/plugins/.eslintrc2.yml index 973d77096e54..dc46e9312954 100644 --- a/tests/fixtures/config-file/plugins/.eslintrc2.yml +++ b/tests/fixtures/config-file/plugins/.eslintrc2.yml @@ -1,3 +1,3 @@ extends: - - plugin:test/foo - - plugin:test/bar + - plugin:with-two-configs/foo + - plugin:with-two-configs/bar diff --git a/tests/fixtures/config-file/subdir/node_modules/@foo/eslint-config-bar.js b/tests/fixtures/config-file/subdir/node_modules/@foo/eslint-config-bar.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-file/subdir/node_modules/@foo/eslint-config.js b/tests/fixtures/config-file/subdir/node_modules/@foo/eslint-config.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-file/subdir/node_modules/eslint-config-eslint-configfoo.js b/tests/fixtures/config-file/subdir/node_modules/eslint-config-eslint-configfoo.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-file/subdir/node_modules/eslint-config-foo.js b/tests/fixtures/config-file/subdir/node_modules/eslint-config-foo.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-file/subdir/node_modules/eslint-plugin-foo.js b/tests/fixtures/config-file/subdir/node_modules/eslint-plugin-foo.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-another-plugin.js b/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-another-plugin.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-example-with-rules-config.js b/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-example-with-rules-config.js new file mode 100644 index 000000000000..b8b846e1e978 --- /dev/null +++ b/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-example-with-rules-config.js @@ -0,0 +1,6 @@ +module.exports = { + rules: { "example-rule": require("../../../rules/custom-rule") }, + + // rulesConfig support removed in 2.0.0, so this should have no effect + rulesConfig: { "example-rule": 1 } +}; diff --git a/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-example.js b/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-example.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-test.js b/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-test.js new file mode 100644 index 000000000000..39fac086a9a1 --- /dev/null +++ b/tests/fixtures/config-hierarchy/plugins/node_modules/eslint-plugin-test.js @@ -0,0 +1,3 @@ +module.exports = { + environments: { example: { globals: { test: false } } } +}; diff --git a/tests/fixtures/parsers/enhanced-parser3.js b/tests/fixtures/parsers/enhanced-parser3.js index bc08e3da47e3..388b80020586 100644 --- a/tests/fixtures/parsers/enhanced-parser3.js +++ b/tests/fixtures/parsers/enhanced-parser3.js @@ -42,6 +42,8 @@ function analyzeScope(ast) { referencer.visit(ast); + scopeManager.isCustomScopeManager = true; + return scopeManager; } diff --git a/tests/fixtures/parsers/linter-test-parsers.js b/tests/fixtures/parsers/linter-test-parsers.js new file mode 100644 index 000000000000..dc81e34ecb9e --- /dev/null +++ b/tests/fixtures/parsers/linter-test-parsers.js @@ -0,0 +1,14 @@ +// Accumulates all of the parsers used by `tests/lib/linter.js`. +// This file will get bundled so that those tests can be run in a browser. + +module.exports = { + enhancedParser: require("./enhanced-parser"), + stubParser: require("./stub-parser"), + unknownLogicalOperator: require("./unknown-operators/unknown-logical-operator"), + unknownLogicalOperatorNested: require("./unknown-operators/unknown-logical-operator-nested"), + lineError: require("./line-error"), + noLineError: require("./no-line-error"), + enhancedParser2: require("./enhanced-parser2"), + enhancedParser3: require("./enhanced-parser3"), + throwsWithOptions: require("./throws-with-options") +}; diff --git a/tests/fixtures/plugins/node_modules/@scope/eslint-plugin-example.js b/tests/fixtures/plugins/node_modules/@scope/eslint-plugin-example.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-example.js b/tests/fixtures/plugins/node_modules/eslint-plugin-example.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-example1.js b/tests/fixtures/plugins/node_modules/eslint-plugin-example1.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-example2.js b/tests/fixtures/plugins/node_modules/eslint-plugin-example2.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/fixtures/plugins/node_modules/eslint-plugin-throws-on-load.js b/tests/fixtures/plugins/node_modules/eslint-plugin-throws-on-load.js new file mode 100644 index 000000000000..1e4791d3a703 --- /dev/null +++ b/tests/fixtures/plugins/node_modules/eslint-plugin-throws-on-load.js @@ -0,0 +1 @@ +throw new Error('error thrown while loading this module'); diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js index fd961cb63537..d67c3c373644 100644 --- a/tests/lib/cli-engine.js +++ b/tests/lib/cli-engine.js @@ -822,18 +822,14 @@ describe("CLIEngine", () => { assert.strictEqual(report.results[0].messages.length, 0); }); - it("should report one fatal message when given a config file and a valid file and invalid parser", () => { + it("should throw an error when given a config file and a valid file and invalid parser", () => { engine = new CLIEngine({ parser: "test11", useEslintrc: false }); - const report = engine.executeOnFiles(["lib/cli.js"]); - - assert.lengthOf(report.results, 1); - assert.lengthOf(report.results[0].messages, 1); - assert.isTrue(report.results[0].messages[0].fatal); + assert.throws(() => engine.executeOnFiles(["lib/cli.js"]), "Cannot find module 'test11'"); }); it("should report zero messages when given a directory with a .js2 file", () => { @@ -3019,19 +3015,21 @@ describe("CLIEngine", () => { it("should return null when a customer formatter doesn't exist", () => { const engine = new CLIEngine(), - formatterPath = getFixturePath("formatters", "doesntexist.js"); + formatterPath = getFixturePath("formatters", "doesntexist.js"), + fullFormatterPath = path.resolve(formatterPath); assert.throws(() => { engine.getFormatter(formatterPath); - }, `There was a problem loading formatter: ${formatterPath}\nError: Cannot find module '${formatterPath}'`); + }, `There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`); }); it("should return null when a built-in formatter doesn't exist", () => { const engine = new CLIEngine(); + const fullFormatterPath = path.resolve(__dirname, "..", "..", "lib", "formatters", "special"); assert.throws(() => { engine.getFormatter("special"); - }, "There was a problem loading formatter: ./formatters/special\nError: Cannot find module './formatters/special'"); + }, `There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`); }); it("should throw if the required formatter exists but has an error", () => { diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 50d80c38472a..b121f0b9632f 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -589,11 +589,10 @@ describe("cli", () => { }); describe("when given an parser name", () => { - it("should exit with error if parser is invalid", () => { + it("should exit with a fatal error if parser is invalid", () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser test111 ${filePath}`); - assert.strictEqual(exit, 1); + assert.throws(() => cli.execute(`--no-ignore --parser test111 ${filePath}`), "Cannot find module 'test111'"); }); it("should exit with no error if parser is valid", () => { diff --git a/tests/lib/config.js b/tests/lib/config.js index ba107e45ce3f..cf4d409e5545 100644 --- a/tests/lib/config.js +++ b/tests/lib/config.js @@ -22,37 +22,10 @@ const DIRECTORY_CONFIG_HIERARCHY = require("../fixtures/config-hierarchy/file-st const linter = new Linter(); -require("shelljs/global"); +const { mkdir, rm, cp } = require("shelljs"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); -/* global mkdir, rm, cp */ - - -/** - * Creates a stubbed Config object that will bypass normal require() to load - * plugins by name from the objects specified. - * @param {Object} plugins The keys are the package names, values are plugin objects. - * @returns {Config} The stubbed instance of Config. - * @private - */ -function createStubbedConfigWithPlugins(plugins) { - - // stub out plugins - const StubbedPlugins = proxyquire("../../lib/config/plugins", plugins); - - // stub out config file to use stubbed plugins - const StubbedConfigFile = proxyquire("../../lib/config/config-file", { - "./plugins": StubbedPlugins - }); - - // stub out Config to use stub config file - return proxyquire("../../lib/config", { - "./config/config-file": StubbedConfigFile, - "./config/plugins": StubbedPlugins - }); -} - /** * Asserts that two configs are equal. This is necessary because assert.deepStrictEqual() * gets confused when properties are in different orders. @@ -136,6 +109,7 @@ describe("Config", () => { fixtureDir = `${os.tmpdir()}/eslint/fixtures`; mkdir("-p", fixtureDir); cp("-r", "./tests/fixtures/config-hierarchy", fixtureDir); + cp("-r", "./tests/fixtures/rules", fixtureDir); }); beforeEach(() => { @@ -154,11 +128,11 @@ describe("Config", () => { // https://github.com/eslint/eslint/issues/2380 it("should not modify baseConfig when format is specified", () => { - const customBaseConfig = { foo: "bar" }, - configHelper = new Config({ baseConfig: customBaseConfig, format: "foo" }, linter); + const customBaseConfig = {}, + configHelper = new Config({ baseConfig: customBaseConfig, cwd: process.cwd(), format: "foo" }, linter); // at one point, customBaseConfig.format would end up equal to "foo"...that's bad - assert.deepStrictEqual(customBaseConfig, { foo: "bar" }); + assert.deepStrictEqual(customBaseConfig, {}); assert.strictEqual(configHelper.options.format, "foo"); }); @@ -166,7 +140,7 @@ describe("Config", () => { const customBaseConfig = { extends: path.resolve(__dirname, "..", "fixtures", "config-extends", "array", ".eslintrc") }; - const configHelper = new Config({ baseConfig: customBaseConfig }, linter); + const configHelper = new Config({ baseConfig: customBaseConfig, cwd: process.cwd() }, linter); assert.deepStrictEqual(configHelper.baseConfig.env, { browser: false, @@ -207,7 +181,7 @@ describe("Config", () => { }); it("should return the path when an .eslintrc file is found", () => { - const configHelper = new Config({}, linter), + const configHelper = new Config({ cwd: process.cwd() }, linter), expected = getFakeFixturePath("broken", ".eslintrc"), actual = Array.from( configHelper.findLocalConfigFiles(getFakeFixturePath("broken")) @@ -217,7 +191,7 @@ describe("Config", () => { }); it("should return an empty array when an .eslintrc file is not found", () => { - const configHelper = new Config({}, linter), + const configHelper = new Config({ cwd: process.cwd() }, linter), actual = Array.from( configHelper.findLocalConfigFiles(getFakeFixturePath()) ); @@ -227,7 +201,7 @@ describe("Config", () => { }); it("should return package.json only when no other config files are found", () => { - const configHelper = new Config({}, linter), + const configHelper = new Config({ cwd: process.cwd() }, linter), expected0 = getFakeFixturePath("packagejson", "subdir", "package.json"), expected1 = getFakeFixturePath("packagejson", ".eslintrc"), actual = Array.from( @@ -239,7 +213,7 @@ describe("Config", () => { }); it("should return the only one config file even if there are multiple found", () => { - const configHelper = new Config({}, linter), + const configHelper = new Config({ cwd: process.cwd() }, linter), expected = getFakeFixturePath("broken", ".eslintrc"), // The first element of the array is the .eslintrc in the same directory. @@ -252,7 +226,7 @@ describe("Config", () => { }); it("should return all possible files when multiple are found", () => { - const configHelper = new Config({}, linter), + const configHelper = new Config({ cwd: process.cwd() }, linter), expected = [ getFakeFixturePath("fileexts/subdir/subsubdir/", ".eslintrc.json"), getFakeFixturePath("fileexts/subdir/", ".eslintrc.yml"), @@ -268,7 +242,7 @@ describe("Config", () => { }); it("should return an empty array when a package.json file is not found", () => { - const configHelper = new Config({}, linter), + const configHelper = new Config({ cwd: process.cwd() }, linter), actual = Array.from(configHelper.findLocalConfigFiles(getFakeFixturePath())); assert.isArray(actual); @@ -357,7 +331,7 @@ describe("Config", () => { // make sure JS-style comments don't throw an error it("should load the config file when there are JS-style comments in the text", () => { const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "comments.json"), - configHelper = new Config({ configFile: configPath }, linter), + configHelper = new Config({ configFile: configPath, cwd: process.cwd() }, linter), semi = configHelper.specificConfig.rules.semi, strict = configHelper.specificConfig.rules.strict; @@ -368,7 +342,7 @@ describe("Config", () => { // make sure YAML files work correctly it("should load the config file when a YAML file is used", () => { const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "env-browser.yaml"), - configHelper = new Config({ configFile: configPath }, linter), + configHelper = new Config({ configFile: configPath, cwd: process.cwd() }, linter), noAlert = configHelper.specificConfig.rules["no-alert"], noUndef = configHelper.specificConfig.rules["no-undef"]; @@ -407,7 +381,7 @@ describe("Config", () => { // Default configuration - blank it("should return a blank config when using no .eslintrc", () => { - const configHelper = new Config({ useEslintrc: false }, linter), + const configHelper = new Config({ useEslintrc: false, cwd: process.cwd() }, linter), file = getFixturePath("broken", "console-wrong-quotes.js"), expected = { rules: {}, @@ -420,7 +394,7 @@ describe("Config", () => { }); it("should return a blank config when baseConfig is set to false and no .eslintrc", () => { - const configHelper = new Config({ baseConfig: false, useEslintrc: false }, linter), + const configHelper = new Config({ baseConfig: false, useEslintrc: false, cwd: process.cwd() }, linter), file = getFixturePath("broken", "console-wrong-quotes.js"), expected = { rules: {}, @@ -435,7 +409,7 @@ describe("Config", () => { // No default configuration it("should return an empty config when not using .eslintrc", () => { - const configHelper = new Config({ useEslintrc: false }, linter), + const configHelper = new Config({ useEslintrc: false, cwd: process.cwd() }, linter), file = getFixturePath("broken", "console-wrong-quotes.js"), actual = configHelper.getConfig(file); @@ -454,7 +428,8 @@ describe("Config", () => { quotes: [2, "single"] } }, - useEslintrc: false + useEslintrc: false, + cwd: process.cwd() }, linter), file = getFixturePath("broken", "console-wrong-quotes.js"), expected = { @@ -471,19 +446,8 @@ describe("Config", () => { }); it("should return a modified config without plugin rules enabled when baseConfig is set to an object with plugin and no .eslintrc", () => { - const customRule = require("../fixtures/rules/custom-rule"); - const examplePluginName = "eslint-plugin-example"; - const requireStubs = {}; - - requireStubs[examplePluginName] = { - rules: { "example-rule": customRule }, - - // rulesConfig support removed in 2.0.0, so this should have no effect - rulesConfig: { "example-rule": 1 } - }; - - const StubbedConfig = proxyquire("../../lib/config", requireStubs); - const configHelper = new StubbedConfig({ + const examplePluginName = "eslint-plugin-example-with-rules-config"; + const configHelper = new Config({ baseConfig: { env: { node: true @@ -493,7 +457,8 @@ describe("Config", () => { }, plugins: [examplePluginName] }, - useEslintrc: false + useEslintrc: false, + cwd: getFixturePath("plugins") }, linter), file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"), expected = { @@ -703,16 +668,9 @@ describe("Config", () => { // Command line configuration - --plugin it("should merge command line plugin with local .eslintrc", () => { - - // stub out Config to use stub config file - const StubbedConfig = createStubbedConfigWithPlugins({ - "eslint-plugin-example": {}, - "eslint-plugin-another-plugin": {} - }); - - const configHelper = new StubbedConfig({ + const configHelper = new Config({ plugins: ["another-plugin"], - cwd: process.cwd() + cwd: getFixturePath("plugins") }, linter), file = getFixturePath("broken", "plugins", "console-wrong-quotes.js"), expected = { @@ -748,7 +706,7 @@ describe("Config", () => { it("should load user config globals", () => { const configPath = path.resolve(__dirname, "..", "fixtures", "globals", "conf.yaml"), - configHelper = new Config({ configFile: configPath, useEslintrc: false }, linter); + configHelper = new Config({ configFile: configPath, useEslintrc: false, cwd: process.cwd() }, linter); const expected = { globals: { @@ -764,7 +722,7 @@ describe("Config", () => { it("should not load disabled environments", () => { const configPath = path.resolve(__dirname, "..", "fixtures", "environments", "disable.yaml"); - const configHelper = new Config({ configFile: configPath, useEslintrc: false }, linter); + const configHelper = new Config({ configFile: configPath, useEslintrc: false, cwd: process.cwd() }, linter); const config = configHelper.getConfig(configPath); @@ -791,7 +749,7 @@ describe("Config", () => { const configPath = path.resolve(__dirname, "..", "fixtures", "config-extends", "error.json"); assert.throws(() => { - const configHelper = new Config({ useEslintrc: false, configFile: configPath }, linter); + const configHelper = new Config({ useEslintrc: false, configFile: configPath, cwd: process.cwd() }, linter); configHelper.getConfig(configPath); }, /Referenced from:.*?error\.json/); @@ -800,7 +758,7 @@ describe("Config", () => { // Keep order with the last array element taking highest precedence it("should make the last element in an array take the highest precedence", () => { const configPath = path.resolve(__dirname, "..", "fixtures", "config-extends", "array", ".eslintrc"), - configHelper = new Config({ useEslintrc: false, configFile: configPath }, linter), + configHelper = new Config({ useEslintrc: false, configFile: configPath, cwd: process.cwd() }, linter), expected = { rules: { "no-empty": 1, "comma-dangle": 2, "no-console": 2 }, env: { browser: false, node: true, es6: true } @@ -872,7 +830,6 @@ describe("Config", () => { parserOptions: {}, env: {}, globals: {}, - parser: void 0, rules: { "home-folder-rule": 2 } @@ -901,7 +858,6 @@ describe("Config", () => { parserOptions: {}, env: {}, globals: {}, - parser: void 0, rules: { "project-level-rule": 2 } @@ -928,7 +884,6 @@ describe("Config", () => { parserOptions: {}, env: {}, globals: {}, - parser: void 0, rules: { quotes: [2, "double"] } @@ -953,7 +908,6 @@ describe("Config", () => { parserOptions: {}, env: {}, globals: {}, - parser: void 0, rules: { "project-level-rule": 2, "subfolder-level-rule": 2 @@ -1158,9 +1112,9 @@ describe("Config", () => { rules: { quotes: [1, "double"] } - }], - useEslintrc: false - } + }] + }, + useEslintrc: false }, linter); assert.throws(() => config.getConfig(targetPath), /Invalid override pattern/); @@ -1178,9 +1132,9 @@ describe("Config", () => { rules: { quotes: [1, "single"] } - }], - useEslintrc: false - } + }] + }, + useEslintrc: false }, linter); assert.throws(() => config.getConfig(targetPath), /Invalid override pattern/); @@ -1470,16 +1424,12 @@ describe("Config", () => { describe("Plugin Environments", () => { it("should load environments from plugin", () => { - - const StubbedConfig = createStubbedConfigWithPlugins({ - "eslint-plugin-test": { - environments: { example: { globals: { test: false } } } - } - }); - const configPath = path.resolve(__dirname, "..", "fixtures", "environments", "plugin.yaml"), - configHelper = new StubbedConfig({ - reset: true, configFile: configPath, useEslintrc: false + configHelper = new Config({ + reset: true, + configFile: configPath, + useEslintrc: false, + cwd: getFixturePath("plugins") }, linter), expected = { env: { diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js index 944ed84260fd..c7a23397b154 100644 --- a/tests/lib/config/config-file.js +++ b/tests/lib/config/config-file.js @@ -8,38 +8,26 @@ // Requirements //------------------------------------------------------------------------------ -const Module = require("module"), - assert = require("chai").assert, +const assert = require("chai").assert, leche = require("leche"), sinon = require("sinon"), path = require("path"), fs = require("fs"), - os = require("os"), yaml = require("js-yaml"), shell = require("shelljs"), espree = require("espree"), ConfigFile = require("../../../lib/config/config-file"), Linter = require("../../../lib/linter"), - Config = require("../../../lib/config"); + Config = require("../../../lib/config"), + relativeModuleResolver = require("../../../lib/util/relative-module-resolver"); -const userHome = os.homedir(); const temp = require("temp").track(); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); -let configContext; //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -/* - * Project path is the project that is including ESLint as a dependency. In the - * case of these tests, it will end up the parent of the "eslint" folder. That's - * fine for the purposes of testing because the tests are just relative to an - * ancestor location. - */ -const PROJECT_PATH = path.resolve(__dirname, "../../../../"), - PROJECT_DEPS_PATH = path.join(PROJECT_PATH, "node_modules"); - /** * Helper function get easily get a path in the fixtures directory. * @param {string} filepath The path to find in the fixtures directory. @@ -84,81 +72,15 @@ function writeTempJsConfigFile(config, filename, existingTmpDir) { return tmpFilePath; } -/** - * Creates a module path relative to the current working directory. - * @param {string} moduleName The full module name. - * @returns {string} A full path for the module local to cwd. - * @private - */ -function getProjectModulePath(moduleName) { - return path.resolve(PROJECT_PATH, "./node_modules", moduleName, "index.js"); -} - -/** - * Creates a module path relative to the given directory. - * @param {string} moduleName The full module name. - * @returns {string} A full path for the module local to the given directory. - * @private - */ -function getRelativeModulePath(moduleName) { - return path.resolve("./node_modules", moduleName, "index.js"); -} - -/** - * Creates a module resolver that always resolves the given mappings. - * @param {Object} mapping A mapping of module name to path. - * @constructor - * @private - */ -function createStubModuleResolver(mapping) { - - /** - * Stubbed module resolver for easier testing. - * @constructor - * @private - */ - return class StubModuleResolver { - resolve(name) { // eslint-disable-line class-methods-use-this - if (Object.prototype.hasOwnProperty.call(mapping, name)) { - return mapping[name]; - } - - throw new Error(`Cannot find module '${name}'`); - } - }; -} - -/** - * Overrides the native module resolver to resolve with the given mappings. - * @param {Object} mapping A mapping of module name to path. - * @returns {void} - * @private - */ -function overrideNativeResolve(mapping) { - let originalFindPath; - - beforeEach(() => { - originalFindPath = Module._findPath; // eslint-disable-line no-underscore-dangle - Module._findPath = function(request, paths, isMain) { // eslint-disable-line no-underscore-dangle - if (Object.prototype.hasOwnProperty.call(mapping, request)) { - return mapping[request]; - } - return originalFindPath.call(this, request, paths, isMain); - }; - }); - afterEach(() => { - Module._findPath = originalFindPath; // eslint-disable-line no-underscore-dangle - }); -} - //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("ConfigFile", () => { + let configContext; beforeEach(() => { - configContext = new Config({}, new Linter()); + configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); }); describe("CONFIG_FILES", () => { @@ -168,54 +90,26 @@ describe("ConfigFile", () => { }); describe("applyExtends()", () => { - - overrideNativeResolve({ - "eslint-plugin-test": getProjectModulePath("eslint-plugin-test") - }); - it("should apply extension 'foo' when specified from root directory config", () => { - - const resolvedPath = path.resolve(PROJECT_PATH, "./node_modules/eslint-config-foo/index.js"); - - const configDeps = { - - // Hacky: need to override isFile for each call for testing - "../util/module-resolver": createStubModuleResolver({ "eslint-config-foo": resolvedPath }), - "import-fresh"(filename) { - return configDeps[filename]; - } - }; - - configDeps[resolvedPath] = { - env: { browser: true } - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - - const config = StubbedConfigFile.applyExtends({ - extends: "foo", + const resolvedPath = relativeModuleResolver("eslint-config-enable-browser-env", getFixturePath(".eslintrc")); + const config = ConfigFile.applyExtends({ + extends: "enable-browser-env", rules: { eqeqeq: 2 } - }, configContext, "/whatever"); + }, configContext, getFixturePath(".eslintrc")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(resolvedPath), filePath: resolvedPath, - extends: "foo", + extends: "enable-browser-env", parserOptions: {}, env: { browser: true }, globals: {}, rules: { eqeqeq: 2 } }); - }); it("should apply all rules when extends config includes 'eslint:all'", () => { - - const configDeps = { - "../util/module-resolver": createStubModuleResolver({}) - }; - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - const config = StubbedConfigFile.applyExtends({ + const config = ConfigFile.applyExtends({ extends: "eslint:all" }, configContext, "/whatever"); @@ -225,15 +119,8 @@ describe("ConfigFile", () => { }); it("should throw an error when extends config module is not found", () => { - - const configDeps = { - "../util/module-resolver": createStubModuleResolver({}) - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - assert.throws(() => { - StubbedConfigFile.applyExtends({ + ConfigFile.applyExtends({ extends: "foo", rules: { eqeqeq: 2 } }, configContext, "/whatever"); @@ -242,15 +129,8 @@ describe("ConfigFile", () => { }); it("should throw an error when an eslint config is not found", () => { - - const configDeps = { - "../util/module-resolver": createStubModuleResolver({}) - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - assert.throws(() => { - StubbedConfigFile.applyExtends({ + ConfigFile.applyExtends({ extends: "eslint:foo", rules: { eqeqeq: 2 } }, configContext, "/whatever"); @@ -259,105 +139,35 @@ describe("ConfigFile", () => { }); it("should throw an error when a parser in a plugin config is not found", () => { - const resolvedPath = path.resolve(PROJECT_PATH, "./node_modules/eslint-plugin-test/index.js"); - - const configDeps = { - "../util/module-resolver": createStubModuleResolver({ - "eslint-plugin-test": resolvedPath - }), - "import-fresh"(filename) { - return configDeps[filename]; - } - }; - - configDeps[resolvedPath] = { - configs: { - bar: { - parser: "babel-eslint" - } - } - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - assert.throws(() => { - StubbedConfigFile.applyExtends({ - extends: "plugin:test/bar", + ConfigFile.applyExtends({ + extends: "plugin:enable-nonexistent-parser/bar", rules: { eqeqeq: 2 } }, configContext, "/whatever"); - }, /Cannot find module 'babel-eslint'/); - + }, /Cannot find module 'nonexistent-parser'/); }); it("should throw an error when a plugin config is not found", () => { - const resolvedPath = path.resolve(PROJECT_PATH, "./node_modules/eslint-plugin-test/index.js"); - - const configDeps = { - "../util/module-resolver": createStubModuleResolver({ - "eslint-plugin-test": resolvedPath - }), - "import-fresh"(filename) { - return configDeps[filename]; - } - }; - - configDeps[resolvedPath] = { - configs: { - baz: {} - } - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - assert.throws(() => { - StubbedConfigFile.applyExtends({ - extends: "plugin:test/bar", + ConfigFile.applyExtends({ + extends: "plugin:enable-nonexistent-parser/baz", rules: { eqeqeq: 2 } }, configContext, "/whatever"); - }, /Failed to load config "plugin:test\/bar" to extend from./); + }, /Failed to load config "plugin:enable-nonexistent-parser\/baz" to extend from./); }); it("should apply extensions recursively when specified from package", () => { - - const resolvedPaths = [ - path.resolve(PROJECT_PATH, "./node_modules/eslint-config-foo/index.js"), - path.resolve(PROJECT_PATH, "./node_modules/eslint-config-bar/index.js") - ]; - - const configDeps = { - - "../util/module-resolver": createStubModuleResolver({ - "eslint-config-foo": resolvedPaths[0], - "eslint-config-bar": resolvedPaths[1] - }), - "import-fresh"(filename) { - return configDeps[filename]; - } - }; - - configDeps[resolvedPaths[0]] = { - extends: "bar", - env: { browser: true } - }; - - configDeps[resolvedPaths[1]] = { - rules: { - bar: 2 - } - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - - const config = StubbedConfigFile.applyExtends({ - extends: "foo", + const resolvedPath = relativeModuleResolver("eslint-config-recursive-dependent", getFixturePath(".eslintrc")); + const config = ConfigFile.applyExtends({ + extends: "recursive-dependent", rules: { eqeqeq: 2 } - }, configContext, "/whatever"); + }, configContext, getFixturePath(".eslintrc.js")); assert.deepStrictEqual(config, { - baseDirectory: path.dirname(resolvedPaths[0]), - filePath: resolvedPaths[0], - extends: "foo", + baseDirectory: path.dirname(resolvedPath), + filePath: resolvedPath, + extends: "recursive-dependent", parserOptions: {}, env: { browser: true }, globals: {}, @@ -483,7 +293,7 @@ describe("ConfigFile", () => { it("should load information from a legacy file", () => { const configFilePath = getFixturePath("legacy/.eslintrc"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -499,7 +309,7 @@ describe("ConfigFile", () => { it("should load information from a JavaScript file", () => { const configFilePath = getFixturePath("js/.eslintrc.js"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -515,13 +325,13 @@ describe("ConfigFile", () => { it("should throw error when loading invalid JavaScript file", () => { assert.throws(() => { - ConfigFile.load(getFixturePath("js/.eslintrc.broken.js"), configContext); + ConfigFile.load(getFixturePath("js/.eslintrc.broken.js"), configContext, getFixturePath("placeholder.js")); }, /Cannot read config file/); }); it("should interpret parser module name when present in a JavaScript file", () => { const configFilePath = getFixturePath("js/.eslintrc.parser.js"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -538,7 +348,7 @@ describe("ConfigFile", () => { it("should interpret parser path when present in a JavaScript file", () => { const configFilePath = getFixturePath("js/.eslintrc.parser2.js"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -555,7 +365,7 @@ describe("ConfigFile", () => { it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => { const configFilePath = getFixturePath("js/.eslintrc.parser3.js"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -572,7 +382,7 @@ describe("ConfigFile", () => { it("should load information from a JSON file", () => { const configFilePath = getFixturePath("json/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -605,15 +415,15 @@ describe("ConfigFile", () => { }, tmpFilename = "fresh-test.json", tmpFilePath = writeTempConfigFile(initialConfig, tmpFilename); - let config = ConfigFile.load(tmpFilePath, configContext); + let config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, Object.assign({}, initialConfig, { baseDirectory: path.dirname(tmpFilePath), filePath: tmpFilePath })); writeTempConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath)); - configContext = new Config({}, new Linter()); - config = ConfigFile.load(tmpFilePath, configContext); + configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); + config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, Object.assign({}, updatedConfig, { baseDirectory: path.dirname(tmpFilePath), filePath: tmpFilePath @@ -622,7 +432,7 @@ describe("ConfigFile", () => { it("should load information from a package.json file", () => { const configFilePath = getFixturePath("package-json/package.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -637,7 +447,7 @@ describe("ConfigFile", () => { it("should throw error when loading invalid package.json file", () => { assert.throws(() => { try { - ConfigFile.load(getFixturePath("broken-package-json/package.json"), configContext); + ConfigFile.load(getFixturePath("broken-package-json/package.json"), configContext, getFixturePath("placeholder.js")); } catch (error) { assert.strictEqual(error.messageTemplate, "failed-to-read-json"); throw error; @@ -668,15 +478,15 @@ describe("ConfigFile", () => { }, tmpFilename = "package.json", tmpFilePath = writeTempConfigFile(initialConfig, tmpFilename); - let config = ConfigFile.load(tmpFilePath, configContext); + let config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, Object.assign({}, initialConfig.eslintConfig, { baseDirectory: path.dirname(tmpFilePath), filePath: tmpFilePath })); writeTempConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath)); - configContext = new Config({}, new Linter()); - config = ConfigFile.load(tmpFilePath, configContext); + configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); + config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, Object.assign({}, updatedConfig.eslintConfig, { baseDirectory: path.dirname(tmpFilePath), filePath: tmpFilePath @@ -702,15 +512,15 @@ describe("ConfigFile", () => { }, tmpFilename = ".eslintrc.js", tmpFilePath = writeTempJsConfigFile(initialConfig, tmpFilename); - let config = ConfigFile.load(tmpFilePath, configContext); + let config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, Object.assign({}, initialConfig, { baseDirectory: path.dirname(tmpFilePath), filePath: tmpFilePath })); writeTempJsConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath)); - configContext = new Config({}, new Linter()); - config = ConfigFile.load(tmpFilePath, configContext); + configContext = new Config({ cwd: getFixturePath(".") }, new Linter()); + config = ConfigFile.load(tmpFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, Object.assign({}, updatedConfig, { baseDirectory: path.dirname(tmpFilePath), filePath: tmpFilePath @@ -719,7 +529,7 @@ describe("ConfigFile", () => { it("should load information from a YAML file", () => { const configFilePath = getFixturePath("yaml/.eslintrc.yaml"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -731,9 +541,9 @@ describe("ConfigFile", () => { }); }); - it("should load information from a YAML file", () => { + it("should load information from an empty YAML file", () => { const configFilePath = getFixturePath("yaml/.eslintrc.empty.yaml"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -747,7 +557,7 @@ describe("ConfigFile", () => { it("should load information from a YML file", () => { const configFilePath = getFixturePath("yml/.eslintrc.yml"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -761,7 +571,7 @@ describe("ConfigFile", () => { it("should load information from a YML file and apply extensions", () => { const configFilePath = getFixturePath("extends/.eslintrc.yml"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -776,7 +586,7 @@ describe("ConfigFile", () => { it("should load information from `extends` chain.", () => { const configFilePath = getFixturePath("extends-chain/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -795,7 +605,7 @@ describe("ConfigFile", () => { it("should load information from `extends` chain with relative path.", () => { const configFilePath = getFixturePath("extends-chain-2/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -813,7 +623,7 @@ describe("ConfigFile", () => { it("should load information from `extends` chain in .eslintrc with relative path.", () => { const configFilePath = getFixturePath("extends-chain-2/relative.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -831,7 +641,7 @@ describe("ConfigFile", () => { it("should load information from `parser` in .eslintrc with relative path.", () => { const configFilePath = getFixturePath("extends-chain-2/parser.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); const parserPath = getFixturePath("extends-chain-2/parser.js"); assert.deepStrictEqual(config, { @@ -862,7 +672,7 @@ describe("ConfigFile", () => { it("should load information from `extends` chain in .eslintrc with relative path.", () => { const configFilePath = path.join(fixturePath, "relative.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -880,8 +690,8 @@ describe("ConfigFile", () => { it("should load information from `parser` in .eslintrc with relative path.", () => { const configFilePath = path.join(fixturePath, "parser.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); - const parserPath = path.join(fixturePath, "parser.js"); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); + const parserPath = fs.realpathSync(path.join(fixturePath, "parser.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -896,58 +706,26 @@ describe("ConfigFile", () => { }); describe("Plugins", () => { - - overrideNativeResolve({ - "eslint-plugin-test": getProjectModulePath("eslint-plugin-test") - }); - it("should load information from a YML file and load plugins", () => { - - const stubConfig = new Config({}, new Linter()); - - stubConfig.plugins.define("eslint-plugin-test", { - environments: { - bar: { globals: { bar: true } } - } - }); - const configFilePath = getFixturePath("plugins/.eslintrc.yml"); - const config = ConfigFile.load(configFilePath, stubConfig); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), filePath: configFilePath, parserOptions: {}, - env: { "test/bar": true }, + env: { "with-environment/bar": true }, globals: {}, - plugins: ["test"], + plugins: ["with-environment"], rules: { - "test/foo": 2 + "with-environment/foo": 2 } }); }); it("should load two separate configs from a plugin", () => { - const stubConfig = new Config({}, new Linter()); - const resolvedPath = path.resolve(PROJECT_PATH, "./node_modules/eslint-plugin-test/index.js"); - - const configDeps = { - "import-fresh"(filename) { - return configDeps[filename]; - } - }; - - configDeps[resolvedPath] = { - configs: { - foo: { rules: { semi: 2, quotes: 1 } }, - bar: { rules: { quotes: 2, yoda: 2 } } - } - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", configDeps); - const configFilePath = getFixturePath("plugins/.eslintrc2.yml"); - const config = StubbedConfigFile.load(configFilePath, stubConfig); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -961,8 +739,8 @@ describe("ConfigFile", () => { yoda: 2 }, extends: [ - "plugin:test/foo", - "plugin:test/bar" + "plugin:with-two-configs/foo", + "plugin:with-two-configs/bar" ] }); }); @@ -971,7 +749,7 @@ describe("ConfigFile", () => { describe("even if config files have Unicode BOM,", () => { it("should read the JSON config file correctly.", () => { const configFilePath = getFixturePath("bom/.eslintrc.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -987,7 +765,7 @@ describe("ConfigFile", () => { it("should read the YAML config file correctly.", () => { const configFilePath = getFixturePath("bom/.eslintrc.yaml"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -1003,7 +781,7 @@ describe("ConfigFile", () => { it("should read the config in package.json correctly.", () => { const configFilePath = getFixturePath("bom/package.json"); - const config = ConfigFile.load(configFilePath, configContext); + const config = ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); assert.deepStrictEqual(config, { baseDirectory: path.dirname(configFilePath), @@ -1022,7 +800,7 @@ describe("ConfigFile", () => { const configFilePath = getFixturePath("invalid/invalid-top-level-property.yml"); try { - ConfigFile.load(configFilePath, configContext); + ConfigFile.load(configFilePath, configContext, getFixturePath("placeholder.js")); } catch (err) { assert.include(err.message, `ESLint configuration in ${configFilePath} is invalid`); return; @@ -1031,169 +809,22 @@ describe("ConfigFile", () => { }); }); - describe("resolve()", () => { - - describe("Relative to CWD", () => { - overrideNativeResolve({ - "eslint-plugin-foo": getProjectModulePath("eslint-plugin-foo") - }); - - leche.withData([ - [".eslintrc", path.resolve(".eslintrc")], - ["eslint-config-foo", getProjectModulePath("eslint-config-foo")], - ["foo", getProjectModulePath("eslint-config-foo")], - ["eslint-configfoo", getProjectModulePath("eslint-config-eslint-configfoo")], - ["@foo/eslint-config", getProjectModulePath("@foo/eslint-config")], - ["@foo/bar", getProjectModulePath("@foo/eslint-config-bar")], - ["plugin:foo/bar", getProjectModulePath("eslint-plugin-foo")] - ], (input, expected) => { - it(`should return ${expected} when passed ${input}`, () => { - - const configDeps = { - "eslint-config-foo": getProjectModulePath("eslint-config-foo"), - "eslint-config-eslint-configfoo": getProjectModulePath("eslint-config-eslint-configfoo"), - "@foo/eslint-config": getProjectModulePath("@foo/eslint-config"), - "@foo/eslint-config-bar": getProjectModulePath("@foo/eslint-config-bar"), - "eslint-plugin-foo": getProjectModulePath("eslint-plugin-foo") - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", { - "../util/module-resolver": createStubModuleResolver(configDeps) - }); - - const result = StubbedConfigFile.resolve(input); - - assert.strictEqual(result.filePath, expected); - }); - }); - }); - - describe("Relative to config file", () => { - - const relativePath = path.resolve("./foo/bar"); - - overrideNativeResolve({ - "@foo/eslint-plugin-bar": getProjectModulePath("@foo/eslint-plugin-bar") - }); - - leche.withData([ - [".eslintrc", path.resolve("./foo/bar", ".eslintrc"), relativePath], - ["eslint-config-foo", getRelativeModulePath("eslint-config-foo", relativePath), relativePath], - ["foo", getRelativeModulePath("eslint-config-foo", relativePath), relativePath], - ["eslint-configfoo", getRelativeModulePath("eslint-config-eslint-configfoo", relativePath), relativePath], - ["@foo/eslint-config", getRelativeModulePath("@foo/eslint-config", relativePath), relativePath], - ["@foo/bar", getRelativeModulePath("@foo/eslint-config-bar", relativePath), relativePath], - ["plugin:@foo/bar/baz", getProjectModulePath("@foo/eslint-plugin-bar"), relativePath] - ], (input, expected, relativeTo) => { - it(`should return ${expected} when passed ${input}`, () => { - - const configDeps = { - "eslint-config-foo": getRelativeModulePath("eslint-config-foo", relativePath), - "eslint-config-eslint-configfoo": getRelativeModulePath("eslint-config-eslint-configfoo", relativePath), - "@foo/eslint-config": getRelativeModulePath("@foo/eslint-config", relativePath), - "@foo/eslint-config-bar": getRelativeModulePath("@foo/eslint-config-bar", relativePath) - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", { - "../util/module-resolver": createStubModuleResolver(configDeps) - }); - - const result = StubbedConfigFile.resolve(input, relativeTo); - - assert.strictEqual(result.filePath, expected); - }); - }); - - leche.withData([ - ["eslint-config-foo/bar", path.resolve("./node_modules", "eslint-config-foo/bar", "index.json"), relativePath], - ["eslint-config-foo/bar", path.resolve("./node_modules", "eslint-config-foo", "bar.json"), relativePath], - ["eslint-config-foo/bar", path.resolve("./node_modules", "eslint-config-foo/bar", "index.js"), relativePath], - ["eslint-config-foo/bar", path.resolve("./node_modules", "eslint-config-foo", "bar.js"), relativePath] - ], (input, expected, relativeTo) => { - it(`should return ${expected} when passed ${input}`, () => { - - const configDeps = { - "eslint-config-foo/bar": expected - }; - - const StubbedConfigFile = proxyquire("../../../lib/config/config-file", { - "../util/module-resolver": createStubModuleResolver(configDeps) - }); - - const result = StubbedConfigFile.resolve(input, relativeTo); - - assert.strictEqual(result.filePath, expected); - }); - }); - - }); - - }); - - describe("getBaseDir()", () => { - - // can only run this test if there's a home directory - if (userHome) { - - it("should return project path when config file is in home directory", () => { - const result = ConfigFile.getBaseDir(userHome); - - assert.strictEqual(result, PROJECT_PATH); - }); - } - - it("should return project path when config file is in an ancestor directory of the project path", () => { - const result = ConfigFile.getBaseDir(path.resolve(PROJECT_PATH, "../../")); - - assert.strictEqual(result, PROJECT_PATH); - }); - - it("should return config file path when config file is in a descendant directory of the project path", () => { - const configFilePath = path.resolve(PROJECT_PATH, "./foo/bar/"), - result = ConfigFile.getBaseDir(path.resolve(PROJECT_PATH, "./foo/bar/")); - - assert.strictEqual(result, configFilePath); - }); - - it("should return project path when config file is not an ancestor or descendant of the project path", () => { - const result = ConfigFile.getBaseDir(path.resolve("/tmp/foo")); - - assert.strictEqual(result, PROJECT_PATH); - }); - - }); - - describe("getLookupPath()", () => { - - // can only run this test if there's a home directory - if (userHome) { - - it("should return project path when config file is in home directory", () => { - const result = ConfigFile.getLookupPath(userHome); + describe("resolve() relative to config file", () => { + leche.withData([ + [".eslintrc", getFixturePath("subdir/.eslintrc")], + ["eslint-config-foo", relativeModuleResolver("eslint-config-foo", getFixturePath("subdir/placeholder.js"))], + ["foo", relativeModuleResolver("eslint-config-foo", getFixturePath("subdir/placeholder.js"))], + ["eslint-configfoo", relativeModuleResolver("eslint-config-eslint-configfoo", getFixturePath("subdir/placeholder.js"))], + ["@foo/eslint-config", relativeModuleResolver("@foo/eslint-config", getFixturePath("subdir/placeholder.js"))], + ["@foo/bar", relativeModuleResolver("@foo/eslint-config-bar", getFixturePath("subdir/placeholder.js"))], + ["plugin:foo/bar", relativeModuleResolver("eslint-plugin-foo", getFixturePath("placeholder.js"))] + ], (input, expected) => { + it(`should return ${expected} when passed ${input}`, () => { + const result = ConfigFile.resolve(input, getFixturePath("subdir/placeholder.js"), getFixturePath(".")); - assert.strictEqual(result, PROJECT_DEPS_PATH); + assert.strictEqual(result.filePath, expected); }); - } - - it("should return project path when config file is in an ancestor directory of the project path", () => { - const result = ConfigFile.getLookupPath(path.resolve(PROJECT_DEPS_PATH, "../../")); - - assert.strictEqual(result, PROJECT_DEPS_PATH); - }); - - it("should return config file path when config file is in a descendant directory of the project path", () => { - const configFilePath = path.resolve(PROJECT_DEPS_PATH, "./foo/bar/node_modules"), - result = ConfigFile.getLookupPath(path.resolve(PROJECT_DEPS_PATH, "./foo/bar/")); - - assert.strictEqual(result, configFilePath); }); - - it("should return project path when config file is not an ancestor or descendant of the project path", () => { - const result = ConfigFile.getLookupPath(path.resolve("/tmp/foo")); - - assert.strictEqual(result, PROJECT_DEPS_PATH); - }); - }); describe("getFilenameFromDirectory()", () => { diff --git a/tests/lib/config/config-initializer.js b/tests/lib/config/config-initializer.js index 3767fe376f3b..14ad333affbb 100644 --- a/tests/lib/config/config-initializer.js +++ b/tests/lib/config/config-initializer.js @@ -34,29 +34,15 @@ describe("configInitializer", () => { npmInstallStub, npmFetchPeerDependenciesStub, init, - localESLintVersion = null; + originalGetESLintVersion, + mockedESLintVersion = null; const log = { info: sinon.spy(), error: sinon.spy() }; const requireStubs = { - "../util/logging": log, - "../util/module-resolver": class ModuleResolver { - - /** - * @returns {string} The path to local eslint to test. - */ - resolve() { // eslint-disable-line class-methods-use-this - if (localESLintVersion) { - return `local-eslint-${localESLintVersion}`; - } - throw new Error("Cannot find module"); - } - }, - "local-eslint-3.18.0": { linter: { version: "3.18.0" }, "@noCallThru": true }, - "local-eslint-3.19.0": { linter: { version: "3.19.0" }, "@noCallThru": true }, - "local-eslint-4.0.0": { linter: { version: "4.0.0" }, "@noCallThru": true } + "../util/logging": log }; /** @@ -97,6 +83,9 @@ describe("configInitializer", () => { "eslint-plugin-react": "^7.0.1" }); init = proxyquire("../../../lib/config/config-initializer", requireStubs); + originalGetESLintVersion = init.getESLintVersion; + + init.getESLintVersion = () => mockedESLintVersion || originalGetESLintVersion(); }); afterEach(() => { @@ -105,6 +94,7 @@ describe("configInitializer", () => { npmInstallStub.restore(); npmCheckStub.restore(); npmFetchPeerDependenciesStub.restore(); + init.getESLintVersion = originalGetESLintVersion; }); after(() => { @@ -276,21 +266,9 @@ describe("configInitializer", () => { }); describe("hasESLintVersionConflict (Note: peerDependencies always `eslint: \"^3.19.0\"` by stubs)", () => { - describe("if local ESLint is not found,", () => { - before(() => { - localESLintVersion = null; - }); - - it("should return false.", () => { - const result = init.hasESLintVersionConflict({ styleguide: "airbnb" }); - - assert.strictEqual(result, false); - }); - }); - - describe("if local ESLint is 3.19.0,", () => { + describe("if current ESLint is 3.19.0,", () => { before(() => { - localESLintVersion = "3.19.0"; + mockedESLintVersion = "3.19.0"; }); it("should return false.", () => { @@ -300,9 +278,9 @@ describe("configInitializer", () => { }); }); - describe("if local ESLint is 4.0.0,", () => { + describe("if current ESLint is 4.0.0,", () => { before(() => { - localESLintVersion = "4.0.0"; + mockedESLintVersion = "4.0.0"; }); it("should return true.", () => { @@ -312,9 +290,9 @@ describe("configInitializer", () => { }); }); - describe("if local ESLint is 3.18.0,", () => { + describe("if current ESLint is 3.18.0,", () => { before(() => { - localESLintVersion = "3.18.0"; + mockedESLintVersion = "3.18.0"; }); it("should return true.", () => { diff --git a/tests/lib/config/plugins.js b/tests/lib/config/plugins.js index afde8af44701..21adf9978f2d 100644 --- a/tests/lib/config/plugins.js +++ b/tests/lib/config/plugins.js @@ -9,11 +9,10 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, + path = require("path"), Plugins = require("../../../lib/config/plugins"), Environments = require("../../../lib/config/environments"); -const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); - //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ @@ -23,42 +22,37 @@ describe("Plugins", () => { describe("get", () => { it("should return null when plugin doesn't exist", () => { - assert.isNull((new Plugins(new Environments(), () => {})).get("foo")); + assert.isNull((new Plugins(new Environments(), { defineRule: () => {}, pluginRootPath: process.cwd() })).get("foo")); }); }); describe("load()", () => { - let StubbedPlugins, + let RelativeLoadedPlugins, rules, environments, plugin, scopedPlugin; beforeEach(() => { - plugin = {}; - scopedPlugin = {}; + plugin = require("../../fixtures/plugins/node_modules/eslint-plugin-example"); + scopedPlugin = require("../../fixtures/plugins/node_modules/@scope/eslint-plugin-example"); environments = new Environments(); rules = new Map(); - StubbedPlugins = new (proxyquire("../../../lib/config/plugins", { - "eslint-plugin-example": plugin, - "@scope/eslint-plugin-example": scopedPlugin, - "eslint-plugin-throws-on-load": { - get rules() { - throw new Error("error thrown while loading this module"); - } - } - }))(environments, rules.set.bind(rules)); + RelativeLoadedPlugins = new Plugins(environments, { + defineRule: rules.set.bind(rules), + pluginRootPath: path.resolve(__dirname, "..", "..", "fixtures", "plugins") + }); }); it("should load a plugin when referenced by short name", () => { - StubbedPlugins.load("example"); - assert.strictEqual(StubbedPlugins.get("example"), plugin); + RelativeLoadedPlugins.load("example"); + assert.strictEqual(RelativeLoadedPlugins.get("example"), plugin); }); it("should load a plugin when referenced by long name", () => { - StubbedPlugins.load("eslint-plugin-example"); - assert.strictEqual(StubbedPlugins.get("example"), plugin); + RelativeLoadedPlugins.load("eslint-plugin-example"); + assert.strictEqual(RelativeLoadedPlugins.get("example"), plugin); }); it("should register environments when plugin has environments", () => { @@ -71,7 +65,7 @@ describe("Plugins", () => { } }; - StubbedPlugins.load("eslint-plugin-example"); + RelativeLoadedPlugins.load("eslint-plugin-example"); assert.deepStrictEqual(environments.get("example/foo"), plugin.environments.foo); assert.deepStrictEqual(environments.get("example/bar"), plugin.environments.bar); @@ -83,7 +77,7 @@ describe("Plugins", () => { qux: {} }; - StubbedPlugins.load("eslint-plugin-example"); + RelativeLoadedPlugins.load("eslint-plugin-example"); assert.deepStrictEqual(rules.get("example/baz"), plugin.rules.baz); assert.deepStrictEqual(rules.get("example/qux"), plugin.rules.qux); @@ -91,28 +85,28 @@ describe("Plugins", () => { it("should throw an error when a plugin has whitespace", () => { assert.throws(() => { - StubbedPlugins.load("whitespace "); + RelativeLoadedPlugins.load("whitespace "); }, /Whitespace found in plugin name 'whitespace '/); assert.throws(() => { - StubbedPlugins.load("whitespace\t"); + RelativeLoadedPlugins.load("whitespace\t"); }, /Whitespace found in plugin name/); assert.throws(() => { - StubbedPlugins.load("whitespace\n"); + RelativeLoadedPlugins.load("whitespace\n"); }, /Whitespace found in plugin name/); assert.throws(() => { - StubbedPlugins.load("whitespace\r"); + RelativeLoadedPlugins.load("whitespace\r"); }, /Whitespace found in plugin name/); }); it("should throw an error when a plugin doesn't exist", () => { assert.throws(() => { - StubbedPlugins.load("nonexistentplugin"); + RelativeLoadedPlugins.load("nonexistentplugin"); }, /Failed to load plugin/); }); it("should rethrow an error that a plugin throws on load", () => { try { - StubbedPlugins.load("throws-on-load"); + RelativeLoadedPlugins.load("throws-on-load"); } catch (err) { assert.strictEqual( err.message, @@ -126,20 +120,20 @@ describe("Plugins", () => { }); it("should load a scoped plugin when referenced by short name", () => { - StubbedPlugins.load("@scope/example"); - assert.strictEqual(StubbedPlugins.get("@scope/example"), scopedPlugin); + RelativeLoadedPlugins.load("@scope/example"); + assert.strictEqual(RelativeLoadedPlugins.get("@scope/example"), scopedPlugin); }); it("should load a scoped plugin when referenced by long name", () => { - StubbedPlugins.load("@scope/eslint-plugin-example"); - assert.strictEqual(StubbedPlugins.get("@scope/example"), scopedPlugin); + RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); + assert.strictEqual(RelativeLoadedPlugins.get("@scope/example"), scopedPlugin); }); it("should register environments when scoped plugin has environments", () => { scopedPlugin.environments = { foo: {} }; - StubbedPlugins.load("@scope/eslint-plugin-example"); + RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); assert.strictEqual(environments.get("@scope/example/foo"), scopedPlugin.environments.foo); }); @@ -148,27 +142,27 @@ describe("Plugins", () => { scopedPlugin.rules = { foo: {} }; - StubbedPlugins.load("@scope/eslint-plugin-example"); + RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); assert.strictEqual(rules.get("@scope/example/foo"), scopedPlugin.rules.foo); }); describe("when referencing a scope plugin and omitting @scope/", () => { it("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", () => { - StubbedPlugins.load("@scope/example"); - assert.strictEqual(StubbedPlugins.get("example"), null); + RelativeLoadedPlugins.load("@scope/example"); + assert.strictEqual(RelativeLoadedPlugins.get("example"), null); }); it("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { - StubbedPlugins.load("@scope/eslint-plugin-example"); - assert.strictEqual(StubbedPlugins.get("example"), null); + RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); + assert.strictEqual(RelativeLoadedPlugins.get("example"), null); }); it("should register environments when scoped plugin has environments, but should not get the environment if '@scope/' is omitted", () => { scopedPlugin.environments = { foo: {} }; - StubbedPlugins.load("@scope/eslint-plugin-example"); + RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); assert.strictEqual(environments.get("example/foo"), null); }); @@ -177,7 +171,7 @@ describe("Plugins", () => { scopedPlugin.rules = { foo: {} }; - StubbedPlugins.load("@scope/eslint-plugin-example"); + RelativeLoadedPlugins.load("@scope/eslint-plugin-example"); assert.isFalse(rules.has("example/foo")); }); @@ -186,26 +180,26 @@ describe("Plugins", () => { describe("loadAll()", () => { - let StubbedPlugins, + let RelativeLoadedPlugins, plugin1, plugin2, rules; const environments = new Environments(); beforeEach(() => { - plugin1 = {}; - plugin2 = {}; + plugin1 = require("../../fixtures/plugins/node_modules/eslint-plugin-example1"); + plugin2 = require("../../fixtures/plugins/node_modules/eslint-plugin-example2"); rules = new Map(); - StubbedPlugins = new (proxyquire("../../../lib/config/plugins", { - "eslint-plugin-example1": plugin1, - "eslint-plugin-example2": plugin2 - }))(environments, rules.set.bind(rules)); + RelativeLoadedPlugins = new Plugins(environments, { + defineRule: rules.set.bind(rules), + pluginRootPath: path.resolve(__dirname, "..", "..", "fixtures", "plugins") + }); }); it("should load plugins when passed multiple plugins", () => { - StubbedPlugins.loadAll(["example1", "example2"]); - assert.strictEqual(StubbedPlugins.get("example1"), plugin1); - assert.strictEqual(StubbedPlugins.get("example2"), plugin2); + RelativeLoadedPlugins.loadAll(["example1", "example2"]); + assert.strictEqual(RelativeLoadedPlugins.get("example1"), plugin1); + assert.strictEqual(RelativeLoadedPlugins.get("example2"), plugin2); }); it("should load environments from plugins when passed multiple plugins", () => { @@ -217,7 +211,7 @@ describe("Plugins", () => { bar: {} }; - StubbedPlugins.loadAll(["example1", "example2"]); + RelativeLoadedPlugins.loadAll(["example1", "example2"]); assert.strictEqual(environments.get("example1/foo"), plugin1.environments.foo); assert.strictEqual(environments.get("example2/bar"), plugin2.environments.bar); }); @@ -231,13 +225,13 @@ describe("Plugins", () => { bar: {} }; - StubbedPlugins.loadAll(["example1", "example2"]); + RelativeLoadedPlugins.loadAll(["example1", "example2"]); assert.strictEqual(rules.get("example1/foo"), plugin1.rules.foo); assert.strictEqual(rules.get("example2/bar"), plugin2.rules.bar); }); it("should throw an error if plugins is not an array", () => { - assert.throws(() => StubbedPlugins.loadAll("example1"), "\"plugins\" value must be an array"); + assert.throws(() => RelativeLoadedPlugins.loadAll("example1"), "\"plugins\" value must be an array"); }); }); diff --git a/tests/lib/linter.js b/tests/lib/linter.js index 564bcee6b216..ab6173c6da30 100644 --- a/tests/lib/linter.js +++ b/tests/lib/linter.js @@ -2,7 +2,6 @@ * @fileoverview Tests for eslint object. * @author Nicholas C. Zakas */ -/* globals window */ "use strict"; @@ -18,14 +17,15 @@ * @private */ function compatRequire(name, windowName) { + /* eslint-disable no-undef */ if (typeof window === "object") { return window[windowName || name]; } if (typeof require === "function") { return require(name); } + /* eslint-enable no-undef */ throw new Error(`Cannot find object '${name}'.`); - } //------------------------------------------------------------------------------ @@ -34,7 +34,8 @@ function compatRequire(name, windowName) { const assert = compatRequire("chai").assert, sinon = compatRequire("sinon"), - path = compatRequire("path"), + esprima = compatRequire("esprima"), + testParsers = compatRequire("../fixtures/parsers/linter-test-parsers", "LinterTestParsers"), Linter = compatRequire("../../lib/linter", "eslint"); //------------------------------------------------------------------------------ @@ -946,7 +947,7 @@ describe("Linter", () => { const parser = { parseForESLint: function parse(code, options) { return { - ast: require("espree").parse(code, options), + ast: esprima.parse(code, options), services: { test: { getMessage() { @@ -969,74 +970,69 @@ describe("Linter", () => { describe("when config has parser", () => { - // custom parser unsupported in browser, only test in Node - if (typeof window === "undefined") { - it("should pass parser as parserPath to all rules when provided on config", () => { - - const alternateParser = "esprima"; - - linter.defineRule("test-rule", sandbox.mock().withArgs( - sinon.match({ parserPath: alternateParser }) - ).returns({})); - - const config = { rules: { "test-rule": 2 }, parser: alternateParser }; + it("should pass parser as parserPath to all rules when provided on config", () => { - linter.verify("0", config, filename); - }); + const alternateParser = "esprima"; - it("should use parseForESLint() in custom parser when custom parser is specified", () => { + linter.defineParser("esprima", esprima); + linter.defineRule("test-rule", sandbox.mock().withArgs( + sinon.match({ parserPath: alternateParser }) + ).returns({})); - const alternateParser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js"); - const config = { rules: {}, parser: alternateParser }; - const messages = linter.verify("0", config, filename); + const config = { rules: { "test-rule": 2 }, parser: alternateParser }; - assert.strictEqual(messages.length, 0); - }); + linter.verify("0", config, filename); + }); - it("should expose parser services when using parseForESLint() and services are specified", () => { + it("should use parseForESLint() in custom parser when custom parser is specified", () => { + const config = { rules: {}, parser: "enhanced-parser" }; - const alternateParser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js"); + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + const messages = linter.verify("0", config, filename); - linter.defineRule("test-service-rule", context => ({ - Literal(node) { - context.report({ - node, - message: context.parserServices.test.getMessage() - }); - } - })); + assert.strictEqual(messages.length, 0); + }); - const config = { rules: { "test-service-rule": 2 }, parser: alternateParser }; - const messages = linter.verify("0", config, filename); + it("should expose parser services when using parseForESLint() and services are specified", () => { + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + linter.defineRule("test-service-rule", context => ({ + Literal(node) { + context.report({ + node, + message: context.parserServices.test.getMessage() + }); + } + })); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hi!"); - }); + const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const messages = linter.verify("0", config, filename); - it("should use the same parserServices if source code object is reused", () => { - const parser = path.resolve(__dirname, "../fixtures/parsers/enhanced-parser.js"); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); + }); - linter.defineRule("test-service-rule", context => ({ - Literal(node) { - context.report({ - node, - message: context.parserServices.test.getMessage() - }); - } - })); + it("should use the same parserServices if source code object is reused", () => { + linter.defineParser("enhanced-parser", testParsers.enhancedParser); + linter.defineRule("test-service-rule", context => ({ + Literal(node) { + context.report({ + node, + message: context.parserServices.test.getMessage() + }); + } + })); - const config = { rules: { "test-service-rule": 2 }, parser }; - const messages = linter.verify("0", config, filename); + const config = { rules: { "test-service-rule": 2 }, parser: "enhanced-parser" }; + const messages = linter.verify("0", config, filename); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "Hi!"); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Hi!"); - const messages2 = linter.verify(linter.getSourceCode(), config, filename); + const messages2 = linter.verify(linter.getSourceCode(), config, filename); - assert.strictEqual(messages2.length, 1); - assert.strictEqual(messages2[0].message, "Hi!"); - }); - } + assert.strictEqual(messages2.length, 1); + assert.strictEqual(messages2[0].message, "Hi!"); + }); it("should pass parser as parserPath to all rules when default parser is used", () => { linter.defineRule("test-rule", sandbox.mock().withArgs( @@ -1204,9 +1200,16 @@ describe("Linter", () => { }); }); + /* + * `eslint-env` comments are processed by doing a full source text match before parsing. + * As a result, if this source file contains `eslint- env` followed by an environment in a string, + * it will actually enable the given envs for this source file. String concatenation is used to avoid this, + * for the environments that aren't supposed to be enabled for this file. + */ + /* eslint-disable no-useless-concat */ describe("when evaluating code containing /*eslint-env */ block", () => { it("variables should be available in global scope", () => { - const code = "/*eslint-env node*/ function f() {} /*eslint-env browser, foo*/"; + const code = "/*eslint-" + "env node*/ function f() {} /*eslint-" + "env browser, foo*/"; const config = { rules: { checker: "error" } }; let spy; @@ -1229,7 +1232,7 @@ describe("Linter", () => { }); describe("when evaluating code containing /*eslint-env */ block with sloppy whitespace", () => { - const code = "/* eslint-env ,, node , no-browser ,, */"; + const code = "/* eslint-" + "env ,, node , no-browser ,, */"; it("variables should be available in global scope", () => { const config = { rules: { checker: "error" } }; @@ -2889,7 +2892,7 @@ describe("Linter", () => { }); it("should not report a violation", () => { - const code = "/*eslint-env mocha,node */ require();describe();"; + const code = "/*eslint-" + "env mocha,node */ require();describe();"; const config = { rules: { "no-undef": 1 } }; @@ -2909,7 +2912,7 @@ describe("Linter", () => { }); it("should not report a violation", () => { - const code = "/*eslint-env amd */ define();require();"; + const code = "/*eslint-" + "env amd */ define();require();"; const config = { rules: { "no-undef": 1 } }; @@ -2919,7 +2922,7 @@ describe("Linter", () => { }); it("should not report a violation", () => { - const code = "/*eslint-env jasmine */ expect();spyOn();"; + const code = "/*eslint-" + "env jasmine */ expect();spyOn();"; const config = { rules: { "no-undef": 1 } }; @@ -2929,7 +2932,7 @@ describe("Linter", () => { }); it("should not report a violation", () => { - const code = "/*globals require: true */ /*eslint-env node */ require = 1;"; + const code = "/*globals require: true */ /*eslint-" + "env node */ require = 1;"; const config = { rules: { "no-undef": 1 } }; @@ -2939,7 +2942,7 @@ describe("Linter", () => { }); it("should not report a violation", () => { - const code = "/*eslint-env node */ process.exit();"; + const code = "/*eslint-" + "env node */ process.exit();"; const config = { rules: {} }; @@ -2949,7 +2952,7 @@ describe("Linter", () => { }); it("should not report a violation", () => { - const code = "/*eslint no-process-exit: 0 */ /*eslint-env node */ process.exit();"; + const code = "/*eslint no-process-exit: 0 */ /*eslint-" + "env node */ process.exit();"; const config = { rules: { "no-undef": 1 } }; @@ -3074,7 +3077,7 @@ describe("Linter", () => { it("should report a violation for env changes", () => { const code = [ - "/*eslint-env browser*/" + "/*eslint-" + "env browser*/" ].join("\n"); const config = { rules: { @@ -3444,8 +3447,8 @@ describe("Linter", () => { assert.strictEqual(messages.length, 0); }); - it("should be able to return in global if there is a comment which has \"eslint-env node\"", () => { - const messages = linter.verify("/* eslint-env node */ return;", null, "eslint-env node"); + it("should be able to return in global if there is a comment which has \"eslint-" + "env node\"", () => { + const messages = linter.verify("/* eslint-" + "env node */ return;", null, "eslint-" + "env node"); assert.strictEqual(messages.length, 0); }); @@ -4471,196 +4474,186 @@ describe("Linter", () => { }); }); - // only test in Node.js, not browser - if (typeof window === "undefined") { - const escope = require("eslint-scope"); - const vk = require("eslint-visitor-keys"); + describe("Custom parser", () => { - describe("Custom parser", () => { + const errorPrefix = "Parsing error: "; - const parserFixtures = path.join(__dirname, "../fixtures/parsers"), - errorPrefix = "Parsing error: "; + it("should have file path passed to it", () => { + const code = "/* this is code */"; + const parseSpy = sinon.spy(testParsers.stubParser, "parse"); - it("should have file path passed to it", () => { - const code = "/* this is code */"; - const parser = path.join(parserFixtures, "stub-parser.js"); - const parseSpy = sinon.spy(require(parser), "parse"); + linter.defineParser("stub-parser", testParsers.stubParser); + linter.verify(code, { parser: "stub-parser" }, filename, true); - linter.verify(code, { parser }, filename, true); + sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + }); - sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); - }); + it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { + const code = "var myDivElement =
;"; - it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parser: "esprima", parserOptions: { jsx: true } }, "filename"); + linter.defineParser("esprima", esprima); + const messages = linter.verify(code, { parser: "esprima", parserOptions: { jsx: true } }, "filename"); - assert.strictEqual(messages.length, 0); - }); + assert.strictEqual(messages.length, 0); + }); - it("should return an error when the custom parser can't be found", () => { - const code = "var myDivElement =
;"; - const messages = linter.verify(code, { parser: "esprima-xyz" }, "filename"); + it("should return an error when the custom parser can't be found", () => { + const code = "var myDivElement =
;"; + const messages = linter.verify(code, { parser: "esprima-xyz" }, "filename"); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, "Cannot find module 'esprima-xyz'"); - }); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Configured parser 'esprima-xyz' was not found."); + }); - it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => { - const code = "null %% 'foo'"; - const parser = path.join(parserFixtures, "unknown-operators", "unknown-logical-operator.js"); + it("should not throw or report errors when the custom parser returns unrecognized operators (https://github.com/eslint/eslint/issues/10475)", () => { + const code = "null %% 'foo'"; - // This shouldn't throw - const messages = linter.verify(code, { parser }, filename, true); + linter.defineParser("unknown-logical-operator", testParsers.unknownLogicalOperator); - assert.strictEqual(messages.length, 0); - }); + // This shouldn't throw + const messages = linter.verify(code, { parser: "unknown-logical-operator" }, filename, true); - it("should not throw or report errors when the custom parser returns nested unrecognized operators (https://github.com/eslint/eslint/issues/10560)", () => { - const code = "foo && bar %% baz"; - const parser = path.join(parserFixtures, "unknown-operators", "unknown-logical-operator-nested.js"); + assert.strictEqual(messages.length, 0); + }); - // This shouldn't throw - const messages = linter.verify(code, { parser }, filename, true); + it("should not throw or report errors when the custom parser returns nested unrecognized operators (https://github.com/eslint/eslint/issues/10560)", () => { + const code = "foo && bar %% baz"; - assert.strictEqual(messages.length, 0); - }); + linter.defineParser("unknown-logical-operator-nested", testParsers.unknownLogicalOperatorNested); - it("should strip leading line: prefix from parser error", () => { - const parser = path.join(parserFixtures, "line-error.js"); - const messages = linter.verify(";", { parser }, "filename"); + // This shouldn't throw + const messages = linter.verify(code, { parser: "unknown-logical-operator-nested" }, filename, true); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + require(parser).expectedError); - }); - - it("should not modify a parser error message without a leading line: prefix", () => { - const parser = path.join(parserFixtures, "no-line-error.js"); - const messages = linter.verify(";", { parser }, "filename"); + assert.strictEqual(messages.length, 0); + }); - assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].severity, 2); - assert.strictEqual(messages[0].message, errorPrefix + require(parser).expectedError); - }); + it("should strip leading line: prefix from parser error", () => { + linter.defineParser("line-error", testParsers.lineError); + const messages = linter.verify(";", { parser: "line-error" }, "filename"); - describe("if a parser provides 'visitorKeys'", () => { - let types = []; - let scopeAnalyzeStub = null; - let sourceCode = null; + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.lineError.expectedError); + }); - beforeEach(() => { - scopeAnalyzeStub = sandbox.spy(escope, "analyze"); + it("should not modify a parser error message without a leading line: prefix", () => { + linter.defineParser("no-line-error", testParsers.noLineError); + const messages = linter.verify(";", { parser: "no-line-error" }, "filename"); - const parser = path.join(parserFixtures, "enhanced-parser2.js"); + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, errorPrefix + testParsers.noLineError.expectedError); + }); - types = []; - linter.defineRule("collect-node-types", () => ({ - "*"(node) { - types.push(node.type); - } - })); - linter.verify("@foo class A {}", { - parser, - rules: { - "collect-node-types": "error" - } - }); + describe("if a parser provides 'visitorKeys'", () => { + let types = []; + let sourceCode; + let scopeManager; - sourceCode = linter.getSourceCode(); - }); + beforeEach(() => { + types = []; + linter.defineRule("collect-node-types", () => ({ + "*"(node) { + types.push(node.type); + } + })); + linter.defineRule("save-scope-manager", context => { + scopeManager = context.getSourceCode().scopeManager; - it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { - assert.deepStrictEqual( - types, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); + return {}; }); - - it("eslint-scope should use the visitorKeys (so 'childVisitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { - assert(scopeAnalyzeStub.calledOnce); - assert.deepStrictEqual( - scopeAnalyzeStub.firstCall.args[1].childVisitorKeys.ClassDeclaration, - vk.unionWith({ ClassDeclaration: ["experimentalDecorators"] }).ClassDeclaration - ); + linter.defineParser("enhanced-parser2", testParsers.enhancedParser2); + linter.verify("@foo class A {}", { + parser: "enhanced-parser2", + rules: { + "collect-node-types": "error", + "save-scope-manager": "error" + } }); - it("should use the same visitorKeys if the source code object is reused", () => { - const types2 = []; - - linter.defineRule("collect-node-types", () => ({ - "*"(node) { - types2.push(node.type); - } - })); - linter.verify(sourceCode, { - rules: { - "collect-node-types": "error" - } - }); - - assert.deepStrictEqual( - types2, - ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] - ); - }); + sourceCode = linter.getSourceCode(); }); - describe("if a parser provides 'scope'", () => { - let scopeAnalyzeStub = null; - let scope = null; - let sourceCode = null; - - beforeEach(() => { - scopeAnalyzeStub = sandbox.spy(escope, "analyze"); + it("Traverser should use the visitorKeys (so 'types' includes 'Decorator')", () => { + assert.deepStrictEqual( + types, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); - const parser = path.join(parserFixtures, "enhanced-parser3.js"); + it("eslint-scope should use the visitorKeys (so 'childVisitorKeys.ClassDeclaration' includes 'experimentalDecorators')", () => { + assert.deepStrictEqual( + scopeManager.__options.childVisitorKeys.ClassDeclaration, // eslint-disable-line no-underscore-dangle + ["experimentalDecorators", "id", "superClass", "body"] + ); + }); - linter.defineRule("save-scope1", context => ({ - Program() { - scope = context.getScope(); - } - })); - linter.verify("@foo class A {}", { parser, rules: { "save-scope1": 2 } }); + it("should use the same visitorKeys if the source code object is reused", () => { + const types2 = []; - sourceCode = linter.getSourceCode(); + linter.defineRule("collect-node-types", () => ({ + "*"(node) { + types2.push(node.type); + } + })); + linter.verify(sourceCode, { + rules: { + "collect-node-types": "error" + } }); - it("should not use eslint-scope analyzer", () => { - assert(scopeAnalyzeStub.notCalled); - }); + assert.deepStrictEqual( + types2, + ["Program", "ClassDeclaration", "Decorator", "Identifier", "Identifier", "ClassBody"] + ); + }); + }); - it("should use the scope (so the global scope has the reference of '@foo')", () => { - assert.strictEqual(scope.references.length, 1); - assert.deepStrictEqual( - scope.references[0].identifier.name, - "foo" - ); - }); + describe("if a parser provides 'scope'", () => { + let scope = null; + let sourceCode = null; - it("should use the same scope if the source code object is reused", () => { - let scope2 = null; + beforeEach(() => { + linter.defineParser("enhanced-parser3", testParsers.enhancedParser3); + linter.defineRule("save-scope1", context => ({ + Program() { + scope = context.getScope(); + } + })); + linter.verify("@foo class A {}", { parser: "enhanced-parser3", rules: { "save-scope1": 2 } }); - linter.defineRule("save-scope2", context => ({ - Program() { - scope2 = context.getScope(); - } - })); - linter.verify(sourceCode, { rules: { "save-scope2": 2 } }, "test.js"); + sourceCode = linter.getSourceCode(); + }); - assert(scope2 !== null); - assert(scope2 === scope); - }); + it("should use the scope (so the global scope has the reference of '@foo')", () => { + assert.strictEqual(scope.references.length, 1); + assert.deepStrictEqual( + scope.references[0].identifier.name, + "foo" + ); }); - it("should not pass any default parserOptions to the parser", () => { - const parser = path.join(parserFixtures, "throws-with-options.js"); + it("should use the same scope if the source code object is reused", () => { + let scope2 = null; - const messages = linter.verify(";", { parser }, "filename"); + linter.defineRule("save-scope2", context => ({ + Program() { + scope2 = context.getScope(); + } + })); + linter.verify(sourceCode, { rules: { "save-scope2": 2 } }, "test.js"); - assert.strictEqual(messages.length, 0); + assert(scope2 !== null); + assert(scope2 === scope); }); }); - } + + it("should not pass any default parserOptions to the parser", () => { + linter.defineParser("throws-with-options", testParsers.throwsWithOptions); + const messages = linter.verify(";", { parser: "throws-with-options" }, "filename"); + + assert.strictEqual(messages.length, 0); + }); + }); }); diff --git a/tests/lib/testers/rule-tester.js b/tests/lib/testers/rule-tester.js index 5a582f97678c..ebe37bbfd73d 100644 --- a/tests/lib/testers/rule-tester.js +++ b/tests/lib/testers/rule-tester.js @@ -621,12 +621,12 @@ describe("RuleTester", () => { invalid: [ { code: "eval(foo)", - parser: "esprima", + parser: require.resolve("esprima"), errors: [{}] } ] }); - assert.strictEqual(spy.args[1][1].parser, "esprima"); + assert.strictEqual(spy.args[1][1].parser, require.resolve("esprima")); }); it("should prevent invalid options schemas", () => { diff --git a/tests/lib/util/module-resolver.js b/tests/lib/util/module-resolver.js deleted file mode 100644 index dea96dd742b7..000000000000 --- a/tests/lib/util/module-resolver.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @fileoverview Tests for ModuleResolver - * @author Nicholas C. Zakas - */ -"use strict"; - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const path = require("path"), - assert = require("chai").assert, - leche = require("leche"), - ModuleResolver = require("../../../lib/util/module-resolver"); - -//------------------------------------------------------------------------------ -// Data -//------------------------------------------------------------------------------ - -const FIXTURES_PATH = path.resolve(__dirname, "../../fixtures/module-resolver"); - -//------------------------------------------------------------------------------ -// Tests -//------------------------------------------------------------------------------ - -describe("ModuleResolver", () => { - - describe("resolve()", () => { - - leche.withData([ - - // resolve just like Node.js - ["leche", "", require.resolve("leche")], - - // resolve with a different location - ["foo", path.resolve(FIXTURES_PATH, "node_modules"), path.resolve(FIXTURES_PATH, "node_modules/foo.js")] - - ], (name, lookupPath, expected) => { - it("should find the correct location of a file", () => { - const resolver = new ModuleResolver(), - result = resolver.resolve(name, lookupPath); - - assert.strictEqual(result, expected); - }); - }); - - }); - -});