diff --git a/.travis.yml b/.travis.yml index 193b6b6b9..db060b4bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,41 +1,50 @@ language: node_js node_js: - - 4 - - 6 - - 8 + - '8' + - '6' + - '4' os: linux env: - - ESLINT_VERSION=2 - - ESLINT_VERSION=3 - ESLINT_VERSION=4 + - ESLINT_VERSION=3 + - ESLINT_VERSION=2 # osx backlog is often deep, so to be polite we can just hit these highlights matrix: include: - - os: osx - env: ESLINT_VERSION=2 + - env: PACKAGE=resolvers/node + node_js: 8 + - env: PACKAGE=resolvers/node + node_js: 6 + - env: PACKAGE=resolvers/node node_js: 4 - - os: osx - env: ESLINT_VERSION=3 + - env: PACKAGE=resolvers/webpack + node_js: 8 + - env: PACKAGE=resolvers/webpack node_js: 6 + - env: PACKAGE=resolvers/webpack + node_js: 4 - os: osx env: ESLINT_VERSION=4 node_js: 8 + - os: osx + env: ESLINT_VERSION=3 + node_js: 6 + - os: osx + env: ESLINT_VERSION=2 + node_js: 4 +before_install: + - 'nvm install-latest-npm' + - 'if [ -n "${PACKAGE-}" ]; then cd "${PACKAGE}"; fi' install: - - if [ ${TRAVIS_NODE_VERSION} == "4" ]; then - npm install -g npm@3; - fi - npm install - - npm install eslint@$ESLINT_VERSION --ignore-scripts || true - # install all resolver deps - - "for resolver in ./resolvers/*; do cd $resolver && npm install && cd ../..; done" + - npm install --no-save eslint@$ESLINT_VERSION --ignore-scripts || true script: - - "npm test" - - "for resolver in ./resolvers/*; do cd $resolver && npm test && cd ../..; done" + - 'npm test' after_success: - npm run coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index 718caef9d..a88cc01aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] - Autofixer for [`order`] rule ([#711], thanks [@tihonove]) +## [2.9.0] - 2018-02-21 +### Added +- Add [`group-exports`] rule: style-guide rule to report use of multiple named exports ([#721], thanks [@robertrossmann]) +- Add [`no-self-import`] rule: forbids a module from importing itself. ([#727], [#449], [#447], thanks [@giodamelio]). +- Add [`no-default-export`] rule ([#889], thanks [@isiahmeadows]) +- Add [`no-useless-path-segments`] rule ([#912], thanks [@graingert] and [@danny-andrews]) +- ... and more! check the commits for v[2.9.0] ## [2.8.0] - 2017-10-18 ### Added @@ -432,17 +439,24 @@ for info on changes for earlier releases. [`unambiguous`]: ./docs/rules/unambiguous.md [`no-anonymous-default-export`]: ./docs/rules/no-anonymous-default-export.md [`exports-last`]: ./docs/rules/exports-last.md +[`group-exports`]: ./docs/rules/group-exports.md +[`no-self-import`]: ./docs/rules/no-self-import.md +[`no-default-export`]: ./docs/rules/no-default-export.md +[`no-useless-path-segments`]: ./docs/rules/no-useless-path-segments.md [`memo-parser`]: ./memo-parser/README.md [#944]: https://github.com/benmosher/eslint-plugin-import/pull/944 [#891]: https://github.com/benmosher/eslint-plugin-import/pull/891 +[#889]: https://github.com/benmosher/eslint-plugin-import/pull/889 [#858]: https://github.com/benmosher/eslint-plugin-import/pull/858 [#843]: https://github.com/benmosher/eslint-plugin-import/pull/843 [#871]: https://github.com/benmosher/eslint-plugin-import/pull/871 [#744]: https://github.com/benmosher/eslint-plugin-import/pull/744 [#742]: https://github.com/benmosher/eslint-plugin-import/pull/742 [#737]: https://github.com/benmosher/eslint-plugin-import/pull/737 +[#727]: https://github.com/benmosher/eslint-plugin-import/pull/727 +[#721]: https://github.com/benmosher/eslint-plugin-import/pull/721 [#712]: https://github.com/benmosher/eslint-plugin-import/pull/712 [#696]: https://github.com/benmosher/eslint-plugin-import/pull/696 [#685]: https://github.com/benmosher/eslint-plugin-import/pull/685 @@ -466,6 +480,7 @@ for info on changes for earlier releases. [#489]: https://github.com/benmosher/eslint-plugin-import/pull/489 [#485]: https://github.com/benmosher/eslint-plugin-import/pull/485 [#461]: https://github.com/benmosher/eslint-plugin-import/pull/461 +[#449]: https://github.com/benmosher/eslint-plugin-import/pull/449 [#444]: https://github.com/benmosher/eslint-plugin-import/pull/444 [#428]: https://github.com/benmosher/eslint-plugin-import/pull/428 [#395]: https://github.com/benmosher/eslint-plugin-import/pull/395 @@ -500,13 +515,13 @@ for info on changes for earlier releases. [#164]: https://github.com/benmosher/eslint-plugin-import/pull/164 [#157]: https://github.com/benmosher/eslint-plugin-import/pull/157 [#314]: https://github.com/benmosher/eslint-plugin-import/pull/314 +[#912]: https://github.com/benmosher/eslint-plugin-import/pull/912 [#886]: https://github.com/benmosher/eslint-plugin-import/issues/886 [#863]: https://github.com/benmosher/eslint-plugin-import/issues/863 [#842]: https://github.com/benmosher/eslint-plugin-import/issues/842 [#839]: https://github.com/benmosher/eslint-plugin-import/issues/839 [#720]: https://github.com/benmosher/eslint-plugin-import/issues/720 -[#711]: https://github.com/benmosher/eslint-plugin-import/issues/711 [#686]: https://github.com/benmosher/eslint-plugin-import/issues/686 [#671]: https://github.com/benmosher/eslint-plugin-import/issues/671 [#660]: https://github.com/benmosher/eslint-plugin-import/issues/660 @@ -532,6 +547,7 @@ for info on changes for earlier releases. [#456]: https://github.com/benmosher/eslint-plugin-import/issues/456 [#453]: https://github.com/benmosher/eslint-plugin-import/issues/453 [#452]: https://github.com/benmosher/eslint-plugin-import/issues/452 +[#447]: https://github.com/benmosher/eslint-plugin-import/issues/447 [#441]: https://github.com/benmosher/eslint-plugin-import/issues/441 [#423]: https://github.com/benmosher/eslint-plugin-import/issues/423 [#416]: https://github.com/benmosher/eslint-plugin-import/issues/416 @@ -567,7 +583,8 @@ for info on changes for earlier releases. [#119]: https://github.com/benmosher/eslint-plugin-import/issues/119 [#89]: https://github.com/benmosher/eslint-plugin-import/issues/89 -[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v2.8.0...HEAD +[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v2.9.0...HEAD +[2.9.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.8.0...v2.9.0 [2.8.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.7.0...v2.8.0 [2.7.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.6.1...v2.7.0 [2.6.1]: https://github.com/benmosher/eslint-plugin-import/compare/v2.6.0...v2.6.1 @@ -664,3 +681,7 @@ for info on changes for earlier releases. [@rosswarren]: https://github.com/rosswarren [@alexgorbatchev]: https://github.com/alexgorbatchev [@tihonove]: https://github.com/tihonove +[@robertrossmann]: https://github.com/robertrossmann +[@isiahmeadows]: https://github.com/isiahmeadows +[@graingert]: https://github.com/graingert +[@danny-andrews]: https://github.com/dany-andrews diff --git a/README.md b/README.md index 11a779cfe..528e58db5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a ## Rules -**Static analysis:** +### Static analysis * Ensure imports point to a file/module that can be resolved. ([`no-unresolved`]) * Ensure named imports correspond to a named export in the remote file. ([`named`]) @@ -23,6 +23,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Forbid `require()` calls with expressions ([`no-dynamic-require`]) * Prevent importing the submodules of other modules ([`no-internal-modules`]) * Forbid Webpack loader syntax in imports ([`no-webpack-loader-syntax`]) +* Forbid a module from importing itself ([`no-self-import`]) [`no-unresolved`]: ./docs/rules/no-unresolved.md [`named`]: ./docs/rules/named.md @@ -33,8 +34,9 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-dynamic-require`]: ./docs/rules/no-dynamic-require.md [`no-internal-modules`]: ./docs/rules/no-internal-modules.md [`no-webpack-loader-syntax`]: ./docs/rules/no-webpack-loader-syntax.md +[`no-self-import`]: ./docs/rules/no-self-import.md -**Helpful warnings:** +### Helpful warnings * Report any invalid exports, i.e. re-export of the same name ([`export`]) @@ -51,7 +53,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md [`no-mutable-exports`]: ./docs/rules/no-mutable-exports.md -**Module systems:** +### Module systems * Report potentially ambiguous parse goal (`script` vs. `module`) ([`unambiguous`]) * Report CommonJS `require` calls and `module.exports` or `exports.*`. ([`no-commonjs`]) @@ -64,7 +66,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md -**Style guide:** +### Style guide * Ensure all imports appear before other statements ([`first`]) * Ensure all exports appear after other statements ([`exports-last`]) @@ -77,7 +79,9 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Limit the maximum number of dependencies a module can have ([`max-dependencies`]) * Forbid unassigned imports ([`no-unassigned-import`]) * Forbid named default exports ([`no-named-default`]) +* Forbid default exports ([`no-default-export`]) * Forbid anonymous values as default exports ([`no-anonymous-default-export`]) +* Prefer named exports to be grouped together in a single export declaration ([`group-exports`]) [`first`]: ./docs/rules/first.md [`exports-last`]: ./docs/rules/exports-last.md @@ -91,6 +95,8 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md [`no-named-default`]: ./docs/rules/no-named-default.md [`no-anonymous-default-export`]: ./docs/rules/no-anonymous-default-export.md +[`group-exports`]: ./docs/rules/group-exports.md +[`no-default-export`]: ./docs/rules/no-default-export.md ## Installation @@ -224,6 +230,19 @@ A list of file extensions that will be parsed as modules and inspected for This defaults to `['.js']`, unless you are using the `react` shared config, in which case it is specified as `['.js', '.jsx']`. +```js +"settings": { + "import/resolver": { + "node": { + "extensions": [ + ".js", + ".jsx" + ] + } + } +} +``` + Note that this is different from (and likely a subset of) any `import/resolver` extensions settings, which may include `.json`, `.coffee`, etc. which will still factor into the `no-unresolved` rule. diff --git a/config/recommended.js b/config/recommended.js index 79561271b..831c5bc29 100644 --- a/config/recommended.js +++ b/config/recommended.js @@ -3,6 +3,8 @@ * @type {Object} */ module.exports = { + plugins: ['import'], + rules: { // analysis/correctness 'import/no-unresolved': 'error', diff --git a/docs/rules/default.md b/docs/rules/default.md index bf40278f0..f69934468 100644 --- a/docs/rules/default.md +++ b/docs/rules/default.md @@ -1,4 +1,4 @@ -# default +# import/default If a default import is requested, this rule will report if there is no default export in the imported module. diff --git a/docs/rules/export.md b/docs/rules/export.md index f2bcb0e54..e99882be8 100644 --- a/docs/rules/export.md +++ b/docs/rules/export.md @@ -1,4 +1,4 @@ -# export +# import/export Reports funny business with exports, like repeated exports of names or defaults. diff --git a/docs/rules/exports-last.md b/docs/rules/exports-last.md index 22b654d2e..291daee48 100644 --- a/docs/rules/exports-last.md +++ b/docs/rules/exports-last.md @@ -1,4 +1,4 @@ -# exports-last +# import/exports-last This rule enforces that all exports are declared at the bottom of the file. This rule will report any export declarations that comes before any non-export statements. diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 6ca33bb44..6e1d2b50a 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -1,4 +1,4 @@ -# extensions - Ensure consistent use of file extension within the import path +# import/extensions - Ensure consistent use of file extension within the import path Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default. Depending on the resolver you can configure more extensions to get resolved automatically. @@ -6,11 +6,35 @@ In order to provide a consistent use of file extensions across your code base, t ## Rule Details -This rule either takes one string option, one object option, or a string and an object option. If it is the string `"never"` (the default value), then the rule forbids the use for any extension. If it is the string `"always"`, then the rule enforces the use of extensions for all import statements. +This rule either takes one string option, one object option, or a string and an object option. If it is the string `"never"` (the default value), then the rule forbids the use for any extension. If it is the string `"always"`, then the rule enforces the use of extensions for all import statements. If it is the string `"ignorePackages"`, then the rule enforces the use of extensions for all import statements except package imports. -By providing an object you can configure each extension separately, so for example `{ "js": "always", "json": "never" }` would always enforce the use of the `.js` extension but never allow the use of the `.json` extension. +``` +"import/extensions": [, "never" | "always" | "ignorePackages"] +``` + +By providing an object you can configure each extension separately. + +``` +"import/extensions": [, { + : "never" | "always" | "ignorePackages" +}] +``` + + For example `{ "js": "always", "json": "never" }` would always enforce the use of the `.js` extension but never allow the use of the `.json` extension. + +By providing both a string and an object, the string will set the default setting for all extensions, and the object can be used to set granular overrides for specific extensions. + +``` +"import/extensions": [ + , + "never" | "always" | "ignorePackages", + { + : "never" | "always" | "ignorePackages" + } +] +``` -By providing both a string and an object, the string will set the default setting for all extensions, and the object can be used to set granular overrides for specific extensions. For example, `[, "never", { "svg": "always" }]` would require that all extensions are omitted, except for "svg". +For example, `["error", "never", { "svg": "always" }]` would require that all extensions are omitted, except for "svg". ### Exception @@ -41,7 +65,7 @@ import foo from './foo.js'; import bar from './bar.json'; -import Component from './Component.jsx' +import Component from './Component.jsx'; import express from 'express/index.js'; ``` @@ -53,7 +77,7 @@ import foo from './foo'; import bar from './bar'; -import Component from './Component' +import Component from './Component'; import express from 'express/index'; @@ -67,7 +91,7 @@ import foo from './foo'; import bar from './bar'; -import Component from './Component' +import Component from './Component'; import express from 'express'; ``` @@ -79,13 +103,48 @@ import foo from './foo.js'; import bar from './bar.json'; -import Component from './Component.jsx' +import Component from './Component.jsx'; import express from 'express/index.js'; import * as path from 'path'; ``` +The following patterns are considered problems when configuration set to "ignorePackages": + +```js +import foo from './foo'; + +import bar from './bar'; + +import Component from './Component'; + +``` + +The following patterns are not considered problems when configuration set to "ignorePackages": + +```js +import foo from './foo.js'; + +import bar from './bar.json'; + +import Component from './Component.jsx'; + +import express from 'express'; + +``` + +The following patterns are not considered problems when configuration set to `['error', 'always', {ignorePackages: true} ]`: + +```js +import Component from './Component.jsx'; + +import baz from 'foo/baz.js'; + +import express from 'express'; + +``` + ## When Not To Use It If you are not concerned about a consistent usage of file extension. diff --git a/docs/rules/first.md b/docs/rules/first.md index fe84b89f2..95acd0152 100644 --- a/docs/rules/first.md +++ b/docs/rules/first.md @@ -1,4 +1,4 @@ -# first +# import/first This rule reports any imports that come after non-import statements. diff --git a/docs/rules/group-exports.md b/docs/rules/group-exports.md new file mode 100644 index 000000000..b0d88f4a0 --- /dev/null +++ b/docs/rules/group-exports.md @@ -0,0 +1,87 @@ +# import/group-exports + +Reports when named exports are not grouped together in a single `export` declaration or when multiple assignments to CommonJS `module.exports` or `exports` object are present in a single file. + +**Rationale:** An `export` declaration or `module.exports` assignment can appear anywhere in the code. By requiring a single export declaration all your exports will remain at one place, making it easier to see what exports a module provides. + +## Rule Details + +This rule warns whenever a single file contains multiple named export declarations or multiple assignments to `module.exports` (or `exports`). + +### Valid + +```js +// A single named export declaration -> ok +export const valid = true +``` + +```js +const first = true +const second = true + +// A single named export declaration -> ok +export { + first, + second, +} +``` + +```js +// A single exports assignment -> ok +module.exports = { + first: true, + second: true +} +``` + +```js +const first = true +const second = true + +// A single exports assignment -> ok +module.exports = { + first, + second, +} +``` + +```js +function test() {} +test.property = true +test.another = true + +// A single exports assignment -> ok +module.exports = test +``` + + +### Invalid + +```js +// Multiple named export statements -> not ok! +export const first = true +export const second = true +``` + +```js +// Multiple exports assignments -> not ok! +exports.first = true +exports.second = true +``` + +```js +// Multiple exports assignments -> not ok! +module.exports = {} +module.exports.first = true +``` + +```js +// Multiple exports assignments -> not ok! +module.exports = () => {} +module.exports.first = true +module.exports.second = true +``` + +## When Not To Use It + +If you do not mind having your exports spread across the file, you can safely turn this rule off. diff --git a/docs/rules/max-dependencies.md b/docs/rules/max-dependencies.md index f4aa2a57a..20d29cf0e 100644 --- a/docs/rules/max-dependencies.md +++ b/docs/rules/max-dependencies.md @@ -1,4 +1,4 @@ -# max-dependencies +# import/max-dependencies Forbid modules to have too many dependencies (`import` or `require` statements). diff --git a/docs/rules/named.md b/docs/rules/named.md index eeb84945a..6dc7c60e5 100644 --- a/docs/rules/named.md +++ b/docs/rules/named.md @@ -1,4 +1,4 @@ -# named +# import/named Verifies that all named imports are part of the set of named exports in the referenced module. diff --git a/docs/rules/namespace.md b/docs/rules/namespace.md index 9716c90d0..4bbbd378e 100644 --- a/docs/rules/namespace.md +++ b/docs/rules/namespace.md @@ -1,4 +1,4 @@ -# namespace +# import/namespace Enforces names exist at the time they are dereferenced, when imported as a full namespace (i.e. `import * as foo from './foo'; foo.bar();` will report if `bar` is not exported by `./foo`.). diff --git a/docs/rules/newline-after-import.md b/docs/rules/newline-after-import.md index 8c5d5760f..4883776c9 100644 --- a/docs/rules/newline-after-import.md +++ b/docs/rules/newline-after-import.md @@ -1,4 +1,4 @@ -# newline-after-import +# import/newline-after-import Enforces having one or more empty lines after the last top-level import statement or require call. +(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule. @@ -73,9 +73,8 @@ const FOO = 'BAR' ## Example options usage -``` +```json { - ... "rules": { "import/newline-after-import": ["error", { "count": 2 }] } diff --git a/docs/rules/no-absolute-path.md b/docs/rules/no-absolute-path.md index aa9cf142d..305e8e605 100644 --- a/docs/rules/no-absolute-path.md +++ b/docs/rules/no-absolute-path.md @@ -1,4 +1,4 @@ -# Forbid import of modules using absolute paths +# import/no-absolute-path: Forbid import of modules using absolute paths Node.js allows the import of modules using an absolute path such as `/home/xyz/file.js`. That is a bad practice as it ties the code using it to your computer, and therefore makes it unusable in packages distributed on `npm` for instance. diff --git a/docs/rules/no-amd.md b/docs/rules/no-amd.md index 515a87637..f7146c134 100644 --- a/docs/rules/no-amd.md +++ b/docs/rules/no-amd.md @@ -1,4 +1,4 @@ -# no-amd +# import/no-amd Reports `require([array], ...)` and `define([array], ...)` function calls at the module scope. Will not report if !=2 arguments, or first argument is not a literal array. diff --git a/docs/rules/no-anonymous-default-export.md b/docs/rules/no-anonymous-default-export.md index 205cb5ea8..c8db89790 100644 --- a/docs/rules/no-anonymous-default-export.md +++ b/docs/rules/no-anonymous-default-export.md @@ -1,4 +1,4 @@ -# no-anonymous-default-export +# import/no-anonymous-default-export Reports if a module's default export is unnamed. This includes several types of unnamed data types; literals, object expressions, arrays, anonymous functions, arrow functions, and anonymous class declarations. @@ -16,6 +16,7 @@ The complete default configuration looks like this. "allowArrowFunction": false, "allowAnonymousClass": false, "allowAnonymousFunction": false, + "allowCallExpression": true, // The true value here is for backward compatibility "allowLiteral": false, "allowObject": false }] @@ -33,6 +34,9 @@ export default class {} export default function () {} +/* eslint import/no-anonymous-default-export: [2, {"allowCallExpression": false}] */ +export default foo(bar) + export default 123 export default {} @@ -59,6 +63,8 @@ export default class {} /* eslint import/no-anonymous-default-export: [2, {"allowAnonymousFunction": true}] */ export default function () {} +export default foo(bar) + /* eslint import/no-anonymous-default-export: [2, {"allowLiteral": true}] */ export default 123 diff --git a/docs/rules/no-commonjs.md b/docs/rules/no-commonjs.md index a4259ef7f..4e823502b 100644 --- a/docs/rules/no-commonjs.md +++ b/docs/rules/no-commonjs.md @@ -1,4 +1,4 @@ -# no-commonjs +# import/no-commonjs Reports `require([string])` function calls. Will not report if >1 argument, or single argument is not a literal string. diff --git a/docs/rules/no-default-export.md b/docs/rules/no-default-export.md new file mode 100644 index 000000000..dc026b00a --- /dev/null +++ b/docs/rules/no-default-export.md @@ -0,0 +1,63 @@ +# no-default-export + +Prohibit default exports. Mostly an inverse of [`prefer-default-export`]. + +[`prefer-default-export`]: ./prefer-default-export.md + +## Rule Details + +The following patterns are considered warnings: + +```javascript +// bad1.js + +// There is a default export. +export const foo = 'foo'; +const bar = 'bar'; +export default 'bar'; +``` + +```javascript +// bad2.js + +// There is a default export. +const foo = 'foo'; +export { foo as default } +``` + +The following patterns are not warnings: + +```javascript +// good1.js + +// There is only a single module export and it's a named export. +export const foo = 'foo'; +``` + +```javascript +// good2.js + +// There is more than one named export in the module. +export const foo = 'foo'; +export const bar = 'bar'; +``` + +```javascript +// good3.js + +// There is more than one named export in the module +const foo = 'foo'; +const bar = 'bar'; +export { foo, bar } +``` + +```javascript +// export-star.js + +// Any batch export will disable this rule. The remote module is not inspected. +export * from './other-module' +``` + +## When Not To Use It + +If you don't care if default imports are used, or if you prefer default imports over named imports. diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md index 989534ac6..7583651f3 100644 --- a/docs/rules/no-deprecated.md +++ b/docs/rules/no-deprecated.md @@ -1,4 +1,4 @@ -# no-deprecated +# import/no-deprecated **Stage: 0** diff --git a/docs/rules/no-duplicates.md b/docs/rules/no-duplicates.md index 86260a6ff..580f36011 100644 --- a/docs/rules/no-duplicates.md +++ b/docs/rules/no-duplicates.md @@ -1,4 +1,4 @@ -# no-duplicates +# import/no-duplicates Reports if a resolved path is imported more than once. diff --git a/docs/rules/no-dynamic-require.md b/docs/rules/no-dynamic-require.md index c48f27202..0f7bb6d37 100644 --- a/docs/rules/no-dynamic-require.md +++ b/docs/rules/no-dynamic-require.md @@ -1,4 +1,4 @@ -# Forbid `require()` calls with expressions +# import/no-dynamic-require: Forbid `require()` calls with expressions The `require` method from CommonJS is used to import modules from different files. Unlike the ES6 `import` syntax, it can be given expressions that will be resolved at runtime. While this is sometimes necessary and useful, in most cases it isn't. Using expressions (for instance, concatenating a path and variable) as the argument makes it harder for tools to do static code analysis, or to find where in the codebase a module is used. diff --git a/docs/rules/no-extraneous-dependencies.md b/docs/rules/no-extraneous-dependencies.md index 36aef8f9d..069dab0ce 100644 --- a/docs/rules/no-extraneous-dependencies.md +++ b/docs/rules/no-extraneous-dependencies.md @@ -1,8 +1,10 @@ -# Forbid the use of extraneous packages +# import/no-extraneous-dependencies: Forbid the use of extraneous packages Forbid the import of external modules that are not declared in the `package.json`'s `dependencies`, `devDependencies`, `optionalDependencies` or `peerDependencies`. The closest parent `package.json` will be used. If no `package.json` is found, the rule will not lint anything. This behaviour can be changed with the rule option `packageDir`. +Modules have to be installed for this rule to work. + ### Options This rule supports the following options: diff --git a/docs/rules/no-internal-modules.md b/docs/rules/no-internal-modules.md index 1d8fc56d2..8d99c3529 100644 --- a/docs/rules/no-internal-modules.md +++ b/docs/rules/no-internal-modules.md @@ -1,4 +1,4 @@ -# no-internal-modules +# import/no-internal-modules Use this rule to prevent importing the submodules of other modules. diff --git a/docs/rules/no-mutable-exports.md b/docs/rules/no-mutable-exports.md index 7667fbe8f..e161e87b1 100644 --- a/docs/rules/no-mutable-exports.md +++ b/docs/rules/no-mutable-exports.md @@ -1,4 +1,4 @@ -# no-mutable-exports +# import/no-mutable-exports Forbids the use of mutable exports with `var` or `let`. diff --git a/docs/rules/no-named-as-default-member.md b/docs/rules/no-named-as-default-member.md index 26770ad8b..b6fdb13dd 100644 --- a/docs/rules/no-named-as-default-member.md +++ b/docs/rules/no-named-as-default-member.md @@ -1,4 +1,4 @@ -# no-named-as-default-member +# import/no-named-as-default-member Reports use of an exported name as a property on the default export. diff --git a/docs/rules/no-named-as-default.md b/docs/rules/no-named-as-default.md index ab0f31d0c..0a92b7b51 100644 --- a/docs/rules/no-named-as-default.md +++ b/docs/rules/no-named-as-default.md @@ -1,4 +1,4 @@ -# no-named-as-default +# import/no-named-as-default Reports use of an exported name as the locally imported name of a default export. diff --git a/docs/rules/no-named-default.md b/docs/rules/no-named-default.md index 651ca6143..86fb41d61 100644 --- a/docs/rules/no-named-default.md +++ b/docs/rules/no-named-default.md @@ -1,4 +1,4 @@ -# no-named-default +# import/no-named-default Reports use of a default export as a locally named import. diff --git a/docs/rules/no-namespace.md b/docs/rules/no-namespace.md index e6b2db5b0..b308d6621 100644 --- a/docs/rules/no-namespace.md +++ b/docs/rules/no-namespace.md @@ -1,4 +1,4 @@ -# no-namespace +# import/no-namespace Reports if namespace import is used. diff --git a/docs/rules/no-nodejs-modules.md b/docs/rules/no-nodejs-modules.md index 59f1e3693..225adab22 100644 --- a/docs/rules/no-nodejs-modules.md +++ b/docs/rules/no-nodejs-modules.md @@ -1,4 +1,4 @@ -# No Node.js builtin modules +# import/no-nodejs-modules: No Node.js builtin modules Forbid the use of Node.js builtin modules. Can be useful for client-side web projects that do not have access to those modules. diff --git a/docs/rules/no-restricted-paths.md b/docs/rules/no-restricted-paths.md index 04a2e7039..bad65ab8e 100644 --- a/docs/rules/no-restricted-paths.md +++ b/docs/rules/no-restricted-paths.md @@ -1,4 +1,4 @@ -# no-restricted-paths - Restrict which files can be imported in a given folder +# import/no-restricted-paths: Restrict which files can be imported in a given folder Some projects contain files which are not always meant to be executed in the same environment. For example consider a web application that contains specific code for the server and some specific code for the browser/client. In this case you don’t want to import server-only files in your client code. diff --git a/docs/rules/no-self-import.md b/docs/rules/no-self-import.md new file mode 100644 index 000000000..089f5e029 --- /dev/null +++ b/docs/rules/no-self-import.md @@ -0,0 +1,30 @@ +# Forbid a module from importing itself + +Forbid a module from importing itself. This can sometimes happen during refactoring. + +## Rule Details + +### Fail + +```js +// foo.js +import foo from './foo'; + +const foo = require('./foo'); +``` + +```js +// index.js +import index from '.'; + +const index = require('.'); +``` + +### Pass + +```js +// foo.js +import bar from './bar'; + +const bar = require('./bar'); +``` diff --git a/docs/rules/no-unassigned-import.md b/docs/rules/no-unassigned-import.md index 85f9b7c3a..fb3065c48 100644 --- a/docs/rules/no-unassigned-import.md +++ b/docs/rules/no-unassigned-import.md @@ -1,4 +1,4 @@ -# Forbid unassigned imports +# import/no-unassigned-import: Forbid unassigned imports With both CommonJS' `require` and the ES6 modules' `import` syntax, it is possible to import a module but not to use its result. This can be done explicitly by not assigning the module to as variable. Doing so can mean either of the following things: - The module is imported but not used diff --git a/docs/rules/no-unresolved.md b/docs/rules/no-unresolved.md index 5c90528e4..30cd8cb2b 100644 --- a/docs/rules/no-unresolved.md +++ b/docs/rules/no-unresolved.md @@ -1,4 +1,4 @@ -# no-unresolved +# import/no-unresolved Ensures an imported module can be resolved to a module on the local filesystem, as defined by standard Node `require.resolve` behavior. diff --git a/docs/rules/no-webpack-loader-syntax.md b/docs/rules/no-webpack-loader-syntax.md index a26012737..37b39a432 100644 --- a/docs/rules/no-webpack-loader-syntax.md +++ b/docs/rules/no-webpack-loader-syntax.md @@ -1,4 +1,4 @@ -# no-webpack-loader-syntax +# import/no-webpack-loader-syntax Forbid Webpack loader syntax in imports. diff --git a/docs/rules/order.md b/docs/rules/order.md index 42cd00026..45bde6acc 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -1,11 +1,11 @@ -# Enforce a convention in module import order +# import/order: Enforce a convention in module import order Enforce a convention in the order of `require()` / `import` statements. +(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule. The order is as shown in the following example: ```js -// 1. node "builtins" +// 1. node "builtin" modules import fs from 'fs'; import path from 'path'; // 2. "external" modules diff --git a/docs/rules/prefer-default-export.md b/docs/rules/prefer-default-export.md index d3b6fbb0d..23e584bcc 100644 --- a/docs/rules/prefer-default-export.md +++ b/docs/rules/prefer-default-export.md @@ -1,4 +1,4 @@ -# prefer-default-export +# import/prefer-default-export When there is only a single export from a module, prefer using default export over named export. diff --git a/docs/rules/unambiguous.md b/docs/rules/unambiguous.md index c8cffcf5e..7955c3fbc 100644 --- a/docs/rules/unambiguous.md +++ b/docs/rules/unambiguous.md @@ -1,4 +1,4 @@ -# unambiguous +# import/unambiguous Warn if a `module` could be mistakenly parsed as a `script` by a consumer leveraging [Unambiguous JavaScript Grammar] to determine correct parsing goal. @@ -51,4 +51,4 @@ a `module`. - [node-eps#13](https://github.com/nodejs/node-eps/issues/13) [`parserOptions.sourceType`]: http://eslint.org/docs/user-guide/configuring#specifying-parser-options -[Unambiguous JavaScript Grammar]: https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md#51-determining-if-source-is-an-es-module +[Unambiguous JavaScript Grammar]: https://github.com/nodejs/node-eps/blob/master/002-es-modules.md#32-determining-if-source-is-an-es-module diff --git a/package.json b/package.json index c101787c5..6282f6c2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "2.8.0", + "version": "2.9.0", "description": "Import with sanity.", "engines": { "node": ">=4" @@ -58,9 +58,11 @@ "cross-env": "^4.0.0", "eslint": "2.x - 4.x", "eslint-import-resolver-node": "file:./resolvers/node", + "eslint-import-resolver-typescript": "^1.0.2", "eslint-import-resolver-webpack": "file:./resolvers/webpack", "eslint-module-utils": "file:./utils", "eslint-plugin-import": "2.x", + "eslint-plugin-typescript": "^0.8.1", "gulp": "^3.9.0", "gulp-babel": "6.1.2", "istanbul": "^0.4.0", @@ -68,10 +70,10 @@ "mocha": "^3.1.2", "nyc": "^8.3.0", "redux": "^3.0.4", - "rimraf": "2.5.2", + "rimraf": "^2.6.2", "sinon": "^2.3.2", - "typescript": "^2.0.3", - "typescript-eslint-parser": "^2.1.0" + "typescript": "^2.6.2", + "typescript-eslint-parser": "^12.0.0" }, "peerDependencies": { "eslint": "2.x - 4.x" @@ -84,7 +86,7 @@ "eslint-import-resolver-node": "^0.3.1", "eslint-module-utils": "^2.1.1", "has": "^1.0.1", - "lodash.cond": "^4.3.0", + "lodash": "^4.17.4", "minimatch": "^3.0.3", "read-pkg-up": "^2.0.0" }, diff --git a/resolvers/README.md b/resolvers/README.md index 09f87e7cd..05ef4ef32 100644 --- a/resolvers/README.md +++ b/resolvers/README.md @@ -49,7 +49,7 @@ the absolute path to the file making the import (`/some/path/to/module.js`) ##### `config` -an object provided via the `import/resolver` setting.`my-cool-resolver` will get `["some", "stuff"]` as its `config`, while +an object provided via the `import/resolver` setting. `my-cool-resolver` will get `["some", "stuff"]` as its `config`, while `node` will get `{ "paths": ["a", "b", "c"] }` provided as `config`. #### Return value diff --git a/resolvers/node/.npmrc b/resolvers/node/.npmrc new file mode 100644 index 000000000..43c97e719 --- /dev/null +++ b/resolvers/node/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/resolvers/node/CHANGELOG.md b/resolvers/node/CHANGELOG.md index 3960a12d4..f0d2358ba 100644 --- a/resolvers/node/CHANGELOG.md +++ b/resolvers/node/CHANGELOG.md @@ -4,9 +4,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## Unreleased + +## v0.3.2 - 2018-01-05 ### Added - `.mjs` extension detected by default to support `experimental-modules` (#939) +### Deps +- update `debug`, `resolve` + ## v0.3.1 - 2017-06-23 ### Changed - bumped `debug` dep to match other packages diff --git a/resolvers/node/package.json b/resolvers/node/package.json index 7143f3fa5..c5859833c 100644 --- a/resolvers/node/package.json +++ b/resolvers/node/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-node", - "version": "0.3.1", + "version": "0.3.2", "description": "Node default behavior import resolution plugin for eslint-plugin-import.", "main": "index.js", "files": [ @@ -27,12 +27,12 @@ }, "homepage": "https://github.com/benmosher/eslint-plugin-import", "dependencies": { - "debug": "^2.6.8", - "resolve": "^1.2.0" + "debug": "^2.6.9", + "resolve": "^1.5.0" }, "devDependencies": { - "chai": "^3.4.1", - "mocha": "^2.3.4", - "nyc": "^7.0.0" + "chai": "^3.5.0", + "mocha": "^3.5.3", + "nyc": "^10.3.2" } } diff --git a/resolvers/webpack/.babelrc b/resolvers/webpack/.babelrc new file mode 120000 index 000000000..bdc08253c --- /dev/null +++ b/resolvers/webpack/.babelrc @@ -0,0 +1 @@ +../../.babelrc \ No newline at end of file diff --git a/resolvers/webpack/.npmrc b/resolvers/webpack/.npmrc new file mode 120000 index 000000000..cba44bb38 --- /dev/null +++ b/resolvers/webpack/.npmrc @@ -0,0 +1 @@ +../../.npmrc \ No newline at end of file diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index 7b8f9a662..1ebc0d351 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -4,6 +4,12 @@ This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## Unreleased +### Breaking (?) +- Fix with `pnpm` ([#968]) + +## 0.8.4 - 2018-01-05 +### Changed +- allow newer version of node-libs-browser ([#969]) ## 0.8.3 - 2017-06-23 ### Changed @@ -88,6 +94,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - `interpret` configs (such as `.babel.js`). Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). +[#969]: https://github.com/benmosher/eslint-plugin-import/pull/969 +[#968]: https://github.com/benmosher/eslint-plugin-import/pull/968 [#683]: https://github.com/benmosher/eslint-plugin-import/pull/683 [#572]: https://github.com/benmosher/eslint-plugin-import/pull/572 [#569]: https://github.com/benmosher/eslint-plugin-import/pull/569 diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index 42f9e9828..e1ffe70d1 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -47,6 +47,7 @@ exports.resolve = function (source, file, settings) { var configPath = get(settings, 'config') , configIndex = get(settings, 'config-index') + , env = get(settings, 'env') , packageDir log('Config path from settings:', configPath) @@ -82,7 +83,7 @@ exports.resolve = function (source, file, settings) { } if (typeof webpackConfig === 'function') { - webpackConfig = webpackConfig() + webpackConfig = webpackConfig(env) } if (Array.isArray(webpackConfig)) { @@ -122,8 +123,8 @@ function createResolveSync(configPath, webpackConfig) { } try { - var webpackFilename = resolve.sync('webpack', { basedir }) - var webpackResolveOpts = { basedir: path.dirname(webpackFilename) } + var webpackFilename = resolve.sync('webpack', { basedir, preserveSymlinks: false }) + var webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false } webpackRequire = function (id) { return require(resolve.sync(id, webpackResolveOpts)) diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 92bad18a8..c4e87f316 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-webpack", - "version": "0.8.3", + "version": "0.8.4", "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", "main": "index.js", "scripts": { @@ -32,13 +32,13 @@ "array-find": "^1.0.0", "debug": "^2.6.8", "enhanced-resolve": "~0.9.0", - "find-root": "^0.1.1", + "find-root": "^1.1.0", "has": "^1.0.1", "interpret": "^1.0.0", "is-absolute": "^0.2.3", - "lodash.get": "^3.7.0", - "node-libs-browser": "^1.0.0", - "resolve": "^1.2.0", + "lodash.get": "^4.4.2", + "node-libs-browser": "^1.0.0 || ^2.0.0", + "resolve": "^1.4.0", "semver": "^5.3.0" }, "peerDependencies": { @@ -46,9 +46,11 @@ "webpack": ">=1.11.0" }, "devDependencies": { + "babel-plugin-istanbul": "^4.1.5", + "babel-preset-es2015-argon": "^0.1.0", + "babel-register": "^6.26.0", "chai": "^3.4.1", "mocha": "^2.3.3", - "nyc": "^7.0.0", - "babel-preset-es2015-argon": "latest" + "nyc": "^7.0.0" } } diff --git a/resolvers/webpack/test/config.js b/resolvers/webpack/test/config.js index 42c5eca1f..2519daf8a 100644 --- a/resolvers/webpack/test/config.js +++ b/resolvers/webpack/test/config.js @@ -91,4 +91,16 @@ describe("config", function () { .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'foo.js')) }) + it('finds the config at option env when config is a function', function() { + var settings = { + config: require(path.join(__dirname, './files/webpack.function.config.js')), + env: { + dummy: true, + }, + } + + expect(resolve('bar', file, settings)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'bar.js')) + }) + }) diff --git a/resolvers/webpack/test/files/some/goofy/path/bar.js b/resolvers/webpack/test/files/some/goofy/path/bar.js new file mode 100644 index 000000000..e69de29bb diff --git a/resolvers/webpack/test/files/webpack.function.config.js b/resolvers/webpack/test/files/webpack.function.config.js index 7f07afda6..ce87dd1b1 100644 --- a/resolvers/webpack/test/files/webpack.function.config.js +++ b/resolvers/webpack/test/files/webpack.function.config.js @@ -1,11 +1,12 @@ var path = require('path') var pluginsTest = require('webpack-resolver-plugin-test') -module.exports = function() { +module.exports = function(env) { return { resolve: { alias: { 'foo': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), + 'bar': env ? path.join(__dirname, 'some', 'goofy', 'path', 'bar.js') : undefined, 'some-alias': path.join(__dirname, 'some'), }, modules: [ diff --git a/src/ExportMap.js b/src/ExportMap.js index c330c4f1d..aefec2ba3 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -409,6 +409,10 @@ ExportMap.parse = function (path, content, context) { case 'ClassDeclaration': case 'TypeAlias': // flowtype with babel-eslint parser case 'InterfaceDeclaration': + case 'TSEnumDeclaration': + case 'TSInterfaceDeclaration': + case 'TSAbstractClassDeclaration': + case 'TSModuleDeclaration': m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) break case 'VariableDeclaration': diff --git a/src/core/importType.js b/src/core/importType.js index d663a1a87..e0941b46b 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -1,4 +1,4 @@ -import cond from 'lodash.cond' +import cond from 'lodash/cond' import builtinModules from 'builtin-modules' import { join } from 'path' @@ -37,11 +37,21 @@ function isExternalModule(name, settings, path) { return externalModuleRegExp.test(name) && isExternalPath(path, name, settings) } +const externalModuleMainRegExp = /^[\w]((?!\/).)*$/ +export function isExternalModuleMain(name, settings, path) { + return externalModuleMainRegExp.test(name) && isExternalPath(path, name, settings) +} + const scopedRegExp = /^@[^/]+\/[^/]+/ function isScoped(name) { return scopedRegExp.test(name) } +const scopedMainRegExp = /^@[^/]+\/?[^/]+$/ +export function isScopedMain(name) { + return scopedMainRegExp.test(name) +} + function isInternalModule(name, settings, path) { return externalModuleRegExp.test(name) && !isExternalPath(path, name, settings) } diff --git a/src/docsUrl.js b/src/docsUrl.js new file mode 100644 index 000000000..3c01c49ad --- /dev/null +++ b/src/docsUrl.js @@ -0,0 +1,7 @@ +import pkg from '../package.json' + +const repoUrl = 'https://github.com/benmosher/eslint-plugin-import' + +export default function docsUrl(ruleName, commitish = `v${pkg.version}`) { + return `${repoUrl}/blob/${commitish}/docs/rules/${ruleName}.md` +} diff --git a/src/index.js b/src/index.js index e5b36b8f5..6c5e11252 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,9 @@ export const rules = { 'extensions': require('./rules/extensions'), 'no-restricted-paths': require('./rules/no-restricted-paths'), 'no-internal-modules': require('./rules/no-internal-modules'), + 'group-exports': require('./rules/group-exports'), + 'no-self-import': require('./rules/no-self-import'), 'no-named-default': require('./rules/no-named-default'), 'no-named-as-default': require('./rules/no-named-as-default'), 'no-named-as-default-member': require('./rules/no-named-as-default-member'), @@ -27,9 +29,11 @@ export const rules = { 'order': require('./rules/order'), 'newline-after-import': require('./rules/newline-after-import'), 'prefer-default-export': require('./rules/prefer-default-export'), + 'no-default-export': require('./rules/no-default-export'), 'no-dynamic-require': require('./rules/no-dynamic-require'), 'unambiguous': require('./rules/unambiguous'), 'no-unassigned-import': require('./rules/no-unassigned-import'), + 'no-useless-path-segments': require('./rules/no-useless-path-segments'), // export 'exports-last': require('./rules/exports-last'), diff --git a/src/rules/default.js b/src/rules/default.js index 28e03fc16..83c0ea95e 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,8 +1,11 @@ import Exports from '../ExportMap' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('default'), + }, }, create: function (context) { diff --git a/src/rules/export.js b/src/rules/export.js index f7f37305e..f6adf0ae8 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,8 +1,11 @@ import ExportMap, { recursivePatternCapture } from '../ExportMap' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('export'), + }, }, create: function (context) { diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js index 91af6b421..2d74ab5f3 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.js @@ -1,3 +1,5 @@ +import docsUrl from '../docsUrl' + function isNonExportStatement({ type }) { return type !== 'ExportDefaultDeclaration' && type !== 'ExportNamedDeclaration' && @@ -5,6 +7,12 @@ function isNonExportStatement({ type }) { } module.exports = { + meta: { + docs: { + url: docsUrl('exports-last'), + }, + }, + create: function (context) { return { Program: function ({ body }) { diff --git a/src/rules/extensions.js b/src/rules/extensions.js index 2036ba055..d50bd0ce8 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -1,18 +1,63 @@ import path from 'path' -import has from 'has' import resolve from 'eslint-module-utils/resolve' -import { isBuiltIn } from '../core/importType' +import { isBuiltIn, isExternalModuleMain, isScopedMain } from '../core/importType' +import docsUrl from '../docsUrl' -const enumValues = { enum: [ 'always', 'never' ] } +const enumValues = { enum: [ 'always', 'ignorePackages', 'never' ] } const patternProperties = { type: 'object', patternProperties: { '.*': enumValues }, } +const properties = { + type: 'object', + properties: { + 'pattern': patternProperties, + 'ignorePackages': { type: 'boolean' }, + }, +} + +function buildProperties(context) { + + const result = { + defaultConfig: 'never', + pattern: {}, + ignorePackages: false, + } + + context.options.forEach(obj => { + + // If this is a string, set defaultConfig to its value + if (typeof obj === 'string') { + result.defaultConfig = obj + return + } + + // If this is not the new structure, transfer all props to result.pattern + if (obj.pattern === undefined && obj.ignorePackages === undefined) { + Object.assign(result.pattern, obj) + return + } + + // If pattern is provided, transfer all props + if (obj.pattern !== undefined) { + Object.assign(result.pattern, obj.pattern) + } + + // If ignorePackages is provided, transfer it to result + if (obj.ignorePackages !== undefined) { + result.ignorePackages = obj.ignorePackages + } + }) + + return result +} module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('extensions'), + }, schema: { anyOf: [ @@ -21,6 +66,19 @@ module.exports = { items: [enumValues], additionalItems: false, }, + { + type: 'array', + items: [ + enumValues, + properties, + ], + additionalItems: false, + }, + { + type: 'array', + items: [properties], + additionalItems: false, + }, { type: 'array', items: [patternProperties], @@ -39,21 +97,19 @@ module.exports = { }, create: function (context) { - const configuration = context.options[0] || 'never' - const defaultConfig = typeof configuration === 'string' ? configuration : null - const modifiers = Object.assign( - {}, - typeof configuration === 'object' ? configuration : context.options[1] - ) - - function isUseOfExtensionRequired(extension) { - if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } - return modifiers[extension] === 'always' + + const props = buildProperties(context) + + function getModifier(extension) { + return props.pattern[extension] || props.defaultConfig + } + + function isUseOfExtensionRequired(extension, isPackageMain) { + return getModifier(extension) === 'always' && (!props.ignorePackages || !isPackageMain) } function isUseOfExtensionForbidden(extension) { - if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } - return modifiers[extension] === 'never' + return getModifier(extension) === 'never' } function isResolvableWithoutExtension(file) { @@ -66,6 +122,10 @@ module.exports = { function checkFileExtension(node) { const { source } = node + + // bail if the declaration doesn't have a source, e.g. "export { foo };" + if (!source) return + const importPath = source.value // don't enforce anything on builtins @@ -77,8 +137,14 @@ module.exports = { // for unresolved, use source value. const extension = path.extname(resolvedPath || importPath).substring(1) - if (!extension || !importPath.endsWith(extension)) { - if (isUseOfExtensionRequired(extension) && !isUseOfExtensionForbidden(extension)) { + // determine if this is a module + const isPackageMain = isExternalModuleMain(importPath, context.settings) + || isScopedMain(importPath) + + if (!extension || !importPath.endsWith(`.${extension}`)) { + const extensionRequired = isUseOfExtensionRequired(extension, isPackageMain) + const extensionForbidden = isUseOfExtensionForbidden(extension) + if (extensionRequired && !extensionForbidden) { context.report({ node: source, message: @@ -97,6 +163,7 @@ module.exports = { return { ImportDeclaration: checkFileExtension, + ExportNamedDeclaration: checkFileExtension, } }, } diff --git a/src/rules/first.js b/src/rules/first.js index 7642cae1d..c5d33569b 100644 --- a/src/rules/first.js +++ b/src/rules/first.js @@ -1,6 +1,10 @@ +import docsUrl from '../docsUrl' + module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('first'), + }, }, create: function (context) { diff --git a/src/rules/group-exports.js b/src/rules/group-exports.js new file mode 100644 index 000000000..96fff24fe --- /dev/null +++ b/src/rules/group-exports.js @@ -0,0 +1,104 @@ +import docsUrl from '../docsUrl' + +const meta = { + docs: { + url: docsUrl('group-exports'), + }, +} +/* eslint-disable max-len */ +const errors = { + ExportNamedDeclaration: 'Multiple named export declarations; consolidate all named exports into a single export declaration', + AssignmentExpression: 'Multiple CommonJS exports; consolidate all exports into a single assignment to `module.exports`', +} +/* eslint-enable max-len */ + +/** + * Returns an array with names of the properties in the accessor chain for MemberExpression nodes + * + * Example: + * + * `module.exports = {}` => ['module', 'exports'] + * `module.exports.property = true` => ['module', 'exports', 'property'] + * + * @param {Node} node AST Node (MemberExpression) + * @return {Array} Array with the property names in the chain + * @private + */ +function accessorChain(node) { + const chain = [] + + do { + chain.unshift(node.property.name) + + if (node.object.type === 'Identifier') { + chain.unshift(node.object.name) + break + } + + node = node.object + } while (node.type === 'MemberExpression') + + return chain +} + +function create(context) { + const nodes = { + modules: new Set(), + commonjs: new Set(), + } + + return { + ExportNamedDeclaration(node) { + nodes.modules.add(node) + }, + + AssignmentExpression(node) { + if (node.left.type !== 'MemberExpression') { + return + } + + const chain = accessorChain(node.left) + + // Assignments to module.exports + // Deeper assignments are ignored since they just modify what's already being exported + // (ie. module.exports.exported.prop = true is ignored) + if (chain[0] === 'module' && chain[1] === 'exports' && chain.length <= 3) { + nodes.commonjs.add(node) + return + } + + // Assignments to exports (exports.* = *) + if (chain[0] === 'exports' && chain.length === 2) { + nodes.commonjs.add(node) + return + } + }, + + 'Program:exit': function onExit() { + // Report multiple `export` declarations (ES2015 modules) + if (nodes.modules.size > 1) { + nodes.modules.forEach(node => { + context.report({ + node, + message: errors[node.type], + }) + }) + } + + // Report multiple `module.exports` assignments (CommonJS) + if (nodes.commonjs.size > 1) { + nodes.commonjs.forEach(node => { + context.report({ + node, + message: errors[node.type], + }) + }) + } + }, + } +} + +module.exports = { + meta, + create, +} diff --git a/src/rules/imports-first.js b/src/rules/imports-first.js index 83eb1249c..7ed9accc4 100644 --- a/src/rules/imports-first.js +++ b/src/rules/imports-first.js @@ -1,5 +1,12 @@ -import first from './first' +import docsUrl from '../docsUrl' -const newMeta = Object.assign({}, first.meta, { deprecated: true }) +const first = require('./first') + +const newMeta = Object.assign({}, first.meta, { + deprecated: true, + docs: { + url: docsUrl('imports-first', '7b25c1cb95ee18acc1531002fd343e1e6031f9ed'), + }, +}) module.exports = Object.assign({}, first, { meta: newMeta }) diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js index 72ce857d9..9af8f7912 100644 --- a/src/rules/max-dependencies.js +++ b/src/rules/max-dependencies.js @@ -1,4 +1,5 @@ import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' const DEFAULT_MAX = 10 @@ -15,7 +16,9 @@ const countDependencies = (dependencies, lastNode, context) => { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('max-dependencies'), + }, schema: [ { diff --git a/src/rules/named.js b/src/rules/named.js index d7589ebe7..aa17dfb40 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,9 +1,12 @@ import * as path from 'path' import Exports from '../ExportMap' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('named'), + }, }, create: function (context) { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 87824c179..71dd57db8 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,9 +1,14 @@ +import declaredScope from 'eslint-module-utils/declaredScope' import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' -import declaredScope from 'eslint-module-utils/declaredScope' +import docsUrl from '../docsUrl' module.exports = { meta: { + docs: { + url: docsUrl('namespace'), + }, + schema: [ { 'type': 'object', @@ -156,6 +161,9 @@ module.exports = { if (pattern.type !== 'ObjectPattern') return for (let property of pattern.properties) { + if (property.type === 'ExperimentalRestProperty') { + continue + } if (property.key.type !== 'Identifier') { context.report({ diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index 579fe43ac..fda1bc763 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -4,6 +4,7 @@ */ import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' import debug from 'debug' const log = debug('eslint-plugin-import:rules:newline-after-import') @@ -44,7 +45,9 @@ function isClassWithDecorator(node) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('newline-after-import'), + }, schema: [ { 'type': 'object', @@ -84,7 +87,8 @@ module.exports = { line: node.loc.end.line, column, }, - message: `Expected empty line after ${type} statement not followed by another ${type}.`, + message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} \ +after ${type} statement not followed by another ${type}.`, fix: fixer => fixer.insertTextAfter( node, '\n'.repeat(EXPECTED_LINE_DIFFERENCE - lineDifference) diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index 0f7b2bc48..b66b8b203 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -1,9 +1,12 @@ -import { isAbsolute } from '../core/importType' import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor' +import { isAbsolute } from '../core/importType' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-absolute-path'), + }, schema: [ makeOptionsSchema() ], }, diff --git a/src/rules/no-amd.js b/src/rules/no-amd.js index 6686be935..3ccb2129d 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.js @@ -3,13 +3,17 @@ * @author Jamund Ferguson */ +import docsUrl from '../docsUrl' + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-amd'), + }, }, create: function (context) { diff --git a/src/rules/no-anonymous-default-export.js b/src/rules/no-anonymous-default-export.js index f333e6db7..491783bf6 100644 --- a/src/rules/no-anonymous-default-export.js +++ b/src/rules/no-anonymous-default-export.js @@ -3,6 +3,8 @@ * @author Duncan Beevers */ +import docsUrl from '../docsUrl' + const defs = { ArrayExpression: { option: 'allowArray', @@ -14,6 +16,12 @@ const defs = { description: 'If `false`, will report default export of an arrow function', message: 'Assign arrow function to a variable before exporting as module default', }, + CallExpression: { + option: 'allowCallExpression', + description: 'If `false`, will report default export of a function call', + message: 'Assign call result to a variable before exporting as module default', + default: true, + }, ClassDeclaration: { option: 'allowAnonymousClass', description: 'If `false`, will report default export of an anonymous class', @@ -43,20 +51,30 @@ const defs = { }, } -const schemaProperties = Object.keys(defs). - map((key) => defs[key]). - reduce((acc, def) => { +const schemaProperties = Object.keys(defs) + .map((key) => defs[key]) + .reduce((acc, def) => { acc[def.option] = { description: def.description, type: 'boolean', - default: false, } return acc }, {}) +const defaults = Object.keys(defs) + .map((key) => defs[key]) + .reduce((acc, def) => { + acc[def.option] = def.hasOwnProperty('default') ? def.default : false + return acc + }, {}) + module.exports = { meta: { + docs: { + url: docsUrl('no-anonymous-default-export'), + }, + schema: [ { type: 'object', @@ -67,7 +85,7 @@ module.exports = { }, create: function (context) { - const options = Object.assign({}, context.options[0]) + const options = Object.assign({}, defaults, context.options[0]) return { 'ExportDefaultDeclaration': (node) => { diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index 62a0804f2..dfa9b0a5c 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -3,6 +3,8 @@ * @author Jamund Ferguson */ +import docsUrl from '../docsUrl' + const EXPORT_MESSAGE = 'Expected "export" or "export default"' , IMPORT_MESSAGE = 'Expected "import" instead of "require()"' @@ -19,7 +21,9 @@ function allowPrimitive(node, context) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-commonjs'), + }, }, create: function (context) { @@ -36,12 +40,21 @@ module.exports = { // exports. if (node.object.name === 'exports') { - context.report({ node, message: EXPORT_MESSAGE }) + const isInScope = context.getScope() + .variables + .some(variable => variable.name === 'exports') + if (! isInScope) { + context.report({ node, message: EXPORT_MESSAGE }) + } } }, 'CallExpression': function (call) { if (context.getScope().type !== 'module') return + if ( + call.parent.type !== 'ExpressionStatement' + && call.parent.type !== 'VariableDeclarator' + ) return if (call.callee.type !== 'Identifier') return if (call.callee.name !== 'require') return diff --git a/src/rules/no-default-export.js b/src/rules/no-default-export.js new file mode 100644 index 000000000..8d240ed6a --- /dev/null +++ b/src/rules/no-default-export.js @@ -0,0 +1,35 @@ +module.exports = { + meta: { + docs: {}, + }, + + create(context) { + // ignore non-modules + if (context.parserOptions.sourceType !== 'module') { + return {} + } + + const preferNamed = 'Prefer named exports.' + const noAliasDefault = ({local}) => + `Do not alias \`${local.name}\` as \`default\`. Just export ` + + `\`${local.name}\` itself instead.` + + return { + ExportDefaultDeclaration(node) { + context.report({node, message: preferNamed}) + }, + + ExportNamedDeclaration(node) { + node.specifiers.forEach(specifier => { + if (specifier.type === 'ExportDefaultSpecifier' && + specifier.exported.name === 'default') { + context.report({node, message: preferNamed}) + } else if (specifier.type === 'ExportSpecifier' && + specifier.exported.name === 'default') { + context.report({node, message: noAliasDefault(specifier)}) + } + }) + }, + } + }, +} diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index e50c2e516..ef96f4163 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,5 +1,6 @@ -import Exports from '../ExportMap' import declaredScope from 'eslint-module-utils/declaredScope' +import Exports from '../ExportMap' +import docsUrl from '../docsUrl' function message(deprecation) { return 'Deprecated' + (deprecation.description ? ': ' + deprecation.description : '.') @@ -16,7 +17,9 @@ function getDeprecation(metadata) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-deprecated'), + }, }, create: function (context) { diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index a63fc44d5..72b305e67 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,4 +1,5 @@ import resolve from 'eslint-module-utils/resolve' +import docsUrl from '../docsUrl' function checkImports(imported, context) { for (let [module, nodes] of imported.entries()) { @@ -12,7 +13,9 @@ function checkImports(imported, context) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-duplicates'), + }, }, create: function (context) { diff --git a/src/rules/no-dynamic-require.js b/src/rules/no-dynamic-require.js index f7d6aad3d..5726d72ca 100644 --- a/src/rules/no-dynamic-require.js +++ b/src/rules/no-dynamic-require.js @@ -1,3 +1,5 @@ +import docsUrl from '../docsUrl' + function isRequire(node) { return node && node.callee && @@ -13,7 +15,9 @@ function isStaticValue(arg) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-dynamic-require'), + }, }, create: function (context) { diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index ce5041c97..bb684e448 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -2,8 +2,10 @@ import path from 'path' import fs from 'fs' import readPkgUp from 'read-pkg-up' import minimatch from 'minimatch' +import resolve from 'eslint-module-utils/resolve' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' function getDependencies(context, packageDir) { try { @@ -54,9 +56,19 @@ function optDepErrorMessage(packageName) { } function reportIfMissing(context, deps, depsOptions, node, name) { + // Do not report when importing types + if (node.importKind === 'type') { + return + } + if (importType(name, context) !== 'external') { return } + + const resolved = resolve(name, context) + if (!resolved) { + return + } const splitName = name.split('/') const packageName = splitName[0][0] === '@' ? splitName.slice(0, 2).join('/') @@ -101,7 +113,9 @@ function testConfig(config, filename) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-extraneous-dependencies'), + }, schema: [ { diff --git a/src/rules/no-internal-modules.js b/src/rules/no-internal-modules.js index 3437d8afe..3e28554fa 100644 --- a/src/rules/no-internal-modules.js +++ b/src/rules/no-internal-modules.js @@ -3,10 +3,13 @@ import minimatch from 'minimatch' import resolve from 'eslint-module-utils/resolve' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-internal-modules'), + }, schema: [ { diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index 0a4db26e5..6bd6941a7 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -1,6 +1,10 @@ +import docsUrl from '../docsUrl' + module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-mutable-exports'), + }, }, create: function (context) { diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index 2ffa19cff..17af25a6f 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -6,6 +6,7 @@ */ import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' +import docsUrl from '../docsUrl' //------------------------------------------------------------------------------ // Rule Definition @@ -13,7 +14,9 @@ import importDeclaration from '../importDeclaration' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-named-as-default-member'), + }, }, create: function(context) { diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index 97a8e99ad..eb9769513 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,9 +1,12 @@ import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-named-as-default'), + }, }, create: function (context) { diff --git a/src/rules/no-named-default.js b/src/rules/no-named-default.js index 0625c1f1e..e25cd4950 100644 --- a/src/rules/no-named-default.js +++ b/src/rules/no-named-default.js @@ -1,6 +1,10 @@ +import docsUrl from '../docsUrl' + module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-named-default'), + }, }, create: function (context) { diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 673735da0..76a11f92d 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -3,6 +3,8 @@ * @author Radek Benkel */ +import docsUrl from '../docsUrl' + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -10,7 +12,9 @@ module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-namespace'), + }, }, create: function (context) { diff --git a/src/rules/no-nodejs-modules.js b/src/rules/no-nodejs-modules.js index 262fec7dd..e73ed379d 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.js @@ -1,5 +1,6 @@ import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' function reportIfMissing(context, node, allowed, name) { if (allowed.indexOf(name) === -1 && importType(name, context) === 'builtin') { @@ -9,7 +10,9 @@ function reportIfMissing(context, node, allowed, name) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-nodejs-modules'), + }, }, create: function (context) { diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 0240cd764..5b20c40d8 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -3,10 +3,13 @@ import path from 'path' import resolve from 'eslint-module-utils/resolve' import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-restricted-paths'), + }, schema: [ { diff --git a/src/rules/no-self-import.js b/src/rules/no-self-import.js new file mode 100644 index 000000000..8a8620c9a --- /dev/null +++ b/src/rules/no-self-import.js @@ -0,0 +1,44 @@ +/** + * @fileOverview Forbids a module from importing itself + * @author Gio d'Amelio + */ + +import resolve from 'eslint-module-utils/resolve' +import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' + +function isImportingSelf(context, node, requireName) { + const filePath = context.getFilename() + + // If the input is from stdin, this test can't fail + if (filePath !== '' && filePath === resolve(requireName, context)) { + context.report({ + node, + message: 'Module imports itself.', + }) + } +} + +module.exports = { + meta: { + docs: { + description: 'Forbid a module from importing itself', + recommended: true, + url: docsUrl('no-self-import'), + }, + + schema: [], + }, + create: function (context) { + return { + ImportDeclaration(node) { + isImportingSelf(context, node, node.source.value) + }, + CallExpression(node) { + if (isStaticRequire(node)) { + isImportingSelf(context, node, node.arguments[0].value) + } + }, + } + }, +} diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js index 2b1499b34..ad081bd1b 100644 --- a/src/rules/no-unassigned-import.js +++ b/src/rules/no-unassigned-import.js @@ -1,7 +1,9 @@ -import isStaticRequire from '../core/staticRequire' import path from 'path' import minimatch from 'minimatch' +import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' + function report(context, node) { context.report({ node, @@ -52,7 +54,9 @@ function create(context) { module.exports = { create, meta: { - docs: {}, + docs: { + url: docsUrl('no-unassigned-import'), + }, schema: [ { 'type': 'object', diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index e5fe2366c..2a5232a1c 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -6,11 +6,14 @@ import resolve, { CASE_SENSITIVE_FS, fileExistsWithCaseSync } from 'eslint-module-utils/resolve' import ModuleCache from 'eslint-module-utils/ModuleCache' import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor' - - +import docsUrl from '../docsUrl' module.exports = { meta: { + docs: { + url: docsUrl('no-unresolved'), + }, + schema: [ makeOptionsSchema({ caseSensitive: { type: 'boolean', default: true }, })], @@ -43,4 +46,3 @@ module.exports = { }, } - diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.js new file mode 100644 index 000000000..b9c4eedda --- /dev/null +++ b/src/rules/no-useless-path-segments.js @@ -0,0 +1,98 @@ +/** + * @fileOverview Ensures that there are no useless path segments + * @author Thomas Grainger + */ + +import path from 'path' +import sumBy from 'lodash/sumBy' +import resolve from 'eslint-module-utils/resolve' +import moduleVisitor from 'eslint-module-utils/moduleVisitor' +import docsUrl from '../docsUrl' + +/** + * convert a potentially relative path from node utils into a true + * relative path. + * + * ../ -> .. + * ./ -> . + * .foo/bar -> ./.foo/bar + * ..foo/bar -> ./..foo/bar + * foo/bar -> ./foo/bar + * + * @param rel {string} relative posix path potentially missing leading './' + * @returns {string} relative posix path that always starts with a ./ + **/ +function toRel(rel) { + const stripped = rel.replace(/\/$/g, '') + return /^((\.\.)|(\.))($|\/)/.test(stripped) ? stripped : `./${stripped}` +} + +function normalize(fn) { + return toRel(path.posix.normalize(fn)) +} + +const countRelParent = x => sumBy(x, v => v === '..') + +module.exports = { + meta: { + docs: { + url: docsUrl('no-useless-path-segments'), + }, + + fixable: 'code', + }, + + create: function (context) { + const currentDir = path.dirname(context.getFilename()) + + function checkSourceValue(source) { + const { value } = source + + function report(proposed) { + context.report({ + node: source, + message: `Useless path segments for "${value}", should be "${proposed}"`, + fix: fixer => fixer.replaceText(source, JSON.stringify(proposed)), + }) + } + + if (!value.startsWith('.')) { + return + } + + const resolvedPath = resolve(value, context) + const normed = normalize(value) + if (normed !== value && resolvedPath === resolve(normed, context)) { + return report(normed) + } + + if (value.startsWith('./')) { + return + } + + if (resolvedPath === undefined) { + return + } + + const expected = path.relative(currentDir, resolvedPath) + const expectedSplit = expected.split(path.sep) + const valueSplit = value.replace(/^\.\//, '').split('/') + const valueNRelParents = countRelParent(valueSplit) + const expectedNRelParents = countRelParent(expectedSplit) + const diff = valueNRelParents - expectedNRelParents + + if (diff <= 0) { + return + } + + return report( + toRel(valueSplit + .slice(0, expectedNRelParents) + .concat(valueSplit.slice(valueNRelParents + diff)) + .join('/')) + ) + } + + return moduleVisitor(checkSourceValue, context.options[0]) + }, +} diff --git a/src/rules/no-webpack-loader-syntax.js b/src/rules/no-webpack-loader-syntax.js index 3d9ba0034..e89fc9c35 100644 --- a/src/rules/no-webpack-loader-syntax.js +++ b/src/rules/no-webpack-loader-syntax.js @@ -1,4 +1,5 @@ import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' function reportIfNonStandard(context, node, name) { if (name.indexOf('!') !== -1) { @@ -10,7 +11,9 @@ function reportIfNonStandard(context, node, name) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('no-webpack-loader-syntax'), + }, }, create: function (context) { diff --git a/src/rules/order.js b/src/rules/order.js index efd30e4f6..81babd7fd 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -2,6 +2,7 @@ import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' +import docsUrl from '../docsUrl' const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index'] @@ -361,7 +362,9 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) { module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('order'), + }, fixable: 'code', schema: [ diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index c6c7b8f18..f9cec8bf0 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -1,8 +1,12 @@ 'use strict' +import docsUrl from '../docsUrl' + module.exports = { meta: { - docs: {}, + docs: { + url: docsUrl('prefer-default-export'), + }, }, create: function(context) { diff --git a/src/rules/unambiguous.js b/src/rules/unambiguous.js index 1fb40f6a0..f89ebad9c 100644 --- a/src/rules/unambiguous.js +++ b/src/rules/unambiguous.js @@ -4,9 +4,14 @@ */ import { isModule } from 'eslint-module-utils/unambiguous' +import docsUrl from '../docsUrl' module.exports = { - meta: {}, + meta: { + docs: { + url: docsUrl('unambiguous'), + }, + }, create: function (context) { // ignore non-modules diff --git a/tests/files/bar.coffee b/tests/files/bar.coffee new file mode 100644 index 000000000..2bc5e4224 --- /dev/null +++ b/tests/files/bar.coffee @@ -0,0 +1 @@ +console.log 'bar' diff --git a/tests/files/bar/index.js b/tests/files/bar/index.js new file mode 100644 index 000000000..bd35e5d0d --- /dev/null +++ b/tests/files/bar/index.js @@ -0,0 +1 @@ +export default 4 diff --git a/tests/files/index.js b/tests/files/index.js new file mode 100644 index 000000000..8eb61aca3 --- /dev/null +++ b/tests/files/index.js @@ -0,0 +1 @@ +// Used in `no-self-import` tests diff --git a/tests/files/internal-modules/package.json b/tests/files/internal-modules/package.json new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/load-error-resolver.js b/tests/files/load-error-resolver.js new file mode 100644 index 000000000..aa9d33010 --- /dev/null +++ b/tests/files/load-error-resolver.js @@ -0,0 +1 @@ +throw new Error('TEST ERROR') diff --git a/tests/files/no-self-import-folder/index.js b/tests/files/no-self-import-folder/index.js new file mode 100644 index 000000000..8eb61aca3 --- /dev/null +++ b/tests/files/no-self-import-folder/index.js @@ -0,0 +1 @@ +// Used in `no-self-import` tests diff --git a/tests/files/no-self-import.js b/tests/files/no-self-import.js new file mode 100644 index 000000000..8eb61aca3 --- /dev/null +++ b/tests/files/no-self-import.js @@ -0,0 +1 @@ +// Used in `no-self-import` tests diff --git a/tests/files/node_modules/@org/not-a-dependency/foo.js b/tests/files/node_modules/@org/not-a-dependency/foo.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/node_modules/@org/not-a-dependency/index.js b/tests/files/node_modules/@org/not-a-dependency/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/node_modules/chai/index.js b/tests/files/node_modules/chai/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/node_modules/exceljs/excel.js b/tests/files/node_modules/exceljs/excel.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/node_modules/exceljs/package.json b/tests/files/node_modules/exceljs/package.json new file mode 100644 index 000000000..70d59eaaa --- /dev/null +++ b/tests/files/node_modules/exceljs/package.json @@ -0,0 +1,3 @@ +{ + "main": "./excel.js" +} diff --git a/tests/files/node_modules/not-a-dependency/index.js b/tests/files/node_modules/not-a-dependency/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/package.json b/tests/files/package.json index 3be7b41ae..0a60f28d3 100644 --- a/tests/files/package.json +++ b/tests/files/package.json @@ -8,7 +8,7 @@ "eslint": "2.x" }, "dependencies": { - "@scope/core": "^1.0.0", + "@org/package": "^1.0.0", "jquery": "^3.1.0", "lodash.cond": "^4.3.0", "pkg-up": "^1.0.0" diff --git a/tests/files/typescript.ts b/tests/files/typescript.ts index f9d296a2b..7f90314e4 100644 --- a/tests/files/typescript.ts +++ b/tests/files/typescript.ts @@ -1,5 +1,37 @@ -type X = string +export type MyType = string +export enum MyEnum { + Foo, + Bar, + Baz +} +export interface Foo { + native: string | number + typedef: MyType + enum: MyEnum +} + +export abstract class Bar { + abstract foo(): Foo -export function getFoo() : X { + method() { + return "foo" + } +} + +export function getFoo() : MyType { return "foo" } + +export module MyModule { + export function ModuleFunction(){} +} + +export namespace MyNamespace { + export function NamespaceFunction(){} + + export module NSModule { + export function NSModuleFunction(){} + } +} + +interface NotExported {} diff --git a/tests/files/with-flow-typed/flow-typed/npm/myflowtyped_v1.x.x.js b/tests/files/with-flow-typed/flow-typed/npm/myflowtyped_v1.x.x.js new file mode 100644 index 000000000..2917b03f5 --- /dev/null +++ b/tests/files/with-flow-typed/flow-typed/npm/myflowtyped_v1.x.x.js @@ -0,0 +1,4 @@ +// flow-typed signature: ____ +// flow-typed version: ____/myflowtyped_v1.x.x/flow_>=v0.33.x + +declare module 'myflowtyped' {} diff --git a/tests/files/with-flow-typed/package.json b/tests/files/with-flow-typed/package.json new file mode 100644 index 000000000..18a1e415e --- /dev/null +++ b/tests/files/with-flow-typed/package.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} diff --git a/tests/src/core/docsUrl.js b/tests/src/core/docsUrl.js new file mode 100644 index 000000000..2ba778a4a --- /dev/null +++ b/tests/src/core/docsUrl.js @@ -0,0 +1,14 @@ +import { expect } from 'chai' + +import pkg from '../../../package.json' +import docsUrl from '../../../src/docsUrl' + +describe('docsUrl', function () { + it('returns the rule documentation URL when given a rule name', function () { + expect(docsUrl('foo')).to.equal(`https://github.com/benmosher/eslint-plugin-import/blob/v${pkg.version}/docs/rules/foo.md`) + }) + + it('supports an optional commit-ish parameter', function () { + expect(docsUrl('foo', 'bar')).to.equal('https://github.com/benmosher/eslint-plugin-import/blob/bar/docs/rules/foo.md') + }) +}) diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index c3da17fe7..096ef533a 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -337,9 +337,25 @@ describe('ExportMap', function () { expect(imports).property('errors').to.be.empty }) - it('has export (getFoo)', function () { + it('has exported function', function () { expect(imports.has('getFoo')).to.be.true }) + + it('has exported typedef', function () { + expect(imports.has('MyType')).to.be.true + }) + + it('has exported enum', function () { + expect(imports.has('MyEnum')).to.be.true + }) + + it('has exported interface', function () { + expect(imports.has('Foo')).to.be.true + }) + + it('has exported abstract class', function () { + expect(imports.has('Bar')).to.be.true + }) }) }) diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 307befcde..3c15303ed 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -118,6 +118,22 @@ describe('resolve', function () { )).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx')) }) + it('reports load exception in a user resolver', function () { + + const testContext = utils.testContext({ 'import/resolver': './load-error-resolver' }) + const testContextReports = [] + testContext.report = function (reportInfo) { + testContextReports.push(reportInfo) + } + + expect(resolve( '../files/exception' + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }) + )).to.equal(undefined) + expect(testContextReports[0]).to.be.an('object') + expect(testContextReports[0].message).to.equal('Resolve error: TEST ERROR') + expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 }) + }) + const caseDescribe = (!CASE_SENSITIVE_FS ? describe : describe.skip) caseDescribe('case sensitivity', function () { let file diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index 6cd632b76..871c62e85 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -53,11 +53,11 @@ ruleTester.run('exports-last', rule, { // Multiline export test({ code: ` - const baz = 'quux' - export default function foo () { + const foo = 'bar' + export default function bar () { const very = 'multiline' } - export const bar = true + export const baz = true `, }), // Many exports diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index 3ebd42c18..8b816daa9 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -1,6 +1,6 @@ import { RuleTester } from 'eslint' import rule from 'rules/extensions' -import { test } from '../utils' +import { test, testFilePath } from '../utils' const ruleTester = new RuleTester() @@ -59,6 +59,63 @@ ruleTester.run('extensions', rule, { test({ code: 'import thing from "./fake-file.js"', options: [ 'always' ] }), test({ code: 'import thing from "non-package"', options: [ 'never' ] }), + test({ + code: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component' + import express from 'express' + `, + options: [ 'ignorePackages' ], + }), + + test({ + code: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component.jsx' + import express from 'express' + `, + options: [ 'always', {ignorePackages: true} ], + }), + + test({ + code: ` + import foo from './foo' + import bar from './bar' + import Component from './Component' + import express from 'express' + `, + options: [ 'never', {ignorePackages: true} ], + }), + + test({ + code: 'import exceljs from "exceljs"', + options: [ 'always', { js: 'never', jsx: 'never' } ], + filename: testFilePath('./internal-modules/plugins/plugin.js'), + settings: { + 'import/resolver': { + 'node': { 'extensions': [ '.js', '.jsx', '.json' ] }, + 'webpack': { 'config': 'webpack.empty.config.js' }, + }, + }, + }), + + // export (#964) + test({ + code: [ + 'export { foo } from "./foo.js"', + 'export { bar }', + ].join('\n'), + options: [ 'always' ], + }), + test({ + code: [ + 'export { foo } from "./foo"', + 'export { bar }', + ].join('\n'), + options: [ 'never' ], + }), ], invalid: [ @@ -142,6 +199,34 @@ ruleTester.run('extensions', rule, { }, ], }), + // extension resolve order (#583/#965) + test({ + code: [ + 'import component from "./bar.jsx"', + 'import data from "./bar.json"', + ].join('\n'), + options: [ { json: 'always', js: 'never', jsx: 'never' } ], + settings: { 'import/resolve': { 'extensions': [ '.jsx', '.json', '.js' ] } }, + errors: [ + { + message: 'Unexpected use of file extension "jsx" for "./bar.jsx"', + line: 1, + column: 23, + }, + ], + }), + test({ + code: 'import "./bar.coffee"', + errors: [ + { + message: 'Unexpected use of file extension "coffee" for "./bar.coffee"', + line: 1, + column: 8, + }, + ], + options: ['never', { js: 'always', jsx: 'always' }], + settings: { 'import/resolve': { 'extensions': ['.coffee', '.js'] } }, + }), test({ code: [ @@ -201,5 +286,78 @@ ruleTester.run('extensions', rule, { ], }), + + test({ + code: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component' + import baz from 'foo/baz' + import express from 'express' + `, + options: [ 'always', {ignorePackages: true} ], + errors: [ + { + message: 'Missing file extension for "./Component"', + line: 4, + column: 31, + }, { + message: 'Missing file extension for "foo/baz"', + line: 5, + column: 25, + }, + ], + }), + + test({ + code: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component.jsx' + import express from 'express' + `, + errors: [ + { + message: 'Unexpected use of file extension "js" for "./foo.js"', + line: 2, + column: 25, + }, { + message: 'Unexpected use of file extension "jsx" for "./Component.jsx"', + line: 4, + column: 31, + }, + ], + options: [ 'never', {ignorePackages: true} ], + }), + + // export (#964) + test({ + code: [ + 'export { foo } from "./foo"', + 'export { bar }', + ].join('\n'), + options: [ 'always' ], + errors: [ + { + message: 'Missing file extension for "./foo"', + line: 1, + column: 21, + }, + ], + }), + test({ + code: [ + 'export { foo } from "./foo.js"', + 'export { bar }', + ].join('\n'), + options: [ 'never' ], + errors: [ + { + message: 'Unexpected use of file extension "js" for "./foo.js"', + line: 1, + column: 21, + }, + ], + }), ], }) diff --git a/tests/src/rules/group-exports.js b/tests/src/rules/group-exports.js new file mode 100644 index 000000000..3b08997e3 --- /dev/null +++ b/tests/src/rules/group-exports.js @@ -0,0 +1,221 @@ +import { test } from '../utils' +import { RuleTester } from 'eslint' +import rule from 'rules/group-exports' + +/* eslint-disable max-len */ +const errors = { + named: 'Multiple named export declarations; consolidate all named exports into a single export declaration', + commonjs: 'Multiple CommonJS exports; consolidate all exports into a single assignment to `module.exports`', +} +/* eslint-enable max-len */ +const ruleTester = new RuleTester() + +ruleTester.run('group-exports', rule, { + valid: [ + test({ code: 'export const test = true' }), + test({ code: ` + export default {} + export const test = true + ` }), + test({ code: ` + const first = true + const second = true + export { + first, + second + } + ` }), + test({ code: ` + export default {} + /* test */ + export const test = true + ` }), + test({ code: ` + export default {} + // test + export const test = true + ` }), + test({ code: ` + export const test = true + /* test */ + export default {} + ` }), + test({ code: ` + export const test = true + // test + export default {} + ` }), + test({ code: 'module.exports = {} '}), + test({ code: ` + module.exports = { test: true, + another: false } + ` }), + test({ code: 'exports.test = true' }), + + test({ code: ` + module.exports = {} + const test = module.exports + ` }), + test({ code: ` + exports.test = true + const test = exports.test + ` }), + test({ code: ` + module.exports = {} + module.exports.too.deep = true + ` }), + test({ code: ` + module.exports.deep.first = true + module.exports.deep.second = true + ` }), + test({ code: ` + module.exports = {} + exports.too.deep = true + ` }), + test({ code: ` + export default {} + const test = true + export { test } + ` }), + test({ code: ` + const test = true + export { test } + const another = true + export default {} + ` }), + test({ code: ` + module.something.else = true + module.something.different = true + ` }), + test({ code: ` + module.exports.test = true + module.something.different = true + ` }), + test({ code: ` + exports.test = true + module.something.different = true + ` }), + test({ code: ` + unrelated = 'assignment' + module.exports.test = true + ` }), + ], + invalid: [ + test({ + code: ` + export const test = true + export const another = true + `, + errors: [ + errors.named, + errors.named, + ], + }), + test({ + code: ` + module.exports = {} + module.exports.test = true + module.exports.another = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = {} + module.exports.test = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = { test: true } + module.exports.another = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports.test = true + module.exports.another = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + exports.test = true + module.exports.another = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = () => {} + module.exports.attached = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = function test() {} + module.exports.attached = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = () => {} + exports.test = true + exports.another = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = "non-object" + module.exports.attached = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + ], + }), + test({ + code: ` + module.exports = "non-object" + module.exports.attached = true + module.exports.another = true + `, + errors: [ + errors.commonjs, + errors.commonjs, + errors.commonjs, + ], + }), + ], +}) diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 8cee0c731..2364c74ed 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -81,6 +81,70 @@ ruleTester.run('named', rule, { 'parser': 'babel-eslint', }), + // TypeScript + test({ + code: 'import { MyType } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { Foo } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { Bar } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { getFoo } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { MyEnum } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: ` + import { MyModule } from "./typescript" + MyModule.ModuleFunction() + `, + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: ` + import { MyNamespace } from "./typescript" + MyNamespace.NSModule.NSModuleFunction() + `, + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + // jsnext test({ code: '/*jsnext*/ import { createStore } from "redux"', @@ -190,6 +254,32 @@ ruleTester.run('named', rule, { }], }), + // TypeScript + test({ + code: 'import { MissingType } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + errors: [{ + message: "MissingType not found in './typescript'", + type: 'Identifier', + }], + }), + test({ + code: 'import { NotExported } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + errors: [{ + message: "NotExported not found in './typescript'", + type: 'Identifier', + }], + }), + // jsnext test({ code: '/*jsnext*/ import { createSnorlax } from "redux"', diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 96eebcc0e..19a69a8d9 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -92,6 +92,20 @@ const valid = [ options: [{ allowComputed: true }], }), + // #656: should handle object-rest properties + test({ + code: `import * as names from './named-exports'; const {a, b, ...rest} = names;`, + parserOptions: { + ecmaFeatures: { + experimentalObjectRestSpread: true, + }, + }, + }), + test({ + code: `import * as names from './named-exports'; const {a, b, ...rest} = names;`, + parser: 'babel-eslint', + }), + ...SYNTAX_CASES, ] diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index aab38b21b..00ebfa432 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -1,7 +1,10 @@ import { RuleTester } from 'eslint' -const IMPORT_ERROR_MESSAGE = 'Expected empty line after import statement not followed by another import.' -const REQUIRE_ERROR_MESSAGE = 'Expected empty line after require statement not followed by another require.' +const IMPORT_ERROR_MESSAGE = 'Expected 1 empty line after import statement not followed by another import.' +const IMPORT_ERROR_MESSAGE_MULTIPLE = (count) => { + return `Expected ${count} empty lines after import statement not followed by another import.` +} +const REQUIRE_ERROR_MESSAGE = 'Expected 1 empty line after require statement not followed by another require.' const ruleTester = new RuleTester() @@ -181,7 +184,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { errors: [ { line: 1, column: 1, - message: IMPORT_ERROR_MESSAGE, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), } ], parserOptions: { sourceType: 'module' }, }, @@ -347,4 +350,4 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parser: 'babel-eslint', }, ], -}); +}) diff --git a/tests/src/rules/no-anonymous-default-export.js b/tests/src/rules/no-anonymous-default-export.js index 9ea18a341..c872cf4d0 100644 --- a/tests/src/rules/no-anonymous-default-export.js +++ b/tests/src/rules/no-anonymous-default-export.js @@ -21,6 +21,7 @@ ruleTester.run('no-anonymous-default-export', rule, { test({ code: 'export default \'foo\'', options: [{ allowLiteral: true }] }), test({ code: 'export default `foo`', options: [{ allowLiteral: true }] }), test({ code: 'export default {}', options: [{ allowObject: true }] }), + test({ code: 'export default foo(bar)', options: [{ allowCallExpression: true }] }), // Allow forbidden types with multiple options test({ code: 'export default 123', options: [{ allowLiteral: true, allowObject: true }] }), @@ -31,6 +32,9 @@ ruleTester.run('no-anonymous-default-export', rule, { test({ code: 'const foo = 123\nexport { foo }' }), test({ code: 'const foo = 123\nexport { foo as default }' }), + // Allow call expressions by default for backwards compatibility + test({ code: 'export default foo(bar)' }), + ...SYNTAX_CASES, ], @@ -43,6 +47,7 @@ ruleTester.run('no-anonymous-default-export', rule, { test({ code: 'export default \'foo\'', errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }), test({ code: 'export default `foo`', errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }), test({ code: 'export default {}', errors: [{ message: 'Assign object to a variable before exporting as module default' }] }), + test({ code: 'export default foo(bar)', options: [{ allowCallExpression: false }], errors: [{ message: 'Assign call result to a variable before exporting as module default' }] }), // Test failure with non-covering exception test({ code: 'export default 123', options: [{ allowObject: true }], errors: [{ message: 'Assign literal to a variable before exporting as module default' }] }), diff --git a/tests/src/rules/no-commonjs.js b/tests/src/rules/no-commonjs.js index 243bb16dc..649d0022f 100644 --- a/tests/src/rules/no-commonjs.js +++ b/tests/src/rules/no-commonjs.js @@ -17,9 +17,19 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { // exports { code: 'export default "x"', parserOptions: { sourceType: 'module' } }, { code: 'export function house() {}', parserOptions: { sourceType: 'module' } }, + { + code: + 'function someFunc() {\n'+ + ' const exports = someComputation();\n'+ + '\n'+ + ' expect(exports.someProp).toEqual({ a: \'value\' });\n'+ + '}', + parserOptions: { sourceType: 'module' }, + }, // allowed requires { code: 'function a() { var x = require("y"); }' }, // nested requires allowed + { code: 'var a = c && require("b")' }, // conditional requires allowed { code: 'require.resolve("help")' }, // methods of require are allowed { code: 'require.ensure([])' }, // webpack specific require.ensure is allowed { code: 'require([], function(a, b, c) {})' }, // AMD require is allowed diff --git a/tests/src/rules/no-default-export.js b/tests/src/rules/no-default-export.js new file mode 100644 index 000000000..6440bfa89 --- /dev/null +++ b/tests/src/rules/no-default-export.js @@ -0,0 +1,121 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/no-default-export') + +ruleTester.run('no-default-export', rule, { + valid: [ + test({ + code: ` + export const foo = 'foo'; + export const bar = 'bar'; + `, + }), + test({ + code: ` + export const foo = 'foo'; + export function bar() {}; + `, + }), + test({ + code: `export const foo = 'foo';`, + }), + test({ + code: ` + const foo = 'foo'; + export { foo }; + `, + }), + test({ + code: `export { foo, bar }`, + }), + test({ + code: `export const { foo, bar } = item;`, + }), + test({ + code: `export const { foo, bar: baz } = item;`, + }), + test({ + code: `export const { foo: { bar, baz } } = item;`, + }), + test({ + code: ` + export const foo = item; + export { item }; + `, + }), + test({ + code: `export * from './foo';`, + }), + test({ + code: `export const { foo } = { foo: "bar" };`, + }), + test({ + code: `export const { foo: { bar } } = { foo: { bar: "baz" } };`, + }), + test({ + code: 'export { a, b } from "foo.js"', + parser: 'babel-eslint', + }), + + // no exports at all + test({ + code: `import * as foo from './foo';`, + }), + test({ + code: `import foo from './foo';`, + }), + test({ + code: `import {default as foo} from './foo';`, + }), + + test({ + code: `export type UserId = number;`, + parser: 'babel-eslint', + }), + test({ + code: 'export foo from "foo.js"', + parser: 'babel-eslint', + }), + test({ + code: `export Memory, { MemoryValue } from './Memory'`, + parser: 'babel-eslint', + }), + ], + invalid: [ + test({ + code: 'export default function bar() {};', + errors: [{ + ruleId: 'ExportDefaultDeclaration', + message: 'Prefer named exports.', + }], + }), + test({ + code: ` + export const foo = 'foo'; + export default bar;`, + errors: [{ + ruleId: 'ExportDefaultDeclaration', + message: 'Prefer named exports.', + }], + }), + test({ + code: 'export { foo as default }', + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Do not alias `foo` as `default`. Just export `foo` itself ' + + 'instead.', + }], + }), + test({ + code: 'export default from "foo.js"', + parser: 'babel-eslint', + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Prefer named exports.', + }], + }), + ], +}) diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index a2e1955ec..a8817931b 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -14,6 +14,7 @@ const packageFileWithSyntaxErrorMessage = (() => { return error.message } })() +const packageDirWithFlowTyped = path.join(__dirname, '../../files/with-flow-typed') ruleTester.run('no-extraneous-dependencies', rule, { valid: [ @@ -30,7 +31,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { test({ code: 'import "fs"'}), test({ code: 'import "./foo"'}), test({ code: 'import "lodash.isarray"'}), - test({ code: 'import "@scope/core"'}), + test({ code: 'import "@org/package"'}), test({ code: 'import "electron"', settings: { 'import/core-modules': ['electron'] } }), test({ code: 'import "eslint"' }), @@ -57,7 +58,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { test({ code: 'import chai from "chai"', options: [{devDependencies: ['*.test.js', '*.spec.js']}], - filename: 'foo.spec.js', + filename: path.join(process.cwd(), 'foo.spec.js'), }), test({ code: 'import chai from "chai"', @@ -69,6 +70,11 @@ ruleTester.run('no-extraneous-dependencies', rule, { code: 'import "doctrine"', options: [{packageDir: path.join(__dirname, '../../../')}], }), + test({ + code: 'import type MyType from "myflowtyped";', + options: [{packageDir: packageDirWithFlowTyped}], + parser: 'babel-eslint', + }), ], invalid: [ test({ @@ -79,17 +85,17 @@ ruleTester.run('no-extraneous-dependencies', rule, { }], }), test({ - code: 'var donthaveit = require("@scope/donthaveit")', + code: 'var donthaveit = require("@org/not-a-dependency")', errors: [{ ruleId: 'no-extraneous-dependencies', - message: '\'@scope/donthaveit\' should be listed in the project\'s dependencies. Run \'npm i -S @scope/donthaveit\' to add it', + message: '\'@org/not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S @org/not-a-dependency\' to add it', }], }), test({ - code: 'var donthaveit = require("@scope/donthaveit/lib/foo")', + code: 'var donthaveit = require("@org/not-a-dependency/foo")', errors: [{ ruleId: 'no-extraneous-dependencies', - message: '\'@scope/donthaveit\' should be listed in the project\'s dependencies. Run \'npm i -S @scope/donthaveit\' to add it', + message: '\'@org/not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S @org/not-a-dependency\' to add it', }], }), test({ diff --git a/tests/src/rules/no-self-import.js b/tests/src/rules/no-self-import.js new file mode 100644 index 000000000..f8549b49e --- /dev/null +++ b/tests/src/rules/no-self-import.js @@ -0,0 +1,121 @@ +import { test, testFilePath } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/no-self-import') + +const error = { + ruleId: 'no-self-import', + message: 'Module imports itself.', +} + +ruleTester.run('no-self-import', rule, { + valid: [ + test({ + code: 'import _ from "lodash"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'import find from "lodash.find"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'import foo from "./foo"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'import foo from "../foo"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'import foo from "foo"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'import foo from "./"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'import foo from "@scope/foo"', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var _ = require("lodash")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var find = require("lodash.find")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var foo = require("./foo")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var foo = require("../foo")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var foo = require("foo")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var foo = require("./")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var foo = require("@scope/foo")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var bar = require("./bar/index")', + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var bar = require("./bar")', + filename: testFilePath('./bar/index.js'), + }), + test({ + code: 'var bar = require("./bar")', + filename: '', + }), + ], + invalid: [ + test({ + code: 'import bar from "./no-self-import"', + errors: [error], + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var bar = require("./no-self-import")', + errors: [error], + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var bar = require("./no-self-import.js")', + errors: [error], + filename: testFilePath('./no-self-import.js'), + }), + test({ + code: 'var bar = require(".")', + errors: [error], + filename: testFilePath('./index.js'), + }), + test({ + code: 'var bar = require("./")', + errors: [error], + filename: testFilePath('./index.js'), + }), + test({ + code: 'var bar = require("././././")', + errors: [error], + filename: testFilePath('./index.js'), + }), + test({ + code: 'var bar = require("../no-self-import-folder")', + errors: [error], + filename: testFilePath('./no-self-import-folder/index.js'), + }), + ], +}) diff --git a/tests/src/rules/no-useless-path-segments.js b/tests/src/rules/no-useless-path-segments.js new file mode 100644 index 000000000..1f4229f5e --- /dev/null +++ b/tests/src/rules/no-useless-path-segments.js @@ -0,0 +1,55 @@ +import { test } from '../utils' +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() +const rule = require('rules/no-useless-path-segments') + +function runResolverTests(resolver) { + ruleTester.run(`no-useless-path-segments (${resolver})`, rule, { + valid: [ + test({ code: 'import "./malformed.js"' }), + test({ code: 'import "./test-module"' }), + test({ code: 'import "./bar/"' }), + test({ code: 'import "."' }), + test({ code: 'import ".."' }), + test({ code: 'import fs from "fs"' }), + ], + + invalid: [ + test({ + code: 'import "./../files/malformed.js"', + errors: [ 'Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], + }), + test({ + code: 'import "./../files/malformed"', + errors: [ 'Useless path segments for "./../files/malformed", should be "../files/malformed"'], + }), + test({ + code: 'import "../files/malformed.js"', + errors: [ 'Useless path segments for "../files/malformed.js", should be "./malformed.js"'], + }), + test({ + code: 'import "../files/malformed"', + errors: [ 'Useless path segments for "../files/malformed", should be "./malformed"'], + }), + test({ + code: 'import "./test-module/"', + errors: [ 'Useless path segments for "./test-module/", should be "./test-module"'], + }), + test({ + code: 'import "./"', + errors: [ 'Useless path segments for "./", should be "."'], + }), + test({ + code: 'import "../"', + errors: [ 'Useless path segments for "../", should be ".."'], + }), + test({ + code: 'import "./deep//a"', + errors: [ 'Useless path segments for "./deep//a", should be "./deep/a"'], + }), + ], + }) +} + +['node', 'webpack'].forEach(runResolverTests) diff --git a/utils/parse.js b/utils/parse.js index 671dc86c0..b921f8778 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -19,7 +19,8 @@ exports.default = function parse(path, content, context) { parserOptions = Object.assign({}, parserOptions) parserOptions.ecmaFeatures = Object.assign({}, parserOptions.ecmaFeatures) - // always attach comments + // always include and attach comments + parserOptions.comment = true parserOptions.attachComment = true // provide the `filePath` like eslint itself does, in `parserOptions` diff --git a/utils/resolve.js b/utils/resolve.js index 8193e7731..b280ca2cf 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -14,6 +14,20 @@ exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS const fileExistsCache = new ModuleCache() +function tryRequire(target) { + let resolved; + try { + // Check if the target exists + resolved = require.resolve(target); + } catch(e) { + // If the target does not exist then just return undefined + return undefined; + } + + // If the target exists then return the loaded module + return require(resolved); +} + // http://stackoverflow.com/a/27382838 exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings) { // don't care if the FS is case-sensitive @@ -135,27 +149,20 @@ function resolverReducer(resolvers, map) { throw new Error('invalid resolver config') } +function getBaseDir(sourceFile) { + return pkgDir.sync(sourceFile) || process.cwd() +} function requireResolver(name, sourceFile) { // Try to resolve package with conventional name - try { - return require(`eslint-import-resolver-${name}`) - } catch (err) { /* continue */ } + let resolver = tryRequire(`eslint-import-resolver-${name}`) || + tryRequire(name) || + tryRequire(path.resolve(getBaseDir(sourceFile), name)) - // Try to resolve package with custom name (@myorg/resolver-name) - try { - return require(name) - } catch (err) { /* continue */ } - - // Try to resolve package with path, relative to closest package.json - // or current working directory - try { - const baseDir = pkgDir.sync(sourceFile) || process.cwd() - // absolute paths ignore base, so this covers both - return require(path.resolve(baseDir, name)) - } catch (err) { /* continue */ } - - // all else failed - throw new Error(`unable to load resolver "${name}".`) + if (!resolver) { + throw new Error(`unable to load resolver "${name}".`) + } else { + return resolver; + } } const erroredContexts = new Set()