diff --git a/.babelrc b/.babelrc index 8621a8adf..c2f08fdc2 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": [ "es2015-loose" ], + "presets": [ "es2015-argon" ], "sourceMaps": "inline", "env": { "test": { diff --git a/.travis.yml b/.travis.yml index 81cf39c23..504c00e82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,5 @@ language: node_js node_js: - # - 0.10 # testing locally only for 0.10 (nyc fails) - # - 0.12 # assume 0.12 works if 0.10 does. - 4 - 6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b6b54f3..073acad07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,28 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] +## [2.0.0]! - 2016-09-30 +### Added +- `recommended` shared config. Roughly `errors` and `warnings` mixed together, + with some `parserOptions` in the mix. ([#402]) +- `react` shared config: added `jsx: true` to `parserOptions.ecmaFeatures`. +- Added [`no-webpack-loader-syntax`] rule: forbid custom Webpack loader syntax in imports. ([#586], thanks [@fson]!) +- Add option `newlines-between: "ignore"` to [`order`] ([#519]) +- Added [`no-unassigned-import`] rule ([#529]) + +### Breaking +- [`import/extensions` setting] defaults to `['.js']`. ([#306]) +- [`import/ignore` setting] defaults to nothing, and ambiguous modules are ignored natively. This means importing from CommonJS modules will no longer be reported by [`default`], [`named`], or [`namespace`], regardless of `import/ignore`. ([#270]) +- [`newline-after-import`]: Removed need for an empty line after an inline `require` call ([#570]) +- [`order`]: Default value for `newlines-between` option is now `ignore` ([#519]) + +### Changed +- `imports-first` is renamed to [`first`]. `imports-first` alias will continue to + exist, but may be removed in a future major release. + +### Fixed +- [`no-internal-modules`]: support `@`-scoped packages ([#577]+[#578], thanks [@spalger]) + ## [1.16.0] - 2016-09-22 ### Added - Added [`no-dynamic-require`] rule: forbid `require()` calls with expressions. ([#567], [#568]) @@ -167,6 +189,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ### Changed - Rearranged rule groups in README in preparation for more style guide rules +### Removed +- support for Node 0.10, via `es6-*` ponyfills. Using native Map/Set/Symbol. + ## [1.4.0] - 2016-03-25 ### Added - Resolver plugin interface v2: more explicit response format that more clearly covers the found-but-core-module case, where there is no path. @@ -300,7 +325,8 @@ for info on changes for earlier releases. [`no-named-as-default-member`]: ./docs/rules/no-named-as-default-member.md [`no-extraneous-dependencies`]: ./docs/rules/no-extraneous-dependencies.md [`extensions`]: ./docs/rules/extensions.md -[`imports-first`]: ./docs/rules/imports-first.md +[`first`]: ./docs/rules/first.md +[`imports-first`]: ./docs/rules/first.md [`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md [`order`]: ./docs/rules/order.md [`named`]: ./docs/rules/named.md @@ -314,7 +340,11 @@ for info on changes for earlier releases. [`max-dependencies`]: ./docs/rules/max-dependencies.md [`no-internal-modules`]: ./docs/rules/no-internal-modules.md [`no-dynamic-require`]: ./docs/rules/no-dynamic-require.md +[`no-webpack-loader-syntax`]: ./docs/rules/no-webpack-loader-syntax.md +[`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md +[#586]: https://github.com/benmosher/eslint-plugin-import/pull/586 +[#578]: https://github.com/benmosher/eslint-plugin-import/pull/578 [#568]: https://github.com/benmosher/eslint-plugin-import/pull/568 [#555]: https://github.com/benmosher/eslint-plugin-import/pull/555 [#538]: https://github.com/benmosher/eslint-plugin-import/pull/538 @@ -361,10 +391,14 @@ for info on changes for earlier releases. [#157]: https://github.com/benmosher/eslint-plugin-import/pull/157 [#314]: https://github.com/benmosher/eslint-plugin-import/pull/314 +[#577]: https://github.com/benmosher/eslint-plugin-import/issues/577 +[#570]: https://github.com/benmosher/eslint-plugin-import/issues/570 [#567]: https://github.com/benmosher/eslint-plugin-import/issues/567 [#566]: https://github.com/benmosher/eslint-plugin-import/issues/566 [#545]: https://github.com/benmosher/eslint-plugin-import/issues/545 [#530]: https://github.com/benmosher/eslint-plugin-import/issues/530 +[#529]: https://github.com/benmosher/eslint-plugin-import/issues/529 +[#519]: https://github.com/benmosher/eslint-plugin-import/issues/519 [#507]: https://github.com/benmosher/eslint-plugin-import/issues/507 [#478]: https://github.com/benmosher/eslint-plugin-import/issues/478 [#456]: https://github.com/benmosher/eslint-plugin-import/issues/456 @@ -374,6 +408,7 @@ for info on changes for earlier releases. [#423]: https://github.com/benmosher/eslint-plugin-import/issues/423 [#416]: https://github.com/benmosher/eslint-plugin-import/issues/416 [#415]: https://github.com/benmosher/eslint-plugin-import/issues/415 +[#402]: https://github.com/benmosher/eslint-plugin-import/issues/402 [#386]: https://github.com/benmosher/eslint-plugin-import/issues/386 [#373]: https://github.com/benmosher/eslint-plugin-import/issues/373 [#370]: https://github.com/benmosher/eslint-plugin-import/issues/370 @@ -382,11 +417,13 @@ for info on changes for earlier releases. [#328]: https://github.com/benmosher/eslint-plugin-import/issues/328 [#317]: https://github.com/benmosher/eslint-plugin-import/issues/317 [#313]: https://github.com/benmosher/eslint-plugin-import/issues/313 +[#306]: https://github.com/benmosher/eslint-plugin-import/issues/306 [#286]: https://github.com/benmosher/eslint-plugin-import/issues/286 [#283]: https://github.com/benmosher/eslint-plugin-import/issues/283 [#281]: https://github.com/benmosher/eslint-plugin-import/issues/281 [#275]: https://github.com/benmosher/eslint-plugin-import/issues/275 [#272]: https://github.com/benmosher/eslint-plugin-import/issues/272 +[#270]: https://github.com/benmosher/eslint-plugin-import/issues/270 [#267]: https://github.com/benmosher/eslint-plugin-import/issues/267 [#266]: https://github.com/benmosher/eslint-plugin-import/issues/266 [#216]: https://github.com/benmosher/eslint-plugin-import/issues/216 @@ -401,7 +438,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/v1.16.0...HEAD +[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.16.0...v2.0.0 [1.16.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/benmosher/eslint-plugin-import/compare/v1.13.0...v1.14.0 @@ -468,3 +506,4 @@ for info on changes for earlier releases. [@spalger]: https://github.com/spalger [@preco21]: https://github.com/preco21 [@skyrpex]: https://github.com/skyrpex +[@fson]: https://github.com/fson diff --git a/README.md b/README.md index 83e71665a..92e2ea3d3 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Restrict which files can be imported in a given folder ([`no-restricted-paths`]) * Forbid import of modules using absolute paths ([`no-absolute-path`]) * 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`]) [`no-unresolved`]: ./docs/rules/no-unresolved.md [`named`]: ./docs/rules/named.md @@ -29,9 +31,12 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-restricted-paths`]: ./docs/rules/no-restricted-paths.md [`no-absolute-path`]: ./docs/rules/no-absolute-path.md [`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 **Helpful warnings:** + * Report any invalid exports, i.e. re-export of the same name ([`export`]) * Report use of exported name as identifier of default export ([`no-named-as-default`]) * Report use of exported name as property of default export ([`no-named-as-default-member`]) @@ -48,10 +53,12 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a **Module systems:** +* Report potentially ambiguous parse goal (`script` vs. `module`) ([`unambiguous`]) * Report CommonJS `require` calls and `module.exports` or `exports.*`. ([`no-commonjs`]) * Report AMD `require` and `define` calls. ([`no-amd`]) * No Node.js builtin modules. ([`no-nodejs-modules`]) +[`unambiguous`]: ./docs/rules/unambiguous.md [`no-commonjs`]: ./docs/rules/no-commonjs.md [`no-amd`]: ./docs/rules/no-amd.md [`no-nodejs-modules`]: ./docs/rules/no-nodejs-modules.md @@ -59,7 +66,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a **Style guide:** -* Ensure all imports appear before other statements ([`imports-first`]) +* Ensure all imports appear before other statements ([`first`]) * Report repeated import of the same module in multiple places ([`no-duplicates`]) * Report namespace imports ([`no-namespace`]) * Ensure consistent use of file extension within the import path ([`extensions`]) @@ -67,8 +74,9 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Enforce a newline after import statements ([`newline-after-import`]) * Prefer a default export if module exports a single name ([`prefer-default-export`]) * Limit the maximum number of dependencies a module can have. ([`max-dependencies`]) +* Forbid unassigned imports. ([`no-unassigned-import`]) -[`imports-first`]: ./docs/rules/imports-first.md +[`first`]: ./docs/rules/first.md [`no-duplicates`]: ./docs/rules/no-duplicates.md [`no-namespace`]: ./docs/rules/no-namespace.md [`extensions`]: ./docs/rules/extensions.md @@ -76,6 +84,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`newline-after-import`]: ./docs/rules/newline-after-import.md [`prefer-default-export`]: ./docs/rules/prefer-default-export.md [`max-dependencies`]: ./docs/rules/max-dependencies.md +[`no-unassigned-import`]: ./docs/rules/no-unassigned-import.md ## Installation @@ -206,40 +215,31 @@ You may set the following settings in your `.eslintrc`: A list of file extensions that will be parsed as modules and inspected for `export`s. -This will default to `['.js']` in the next major revision of this plugin, unless -you are using the `react` shared config, in which case it is specified as `['.js', '.jsx']`. +This defaults to `['.js']`, unless you are using the `react` shared config, +in which case it is specified as `['.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. -Also, `import/ignore` patterns will overrule this list, so `node_modules` that -end in `.js` will still be ignored by default. +Also, the following `import/ignore` patterns will overrule this list. #### `import/ignore` A list of regex strings that, if matched by a path, will not report the matching module if no `export`s are found. In practice, this means rules other than [`no-unresolved`](./docs/rules/no-unresolved.md#ignore) will not report on any -`import`s with (absolute) paths matching this pattern, _unless_ `export`s were -found when parsing. This allows you to ignore `node_modules` but still properly -lint packages that define a [`jsnext:main`] in `package.json` (Redux, D3's v4 packages, etc.). +`import`s with (absolute filesystem) paths matching this pattern. `no-unresolved` has its own [`ignore`](./docs/rules/no-unresolved.md#ignore) setting. -**Note**: setting this explicitly will replace the default of `node_modules`, so you -may need to include it in your own list if you still want to ignore it. Example: - ```yaml settings: import/ignore: - - node_modules # mostly CommonJS (ignored by default) - \.coffee$ # fraught with parse errors - \.(scss|less|css)$ # can't parse unprocessed CSS modules, either ``` -[`jsnext:main`]: https://github.com/rollup/rollup/wiki/jsnext:main - #### `import/core-modules` An array of additional modules to consider as "core" modules--modules that should diff --git a/config/react-native.js b/config/react-native.js index d209e2c3d..fbc8652c9 100644 --- a/config/react-native.js +++ b/config/react-native.js @@ -6,7 +6,7 @@ module.exports = { 'import/resolver': { node: { // Note: will not complain if only _one_ of these files exists. - extensions: ['.js', '.ios.js', '.android.js'], + extensions: ['.js', '.web.js', '.ios.js', '.android.js'], }, }, }, diff --git a/config/react.js b/config/react.js index c8bd7ade9..fe1b5f2ec 100644 --- a/config/react.js +++ b/config/react.js @@ -1,8 +1,18 @@ /** - * - adds `.jsx` as an extension + * Adds `.jsx` as an extension, and enables JSX parsing. + * + * Even if _you_ aren't using JSX (or .jsx) directly, if your dependencies + * define jsnext:main and have JSX internally, you may run into problems + * if you don't enable these settings at the top level. */ module.exports = { + settings: { 'import/extensions': ['.js', '.jsx'], }, + + parserOptions: { + ecmaFeatures: { jsx: true }, + }, + } diff --git a/config/recommended.js b/config/recommended.js new file mode 100644 index 000000000..79561271b --- /dev/null +++ b/config/recommended.js @@ -0,0 +1,28 @@ +/** + * The basics. + * @type {Object} + */ +module.exports = { + rules: { + // analysis/correctness + 'import/no-unresolved': 'error', + 'import/named': 'error', + 'import/namespace': 'error', + 'import/default': 'error', + 'import/export': 'error', + + // red flags (thus, warnings) + 'import/no-named-as-default': 'warn', + 'import/no-named-as-default-member': 'warn', + 'import/no-duplicates': 'warn', + 'import/unambiguous': 'warn', + }, + + // need all these for parsing dependencies (even if _your_ code doesn't need + // all of them) + parserOptions: { + sourceType: 'module', + ecmaVersion: 6, + ecmaFeatures: { experimentalObjectRestSpread: true }, + }, +} diff --git a/docs/rules/default.md b/docs/rules/default.md index b520dbc39..bf40278f0 100644 --- a/docs/rules/default.md +++ b/docs/rules/default.md @@ -6,12 +6,14 @@ export in the imported module. For [ES7], reports if a default is named and exported but is not found in the referenced module. -Note: for modules, the plugin will find exported names (including defaults) +Note: for packages, the plugin will find exported names from [`jsnext:main`], if present in `package.json`. -Redux's npm module includes this key, and thereby is lintable, for example. Otherwise, -the whole `node_modules` folder is ignored by default ([`import/ignore`]) as most published modules are -formatted in CommonJS, which [at time of this writing](https://github.com/benmosher/eslint-plugin-import/issues/13) -is not able to be analyzed for exports. +Redux's npm module includes this key, and thereby is lintable, for example. + +A module path that is [ignored] or not [unambiguously an ES module] will not be reported when imported. + +[ignored]: ../README.md#importignore +[unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar ## Rule Details diff --git a/docs/rules/imports-first.md b/docs/rules/first.md similarity index 99% rename from docs/rules/imports-first.md rename to docs/rules/first.md index 0d5c762f5..fe84b89f2 100644 --- a/docs/rules/imports-first.md +++ b/docs/rules/first.md @@ -1,4 +1,4 @@ -# imports-first +# first This rule reports any imports that come after non-import statements. diff --git a/docs/rules/named.md b/docs/rules/named.md index 625079e27..b621310c5 100644 --- a/docs/rules/named.md +++ b/docs/rules/named.md @@ -4,11 +4,14 @@ Verifies that all named imports are part of the set of named exports in the refe For `export`, verifies that all named exports exist in the referenced module. -Note: for modules, the plugin will find exported names from [`jsnext:main`], if present in `package.json`. -Redux's npm module includes this key, and thereby is lintable, for example. Otherwise, -the whole `node_modules` folder is ignored by default ([`import/ignore`]) as most published modules are -formatted in CommonJS, which [at time of this writing](https://github.com/benmosher/eslint-plugin-import/issues/13) -is not able to be analyzed for exports. +Note: for packages, the plugin will find exported names +from [`jsnext:main`], if present in `package.json`. +Redux's npm module includes this key, and thereby is lintable, for example. + +A module path that is [ignored] or not [unambiguously an ES module] will not be reported when imported. + +[ignored]: ../README.md#importignore +[unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar ## Rule Details diff --git a/docs/rules/namespace.md b/docs/rules/namespace.md index 6ff8531d9..9716c90d0 100644 --- a/docs/rules/namespace.md +++ b/docs/rules/namespace.md @@ -8,11 +8,14 @@ Also, will report for computed references (i.e. `foo["bar"]()`). Reports on assignment to a member of an imported namespace. -Note: for modules, the plugin will find exported names from [`jsnext:main`], if present in `package.json`. -Redux's npm module includes this key, and thereby is lintable, for example. Otherwise, -the whole `node_modules` folder is ignored by default ([`import/ignore`]) as most published modules are -formatted in CommonJS, which [at time of this writing](https://github.com/benmosher/eslint-plugin-import/issues/13) -is not able to be analyzed for exports. +Note: for packages, the plugin will find exported names +from [`jsnext:main`], if present in `package.json`. +Redux's npm module includes this key, and thereby is lintable, for example. + +A module path that is [ignored] or not [unambiguously an ES module] will not be reported when imported. + +[ignored]: ../README.md#importignore +[unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar ## Rule Details diff --git a/docs/rules/newline-after-import.md b/docs/rules/newline-after-import.md index 57aab454d..0ad2cba01 100644 --- a/docs/rules/newline-after-import.md +++ b/docs/rules/newline-after-import.md @@ -1,11 +1,9 @@ # newline-after-import -Reports if there's no new line after last import/require in group. +Enforces having an empty line after the last top-level import statement or require call. ## Rule Details -**NOTE**: In each of those examples you can replace `import` call with `require`. - Valid: ```js @@ -21,6 +19,13 @@ import { bar } from 'bar-lib' const FOO = 'BAR' ``` +```js +const FOO = require('./foo') +const BAR = require('./bar') + +const BAZ = 1 +``` + ...whereas here imports will be reported: ```js @@ -35,6 +40,12 @@ const FOO = 'BAR' import { bar } from 'bar-lib' ``` +```js +const FOO = require('./foo') +const BAZ = 1 +const BAR = require('./bar') +``` + ## When Not To Use It If you like to visually group module imports with its usage, you don't want to use this rule. diff --git a/docs/rules/no-unassigned-import.md b/docs/rules/no-unassigned-import.md new file mode 100644 index 000000000..1b25b0169 --- /dev/null +++ b/docs/rules/no-unassigned-import.md @@ -0,0 +1,37 @@ +# 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 +- The module has side-effects (like [`should`](https://www.npmjs.com/package/should)). Having side-effects, makes it hard to know whether the module is actually used or can be removed. It can also make it harder to test or mock parts of your application. + +This rule aims to remove modules with side-effects by reporting when a module is imported but not assigned. + +## Fail + +```js +import 'should' +require('should') +``` + + +## Pass + +```js +import _ from 'foo' +import _, {foo} from 'foo' +import _, {foo as bar} from 'foo' +import {foo as bar} from 'foo' +import * as _ from 'foo' + +const _ = require('foo') +const {foo} = require('foo') +const {foo: bar} = require('foo') +const [a, b] = require('foo') +const _ = require('foo') + +// Module is not assigned, but it is used +bar(require('foo')) +require('foo').bar +require('foo').bar() +require('foo')() +``` diff --git a/docs/rules/no-webpack-loader-syntax.md b/docs/rules/no-webpack-loader-syntax.md new file mode 100644 index 000000000..a26012737 --- /dev/null +++ b/docs/rules/no-webpack-loader-syntax.md @@ -0,0 +1,36 @@ +# no-webpack-loader-syntax + +Forbid Webpack loader syntax in imports. + +[Webpack](http://webpack.github.io) allows specifying the [loaders](http://webpack.github.io/docs/loaders.html) to use in the import source string using a special syntax like this: +```js +var moduleWithOneLoader = require("my-loader!./my-awesome-module"); +``` + +This syntax is non-standard, so it couples the code to Webpack. The recommended way to specify Webpack loader configuration is in a [Webpack configuration file](http://webpack.github.io/docs/loaders.html#loaders-by-config). + +## Rule Details + +### Fail + +```js +import myModule from 'my-loader!my-module'; +import theme from 'style!css!./theme.css'; + +var myModule = require('my-loader!./my-module'); +var theme = require('style!css!./theme.css'); +``` + +### Pass + +```js +import myModule from 'my-module'; +import theme from './theme.css'; + +var myModule = require('my-module'); +var theme = require('./theme.css'); +``` + +## When Not To Use It + +If you have a project that doesn't use Webpack you can safely disable this rule. diff --git a/docs/rules/order.md b/docs/rules/order.md index 5a542e7b6..46a1dbd7f 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -92,12 +92,12 @@ You can set the options like this: "import/order": ["error", {"groups": ["index", "sibling", "parent", "internal", "external", "builtin"]}] ``` -### `newlines-between: [always|never]`: +### `newlines-between: [ignore|always|never]`: Enforces or forbids new lines between import groups: -- If omitted, assertion messages will be neither enforced nor forbidden. +- If set to `ignore`, no errors related to new lines between import groups will be reported (default). - If set to `always`, at least one new line between each group will be enforced, and new lines inside a group will be forbidden. To prevent multiple lines between imports, core `no-multiple-empty-lines` rule can be used. - If set to `never`, no new lines are allowed in the entire import section. diff --git a/docs/rules/unambiguous.md b/docs/rules/unambiguous.md new file mode 100644 index 000000000..52ff127df --- /dev/null +++ b/docs/rules/unambiguous.md @@ -0,0 +1,54 @@ +# unambiguous + +Warn if a `module` could be mistakely parsed as a `script` by a consumer leveraging +[Unambiguous JavaScript Grammar] to determine correct parsing goal. + +Will respect the [`parserOptions.sourceType`] from ESLint config, i.e. files parsed +as `script` per that setting will not be reported. + +This plugin uses [Unambiguous JavaScript Grammar] internally to decide whether +dependencies should be parsed as modules and searched for exports matching the +`import`ed names, so it may be beneficial to keep this rule on even if your application +will run in an explicit `module`-only environment. + +## Rule Details + +For files parsed as `module` by ESLint, the following are valid: + +```js +import 'foo' +function x() { return 42 } +``` + +```js +export function x() { return 42 } +``` + +```js +(function x() { return 42 })() +export {} // simple way to mark side-effects-only file as 'module' without any imports/exports +``` + +...whereas the following file would be reported: +```js +(function x() { return 42 })() +``` + +## When Not To Use It + +If your application environment will always know via [some other means](https://github.com/nodejs/node-eps/issues/13) +how to parse, regardless of syntax, you may not need this rule. + +Remember, though, that this plugin uses this strategy internally, so if you were +to `import` from a module with no `import`s or `export`s, this plugin would not +report it as it would not be clear whether it should be considered a `script` or +a `module`. + +## Further Reading + +- [Unambiguous JavaScript Grammar] +- [`parserOptions.sourceType`] +- [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 diff --git a/memo-parser/index.js b/memo-parser/index.js index 6d6d3cb08..d8296ac37 100644 --- a/memo-parser/index.js +++ b/memo-parser/index.js @@ -1,8 +1,8 @@ "use strict" const crypto = require('crypto') - , moduleRequire = require('../lib/core/module-require').default - , hashObject = require('../lib/core/hash').hashObject + , moduleRequire = require('eslint-module-utils/module-require').default + , hashObject = require('eslint-module-utils/hash').hashObject const cache = new Map() @@ -22,7 +22,7 @@ exports.parse = function parse(content, options) { const keyHash = crypto.createHash('sha256') keyHash.update(content) - hashObject(keyHash, options) + hashObject(options, keyHash) const key = keyHash.digest('hex') diff --git a/package.json b/package.json index 1a42b7f73..c94dd523d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,10 @@ { "name": "eslint-plugin-import", - "version": "1.16.0", + "version": "2.0.0", "description": "Import with sanity.", + "engines": { + "node": ">=4" + }, "main": "lib/index.js", "directories": { "test": "tests" @@ -14,8 +17,9 @@ "scripts": { "watch": "cross-env NODE_PATH=./src mocha --watch --compilers js:babel-register --recursive tests/src", "cover": "gulp pretest && cross-env NODE_PATH=./lib istanbul cover --dir reports/coverage _mocha tests/lib/ -- --recursive -R progress", + "pretest": "linklocal", "posttest": "eslint ./src", - "test": "cross-env BABEL_ENV=test NODE_PATH=./src nyc mocha --recursive tests/src -t 5s", + "test": "cross-env BABEL_ENV=test NODE_PATH=./src nyc -s mocha -R dot --recursive tests/src -t 5s", "test-compiled": "npm run prepublish && NODE_PATH=./lib mocha --compilers js:babel-register --recursive tests/src", "coverage-report": "npm t && nyc report --reporter html", "test-all": "npm test && for resolver in ./resolvers/*; do cd $resolver && npm test && cd ../..; done", @@ -46,25 +50,26 @@ "devDependencies": { "babel-eslint": "next", "babel-plugin-istanbul": "^2.0.1", - "babel-preset-es2015": "^6.6.0", - "babel-preset-es2015-loose": "^7.0.0", + "babel-preset-es2015-argon": "latest", "babel-register": "6.9.0", "chai": "^3.4.0", "coveralls": "^2.11.4", - "cross-env": "^2.0.0", - "eslint": "2.x", + "cross-env": "^3.0.0", + "eslint": "3.x", "eslint-import-resolver-node": "file:./resolvers/node", "eslint-import-resolver-webpack": "file:./resolvers/webpack", + "eslint-module-utils": "file:./utils", "eslint-plugin-import": "next", "gulp": "^3.9.0", "gulp-babel": "6.1.2", "istanbul": "^0.4.0", + "linklocal": "^2.6.0", "mocha": "^2.2.1", "nyc": "^8.3.0", "redux": "^3.0.4", "rimraf": "2.5.2", - "typescript": "^1.8.10", - "typescript-eslint-parser": "^0.3.0" + "typescript": "^2.0.3", + "typescript-eslint-parser": "^0.4.0" }, "peerDependencies": { "eslint": "2.x - 3.x" @@ -74,17 +79,11 @@ "contains-path": "^0.1.0", "debug": "^2.2.0", "doctrine": "1.3.x", - "es6-map": "^0.1.3", - "es6-set": "^0.1.4", "eslint-import-resolver-node": "^0.2.0", + "eslint-module-utils": "^1.0.0", "has": "^1.0.1", "lodash.cond": "^4.3.0", - "lodash.endswith": "^4.0.1", - "lodash.find": "^4.3.0", - "lodash.findindex": "^4.3.0", "minimatch": "^3.0.3", - "object-assign": "^4.0.1", - "pkg-dir": "^1.0.0", "pkg-up": "^1.0.0" }, "nyc": { diff --git a/resolvers/webpack/.babelrc b/resolvers/webpack/.babelrc index 7a870ac67..eba2945b2 100644 --- a/resolvers/webpack/.babelrc +++ b/resolvers/webpack/.babelrc @@ -1 +1 @@ -{ "presets": ["es2015"] } \ No newline at end of file +{ "presets": ["es2015-argon"] } diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 217485913..cc377d17b 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -48,6 +48,7 @@ "devDependencies": { "chai": "^3.4.1", "mocha": "^2.3.3", - "nyc": "^7.0.0" + "nyc": "^7.0.0", + "babel-preset-es2015-argon": "latest" } } diff --git a/src/ExportMap.js b/src/ExportMap.js new file mode 100644 index 000000000..29c894e3d --- /dev/null +++ b/src/ExportMap.js @@ -0,0 +1,473 @@ +import fs from 'fs' + +import doctrine from 'doctrine' + +import debug from 'debug' + +import parse from 'eslint-module-utils/parse' +import resolve from 'eslint-module-utils/resolve' +import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore' + +import { hashObject } from 'eslint-module-utils/hash' +import * as unambiguous from 'eslint-module-utils/unambiguous' + +const log = debug('eslint-plugin-import:ExportMap') + +const exportCache = new Map() + +export default class ExportMap { + constructor(path) { + this.path = path + this.namespace = new Map() + // todo: restructure to key on path, value is resolver + map of names + this.reexports = new Map() + this.dependencies = new Map() + this.errors = [] + } + + get hasDefault() { return this.get('default') != null } // stronger than this.has + + get size() { + let size = this.namespace.size + this.reexports.size + this.dependencies.forEach(dep => size += dep().size) + return size + } + + /** + * Note that this does not check explicitly re-exported names for existence + * in the base namespace, but it will expand all `export * from '...'` exports + * if not found in the explicit namespace. + * @param {string} name + * @return {Boolean} true if `name` is exported by this module. + */ + has(name) { + if (this.namespace.has(name)) return true + if (this.reexports.has(name)) return true + + // default exports must be explicitly re-exported (#328) + if (name !== 'default') { + for (let dep of this.dependencies.values()) { + let innerMap = dep() + + // todo: report as unresolved? + if (!innerMap) continue + + if (innerMap.has(name)) return true + } + } + + return false + } + + /** + * ensure that imported name fully resolves. + * @param {[type]} name [description] + * @return {Boolean} [description] + */ + hasDeep(name) { + if (this.namespace.has(name)) return { found: true, path: [this] } + + if (this.reexports.has(name)) { + const reexports = this.reexports.get(name) + , imported = reexports.getImport() + + // if import is ignored, return explicit 'null' + if (imported == null) return { found: true, path: [this] } + + // safeguard against cycles, only if name matches + if (imported.path === this.path && reexports.local === name) { + return { found: false, path: [this] } + } + + const deep = imported.hasDeep(reexports.local) + deep.path.unshift(this) + + return deep + } + + + // default exports must be explicitly re-exported (#328) + if (name !== 'default') { + for (let dep of this.dependencies.values()) { + let innerMap = dep() + // todo: report as unresolved? + if (!innerMap) continue + + // safeguard against cycles + if (innerMap.path === this.path) continue + + let innerValue = innerMap.hasDeep(name) + if (innerValue.found) { + innerValue.path.unshift(this) + return innerValue + } + } + } + + return { found: false, path: [this] } + } + + get(name) { + if (this.namespace.has(name)) return this.namespace.get(name) + + if (this.reexports.has(name)) { + const reexports = this.reexports.get(name) + , imported = reexports.getImport() + + // if import is ignored, return explicit 'null' + if (imported == null) return null + + // safeguard against cycles, only if name matches + if (imported.path === this.path && reexports.local === name) return undefined + + return imported.get(reexports.local) + } + + // default exports must be explicitly re-exported (#328) + if (name !== 'default') { + for (let dep of this.dependencies.values()) { + let innerMap = dep() + // todo: report as unresolved? + if (!innerMap) continue + + // safeguard against cycles + if (innerMap.path === this.path) continue + + let innerValue = innerMap.get(name) + if (innerValue !== undefined) return innerValue + } + } + + return undefined + } + + forEach(callback, thisArg) { + this.namespace.forEach((v, n) => + callback.call(thisArg, v, n, this)) + + this.reexports.forEach((reexports, name) => { + const reexported = reexports.getImport() + // can't look up meta for ignored re-exports (#348) + callback.call(thisArg, reexported && reexported.get(reexports.local), name, this) + }) + + this.dependencies.forEach(dep => dep().forEach((v, n) => + n !== 'default' && callback.call(thisArg, v, n, this))) + } + + // todo: keys, values, entries? + + reportErrors(context, declaration) { + context.report({ + node: declaration.source, + message: `Parse errors in imported module '${declaration.source.value}': ` + + `${this.errors + .map(e => `${e.message} (${e.lineNumber}:${e.column})`) + .join(', ')}`, + }) + } +} + +/** + * parse docs from the first node that has leading comments + * @param {...[type]} nodes [description] + * @return {{doc: object}} + */ +function captureDoc(docStyleParsers) { + const metadata = {} + , nodes = Array.prototype.slice.call(arguments, 1) + + // 'some' short-circuits on first 'true' + nodes.some(n => { + if (!n.leadingComments) return false + + for (let name in docStyleParsers) { + const doc = docStyleParsers[name](n.leadingComments) + if (doc) { + metadata.doc = doc + } + } + + return true + }) + + return metadata +} + +const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +} + +/** + * parse JSDoc from leading comments + * @param {...[type]} comments [description] + * @return {{doc: object}} + */ +function captureJsDoc(comments) { + let doc + + // capture XSDoc + comments.forEach(comment => { + // skip non-block comments + if (comment.value.slice(0, 4) !== '*\n *') return + try { + doc = doctrine.parse(comment.value, { unwrap: true }) + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }) + + return doc +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = [] + for (let i = 0; i < comments.length; i++) { + const comment = comments[i] + if (comment.value.match(/^\s*$/)) break + lines.push(comment.value.trim()) + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/) + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + } + } +} + +ExportMap.get = function (source, context) { + const path = resolve(source, context) + if (path == null) return null + + return ExportMap.for(path, context) +} + +ExportMap.for = function (path, context) { + let exportMap + + const cacheKey = hashObject({ + settings: context.settings, + parserPath: context.parserPath, + parserOptions: context.parserOptions, + path, + }).digest('hex') + + exportMap = exportCache.get(cacheKey) + + // return cached ignore + if (exportMap === null) return null + + const stats = fs.statSync(path) + if (exportMap != null) { + // date equality check + if (exportMap.mtime - stats.mtime === 0) { + return exportMap + } + // future: check content equality? + } + + // check valid extensions first + if (!hasValidExtension(path, context)) { + exportCache.set(cacheKey, null) + return null + } + + const content = fs.readFileSync(path, { encoding: 'utf8' }) + + // check for and cache ignore + if (isIgnored(path, context) && !unambiguous.potentialModulePattern.test(content)) { + exportCache.set(cacheKey, null) + return null + } + + exportMap = ExportMap.parse(path, content, context) + + // ambiguous modules return null + if (exportMap == null) return null + + exportMap.mtime = stats.mtime + + exportCache.set(cacheKey, exportMap) + return exportMap +} + + +ExportMap.parse = function (path, content, context) { + var m = new ExportMap(path) + + try { + var ast = parse(path, content, context) + } catch (err) { + log('parse error:', path, err) + m.errors.push(err) + return m // can't continue + } + + if (!unambiguous.isModule(ast)) return null + + const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] + const docStyleParsers = {} + docstyle.forEach(style => { + docStyleParsers[style] = availableDocStyleParsers[style] + }) + + // attempt to collect module doc + ast.comments.some(c => { + if (c.type !== 'Block') return false + try { + const doc = doctrine.parse(c.value, { unwrap: true }) + if (doc.tags.some(t => t.title === 'module')) { + m.doc = doc + return true + } + } catch (err) { /* ignore */ } + return false + }) + + const namespaces = new Map() + + function remotePath(node) { + return resolve.relative(node.source.value, path, context.settings) + } + + function resolveImport(node) { + const rp = remotePath(node) + if (rp == null) return null + return ExportMap.for(rp, context) + } + + function getNamespace(identifier) { + if (!namespaces.has(identifier.name)) return + + return function () { + return resolveImport(namespaces.get(identifier.name)) + } + } + + function addNamespace(object, identifier) { + const nsfn = getNamespace(identifier) + if (nsfn) { + Object.defineProperty(object, 'namespace', { get: nsfn }) + } + + return object + } + + + ast.body.forEach(function (n) { + + if (n.type === 'ExportDefaultDeclaration') { + const exportMeta = captureDoc(docStyleParsers, n) + if (n.declaration.type === 'Identifier') { + addNamespace(exportMeta, n.declaration) + } + m.namespace.set('default', exportMeta) + return + } + + if (n.type === 'ExportAllDeclaration') { + let remoteMap = remotePath(n) + if (remoteMap == null) return + m.dependencies.set(remoteMap, () => ExportMap.for(remoteMap, context)) + return + } + + // capture namespaces in case of later export + if (n.type === 'ImportDeclaration') { + let ns + if (n.specifiers.some(s => s.type === 'ImportNamespaceSpecifier' && (ns = s))) { + namespaces.set(ns.local.name, n) + } + return + } + + if (n.type === 'ExportNamedDeclaration'){ + // capture declaration + if (n.declaration != null) { + switch (n.declaration.type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'TypeAlias': // flowtype with babel-eslint parser + m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) + break + case 'VariableDeclaration': + n.declaration.declarations.forEach((d) => + recursivePatternCapture(d.id, + id => m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) + break + } + } + + n.specifiers.forEach((s) => { + const exportMeta = {} + let local + + switch (s.type) { + case 'ExportDefaultSpecifier': + if (!n.source) return + local = 'default' + break + case 'ExportNamespaceSpecifier': + m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { + get() { return resolveImport(n) }, + })) + return + case 'ExportSpecifier': + if (!n.source) { + m.namespace.set(s.exported.name, addNamespace(exportMeta, s.local)) + return + } + // else falls through + default: + local = s.local.name + break + } + + // todo: JSDoc + m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(n) }) + }) + } + }) + + return m +} + + +/** + * Traverse a pattern/identifier node, calling 'callback' + * for each leaf identifier. + * @param {node} pattern + * @param {Function} callback + * @return {void} + */ +export function recursivePatternCapture(pattern, callback) { + switch (pattern.type) { + case 'Identifier': // base case + callback(pattern) + break + + case 'ObjectPattern': + pattern.properties.forEach(p => { + recursivePatternCapture(p.value, callback) + }) + break + + case 'ArrayPattern': + pattern.elements.forEach((element) => { + if (element == null) return + recursivePatternCapture(element, callback) + }) + break + } +} diff --git a/src/core/getExports.js b/src/core/getExports.js deleted file mode 100644 index 56b427e32..000000000 --- a/src/core/getExports.js +++ /dev/null @@ -1,484 +0,0 @@ -import Map from 'es6-map' - -import * as fs from 'fs' - -import { createHash } from 'crypto' -import * as doctrine from 'doctrine' - -import debug from 'debug' - -import parse from './parse' -import resolve, { relative as resolveRelative } from './resolve' -import isIgnored, { hasValidExtension } from './ignore' - -import { hashObject } from './hash' - -const log = debug('eslint-plugin-import:ExportMap') - -const exportCache = new Map() - -/** - * detect exports without a full parse. - * used primarily to ignore the import/ignore setting, iif it looks like - * there might be something there (i.e., jsnext:main is set). - * @type {RegExp} - */ -const hasExports = new RegExp('(^|[\\n;])\\s*export\\s[\\w{*]') - -export default class ExportMap { - constructor(path) { - this.path = path - this.namespace = new Map() - // todo: restructure to key on path, value is resolver + map of names - this.reexports = new Map() - this.dependencies = new Map() - this.errors = [] - } - - get hasDefault() { return this.get('default') != null } // stronger than this.has - - get size() { - let size = this.namespace.size + this.reexports.size - this.dependencies.forEach(dep => size += dep().size) - return size - } - - static get(source, context) { - - var path = resolve(source, context) - if (path == null) return null - - return ExportMap.for(path, context) - } - - static for(path, context) { - let exportMap - - const cacheKey = hashObject(createHash('sha256'), { - settings: context.settings, - parserPath: context.parserPath, - parserOptions: context.parserOptions, - path, - }).digest('hex') - - exportMap = exportCache.get(cacheKey) - - // return cached ignore - if (exportMap === null) return null - - const stats = fs.statSync(path) - if (exportMap != null) { - // date equality check - if (exportMap.mtime - stats.mtime === 0) { - return exportMap - } - // future: check content equality? - } - - // check valid extensions first - if (!hasValidExtension(path, context)) { - exportCache.set(cacheKey, null) - return null - } - - const content = fs.readFileSync(path, { encoding: 'utf8' }) - - // check for and cache ignore - if (isIgnored(path, context) && !hasExports.test(content)) { - exportCache.set(cacheKey, null) - return null - } - - exportMap = ExportMap.parse(path, content, context) - exportMap.mtime = stats.mtime - - exportCache.set(cacheKey, exportMap) - return exportMap - } - - static parse(path, content, context) { - var m = new ExportMap(path) - - try { - var ast = parse(path, content, context) - } catch (err) { - log('parse error:', path, err) - m.errors.push(err) - return m // can't continue - } - - const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc'] - const docStyleParsers = {} - docstyle.forEach(style => { - docStyleParsers[style] = availableDocStyleParsers[style] - }) - - // attempt to collect module doc - ast.comments.some(c => { - if (c.type !== 'Block') return false - try { - const doc = doctrine.parse(c.value, { unwrap: true }) - if (doc.tags.some(t => t.title === 'module')) { - m.doc = doc - return true - } - } catch (err) { /* ignore */ } - return false - }) - - const namespaces = new Map() - - function remotePath(node) { - return resolveRelative(node.source.value, path, context.settings) - } - - function resolveImport(node) { - const rp = remotePath(node) - if (rp == null) return null - return ExportMap.for(rp, context) - } - - function getNamespace(identifier) { - if (!namespaces.has(identifier.name)) return - - return function () { - return resolveImport(namespaces.get(identifier.name)) - } - } - - function addNamespace(object, identifier) { - const nsfn = getNamespace(identifier) - if (nsfn) { - Object.defineProperty(object, 'namespace', { get: nsfn }) - } - - return object - } - - - ast.body.forEach(function (n) { - - if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(docStyleParsers, n) - if (n.declaration.type === 'Identifier') { - addNamespace(exportMeta, n.declaration) - } - m.namespace.set('default', exportMeta) - return - } - - if (n.type === 'ExportAllDeclaration') { - let remoteMap = remotePath(n) - if (remoteMap == null) return - m.dependencies.set(remoteMap, () => ExportMap.for(remoteMap, context)) - return - } - - // capture namespaces in case of later export - if (n.type === 'ImportDeclaration') { - let ns - if (n.specifiers.some(s => s.type === 'ImportNamespaceSpecifier' && (ns = s))) { - namespaces.set(ns.local.name, n) - } - return - } - - if (n.type === 'ExportNamedDeclaration'){ - // capture declaration - if (n.declaration != null) { - switch (n.declaration.type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'TypeAlias': // flowtype with babel-eslint parser - m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) - break - case 'VariableDeclaration': - n.declaration.declarations.forEach((d) => - recursivePatternCapture(d.id, id => - m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) - break - } - } - - n.specifiers.forEach((s) => { - const exportMeta = {} - let local - - switch (s.type) { - case 'ExportDefaultSpecifier': - if (!n.source) return - local = 'default' - break - case 'ExportNamespaceSpecifier': - m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { - get() { return resolveImport(n) }, - })) - return - case 'ExportSpecifier': - if (!n.source) { - m.namespace.set(s.exported.name, addNamespace(exportMeta, s.local)) - return - } - // else falls through - default: - local = s.local.name - break - } - - // todo: JSDoc - m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(n) }) - }) - } - }) - - return m - } - - /** - * Note that this does not check explicitly re-exported names for existence - * in the base namespace, but it will expand all `export * from '...'` exports - * if not found in the explicit namespace. - * @param {string} name - * @return {Boolean} true if `name` is exported by this module. - */ - has(name) { - if (this.namespace.has(name)) return true - if (this.reexports.has(name)) return true - - // default exports must be explicitly re-exported (#328) - let foundInnerMapName = false - if (name !== 'default') { - this.dependencies.forEach((dep) => { - if (!foundInnerMapName) { - let innerMap = dep() - - // todo: report as unresolved? - if (innerMap && innerMap.has(name)) foundInnerMapName = true - } - }) - } - - return foundInnerMapName - } - - /** - * ensure that imported name fully resolves. - * @param {[type]} name [description] - * @return {Boolean} [description] - */ - hasDeep(name) { - if (this.namespace.has(name)) return { found: true, path: [this] } - - if (this.reexports.has(name)) { - const { local, getImport } = this.reexports.get(name) - , imported = getImport() - - // if import is ignored, return explicit 'null' - if (imported == null) return { found: true, path: [this] } - - // safeguard against cycles, only if name matches - if (imported.path === this.path && local === name) return { found: false, path: [this] } - - const deep = imported.hasDeep(local) - deep.path.unshift(this) - - return deep - } - - - // default exports must be explicitly re-exported (#328) - let returnValue = { found: false, path: [this] } - if (name !== 'default') { - this.dependencies.forEach((dep) => { - if (!returnValue.found) { - let innerMap = dep() - // todo: report as unresolved? - if (innerMap) { - - // safeguard against cycles - if (innerMap.path !== this.path) { - - let innerValue = innerMap.hasDeep(name) - if (innerValue.found) { - innerValue.path.unshift(this) - returnValue = innerValue - } - } - } - } - }) - } - - return returnValue - } - - get(name) { - if (this.namespace.has(name)) return this.namespace.get(name) - - if (this.reexports.has(name)) { - const { local, getImport } = this.reexports.get(name) - , imported = getImport() - - // if import is ignored, return explicit 'null' - if (imported == null) return null - - // safeguard against cycles, only if name matches - if (imported.path === this.path && local === name) return undefined - - return imported.get(local) - } - - // default exports must be explicitly re-exported (#328) - let returnValue = undefined - if (name !== 'default') { - this.dependencies.forEach((dep) => { - if (returnValue === undefined) { - let innerMap = dep() - // todo: report as unresolved? - if (innerMap) { - - // safeguard against cycles - if (innerMap.path !== this.path) { - - let innerValue = innerMap.get(name) - if (innerValue !== undefined) returnValue = innerValue - } - } - } - }) - } - - return returnValue - } - - forEach(callback, thisArg) { - this.namespace.forEach((v, n) => - callback.call(thisArg, v, n, this)) - - this.reexports.forEach(({ getImport, local }, name) => { - const reexported = getImport() - // can't look up meta for ignored re-exports (#348) - callback.call(thisArg, reexported && reexported.get(local), name, this) - }) - - this.dependencies.forEach(dep => dep().forEach((v, n) => - n !== 'default' && callback.call(thisArg, v, n, this))) - } - - // todo: keys, values, entries? - - reportErrors(context, declaration) { - context.report({ - node: declaration.source, - message: `Parse errors in imported module '${declaration.source.value}': ` + - `${this.errors - .map(e => `${e.message} (${e.lineNumber}:${e.column})`) - .join(', ')}`, - }) - } -} - -/** - * parse docs from the first node that has leading comments - * @param {...[type]} nodes [description] - * @return {{doc: object}} - */ -function captureDoc(docStyleParsers, ...nodes) { - const metadata = {} - - // 'some' short-circuits on first 'true' - nodes.some(n => { - if (!n.leadingComments) return false - - for (let name in docStyleParsers) { - const doc = docStyleParsers[name](n.leadingComments) - if (doc) { - metadata.doc = doc - } - } - - return true - }) - - return metadata -} - -const availableDocStyleParsers = { - jsdoc: captureJsDoc, - tomdoc: captureTomDoc, -} - -/** - * parse JSDoc from leading comments - * @param {...[type]} comments [description] - * @return {{doc: object}} - */ -function captureJsDoc(comments) { - let doc - - // capture XSDoc - comments.forEach(comment => { - // skip non-block comments - if (comment.value.slice(0, 4) !== '*\n *') return - try { - doc = doctrine.parse(comment.value, { unwrap: true }) - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ - } - }) - - return doc -} - -/** - * parse TomDoc section from comments - */ -function captureTomDoc(comments) { - // collect lines up to first paragraph break - const lines = [] - for (let i = 0; i < comments.length; i++) { - const comment = comments[i] - if (comment.value.match(/^\s*$/)) break - lines.push(comment.value.trim()) - } - - // return doctrine-like object - const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/) - if (statusMatch) { - return { - description: statusMatch[2], - tags: [{ - title: statusMatch[1].toLowerCase(), - description: statusMatch[2], - }], - } - } -} - -/** - * Traverse a pattern/identifier node, calling 'callback' - * for each leaf identifier. - * @param {node} pattern - * @param {Function} callback - * @return {void} - */ -export function recursivePatternCapture(pattern, callback) { - switch (pattern.type) { - case 'Identifier': // base case - callback(pattern) - break - - case 'ObjectPattern': - pattern.properties.forEach(({ value }) => { - recursivePatternCapture(value, callback) - }) - break - - case 'ArrayPattern': - pattern.elements.forEach((element) => { - if (element == null) return - recursivePatternCapture(element, callback) - }) - break - } -} diff --git a/src/core/hash.js b/src/core/hash.js deleted file mode 100644 index 10e8535ae..000000000 --- a/src/core/hash.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * utilities for hashing config objects. - * basically iteratively updates hash with a JSON-like format - */ - -const stringify = JSON.stringify - -export default function hashify(hash, value) { - if (value instanceof Array) { - hashArray(hash, value) - } else if (value instanceof Object) { - hashObject(hash, value) - } else { - hash.update(stringify(value) || 'undefined') - } - - return hash -} - -export function hashArray(hash, array) { - hash.update('[') - for (let i = 0; i < array.length; i++) { - hashify(hash, array[i]) - hash.update(',') - } - hash.update(']') - - return hash -} - -export function hashObject(hash, object) { - hash.update('{') - Object.keys(object).sort().forEach(key => { - hash.update(stringify(key)) - hash.update(':') - hashify(hash, object[key]) - hash.update(',') - }) - hash.update('}') - - return hash -} diff --git a/src/core/ignore.js b/src/core/ignore.js deleted file mode 100644 index cc679cf3e..000000000 --- a/src/core/ignore.js +++ /dev/null @@ -1,58 +0,0 @@ -import { extname } from 'path' -import Set from 'es6-set' - -// one-shot memoized -let cachedSet, lastSettings -function validExtensions({ settings }) { - if (cachedSet && settings === lastSettings) { - return cachedSet - } - - // todo: add 'mjs'? - lastSettings = settings - // breaking: default to '.js' - // cachedSet = new Set(settings['import/extensions'] || [ '.js' ]) - cachedSet = 'import/extensions' in settings - ? makeValidExtensionSet(settings) - : { has: () => true } // the set of all elements - - return cachedSet -} - -function makeValidExtensionSet(settings) { - // start with explicit JS-parsed extensions - const exts = new Set(settings['import/extensions']) - - // all alternate parser extensions are also valid - if ('import/parsers' in settings) { - for (let parser in settings['import/parsers']) { - settings['import/parsers'][parser] - .forEach(ext => exts.add(ext)) - } - } - - return exts -} - -export default function ignore(path, context) { - // ignore node_modules by default - const ignoreStrings = context.settings['import/ignore'] - ? [].concat(context.settings['import/ignore']) - : ['node_modules'] - - // check extension list first (cheap) - if (!hasValidExtension(path, context)) return true - - if (ignoreStrings.length === 0) return false - - for (var i = 0; i < ignoreStrings.length; i++) { - var regex = new RegExp(ignoreStrings[i]) - if (regex.test(path)) return true - } - - return false -} - -export function hasValidExtension(path, context) { - return validExtensions(context).has(extname(path)) -} diff --git a/src/core/importType.js b/src/core/importType.js index 5236b1d2a..869ca7496 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -2,7 +2,7 @@ import cond from 'lodash.cond' import builtinModules from 'builtin-modules' import { join } from 'path' -import resolve from './resolve' +import resolve from 'eslint-module-utils/resolve' function constant(value) { return () => value diff --git a/src/index.js b/src/index.js index 2583e89a6..68fd8855e 100644 --- a/src/index.js +++ b/src/index.js @@ -16,21 +16,29 @@ export const rules = { 'no-commonjs': require('./rules/no-commonjs'), 'no-amd': require('./rules/no-amd'), 'no-duplicates': require('./rules/no-duplicates'), - 'imports-first': require('./rules/imports-first'), + 'first': require('./rules/first'), 'max-dependencies': require('./rules/max-dependencies'), 'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'), 'no-absolute-path': require('./rules/no-absolute-path'), 'no-nodejs-modules': require('./rules/no-nodejs-modules'), + 'no-webpack-loader-syntax': require('./rules/no-webpack-loader-syntax'), 'order': require('./rules/order'), 'newline-after-import': require('./rules/newline-after-import'), 'prefer-default-export': require('./rules/prefer-default-export'), 'no-dynamic-require': require('./rules/no-dynamic-require'), + 'unambiguous': require('./rules/unambiguous'), + 'no-unassigned-import': require('./rules/no-unassigned-import'), // metadata-based 'no-deprecated': require('./rules/no-deprecated'), + + // deprecated aliases to rules + 'imports-first': require('./rules/first'), } export const configs = { + 'recommended': require('../config/recommended'), + 'errors': require('../config/errors'), 'warnings': require('../config/warnings'), diff --git a/src/rules/default.js b/src/rules/default.js index ed87e5442..28e03fc16 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -1,31 +1,37 @@ -import Exports from '../core/getExports' +import Exports from '../ExportMap' -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, + }, - function checkDefault(specifierType, node) { + create: function (context) { - // poor man's Array.find - let defaultSpecifier - node.specifiers.some((n) => { - if (n.type === specifierType) { - defaultSpecifier = n - return true - } - }) + function checkDefault(specifierType, node) { + + // poor man's Array.find + let defaultSpecifier + node.specifiers.some((n) => { + if (n.type === specifierType) { + defaultSpecifier = n + return true + } + }) - if (!defaultSpecifier) return - var imports = Exports.get(node.source.value, context) - if (imports == null) return + if (!defaultSpecifier) return + var imports = Exports.get(node.source.value, context) + if (imports == null) return - if (imports.errors.length) { - imports.reportErrors(context, node) - } else if (imports.get('default') === undefined) { - context.report(defaultSpecifier, 'No default export found in module.') + if (imports.errors.length) { + imports.reportErrors(context, node) + } else if (imports.get('default') === undefined) { + context.report(defaultSpecifier, 'No default export found in module.') + } } - } - return { - 'ImportDeclaration': checkDefault.bind(null, 'ImportDefaultSpecifier'), - 'ExportNamedDeclaration': checkDefault.bind(null, 'ExportDefaultSpecifier'), - } + return { + 'ImportDeclaration': checkDefault.bind(null, 'ImportDefaultSpecifier'), + 'ExportNamedDeclaration': checkDefault.bind(null, 'ExportDefaultSpecifier'), + } + }, } diff --git a/src/rules/export.js b/src/rules/export.js index 0f1eec415..f7f37305e 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -1,75 +1,78 @@ -import Map from 'es6-map' -import Set from 'es6-set' +import ExportMap, { recursivePatternCapture } from '../ExportMap' -import ExportMap, { recursivePatternCapture } from '../core/getExports' +module.exports = { + meta: { + docs: {}, + }, -module.exports = function (context) { - const named = new Map() + create: function (context) { + const named = new Map() - function addNamed(name, node) { - let nodes = named.get(name) + function addNamed(name, node) { + let nodes = named.get(name) - if (nodes == null) { - nodes = new Set() - named.set(name, nodes) - } - - nodes.add(node) - } - - return { - 'ExportDefaultDeclaration': (node) => addNamed('default', node), - - 'ExportSpecifier': function (node) { - addNamed(node.exported.name, node.exported) - }, - - 'ExportNamedDeclaration': function (node) { - if (node.declaration == null) return - - if (node.declaration.id != null) { - addNamed(node.declaration.id.name, node.declaration.id) + if (nodes == null) { + nodes = new Set() + named.set(name, nodes) } - if (node.declaration.declarations == null) return - - node.declaration.declarations.forEach(declaration => { - recursivePatternCapture(declaration.id, v => addNamed(v.name, v)) - }) - }, - - 'ExportAllDeclaration': function (node) { - if (node.source == null) return // not sure if this is ever true - - const remoteExports = ExportMap.get(node.source.value, context) - if (remoteExports == null) return + nodes.add(node) + } - if (remoteExports.errors.length) { - remoteExports.reportErrors(context, node) - return - } - let any = false - remoteExports.forEach((v, name) => - name !== 'default' && - (any = true) && // poor man's filter - addNamed(name, node)) - - if (!any) { - context.report(node.source, - `No named exports found in module '${node.source.value}'.`) - } - }, - - 'Program:exit': function () { - named.forEach((nodes, name) => { - if (nodes.size <= 1) return - - nodes.forEach(node => { - if (name === 'default') { - context.report(node, 'Multiple default exports.') - } else context.report(node, `Multiple exports of name '${name}'.`) - }) - }) - }, - } + return { + 'ExportDefaultDeclaration': (node) => addNamed('default', node), + + 'ExportSpecifier': function (node) { + addNamed(node.exported.name, node.exported) + }, + + 'ExportNamedDeclaration': function (node) { + if (node.declaration == null) return + + if (node.declaration.id != null) { + addNamed(node.declaration.id.name, node.declaration.id) + } + + if (node.declaration.declarations != null) { + for (let declaration of node.declaration.declarations) { + recursivePatternCapture(declaration.id, v => addNamed(v.name, v)) + } + } + }, + + 'ExportAllDeclaration': function (node) { + if (node.source == null) return // not sure if this is ever true + + const remoteExports = ExportMap.get(node.source.value, context) + if (remoteExports == null) return + + if (remoteExports.errors.length) { + remoteExports.reportErrors(context, node) + return + } + let any = false + remoteExports.forEach((v, name) => + name !== 'default' && + (any = true) && // poor man's filter + addNamed(name, node)) + + if (!any) { + context.report(node.source, + `No named exports found in module '${node.source.value}'.`) + } + }, + + 'Program:exit': function () { + for (let [name, nodes] of named) { + if (nodes.size <= 1) continue + + for (let node of nodes) { + if (name === 'default') { + context.report(node, 'Multiple default exports.') + } else context.report(node, `Multiple exports of name '${name}'.`) + } + } + }, + } + }, } diff --git a/src/rules/extensions.js b/src/rules/extensions.js index 1522ffe7c..48860fa5f 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -1,98 +1,103 @@ import path from 'path' -import endsWith from 'lodash.endswith' import has from 'has' import assign from 'object-assign' -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' import { isBuiltIn } from '../core/importType' -module.exports = function (context) { - const configuration = context.options[0] || 'never' - const defaultConfig = typeof configuration === 'string' ? configuration : null - const modifiers = assign( - {}, - typeof configuration === 'object' ? configuration : context.options[1] - ) +const enumValues = { enum: [ 'always', 'never' ] } +const patternProperties = { + type: 'object', + patternProperties: { '.*': enumValues }, +} - function isUseOfExtensionRequired(extension) { - if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } - return modifiers[extension] === 'always' - } +module.exports = { + meta: { + docs: {}, - function isUseOfExtensionForbidden(extension) { - if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } - return modifiers[extension] === 'never' - } + schema: { + anyOf: [ + { + type: 'array', + items: [enumValues], + additionalItems: false, + }, + { + type: 'array', + items: [patternProperties], + additionalItems: false, + }, + { + type: 'array', + items: [ + enumValues, + patternProperties, + ], + additionalItems: false, + }, + ], + }, + }, - function isResolvableWithoutExtension(file) { - const extension = path.extname(file) - const fileWithoutExtension = file.slice(0, -extension.length) - const resolvedFileWithoutExtension = resolve(fileWithoutExtension, context) + create: function (context) { + const configuration = context.options[0] || 'never' + const defaultConfig = typeof configuration === 'string' ? configuration : null + const modifiers = assign( + {}, + typeof configuration === 'object' ? configuration : context.options[1] + ) - return resolvedFileWithoutExtension === resolve(file, context) - } + function isUseOfExtensionRequired(extension) { + if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } + return modifiers[extension] === 'always' + } - function checkFileExtension(node) { - const { source } = node - const importPath = source.value + function isUseOfExtensionForbidden(extension) { + if (!has(modifiers, extension)) { modifiers[extension] = defaultConfig } + return modifiers[extension] === 'never' + } - // don't enforce anything on builtins - if (isBuiltIn(importPath, context.settings)) return + function isResolvableWithoutExtension(file) { + const extension = path.extname(file) + const fileWithoutExtension = file.slice(0, -extension.length) + const resolvedFileWithoutExtension = resolve(fileWithoutExtension, context) - const resolvedPath = resolve(importPath, context) + return resolvedFileWithoutExtension === resolve(file, context) + } - // get extension from resolved path, if possible. - // for unresolved, use source value. - const extension = path.extname(resolvedPath || importPath).substring(1) + function checkFileExtension(node) { + const { source } = node + const importPath = source.value - if (!extension || !endsWith(importPath, extension)) { - if (isUseOfExtensionRequired(extension) && !isUseOfExtensionForbidden(extension)) { - context.report({ - node: source, - message: - `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPath}"`, - }) - } - } else if (extension) { - if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) { - context.report({ - node: source, - message: `Unexpected use of file extension "${extension}" for "${importPath}"`, - }) - } - } - } + // don't enforce anything on builtins + if (isBuiltIn(importPath, context.settings)) return - return { - ImportDeclaration: checkFileExtension, - } -} + const resolvedPath = resolve(importPath, context) -const enumValues = { enum: [ 'always', 'never' ] } -const patternProperties = { - type: 'object', - patternProperties: { '.*': enumValues }, -} + // get extension from resolved path, if possible. + // for unresolved, use source value. + const extension = path.extname(resolvedPath || importPath).substring(1) -module.exports.schema = { - anyOf: [ - { - type: 'array', - items: [enumValues], - additionalItems: false, - }, - { - type: 'array', - items: [patternProperties], - additionalItems: false, - }, - { - type: 'array', - items: [ - enumValues, - patternProperties, - ], - additionalItems: false, - }, - ], + if (!extension || !importPath.endsWith(extension)) { + if (isUseOfExtensionRequired(extension) && !isUseOfExtensionForbidden(extension)) { + context.report({ + node: source, + message: + `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPath}"`, + }) + } + } else if (extension) { + if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) { + context.report({ + node: source, + message: `Unexpected use of file extension "${extension}" for "${importPath}"`, + }) + } + } + } + + return { + ImportDeclaration: checkFileExtension, + } + }, } diff --git a/src/rules/first.js b/src/rules/first.js new file mode 100644 index 000000000..7642cae1d --- /dev/null +++ b/src/rules/first.js @@ -0,0 +1,51 @@ +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + function isPossibleDirective (node) { + return node.type === 'ExpressionStatement' && + node.expression.type === 'Literal' && + typeof node.expression.value === 'string' + } + + return { + 'Program': function (n) { + const body = n.body + , absoluteFirst = context.options[0] === 'absolute-first' + let nonImportCount = 0 + , anyExpressions = false + , anyRelative = false + body.forEach(function (node){ + if (!anyExpressions && isPossibleDirective(node)) { + return + } + + anyExpressions = true + + if (node.type === 'ImportDeclaration') { + if (absoluteFirst) { + if (/^\./.test(node.source.value)) { + anyRelative = true + } else if (anyRelative) { + context.report({ + node: node.source, + message: 'Absolute imports should come before relative imports.', + }) + } + } + if (nonImportCount > 0) { + context.report({ + node, + message: 'Import in body of module; reorder to top.', + }) + } + } else { + nonImportCount++ + } + }) + }, + } + }, +} diff --git a/src/rules/imports-first.js b/src/rules/imports-first.js deleted file mode 100644 index 790c81b7f..000000000 --- a/src/rules/imports-first.js +++ /dev/null @@ -1,45 +0,0 @@ -module.exports = function (context) { - function isPossibleDirective (node) { - return node.type === 'ExpressionStatement' && - node.expression.type === 'Literal' && - typeof node.expression.value === 'string' - } - - return { - 'Program': function (n) { - const body = n.body - , absoluteFirst = context.options[0] === 'absolute-first' - let nonImportCount = 0 - , anyExpressions = false - , anyRelative = false - body.forEach(function (node){ - if (!anyExpressions && isPossibleDirective(node)) { - return - } - - anyExpressions = true - - if (node.type === 'ImportDeclaration') { - if (absoluteFirst) { - if (/^\./.test(node.source.value)) { - anyRelative = true - } else if (anyRelative) { - context.report({ - node: node.source, - message: 'Absolute imports should come before relative imports.', - }) - } - } - if (nonImportCount > 0) { - context.report({ - node, - message: 'Import in body of module; reorder to top.', - }) - } - } else { - nonImportCount++ - } - }) - }, - } -} diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js index 396e21e54..c28afd086 100644 --- a/src/rules/max-dependencies.js +++ b/src/rules/max-dependencies.js @@ -14,36 +14,42 @@ const countDependencies = (dependencies, lastNode, context) => { } } -module.exports = context => { - const dependencies = new Set() // keep track of dependencies - let lastNode // keep track of the last node to report on - - return { - ImportDeclaration(node) { - dependencies.add(node.source.value) - lastNode = node.source - }, - - CallExpression(node) { - if (isStaticRequire(node)) { - const [ requirePath ] = node.arguments - dependencies.add(requirePath.value) - lastNode = node - } - }, - - 'Program:exit': function () { - countDependencies(dependencies, lastNode, context) - }, - } -} +module.exports = { + meta: { + docs: {}, + + schema: [ + { + 'type': 'object', + 'properties': { + 'max': { 'type': 'number' }, + }, + 'additionalProperties': false, + }, + ], + }, -module.exports.schema = [ - { - 'type': 'object', - 'properties': { - 'max': { 'type': 'number' }, - }, - 'additionalProperties': false, + create: context => { + const dependencies = new Set() // keep track of dependencies + let lastNode // keep track of the last node to report on + + return { + ImportDeclaration(node) { + dependencies.add(node.source.value) + lastNode = node.source + }, + + CallExpression(node) { + if (isStaticRequire(node)) { + const [ requirePath ] = node.arguments + dependencies.add(requirePath.value) + lastNode = node + } + }, + + 'Program:exit': function () { + countDependencies(dependencies, lastNode, context) + }, + } }, -] +} diff --git a/src/rules/named.js b/src/rules/named.js index ca3b0ebb5..d7589ebe7 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -1,54 +1,60 @@ import * as path from 'path' -import Exports from '../core/getExports' +import Exports from '../ExportMap' -module.exports = function (context) { - function checkSpecifiers(key, type, node) { - if (node.source == null) return // local export, ignore +module.exports = { + meta: { + docs: {}, + }, - if (!node.specifiers - .some(function (im) { return im.type === type })) { - return // no named imports/exports - } + create: function (context) { + function checkSpecifiers(key, type, node) { + if (node.source == null) return // local export, ignore + + if (!node.specifiers + .some(function (im) { return im.type === type })) { + return // no named imports/exports + } - const imports = Exports.get(node.source.value, context) - if (imports == null) return + const imports = Exports.get(node.source.value, context) + if (imports == null) return - if (imports.errors.length) { - imports.reportErrors(context, node) - return - } + if (imports.errors.length) { + imports.reportErrors(context, node) + return + } - node.specifiers.forEach(function (im) { - if (im.type !== type) return + node.specifiers.forEach(function (im) { + if (im.type !== type) return - const deepLookup = imports.hasDeep(im[key].name) + const deepLookup = imports.hasDeep(im[key].name) - if (!deepLookup.found) { - if (deepLookup.path.length > 1) { - const deepPath = deepLookup.path - .map(i => path.relative(path.dirname(context.getFilename()), i.path)) - .join(' -> ') + if (!deepLookup.found) { + if (deepLookup.path.length > 1) { + const deepPath = deepLookup.path + .map(i => path.relative(path.dirname(context.getFilename()), i.path)) + .join(' -> ') - context.report(im[key], - `${im[key].name} not found via ${deepPath}`) - } else { - context.report(im[key], - im[key].name + ' not found in \'' + node.source.value + '\'') + context.report(im[key], + `${im[key].name} not found via ${deepPath}`) + } else { + context.report(im[key], + im[key].name + ' not found in \'' + node.source.value + '\'') + } } - } - }) - } + }) + } - return { - 'ImportDeclaration': checkSpecifiers.bind( null - , 'imported' - , 'ImportSpecifier' - ), + return { + 'ImportDeclaration': checkSpecifiers.bind( null + , 'imported' + , 'ImportSpecifier' + ), - 'ExportNamedDeclaration': checkSpecifiers.bind( null - , 'local' - , 'ExportSpecifier' - ), - } + 'ExportNamedDeclaration': checkSpecifiers.bind( null + , 'local' + , 'ExportSpecifier' + ), + } + }, } diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 74e45ccaf..87824c179 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -1,52 +1,87 @@ -import Map from 'es6-map' - -import Exports from '../core/getExports' +import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' -import declaredScope from '../core/declaredScope' - -exports.meta = { - schema: [ - { - 'type': 'object', - 'properties': { - 'allowComputed': { - 'description': - 'If `false`, will report computed (and thus, un-lintable) references ' + - 'to namespace members.', - 'type': 'boolean', - 'default': false, +import declaredScope from 'eslint-module-utils/declaredScope' + +module.exports = { + meta: { + schema: [ + { + 'type': 'object', + 'properties': { + 'allowComputed': { + 'description': + 'If `false`, will report computed (and thus, un-lintable) references ' + + 'to namespace members.', + 'type': 'boolean', + 'default': false, + }, }, + 'additionalProperties': false, }, - 'additionalProperties': false, - }, - ], -} + ], + }, + + create: function namespaceRule(context) { -exports.create = function namespaceRule(context) { + // read options + const { + allowComputed = false, + } = context.options[0] || {} - // read options - const { - allowComputed = false, - } = context.options[0] || {} + const namespaces = new Map() - const namespaces = new Map() + function makeMessage(last, namepath) { + return `'${last.name}' not found in` + + (namepath.length > 1 ? ' deeply ' : ' ') + + `imported namespace '${namepath.join('.')}'.` + } - function makeMessage(last, namepath) { - return `'${last.name}' not found in` + - (namepath.length > 1 ? ' deeply ' : ' ') + - `imported namespace '${namepath.join('.')}'.` - } + return { - return { + // pick up all imports at body entry time, to properly respect hoisting + 'Program': function ({ body }) { + function processBodyStatement(declaration) { + if (declaration.type !== 'ImportDeclaration') return - // pick up all imports at body entry time, to properly respect hoisting - 'Program': function ({ body }) { - function processBodyStatement(declaration) { - if (declaration.type !== 'ImportDeclaration') return + if (declaration.specifiers.length === 0) return - if (declaration.specifiers.length === 0) return + const imports = Exports.get(declaration.source.value, context) + if (imports == null) return null + + if (imports.errors.length) { + imports.reportErrors(context, declaration) + return + } - const imports = Exports.get(declaration.source.value, context) + for (let specifier of declaration.specifiers) { + switch (specifier.type) { + case 'ImportNamespaceSpecifier': + if (!imports.size) { + context.report(specifier, + `No exported names found in module '${declaration.source.value}'.`) + } + namespaces.set(specifier.local.name, imports) + break + case 'ImportDefaultSpecifier': + case 'ImportSpecifier': { + const meta = imports.get( + // default to 'default' for default http://i.imgur.com/nj6qAWy.jpg + specifier.imported ? specifier.imported.name : 'default') + if (!meta || !meta.namespace) break + namespaces.set(specifier.local.name, meta.namespace) + break + } + } + } + } + body.forEach(processBodyStatement) + }, + + // same as above, but does not add names to local map + 'ExportNamespaceSpecifier': function (namespace) { + var declaration = importDeclaration(context) + + var imports = Exports.get(declaration.source.value, context) if (imports == null) return null if (imports.errors.length) { @@ -54,128 +89,98 @@ exports.create = function namespaceRule(context) { return } - declaration.specifiers.forEach((specifier) => { - switch (specifier.type) { - case 'ImportNamespaceSpecifier': - if (!imports.size) { - context.report(specifier, - `No exported names found in module '${declaration.source.value}'.`) - } - namespaces.set(specifier.local.name, imports) - break - case 'ImportDefaultSpecifier': - case 'ImportSpecifier': { - const meta = imports.get( - // default to 'default' for default http://i.imgur.com/nj6qAWy.jpg - specifier.imported ? specifier.imported.name : 'default') - if (!meta || !meta.namespace) break - namespaces.set(specifier.local.name, meta.namespace) - break + if (!imports.size) { + context.report(namespace, + `No exported names found in module '${declaration.source.value}'.`) + } + }, + + // todo: check for possible redefinition + + 'MemberExpression': function (dereference) { + if (dereference.object.type !== 'Identifier') return + if (!namespaces.has(dereference.object.name)) return + + if (dereference.parent.type === 'AssignmentExpression' && + dereference.parent.left === dereference) { + context.report(dereference.parent, + `Assignment to member of namespace '${dereference.object.name}'.`) + } + + // go deep + var namespace = namespaces.get(dereference.object.name) + var namepath = [dereference.object.name] + // while property is namespace and parent is member expression, keep validating + while (namespace instanceof Exports && + dereference.type === 'MemberExpression') { + + if (dereference.computed) { + if (!allowComputed) { + context.report(dereference.property, + 'Unable to validate computed reference to imported namespace \'' + + dereference.object.name + '\'.') } + return } - }) - } - body.forEach(processBodyStatement) - }, - - // same as above, but does not add names to local map - 'ExportNamespaceSpecifier': function (namespace) { - var declaration = importDeclaration(context) - - var imports = Exports.get(declaration.source.value, context) - if (imports == null) return null - - if (imports.errors.length) { - imports.reportErrors(context, declaration) - return - } - - if (!imports.size) { - context.report(namespace, - `No exported names found in module '${declaration.source.value}'.`) - } - }, - - // todo: check for possible redefinition - - 'MemberExpression': function (dereference) { - if (dereference.object.type !== 'Identifier') return - if (!namespaces.has(dereference.object.name)) return - - if (dereference.parent.type === 'AssignmentExpression' && - dereference.parent.left === dereference) { - context.report(dereference.parent, - `Assignment to member of namespace '${dereference.object.name}'.`) - } - - // go deep - var namespace = namespaces.get(dereference.object.name) - var namepath = [dereference.object.name] - // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && - dereference.type === 'MemberExpression') { - - if (dereference.computed) { - if (!allowComputed) { - context.report(dereference.property, - 'Unable to validate computed reference to imported namespace \'' + - dereference.object.name + '\'.') + + if (!namespace.has(dereference.property.name)) { + context.report( + dereference.property, + makeMessage(dereference.property, namepath)) + break } - return - } - if (!namespace.has(dereference.property.name)) { - context.report( - dereference.property, - makeMessage(dereference.property, namepath)) - break + const exported = namespace.get(dereference.property.name) + if (exported == null) return + + // stash and pop + namepath.push(dereference.property.name) + namespace = exported.namespace + dereference = dereference.parent } - const exported = namespace.get(dereference.property.name) - if (exported == null) return - - // stash and pop - namepath.push(dereference.property.name) - namespace = exported.namespace - dereference = dereference.parent - } - - }, - - 'VariableDeclarator': function ({ id, init }) { - if (init == null) return - if (init.type !== 'Identifier') return - if (!namespaces.has(init.name)) return - - // check for redefinition in intermediate scopes - if (declaredScope(context, init.name) !== 'module') return - - // DFS traverse child namespaces - function testKey(pattern, namespace, path = [init.name]) { - if (!(namespace instanceof Exports)) return - - if (pattern.type !== 'ObjectPattern') return - - pattern.properties.forEach((property) => { - if (property.key.type !== 'Identifier') { - context.report({ - node: property, - message: 'Only destructure top-level names.', - }) - } else if (!namespace.has(property.key.name)) { - context.report({ - node: property, - message: makeMessage(property.key, path), - }) - } else { + }, + + 'VariableDeclarator': function ({ id, init }) { + if (init == null) return + if (init.type !== 'Identifier') return + if (!namespaces.has(init.name)) return + + // check for redefinition in intermediate scopes + if (declaredScope(context, init.name) !== 'module') return + + // DFS traverse child namespaces + function testKey(pattern, namespace, path = [init.name]) { + if (!(namespace instanceof Exports)) return + + if (pattern.type !== 'ObjectPattern') return + + for (let property of pattern.properties) { + + if (property.key.type !== 'Identifier') { + context.report({ + node: property, + message: 'Only destructure top-level names.', + }) + continue + } + + if (!namespace.has(property.key.name)) { + context.report({ + node: property, + message: makeMessage(property.key, path), + }) + continue + } + path.push(property.key.name) testKey(property.value, namespace.get(property.key.name).namespace, path) path.pop() } - }) - } + } - testKey(id, namespaces.get(init.name)) - }, - } + testKey(id, namespaces.get(init.name)) + }, + } + }, } diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index 4c429b6e9..3eb692198 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -4,10 +4,8 @@ */ import isStaticRequire from '../core/staticRequire' -import findIndex from 'lodash.findindex' import debug from 'debug' - const log = debug('eslint-plugin-import:rules:newline-after-import') //------------------------------------------------------------------------------ @@ -33,7 +31,7 @@ function getScopeBody(scope) { } function findNodeIndexInScopeBody(body, nodeToFind) { - return findIndex(body, (node) => containsNodeOrEqual(node, nodeToFind)) + return body.findIndex((node) => containsNodeOrEqual(node, nodeToFind)) } function getLineDifference(node, nextNode) { @@ -41,65 +39,57 @@ function getLineDifference(node, nextNode) { } -module.exports = function (context) { - const scopes = [] - let scopeIndex = 0 +module.exports = { + meta: { + docs: {}, + }, + create: function (context) { + let level = 0 + const requireCalls = [] + + function checkForNewLine(node, nextNode, type) { + if (getLineDifference(node, nextNode) < 2) { + let column = node.loc.start.column - function checkForNewLine(node, nextNode, type) { - if (getLineDifference(node, nextNode) < 2) { - let column = node.loc.start.column + if (node.loc.start.line !== node.loc.end.line) { + column = 0 + } - if (node.loc.start.line !== node.loc.end.line) { - column = 0 + context.report({ + loc: { + line: node.loc.end.line, + column, + }, + message: `Expected empty line after ${type} statement not followed by another ${type}.`, + }) } + } - context.report({ - loc: { - line: node.loc.end.line, - column, - }, - message: `Expected empty line after ${type} statement not followed by another ${type}.`, - }) + function incrementLevel() { + level++ + } + function decrementLevel() { + level-- } - } - return { - ImportDeclaration: function (node) { - const { parent } = node - const nodePosition = parent.body.indexOf(node) - const nextNode = parent.body[nodePosition + 1] + return { + ImportDeclaration: function (node) { + const { parent } = node + const nodePosition = parent.body.indexOf(node) + const nextNode = parent.body[nodePosition + 1] - if (nextNode && nextNode.type !== 'ImportDeclaration') { - checkForNewLine(node, nextNode, 'import') - } - }, - Program: function () { - scopes.push({ scope: context.getScope(), requireCalls: [] }) - }, - CallExpression: function(node) { - const scope = context.getScope() - if (isStaticRequire(node)) { - const currentScope = scopes[scopeIndex] - - if (scope === currentScope.scope) { - currentScope.requireCalls.push(node) - } else { - scopes.push({ scope, requireCalls: [ node ] }) - scopeIndex += 1 + if (nextNode && nextNode.type !== 'ImportDeclaration') { + checkForNewLine(node, nextNode, 'import') } - } - }, - 'Program:exit': function () { - log('exit processing for', context.getFilename()) - scopes.forEach(function ({ scope, requireCalls }) { - const scopeBody = getScopeBody(scope) - - // skip non-array scopes (i.e. arrow function expressions) - if (!scopeBody || !(scopeBody instanceof Array)) { - log('invalid scope:', scopeBody) - return + }, + CallExpression: function(node) { + if (isStaticRequire(node) && level === 0) { + requireCalls.push(node) } - + }, + 'Program:exit': function () { + log('exit processing for', context.getFilename()) + const scopeBody = getScopeBody(context.getScope()) log('got scope:', scopeBody) requireCalls.forEach(function (node, index) { @@ -120,7 +110,17 @@ module.exports = function (context) { checkForNewLine(statementWithRequireCall, nextStatement, 'require') } }) - }) - }, - } + }, + FunctionDeclaration: incrementLevel, + FunctionExpression: incrementLevel, + ArrowFunctionExpression: incrementLevel, + BlockStatement: incrementLevel, + ObjectExpression: incrementLevel, + 'FunctionDeclaration:exit': decrementLevel, + 'FunctionExpression:exit': decrementLevel, + 'ArrowFunctionExpression:exit': decrementLevel, + 'BlockStatement:exit': decrementLevel, + 'ObjectExpression:exit': decrementLevel, + } + }, } diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index 08c8b80fe..33da932fd 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -7,15 +7,21 @@ function reportIfMissing(context, node, name) { } } -module.exports = function (context) { - return { - ImportDeclaration: function handleImports(node) { - reportIfMissing(context, node, node.source.value) - }, - CallExpression: function handleRequires(node) { - if (isStaticRequire(node)) { - reportIfMissing(context, node, node.arguments[0].value) - } - }, - } +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + return { + ImportDeclaration: function handleImports(node) { + reportIfMissing(context, node, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfMissing(context, node, node.arguments[0].value) + } + }, + } + }, } diff --git a/src/rules/no-amd.js b/src/rules/no-amd.js index 86f1c9952..6686be935 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.js @@ -7,27 +7,33 @@ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, + }, - return { + create: function (context) { - 'CallExpression': function (node) { - if (context.getScope().type !== 'module') return + return { - if (node.callee.type !== 'Identifier') return - if (node.callee.name !== 'require' && - node.callee.name !== 'define') return + 'CallExpression': function (node) { + if (context.getScope().type !== 'module') return - // todo: capture define((require, module, exports) => {}) form? - if (node.arguments.length !== 2) return + if (node.callee.type !== 'Identifier') return + if (node.callee.name !== 'require' && + node.callee.name !== 'define') return - const modules = node.arguments[0] - if (modules.type !== 'ArrayExpression') return + // todo: capture define((require, module, exports) => {}) form? + if (node.arguments.length !== 2) return - // todo: check second arg type? (identifier or callback) + const modules = node.arguments[0] + if (modules.type !== 'ArrayExpression') return - context.report(node, `Expected imports instead of AMD ${node.callee.name}().`) - }, - } + // todo: check second arg type? (identifier or callback) + context.report(node, `Expected imports instead of AMD ${node.callee.name}().`) + }, + } + + }, } diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index eff79660f..62a0804f2 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -6,54 +6,59 @@ const EXPORT_MESSAGE = 'Expected "export" or "export default"' , IMPORT_MESSAGE = 'Expected "import" instead of "require()"' +function allowPrimitive(node, context) { + if (context.options.indexOf('allow-primitive-modules') < 0) return false + if (node.parent.type !== 'AssignmentExpression') return false + return (node.parent.right.type !== 'ObjectExpression') +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function (context) { +module.exports = { + meta: { + docs: {}, + }, - return { + create: function (context) { - 'MemberExpression': function (node) { + return { - // module.exports - if (node.object.name === 'module' && node.property.name === 'exports') { - if (allowPrimitive(node, context)) return - context.report({ node, message: EXPORT_MESSAGE }) - } + 'MemberExpression': function (node) { - // exports. - if (node.object.name === 'exports') { - context.report({ node, message: EXPORT_MESSAGE }) - } + // module.exports + if (node.object.name === 'module' && node.property.name === 'exports') { + if (allowPrimitive(node, context)) return + context.report({ node, message: EXPORT_MESSAGE }) + } - }, - 'CallExpression': function (call) { - if (context.getScope().type !== 'module') return + // exports. + if (node.object.name === 'exports') { + context.report({ node, message: EXPORT_MESSAGE }) + } - if (call.callee.type !== 'Identifier') return - if (call.callee.name !== 'require') return + }, + 'CallExpression': function (call) { + if (context.getScope().type !== 'module') return - if (call.arguments.length !== 1) return - var module = call.arguments[0] + if (call.callee.type !== 'Identifier') return + if (call.callee.name !== 'require') return - if (module.type !== 'Literal') return - if (typeof module.value !== 'string') return + if (call.arguments.length !== 1) return + var module = call.arguments[0] - // keeping it simple: all 1-string-arg `require` calls are reported - context.report({ - node: call.callee, - message: IMPORT_MESSAGE, - }) - }, - } + if (module.type !== 'Literal') return + if (typeof module.value !== 'string') return -} + // keeping it simple: all 1-string-arg `require` calls are reported + context.report({ + node: call.callee, + message: IMPORT_MESSAGE, + }) + }, + } - // allow non-objects as module.exports -function allowPrimitive(node, context) { - if (context.options.indexOf('allow-primitive-modules') < 0) return false - if (node.parent.type !== 'AssignmentExpression') return false - return (node.parent.right.type !== 'ObjectExpression') + }, } diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index 882f5e48c..e50c2e516 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -1,134 +1,138 @@ -import Map from 'es6-map' +import Exports from '../ExportMap' +import declaredScope from 'eslint-module-utils/declaredScope' -import Exports from '../core/getExports' -import declaredScope from '../core/declaredScope' +function message(deprecation) { + return 'Deprecated' + (deprecation.description ? ': ' + deprecation.description : '.') +} -module.exports = function (context) { - const deprecated = new Map() - , namespaces = new Map() +function getDeprecation(metadata) { + if (!metadata || !metadata.doc) return - function checkSpecifiers(node) { - if (node.type !== 'ImportDeclaration') return - if (node.source == null) return // local export, ignore + let deprecation + if (metadata.doc.tags.some(t => t.title === 'deprecated' && (deprecation = t))) { + return deprecation + } +} - const imports = Exports.get(node.source.value, context) - if (imports == null) return +module.exports = { + meta: { + docs: {}, + }, - let moduleDeprecation - if (imports.doc && - imports.doc.tags.some(t => t.title === 'deprecated' && (moduleDeprecation = t))) { - context.report({ node, message: message(moduleDeprecation) }) - } + create: function (context) { + const deprecated = new Map() + , namespaces = new Map() - if (imports.errors.length) { - imports.reportErrors(context, node) - return - } + function checkSpecifiers(node) { + if (node.type !== 'ImportDeclaration') return + if (node.source == null) return // local export, ignore - node.specifiers.forEach(function (im) { - let imported, local - switch (im.type) { + const imports = Exports.get(node.source.value, context) + if (imports == null) return + let moduleDeprecation + if (imports.doc && + imports.doc.tags.some(t => t.title === 'deprecated' && (moduleDeprecation = t))) { + context.report({ node, message: message(moduleDeprecation) }) + } - case 'ImportNamespaceSpecifier':{ - if (!imports.size) return - namespaces.set(im.local.name, imports) - return - } + if (imports.errors.length) { + imports.reportErrors(context, node) + return + } - case 'ImportDefaultSpecifier': - imported = 'default' - local = im.local.name - break + node.specifiers.forEach(function (im) { + let imported, local + switch (im.type) { - case 'ImportSpecifier': - imported = im.imported.name - local = im.local.name - break - default: return // can't handle this one - } + case 'ImportNamespaceSpecifier':{ + if (!imports.size) return + namespaces.set(im.local.name, imports) + return + } - // unknown thing can't be deprecated - const exported = imports.get(imported) - if (exported == null) return + case 'ImportDefaultSpecifier': + imported = 'default' + local = im.local.name + break - // capture import of deep namespace - if (exported.namespace) namespaces.set(local, exported.namespace) + case 'ImportSpecifier': + imported = im.imported.name + local = im.local.name + break - const deprecation = getDeprecation(imports.get(imported)) - if (!deprecation) return + default: return // can't handle this one + } - context.report({ node: im, message: message(deprecation) }) + // unknown thing can't be deprecated + const exported = imports.get(imported) + if (exported == null) return - deprecated.set(local, deprecation) + // capture import of deep namespace + if (exported.namespace) namespaces.set(local, exported.namespace) - }) - } + const deprecation = getDeprecation(imports.get(imported)) + if (!deprecation) return - return { - 'Program': ({ body }) => body.forEach(checkSpecifiers), + context.report({ node: im, message: message(deprecation) }) - 'Identifier': function (node) { - if (node.parent.type === 'MemberExpression' && node.parent.property === node) { - return // handled by MemberExpression - } + deprecated.set(local, deprecation) - // ignore specifier identifiers - if (node.parent.type.slice(0, 6) === 'Import') return + }) + } - if (!deprecated.has(node.name)) return + return { + 'Program': ({ body }) => body.forEach(checkSpecifiers), - if (declaredScope(context, node.name) !== 'module') return - context.report({ - node, - message: message(deprecated.get(node.name)), - }) - }, + 'Identifier': function (node) { + if (node.parent.type === 'MemberExpression' && node.parent.property === node) { + return // handled by MemberExpression + } - 'MemberExpression': function (dereference) { - if (dereference.object.type !== 'Identifier') return - if (!namespaces.has(dereference.object.name)) return + // ignore specifier identifiers + if (node.parent.type.slice(0, 6) === 'Import') return - if (declaredScope(context, dereference.object.name) !== 'module') return + if (!deprecated.has(node.name)) return - // go deep - var namespace = namespaces.get(dereference.object.name) - var namepath = [dereference.object.name] - // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && - dereference.type === 'MemberExpression') { + if (declaredScope(context, node.name) !== 'module') return + context.report({ + node, + message: message(deprecated.get(node.name)), + }) + }, - // ignore computed parts for now - if (dereference.computed) return + 'MemberExpression': function (dereference) { + if (dereference.object.type !== 'Identifier') return + if (!namespaces.has(dereference.object.name)) return - const metadata = namespace.get(dereference.property.name) + if (declaredScope(context, dereference.object.name) !== 'module') return - if (!metadata) break - const deprecation = getDeprecation(metadata) + // go deep + var namespace = namespaces.get(dereference.object.name) + var namepath = [dereference.object.name] + // while property is namespace and parent is member expression, keep validating + while (namespace instanceof Exports && + dereference.type === 'MemberExpression') { - if (deprecation) { - context.report({ node: dereference.property, message: message(deprecation) }) - } + // ignore computed parts for now + if (dereference.computed) return - // stash and pop - namepath.push(dereference.property.name) - namespace = metadata.namespace - dereference = dereference.parent - } - }, - } -} + const metadata = namespace.get(dereference.property.name) -function message(deprecation) { - return 'Deprecated' + (deprecation.description ? ': ' + deprecation.description : '.') -} + if (!metadata) break + const deprecation = getDeprecation(metadata) -function getDeprecation(metadata) { - if (!metadata || !metadata.doc) return + if (deprecation) { + context.report({ node: dereference.property, message: message(deprecation) }) + } - let deprecation - if (metadata.doc.tags.some(t => t.title === 'deprecated' && (deprecation = t))) { - return deprecation - } + // stash and pop + namepath.push(dereference.property.name) + namespace = metadata.namespace + dereference = dereference.parent + } + }, + } + }, } diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 1c573bd53..a63fc44d5 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,37 +1,40 @@ -import Map from 'es6-map' -import Set from 'es6-set' - -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' function checkImports(imported, context) { - imported.forEach((nodes, module) => { + for (let [module, nodes] of imported.entries()) { if (nodes.size > 1) { - nodes.forEach((node) => { + for (let node of nodes) { context.report(node, `'${module}' imported multiple times.`) - }) + } } - }) + } } -module.exports = function (context) { - const imported = new Map() - const typesImported = new Map() - return { - 'ImportDeclaration': function (n) { - // resolved path will cover aliased duplicates - const resolvedPath = resolve(n.source.value, context) || n.source.value - const importMap = n.importKind === 'type' ? typesImported : imported +module.exports = { + meta: { + docs: {}, + }, - if (importMap.has(resolvedPath)) { - importMap.get(resolvedPath).add(n.source) - } else { - importMap.set(resolvedPath, new Set([n.source])) - } - }, + create: function (context) { + const imported = new Map() + const typesImported = new Map() + return { + 'ImportDeclaration': function (n) { + // resolved path will cover aliased duplicates + const resolvedPath = resolve(n.source.value, context) || n.source.value + const importMap = n.importKind === 'type' ? typesImported : imported - 'Program:exit': function () { - checkImports(imported, context) - checkImports(typesImported, context) - }, - } + if (importMap.has(resolvedPath)) { + importMap.get(resolvedPath).add(n.source) + } else { + importMap.set(resolvedPath, new Set([n.source])) + } + }, + + 'Program:exit': function () { + checkImports(imported, context) + checkImports(typesImported, context) + }, + } + }, } diff --git a/src/rules/no-dynamic-require.js b/src/rules/no-dynamic-require.js index 8a0efae6c..f7d6aad3d 100644 --- a/src/rules/no-dynamic-require.js +++ b/src/rules/no-dynamic-require.js @@ -11,15 +11,21 @@ function isStaticValue(arg) { (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) } -module.exports = function (context) { - return { - CallExpression(node) { - if (isRequire(node) && !isStaticValue(node.arguments[0])) { - context.report({ - node, - message: 'Calls to require() should use string literals', - }) - } - }, - } +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + return { + CallExpression(node) { + if (isRequire(node) && !isStaticValue(node.arguments[0])) { + context.report({ + node, + message: 'Calls to require() should use string literals', + }) + } + }, + } + }, } diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index 65bdb27e5..a14bb611d 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -81,42 +81,48 @@ function testConfig(config, filename) { return config.some(c => minimatch(filename, c)) } -module.exports = function (context) { - const options = context.options[0] || {} - const filename = context.getFilename() - const deps = getDependencies(context) +module.exports = { + meta: { + docs: {}, + + schema: [ + { + 'type': 'object', + 'properties': { + 'devDependencies': { 'type': ['boolean', 'array'] }, + 'optionalDependencies': { 'type': ['boolean', 'array'] }, + 'peerDependencies': { 'type': ['boolean', 'array'] }, + }, + 'additionalProperties': false, + }, + ], + }, - if (!deps) { - return {} - } + create: function (context) { + const options = context.options[0] || {} + const filename = context.getFilename() + const deps = getDependencies(context) - const depsOptions = { - allowDevDeps: testConfig(options.devDependencies, filename) !== false, - allowOptDeps: testConfig(options.optionalDependencies, filename) !== false, - allowPeerDeps: testConfig(options.peerDependencies, filename) !== false, - } + if (!deps) { + return {} + } - // todo: use module visitor from module-utils core - return { - ImportDeclaration: function (node) { - reportIfMissing(context, deps, depsOptions, node, node.source.value) - }, - CallExpression: function handleRequires(node) { - if (isStaticRequire(node)) { - reportIfMissing(context, deps, depsOptions, node, node.arguments[0].value) - } - }, - } -} + const depsOptions = { + allowDevDeps: testConfig(options.devDependencies, filename) !== false, + allowOptDeps: testConfig(options.optionalDependencies, filename) !== false, + allowPeerDeps: testConfig(options.peerDependencies, filename) !== false, + } -module.exports.schema = [ - { - 'type': 'object', - 'properties': { - 'devDependencies': { 'type': ['boolean', 'array'] }, - 'optionalDependencies': { 'type': ['boolean', 'array'] }, - 'peerDependencies': { 'type': ['boolean', 'array'] }, - }, - 'additionalProperties': false, + // todo: use module visitor from module-utils core + return { + ImportDeclaration: function (node) { + reportIfMissing(context, deps, depsOptions, node, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfMissing(context, deps, depsOptions, node, node.arguments[0].value) + } + }, + } }, -] +} diff --git a/src/rules/no-internal-modules.js b/src/rules/no-internal-modules.js index 85452df67..3437d8afe 100644 --- a/src/rules/no-internal-modules.js +++ b/src/rules/no-internal-modules.js @@ -1,92 +1,98 @@ -import find from 'lodash.find' import minimatch from 'minimatch' -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' -module.exports = function noReachingInside(context) { - const options = context.options[0] || {} - const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p)) +module.exports = { + meta: { + docs: {}, - // test if reaching to this destination is allowed - function reachingAllowed(importPath) { - return !!find(allowRegexps, re => re.test(importPath)) - } + schema: [ + { + type: 'object', + properties: { + allow: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + additionalProperties: false, + }, + ], + }, - // minimatch patterns are expected to use / path separators, like import - // statements, so normalize paths to use the same - function normalizeSep(somePath) { - return somePath.split('\\').join('/') - } + create: function noReachingInside(context) { + const options = context.options[0] || {} + const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p)) - // find a directory that is being reached into, but which shouldn't be - function isReachViolation(importPath) { - const steps = normalizeSep(importPath) - .split('/') - .reduce((acc, step) => { - if (!step || step === '.') { - return acc - } else if (step === '..') { - return acc.slice(0, -1) - } else { - return acc.concat(step) - } - }, []) + // test if reaching to this destination is allowed + function reachingAllowed(importPath) { + return allowRegexps.some(re => re.test(importPath)) + } + + // minimatch patterns are expected to use / path separators, like import + // statements, so normalize paths to use the same + function normalizeSep(somePath) { + return somePath.split('\\').join('/') + } - if (steps.length <= 1) return false + // find a directory that is being reached into, but which shouldn't be + function isReachViolation(importPath) { + const steps = normalizeSep(importPath) + .split('/') + .reduce((acc, step) => { + if (!step || step === '.') { + return acc + } else if (step === '..') { + return acc.slice(0, -1) + } else { + return acc.concat(step) + } + }, []) - // before trying to resolve, see if the raw import (with relative - // segments resolved) matches an allowed pattern - const justSteps = steps.join('/') - if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) return false + const nonScopeSteps = steps.filter(step => step.indexOf('@') !== 0) + if (nonScopeSteps.length <= 1) return false - // if the import statement doesn't match directly, try to match the - // resolved path if the import is resolvable - const resolved = resolve(importPath, context) - if (!resolved || reachingAllowed(normalizeSep(resolved))) return false + // before trying to resolve, see if the raw import (with relative + // segments resolved) matches an allowed pattern + const justSteps = steps.join('/') + if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) return false - // this import was not allowed by the allowed paths, and reaches - // so it is a violation - return true - } + // if the import statement doesn't match directly, try to match the + // resolved path if the import is resolvable + const resolved = resolve(importPath, context) + if (!resolved || reachingAllowed(normalizeSep(resolved))) return false - function checkImportForReaching(importPath, node) { - const potentialViolationTypes = ['parent', 'index', 'sibling', 'external', 'internal'] - if (potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 && - isReachViolation(importPath) - ) { - context.report({ - node, - message: `Reaching to "${importPath}" is not allowed.`, - }) + // this import was not allowed by the allowed paths, and reaches + // so it is a violation + return true } - } - return { - ImportDeclaration(node) { - checkImportForReaching(node.source.value, node.source) - }, - CallExpression(node) { - if (isStaticRequire(node)) { - const [ firstArgument ] = node.arguments - checkImportForReaching(firstArgument.value, firstArgument) + function checkImportForReaching(importPath, node) { + const potentialViolationTypes = ['parent', 'index', 'sibling', 'external', 'internal'] + if (potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 && + isReachViolation(importPath) + ) { + context.report({ + node, + message: `Reaching to "${importPath}" is not allowed.`, + }) } - }, - } -} + } -module.exports.schema = [ - { - type: 'object', - properties: { - allow: { - type: 'array', - items: { - type: 'string', - }, + return { + ImportDeclaration(node) { + checkImportForReaching(node.source.value, node.source) }, - }, - additionalProperties: false, + CallExpression(node) { + if (isStaticRequire(node)) { + const [ firstArgument ] = node.arguments + checkImportForReaching(firstArgument.value, firstArgument) + } + }, + } }, -] +} diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index fc4cf2e2e..8d16f8068 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -1,45 +1,51 @@ -module.exports = function (context) { - function checkDeclaration(node) { - const {kind} = node - if (kind === 'var' || kind === 'let') { - context.report(node, `Exporting mutable '${kind}' binding, use 'const' instead.`) +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + function checkDeclaration(node) { + const {kind} = node + if (kind === 'var' || kind === 'let') { + context.report(node, `Exporting mutable '${kind}' binding, use 'const' instead.`) + } } - } - function checkDeclarationsInScope({variables}, name) { - variables.forEach((variable) => { - if (variable.name === name) { - variable.defs.forEach((def) => { - if (def.type === 'Variable') { - checkDeclaration(def.parent) + function checkDeclarationsInScope({variables}, name) { + for (let variable of variables) { + if (variable.name === name) { + for (let def of variable.defs) { + if (def.type === 'Variable') { + checkDeclaration(def.parent) + } } - }) + } } - }) - } + } - function handleExportDefault(node) { - const scope = context.getScope() + function handleExportDefault(node) { + const scope = context.getScope() - if (node.declaration.name) { - checkDeclarationsInScope(scope, node.declaration.name) + if (node.declaration.name) { + checkDeclarationsInScope(scope, node.declaration.name) + } } - } - function handleExportNamed(node) { - const scope = context.getScope() + function handleExportNamed(node) { + const scope = context.getScope() - if (node.declaration) { - checkDeclaration(node.declaration) - } else if (!node.source) { - node.specifiers.forEach((specifier) => { - checkDeclarationsInScope(scope, specifier.local.name) - }) + if (node.declaration) { + checkDeclaration(node.declaration) + } else if (!node.source) { + for (let specifier of node.specifiers) { + checkDeclarationsInScope(scope, specifier.local.name) + } + } } - } - return { - 'ExportDefaultDeclaration': handleExportDefault, - 'ExportNamedDeclaration': handleExportNamed, - } + return { + 'ExportDefaultDeclaration': handleExportDefault, + 'ExportNamedDeclaration': handleExportNamed, + } + }, } diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index fc836a403..2ffa19cff 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -4,76 +4,76 @@ * @copyright 2016 Desmond Brand. All rights reserved. * See LICENSE in root directory for full license. */ - -import Map from 'es6-map' - -import Exports from '../core/getExports' +import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = function(context) { +module.exports = { + meta: { + docs: {}, + }, - const fileImports = new Map() - const allPropertyLookups = new Map() + create: function(context) { - function handleImportDefault(node) { - const declaration = importDeclaration(context) - const exportMap = Exports.get(declaration.source.value, context) - if (exportMap == null) return + const fileImports = new Map() + const allPropertyLookups = new Map() - if (exportMap.errors.length) { - exportMap.reportErrors(context, declaration) - return - } + function handleImportDefault(node) { + const declaration = importDeclaration(context) + const exportMap = Exports.get(declaration.source.value, context) + if (exportMap == null) return - fileImports.set(node.local.name, { - exportMap, - sourcePath: declaration.source.value, - }) - } + if (exportMap.errors.length) { + exportMap.reportErrors(context, declaration) + return + } + + fileImports.set(node.local.name, { + exportMap, + sourcePath: declaration.source.value, + }) + } - function storePropertyLookup(objectName, propName, node) { - const lookups = allPropertyLookups.get(objectName) || [] - lookups.push({node, propName}) - allPropertyLookups.set(objectName, lookups) - } + function storePropertyLookup(objectName, propName, node) { + const lookups = allPropertyLookups.get(objectName) || [] + lookups.push({node, propName}) + allPropertyLookups.set(objectName, lookups) + } - function handlePropLookup(node) { - const objectName = node.object.name - const propName = node.property.name - storePropertyLookup(objectName, propName, node) - } + function handlePropLookup(node) { + const objectName = node.object.name + const propName = node.property.name + storePropertyLookup(objectName, propName, node) + } - function handleDestructuringAssignment(node) { - const isDestructure = ( - node.id.type === 'ObjectPattern' && - node.init != null && - node.init.type === 'Identifier' - ) - if (!isDestructure) return + function handleDestructuringAssignment(node) { + const isDestructure = ( + node.id.type === 'ObjectPattern' && + node.init != null && + node.init.type === 'Identifier' + ) + if (!isDestructure) return - const objectName = node.init.name - node.id.properties.forEach(({key}) => { - if (key != null) { // rest properties are null + const objectName = node.init.name + for (const { key } of node.id.properties) { + if (key == null) continue // true for rest properties storePropertyLookup(objectName, key.name, key) } - }) - } + } - function handleProgramExit() { - allPropertyLookups.forEach((lookups, objectName) => { - const fileImport = fileImports.get(objectName) - if (fileImport == null) return + function handleProgramExit() { + allPropertyLookups.forEach((lookups, objectName) => { + const fileImport = fileImports.get(objectName) + if (fileImport == null) return + + for (const {propName, node} of lookups) { + // the default import can have a "default" property + if (propName === 'default') continue + if (!fileImport.exportMap.namespace.has(propName)) continue - lookups.forEach(({propName, node}) => { - // the default import can have a "default" property - if (propName === 'default') { - return - } - if (fileImport.exportMap.namespace.has(propName)) { context.report({ node, message: ( @@ -85,13 +85,13 @@ module.exports = function(context) { }) } }) - }) - } + } - return { - 'ImportDefaultSpecifier': handleImportDefault, - 'MemberExpression': handlePropLookup, - 'VariableDeclarator': handleDestructuringAssignment, - 'Program:exit': handleProgramExit, - } + return { + 'ImportDefaultSpecifier': handleImportDefault, + 'MemberExpression': handlePropLookup, + 'VariableDeclarator': handleDestructuringAssignment, + 'Program:exit': handleProgramExit, + } + }, } diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index bba744d51..97a8e99ad 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -1,32 +1,38 @@ -import Exports from '../core/getExports' +import Exports from '../ExportMap' import importDeclaration from '../importDeclaration' -module.exports = function (context) { - function checkDefault(nameKey, defaultSpecifier) { - // #566: default is a valid specifier - if (defaultSpecifier[nameKey].name === 'default') return +module.exports = { + meta: { + docs: {}, + }, - var declaration = importDeclaration(context) + create: function (context) { + function checkDefault(nameKey, defaultSpecifier) { + // #566: default is a valid specifier + if (defaultSpecifier[nameKey].name === 'default') return - var imports = Exports.get(declaration.source.value, context) - if (imports == null) return + var declaration = importDeclaration(context) - if (imports.errors.length) { - imports.reportErrors(context, declaration) - return - } + var imports = Exports.get(declaration.source.value, context) + if (imports == null) return + + if (imports.errors.length) { + imports.reportErrors(context, declaration) + return + } - if (imports.has('default') && - imports.has(defaultSpecifier[nameKey].name)) { + if (imports.has('default') && + imports.has(defaultSpecifier[nameKey].name)) { - context.report(defaultSpecifier, - 'Using exported name \'' + defaultSpecifier[nameKey].name + - '\' as identifier for default export.') + context.report(defaultSpecifier, + 'Using exported name \'' + defaultSpecifier[nameKey].name + + '\' as identifier for default export.') + } + } + return { + 'ImportDefaultSpecifier': checkDefault.bind(null, 'local'), + 'ExportDefaultSpecifier': checkDefault.bind(null, 'exported'), } - } - return { - 'ImportDefaultSpecifier': checkDefault.bind(null, 'local'), - 'ExportDefaultSpecifier': checkDefault.bind(null, 'exported'), - } + }, } diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 2cd45d322..673735da0 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -8,10 +8,16 @@ //------------------------------------------------------------------------------ -module.exports = function (context) { - return { - 'ImportNamespaceSpecifier': function (node) { - context.report(node, `Unexpected namespace import.`) - }, - } +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + return { + 'ImportNamespaceSpecifier': function (node) { + context.report(node, `Unexpected namespace import.`) + }, + } + }, } diff --git a/src/rules/no-nodejs-modules.js b/src/rules/no-nodejs-modules.js index 9160b9e8d..262fec7dd 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.js @@ -7,18 +7,24 @@ function reportIfMissing(context, node, allowed, name) { } } -module.exports = function (context) { - const options = context.options[0] || {} - const allowed = options.allow || [] +module.exports = { + meta: { + docs: {}, + }, - return { - ImportDeclaration: function handleImports(node) { - reportIfMissing(context, node, allowed, node.source.value) - }, - CallExpression: function handleRequires(node) { - if (isStaticRequire(node)) { - reportIfMissing(context, node, allowed, node.arguments[0].value) - } - }, - } + create: function (context) { + const options = context.options[0] || {} + const allowed = options.allow || [] + + return { + ImportDeclaration: function handleImports(node) { + reportIfMissing(context, node, allowed, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfMissing(context, node, allowed, node.arguments[0].value) + } + }, + } + }, } diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index d07ebd873..0240cd764 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -1,71 +1,77 @@ import containsPath from 'contains-path' import path from 'path' -import resolve from '../core/resolve' +import resolve from 'eslint-module-utils/resolve' import isStaticRequire from '../core/staticRequire' -module.exports = function noRestrictedPaths(context) { - const options = context.options[0] || {} - const restrictedPaths = options.zones || [] - const basePath = options.basePath || process.cwd() - const currentFilename = context.getFilename() - const matchingZones = restrictedPaths.filter((zone) => { - const targetPath = path.resolve(basePath, zone.target) +module.exports = { + meta: { + docs: {}, - return containsPath(currentFilename, targetPath) - }) + schema: [ + { + type: 'object', + properties: { + zones: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + target: { type: 'string' }, + from: { type: 'string' }, + }, + additionalProperties: false, + }, + }, + basePath: { type: 'string' }, + }, + additionalProperties: false, + }, + ], + }, - function checkForRestrictedImportPath(importPath, node) { - const absoluteImportPath = resolve(importPath, context) + create: function noRestrictedPaths(context) { + const options = context.options[0] || {} + const restrictedPaths = options.zones || [] + const basePath = options.basePath || process.cwd() + const currentFilename = context.getFilename() + const matchingZones = restrictedPaths.filter((zone) => { + const targetPath = path.resolve(basePath, zone.target) - if (!absoluteImportPath) { - return - } + return containsPath(currentFilename, targetPath) + }) - matchingZones.forEach((zone) => { - const absoluteFrom = path.resolve(basePath, zone.from) + function checkForRestrictedImportPath(importPath, node) { + const absoluteImportPath = resolve(importPath, context) - if (containsPath(absoluteImportPath, absoluteFrom)) { - context.report({ - node, - message: `Unexpected path "${importPath}" imported in restricted zone.`, - }) + if (!absoluteImportPath) { + return } - }) - } - return { - ImportDeclaration(node) { - checkForRestrictedImportPath(node.source.value, node.source) - }, - CallExpression(node) { - if (isStaticRequire(node)) { - const [ firstArgument ] = node.arguments + matchingZones.forEach((zone) => { + const absoluteFrom = path.resolve(basePath, zone.from) - checkForRestrictedImportPath(firstArgument.value, firstArgument) - } - }, - } -} + if (containsPath(absoluteImportPath, absoluteFrom)) { + context.report({ + node, + message: `Unexpected path "${importPath}" imported in restricted zone.`, + }) + } + }) + } -module.exports.schema = [ - { - type: 'object', - properties: { - zones: { - type: 'array', - minItems: 1, - items: { - type: 'object', - properties: { - target: { type: 'string' }, - from: { type: 'string' }, - }, - additionalProperties: false, - }, + return { + ImportDeclaration(node) { + checkForRestrictedImportPath(node.source.value, node.source) }, - basePath: { type: 'string' }, - }, - additionalProperties: false, + CallExpression(node) { + if (isStaticRequire(node)) { + const [ firstArgument ] = node.arguments + + checkForRestrictedImportPath(firstArgument.value, firstArgument) + } + }, + } }, -] +} diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js new file mode 100644 index 000000000..a5503f196 --- /dev/null +++ b/src/rules/no-unassigned-import.js @@ -0,0 +1,41 @@ +import isStaticRequire from '../core/staticRequire' + +function report(context, node) { + context.report({ + node, + message: 'Imported module should be assigned', + }) +} + +function create(context) { + return { + ImportDeclaration(node) { + if (node.specifiers.length === 0) { + report(context, node) + } + }, + ExpressionStatement(node) { + if (node.expression.type === 'CallExpression' && isStaticRequire(node.expression)) { + report(context, node.expression) + } + }, + } +} + +module.exports = { + create, + meta: { + docs: {}, + schema: [ + { + 'type': 'object', + 'properties': { + 'devDependencies': { 'type': ['boolean', 'array'] }, + 'optionalDependencies': { 'type': ['boolean', 'array'] }, + 'peerDependencies': { 'type': ['boolean', 'array'] }, + }, + 'additionalProperties': false, + }, + ], + }, +} diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 71b92a6da..e5fe2366c 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -3,100 +3,44 @@ * @author Ben Mosher */ -import resolve from '../core/resolve' +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' -module.exports = function (context) { - let ignoreRegExps = [] - if (context.options[0] != null && context.options[0].ignore != null) { - ignoreRegExps = context.options[0].ignore.map(p => new RegExp(p)) - } - function checkSourceValue(source) { - if (source == null) return - - if (ignoreRegExps.some(re => re.test(source.value))) return - - if (resolve(source.value, context) === undefined) { - context.report(source, - 'Unable to resolve path to module \'' + source.value + '\'.') - } - } - - // for import-y declarations - function checkSource(node) { - checkSourceValue(node.source) - } - - // for CommonJS `require` calls - // adapted from @mctep: http://git.io/v4rAu - function checkCommon(call) { - if (call.callee.type !== 'Identifier') return - if (call.callee.name !== 'require') return - if (call.arguments.length !== 1) return - - const modulePath = call.arguments[0] - if (modulePath.type !== 'Literal') return - if (typeof modulePath.value !== 'string') return - - checkSourceValue(modulePath) - } +module.exports = { + meta: { + schema: [ makeOptionsSchema({ + caseSensitive: { type: 'boolean', default: true }, + })], + }, - function checkAMD(call) { - if (call.callee.type !== 'Identifier') return - if (call.callee.name !== 'require' && - call.callee.name !== 'define') return - if (call.arguments.length !== 2) return + create: function (context) { - const modules = call.arguments[0] - if (modules.type !== 'ArrayExpression') return + function checkSourceValue(source) { + const shouldCheckCase = !CASE_SENSITIVE_FS && + (!context.options[0] || context.options[0].caseSensitive !== false) - modules.elements.forEach((element) => { - if (element.type === 'Literal' && - typeof element.value === 'string') { + const resolvedPath = resolve(source.value, context) - // magic modules: http://git.io/vByan - if (element.value !== 'require' && - element.value !== 'exports') { - checkSourceValue(element) - } + if (resolvedPath === undefined) { + context.report(source, + `Unable to resolve path to module '${source.value}'.`) } - }) - } - const visitors = { - 'ImportDeclaration': checkSource, - 'ExportNamedDeclaration': checkSource, - 'ExportAllDeclaration': checkSource, - } - - if (context.options[0] != null) { - const { commonjs, amd } = context.options[0] + else if (shouldCheckCase) { + const cacheSettings = ModuleCache.getSettings(context.settings) + if (!fileExistsWithCaseSync(resolvedPath, cacheSettings)) { + context.report(source, + `Casing of ${source.value} does not match the underlying filesystem.`) + } - if (commonjs || amd) { - visitors['CallExpression'] = function (call) { - if (commonjs) checkCommon(call) - if (amd) checkAMD(call) } } - } - return visitors -} + return moduleVisitor(checkSourceValue, context.options[0]) -module.exports.schema = [ - { - 'type': 'object', - 'properties': { - 'commonjs': { 'type': 'boolean' }, - 'amd': { 'type': 'boolean' }, - 'ignore': { - 'type': 'array', - 'minItems': 1, - 'items': { 'type': 'string' }, - 'uniqueItems': true, - }, - }, - 'additionalProperties': false, }, -] +} + diff --git a/src/rules/no-webpack-loader-syntax.js b/src/rules/no-webpack-loader-syntax.js new file mode 100644 index 000000000..3d9ba0034 --- /dev/null +++ b/src/rules/no-webpack-loader-syntax.js @@ -0,0 +1,28 @@ +import isStaticRequire from '../core/staticRequire' + +function reportIfNonStandard(context, node, name) { + if (name.indexOf('!') !== -1) { + context.report(node, `Unexpected '!' in '${name}'. ` + + 'Do not use import syntax to configure webpack loaders.' + ) + } +} + +module.exports = { + meta: { + docs: {}, + }, + + create: function (context) { + return { + ImportDeclaration: function handleImports(node) { + reportIfNonStandard(context, node, node.source.value) + }, + CallExpression: function handleRequires(node) { + if (isStaticRequire(node)) { + reportIfNonStandard(context, node, node.arguments[0].value) + } + }, + } + }, +} diff --git a/src/rules/order.js b/src/rules/order.js index ed5730ab1..da4cae1ae 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -1,6 +1,5 @@ 'use strict' -import find from 'lodash.find' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' @@ -34,7 +33,7 @@ function findOutOfOrder(imported) { function reportOutOfOrder(context, imported, outOfOrder, order) { outOfOrder.forEach(function (imp) { - const found = find(imported, function hasHigherRank(importedItem) { + const found = imported.find(function hasHigherRank(importedItem) { return importedItem.rank > imp.rank }) context.report(imp.node, '`' + imp.name + '` import should occur ' + order + @@ -145,77 +144,84 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) { }) } -module.exports = function importOrderRule (context) { - const options = context.options[0] || {} - let ranks +module.exports = { + meta: { + docs: {}, - try { - ranks = convertGroupsToRanks(options.groups || defaultGroups) - } catch (error) { - // Malformed configuration - return { - Program: function(node) { - context.report(node, error.message) + schema: [ + { + type: 'object', + properties: { + groups: { + type: 'array', + }, + 'newlines-between': { + enum: [ 'ignore', 'always', 'never' ], + }, + }, + additionalProperties: false, }, - } - } - let imported = [] - let level = 0 - - function incrementLevel() { - level++ - } - function decrementLevel() { - level-- - } + ], + }, - return { - ImportDeclaration: function handleImports(node) { - if (node.specifiers.length) { // Ignoring unassigned imports - const name = node.source.value - registerNode(context, node, name, 'import', ranks, imported) - } - }, - CallExpression: function handleRequires(node) { - if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) { - return - } - const name = node.arguments[0].value - registerNode(context, node, name, 'require', ranks, imported) - }, - 'Program:exit': function reportAndReset() { - makeOutOfOrderReport(context, imported) - - if ('newlines-between' in options) { - makeNewlinesBetweenReport(context, imported, options['newlines-between']) + create: function importOrderRule (context) { + const options = context.options[0] || {} + const newlinesBetweenImports = options['newlines-between'] || 'ignore' + let ranks + + try { + ranks = convertGroupsToRanks(options.groups || defaultGroups) + } catch (error) { + // Malformed configuration + return { + Program: function(node) { + context.report(node, error.message) + }, } + } + let imported = [] + let level = 0 - imported = [] - }, - FunctionDeclaration: incrementLevel, - FunctionExpression: incrementLevel, - ArrowFunctionExpression: incrementLevel, - BlockStatement: incrementLevel, - ObjectExpression: incrementLevel, - 'FunctionDeclaration:exit': decrementLevel, - 'FunctionExpression:exit': decrementLevel, - 'ArrowFunctionExpression:exit': decrementLevel, - 'BlockStatement:exit': decrementLevel, - 'ObjectExpression:exit': decrementLevel, - } -} + function incrementLevel() { + level++ + } + function decrementLevel() { + level-- + } -module.exports.schema = [ - { - type: 'object', - properties: { - groups: { - type: 'array', + return { + ImportDeclaration: function handleImports(node) { + if (node.specifiers.length) { // Ignoring unassigned imports + const name = node.source.value + registerNode(context, node, name, 'import', ranks, imported) + } }, - 'newlines-between': { - enum: [ 'always', 'never' ], + CallExpression: function handleRequires(node) { + if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) { + return + } + const name = node.arguments[0].value + registerNode(context, node, name, 'require', ranks, imported) }, - }, - additionalProperties: false, + 'Program:exit': function reportAndReset() { + makeOutOfOrderReport(context, imported) + + if (newlinesBetweenImports !== 'ignore') { + makeNewlinesBetweenReport(context, imported, newlinesBetweenImports) + } + + imported = [] + }, + FunctionDeclaration: incrementLevel, + FunctionExpression: incrementLevel, + ArrowFunctionExpression: incrementLevel, + BlockStatement: incrementLevel, + ObjectExpression: incrementLevel, + 'FunctionDeclaration:exit': decrementLevel, + 'FunctionExpression:exit': decrementLevel, + 'ArrowFunctionExpression:exit': decrementLevel, + 'BlockStatement:exit': decrementLevel, + 'ObjectExpression:exit': decrementLevel, + } }, -] +} diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index 2bd4783eb..afff03341 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -1,63 +1,69 @@ 'use strict' -module.exports = function(context) { - let specifierExportCount = 0 - let hasDefaultExport = false - let hasStarExport = false - let namedExportNode = null - - return { - 'ExportSpecifier': function(node) { - if (node.exported.name === 'default') { - hasDefaultExport = true - } else { - specifierExportCount++ - namedExportNode = node - } - }, - - 'ExportNamedDeclaration': function(node) { - // if there are specifiers, node.declaration should be null - if (!node.declaration) return - - function captureDeclaration(identifierOrPattern) { - if (identifierOrPattern.type === 'ObjectPattern') { - // recursively capture - identifierOrPattern.properties - .forEach(function(property) { - captureDeclaration(property.value) - }) +module.exports = { + meta: { + docs: {}, + }, + + create: function(context) { + let specifierExportCount = 0 + let hasDefaultExport = false + let hasStarExport = false + let namedExportNode = null + + return { + 'ExportSpecifier': function(node) { + if (node.exported.name === 'default') { + hasDefaultExport = true } else { - // assume it's a single standard identifier specifierExportCount++ + namedExportNode = node + } + }, + + 'ExportNamedDeclaration': function(node) { + // if there are specifiers, node.declaration should be null + if (!node.declaration) return + + function captureDeclaration(identifierOrPattern) { + if (identifierOrPattern.type === 'ObjectPattern') { + // recursively capture + identifierOrPattern.properties + .forEach(function(property) { + captureDeclaration(property.value) + }) + } else { + // assume it's a single standard identifier + specifierExportCount++ + } + } + + if (node.declaration.declarations) { + node.declaration.declarations.forEach(function(declaration) { + captureDeclaration(declaration.id) + }) + } + else { + // captures 'export function foo() {}' syntax + specifierExportCount++ + } + + namedExportNode = node + }, + + 'ExportDefaultDeclaration': function() { + hasDefaultExport = true + }, + + 'ExportAllDeclaration': function() { + hasStarExport = true + }, + + 'Program:exit': function() { + if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport) { + context.report(namedExportNode, 'Prefer default export.') } - } - - if (node.declaration.declarations) { - node.declaration.declarations.forEach(function(declaration) { - captureDeclaration(declaration.id) - }) - } - else { - // captures 'export function foo() {}' syntax - specifierExportCount++ - } - - namedExportNode = node - }, - - 'ExportDefaultDeclaration': function() { - hasDefaultExport = true - }, - - 'ExportAllDeclaration': function() { - hasStarExport = true - }, - - 'Program:exit': function() { - if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport) { - context.report(namedExportNode, 'Prefer default export.') - } - }, - } + }, + } + }, } diff --git a/src/rules/unambiguous.js b/src/rules/unambiguous.js new file mode 100644 index 000000000..1fb40f6a0 --- /dev/null +++ b/src/rules/unambiguous.js @@ -0,0 +1,29 @@ +/** + * @fileOverview Report modules that could parse incorrectly as scripts. + * @author Ben Mosher + */ + +import { isModule } from 'eslint-module-utils/unambiguous' + +module.exports = { + meta: {}, + + create: function (context) { + // ignore non-modules + if (context.parserOptions.sourceType !== 'module') { + return {} + } + + return { + Program: function (ast) { + if (!isModule(ast)) { + context.report({ + node: ast, + message: 'This module could be parsed as a valid script.', + }) + } + }, + } + + }, +} diff --git a/tests/files/node_modules/@org/package/index.js b/tests/files/node_modules/@org/package/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/files/node_modules/@org/package/internal.js b/tests/files/node_modules/@org/package/internal.js new file mode 100644 index 000000000..e69de29bb diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 80f64d8b4..965cba1fc 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -1,12 +1,11 @@ -import assign from 'object-assign' import { expect } from 'chai' -import ExportMap from 'core/getExports' +import ExportMap from '../../../src/ExportMap' import * as fs from 'fs' import { getFilename } from '../utils' -describe('getExports', function () { +describe('ExportMap', function () { const fakeContext = { getFilename: getFilename, settings: {}, @@ -46,7 +45,7 @@ describe('getExports', function () { const firstAccess = ExportMap.get('./named-exports', fakeContext) expect(firstAccess).to.exist - const differentSettings = assign( + const differentSettings = Object.assign( {}, fakeContext, { parserPath: 'espree' }) @@ -282,21 +281,21 @@ describe('getExports', function () { }) context('issue #210: self-reference', function () { - it("doesn't crash", function () { + it(`doesn't crash`, function () { expect(() => ExportMap.get('./narcissist', fakeContext)).not.to.throw(Error) }) - it("'has' circular reference", function () { + it(`'has' circular reference`, function () { expect(ExportMap.get('./narcissist', fakeContext)) .to.exist.and.satisfy(m => m.has('soGreat')) }) - it("can 'get' circular reference", function () { + it(`can 'get' circular reference`, function () { expect(ExportMap.get('./narcissist', fakeContext)) .to.exist.and.satisfy(m => m.get('soGreat') != null) }) }) context('issue #478: never parse non-whitelist extensions', function () { - const context = assign({}, fakeContext, + const context = Object.assign({}, fakeContext, { settings: { 'import/extensions': ['.js'] } }) let imports @@ -318,7 +317,7 @@ describe('getExports', function () { configs.forEach(([description, parserConfig]) => { describe(description, function () { - const context = assign({}, fakeContext, + const context = Object.assign({}, fakeContext, { settings: { 'import/extensions': ['.js'], 'import/parsers': parserConfig, @@ -333,6 +332,10 @@ describe('getExports', function () { expect(imports).to.exist }) + it('has no parse errors', function () { + expect(imports).property('errors').to.be.empty + }) + it('has export (getFoo)', function () { expect(imports.has('getFoo')).to.be.true }) diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index e7a044bed..0793a70c2 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -1,6 +1,6 @@ import * as fs from 'fs' import { expect } from 'chai' -import parse from 'core/parse' +import parse from 'eslint-module-utils/parse' import { getFilename } from '../utils' diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 4e6845d86..e8f255f34 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -1,6 +1,7 @@ import { expect } from 'chai' -import resolve, { CASE_SENSITIVE_FS } from 'core/resolve' +import resolve, { CASE_SENSITIVE_FS, fileExistsWithCaseSync } from 'eslint-module-utils/resolve' +import ModuleCache from 'eslint-module-utils/ModuleCache' import * as fs from 'fs' import * as utils from '../utils' @@ -26,22 +27,37 @@ describe('resolve', function () { expect(file).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx')) }) - it('should test case sensitivity', function () { - // Note the spelling error 'MyUncoolComponent' vs 'MyUnCoolComponent' - var file = resolve( './jsx/MyUncoolComponent' - , utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) - ) - - expect(file, 'path to ./jsx/MyUncoolComponent').to.be.undefined + const caseDescribe = (!CASE_SENSITIVE_FS ? describe : describe.skip) + caseDescribe('case sensitivity', function () { + let file + const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) + before('resolve', function () { + file = resolve( + // Note the case difference 'MyUncoolComponent' vs 'MyUnCoolComponent' + './jsx/MyUncoolComponent', testContext) + }) + it('resolves regardless of case', function () { + expect(file, 'path to ./jsx/MyUncoolComponent').to.exist + }) + it('detects case does not match FS', function () { + expect(fileExistsWithCaseSync(file, ModuleCache.getSettings(testContext))) + .to.be.false + }) }) - describe('case cache correctness', function () { + describe('rename cache correctness', function () { const context = utils.testContext({ 'import/cache': { 'lifetime': 1 }, }) + const infiniteContexts = [ '∞', 'Infinity' ].map(inf => [inf, + utils.testContext({ + 'import/cache': { 'lifetime': inf }, + })]) + + const pairs = [ - ['./CaseyKasem.js', './CASEYKASEM.js'], + ['./CaseyKasem.js', './CASEYKASEM2.js'], ] pairs.forEach(([original, changed]) => { @@ -52,6 +68,13 @@ describe('resolve', function () { expect(resolve(changed, context)).not.to.exist }) + // settings are part of cache key + before('warm up infinite entries', function () { + infiniteContexts.forEach(([,c]) => { + expect(resolve(original, c)).to.exist + }) + }) + before('rename', function (done) { fs.rename( utils.testFilePath(original), @@ -64,33 +87,29 @@ describe('resolve', function () { utils.testFilePath(changed), exists => done(exists ? null : new Error('new file does not exist')))) - // these tests fail on a case-sensitive file system - // because nonexistent files aren't cached - if (!CASE_SENSITIVE_FS) { - it('gets cached values within cache lifetime', function () { - // get cached values initially - expect(resolve(original, context)).to.exist - expect(resolve(changed, context)).not.to.exist - }) + it('gets cached values within cache lifetime', function () { + // get cached values initially + expect(resolve(original, context)).to.exist + }) + + it('gets updated values immediately', function () { + // get cached values initially + expect(resolve(changed, context)).to.exist + }) + + // special behavior for infinity + describe('infinite cache', function () { + this.timeout(1500) + + before((done) => setTimeout(done, 1100)) - // special behavior for infinity - describe('infinite cache', function () { - this.timeout(1200) - before((done) => setTimeout(done, 1100)) - - const lifetimes = [ '∞', 'Infinity' ] - lifetimes.forEach(inf => { - const infiniteContext = utils.testContext({ - 'import/cache': { 'lifetime': inf }, - }) - - it(`lifetime: ${inf} still gets cached values after ~1s`, function () { - expect(resolve(original, infiniteContext)).to.exist - expect(resolve(changed, infiniteContext)).not.to.exist - }) + infiniteContexts.forEach(([inf, infiniteContext]) => { + it(`lifetime: ${inf} still gets cached values after ~1s`, function () { + expect(resolve(original, infiniteContext), original).to.exist }) }) - } + + }) describe('finite cache', function () { this.timeout(1200) diff --git a/tests/src/rules/default.js b/tests/src/rules/default.js index 003728295..5186e56ca 100644 --- a/tests/src/rules/default.js +++ b/tests/src/rules/default.js @@ -1,6 +1,8 @@ import { test, SYNTAX_CASES } from '../utils' import { RuleTester } from 'eslint' +import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' + var ruleTester = new RuleTester() , rule = require('rules/default') @@ -22,8 +24,7 @@ ruleTester.run('default', rule, { // core modules always have a default test({ code: 'import crypto from "crypto";' }), - test({ code: 'import common from "./common";' - , settings: { 'import/ignore': ['common'] } }), + test({ code: 'import common from "./common";' }), // es7 export syntax test({ code: 'export bar from "./bar"' @@ -86,7 +87,6 @@ ruleTester.run('default', rule, { parser: 'babel-eslint', }), - ...SYNTAX_CASES, ], @@ -96,21 +96,11 @@ ruleTester.run('default', rule, { errors: ["Parse errors in imported module './jsx/FooES7.js': Unexpected token = (6:16)"], }), - test({ - code: 'import crypto from "./common";', - settings: { 'import/ignore': ['foo'] }, - errors: [{ message: 'No default export found in module.' - , type: 'ImportDefaultSpecifier'}]}), test({ code: 'import baz from "./named-exports";', errors: [{ message: 'No default export found in module.' , type: 'ImportDefaultSpecifier'}]}), - test({ - code: 'import bar from "./common";', - errors: [{ message: 'No default export found in module.' - , type: 'ImportDefaultSpecifier'}]}), - test({ code: "import Foo from './jsx/FooES7.js';", errors: ["Parse errors in imported module './jsx/FooES7.js': Unexpected token = (6:16)"], @@ -146,3 +136,20 @@ ruleTester.run('default', rule, { }), ], }) + +// #311: import of mismatched case +if (!CASE_SENSITIVE_FS) { + ruleTester.run('default (path case-insensitivity)', rule, { + valid: [ + test({ + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + }), + ], + invalid: [ + test({ + code: 'import bar from "./Named-Exports"', + errors: ['No default export found in module.'], + }), + ], + }) +} diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index 91e3a000b..84598677d 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -2,7 +2,7 @@ import { test, SYNTAX_CASES } from '../utils' import { RuleTester } from 'eslint' -var ruleTester = new RuleTester({ parser: 'babel-eslint' }) +var ruleTester = new RuleTester() , rule = require('rules/export') ruleTester.run('export', rule, { @@ -30,37 +30,37 @@ ruleTester.run('export', rule, { invalid: [ // multiple defaults - test({ - code: 'export default foo; export default bar', - errors: ['Multiple default exports.', 'Multiple default exports.'], - }), - test({ - code: 'export default function foo() {}; ' + - 'export default function bar() {}', - errors: ['Multiple default exports.', 'Multiple default exports.'], - }), + // test({ + // code: 'export default foo; export default bar', + // errors: ['Multiple default exports.', 'Multiple default exports.'], + // }), + // test({ + // code: 'export default function foo() {}; ' + + // 'export default function bar() {}', + // errors: ['Multiple default exports.', 'Multiple default exports.'], + // }), - test({ - code: 'export function foo() {}; ' + - 'export { bar as foo }', - errors: ["Multiple exports of name 'foo'.", "Multiple exports of name 'foo'."], - }), - test({ - code: 'export {foo}; export {foo};', - errors: ["Multiple exports of name 'foo'.", "Multiple exports of name 'foo'."], - }), - test({ - code: 'export {foo}; export {bar as foo};', - errors: ["Multiple exports of name 'foo'.", "Multiple exports of name 'foo'."], - }), - test({ - code: 'export var foo = "foo"; export var foo = "bar";', - errors: ["Multiple exports of name 'foo'.", "Multiple exports of name 'foo'."], - }), - test({ - code: 'export var foo = "foo", foo = "bar";', - errors: ["Multiple exports of name 'foo'.", "Multiple exports of name 'foo'."], - }), + // test({ + // code: 'export function foo() {}; ' + + // 'export { bar as foo }', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), + // test({ + // code: 'export {foo}; export {foo};', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), + // test({ + // code: 'export {foo}; export {bar as foo};', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), + // test({ + // code: 'export var foo = "foo"; export var foo = "bar";', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), + // test({ + // code: 'export var foo = "foo", foo = "bar";', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), test({ code: 'export { foo }; export * from "./export-all"', errors: ['Multiple exports of name \'foo\'.', @@ -76,31 +76,27 @@ ruleTester.run('export', rule, { test({ code: 'export * from "./malformed.js"', errors: [{ - message: "Parse errors in imported module './malformed.js': Line 1: Unexpected token (1:12)", + message: "Parse errors in imported module './malformed.js': 'return' outside of function (1:1)", type: 'Literal', }], }), - test({ - code: 'export var { foo, bar } = object; export var foo = "bar"', - errors: ['Multiple exports of name \'foo\'.', - 'Multiple exports of name \'foo\'.'], - }), - test({ - code: 'export var { bar: { foo } } = object; export var foo = "bar"', - errors: ['Multiple exports of name \'foo\'.', - 'Multiple exports of name \'foo\'.'], - }), - test({ - code: 'export var [ foo, bar ] = array; export var bar = "baz"', - errors: ['Multiple exports of name \'bar\'.', - 'Multiple exports of name \'bar\'.'], - }), - test({ - code: 'export var [ foo, /*sparse*/, { bar } ] = array; export var bar = "baz"', - errors: ['Multiple exports of name \'bar\'.', - 'Multiple exports of name \'bar\'.'], - }), + // test({ + // code: 'export var { foo, bar } = object; export var foo = "bar"', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), + // test({ + // code: 'export var { bar: { foo } } = object; export var foo = "bar"', + // errors: ['Parsing error: Duplicate export \'foo\''], + // }), + // test({ + // code: 'export var [ foo, bar ] = array; export var bar = "baz"', + // errors: ['Parsing error: Duplicate export \'bar\''], + // }), + // test({ + // code: 'export var [ foo, /*sparse*/, { bar } ] = array; export var bar = "baz"', + // errors: ['Parsing error: Duplicate export \'bar\''], + // }), // #328: "export * from" does not export a default diff --git a/tests/src/rules/imports-first.js b/tests/src/rules/first.js similarity index 89% rename from tests/src/rules/imports-first.js rename to tests/src/rules/first.js index 497387269..0a7d5247e 100644 --- a/tests/src/rules/imports-first.js +++ b/tests/src/rules/first.js @@ -1,11 +1,11 @@ import { test } from '../utils' -import { linter, RuleTester } from 'eslint' +import { RuleTester } from 'eslint' const ruleTester = new RuleTester() - , rule = require('rules/imports-first') + , rule = require('rules/first') -ruleTester.run('imports-first', rule, { +ruleTester.run('first', rule, { valid: [ test({ code: "import { x } from './foo'; import { y } from './bar';\ export { x, y }" }) diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index caaed2a89..668182d47 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -1,6 +1,9 @@ import { test, SYNTAX_CASES } from '../utils' import { RuleTester } from 'eslint' +import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' + + var ruleTester = new RuleTester() , rule = require('rules/named') @@ -40,9 +43,6 @@ ruleTester.run('named', rule, { test({ code: 'import { someThing } from "./test-module"' }), - // node_modules/a only exports 'foo', should be ignored though - test({ code: 'import { zoob } from "a"' }), - // export tests test({ code: 'export { foo } from "./bar"' }), test({ code: 'export { foo as bar } from "./bar"' }), @@ -96,23 +96,24 @@ ruleTester.run('named', rule, { settings: { 'import/ignore': ['common'] }, }), + // ignore CJS by default. always ignore ignore list + test({ code: 'import {a, b, d} from "./common"' }), + test({ + code: 'import { baz } from "./bar"', + settings: { 'import/ignore': ['bar'] }, + }), + test({ + code: 'import { common } from "./re-export-default"', + }), + ...SYNTAX_CASES, ], invalid: [ - test({ code: 'import { zoob } from "a"' - , settings: { 'import/ignore': [] } - , errors: [ error('zoob', 'a') ] }), - test({ code: 'import { somethingElse } from "./test-module"' , errors: [ error('somethingElse', './test-module') ] }), - test({code: 'import {a, b, d} from "./common"', - errors: [ error('a', './common') - , error('b', './common') - , error('d', './common') ]}), - test({code: 'import { baz } from "./bar"', errors: [error('baz', './bar')]}), @@ -126,9 +127,6 @@ ruleTester.run('named', rule, { test({code: 'import { a } from "./default-export"', errors: [error('a', './default-export')]}), - test({code: 'import { a } from "./common"', args: [2, 'es6-only'], - errors: [error('a', './common')]}), - test({code: 'import { ActionTypess } from "./qc"', errors: [error('ActionTypess', './qc')]}), @@ -201,22 +199,13 @@ ruleTester.run('named', rule, { code: 'import { baz } from "es6-module"', errors: ["baz not found in 'es6-module'"], }), - test({ - code: 'import { baz } from "./bar"', - settings: { 'import/ignore': ['bar'] }, - errors: ["baz not found in './bar'"], - }), // issue #251 test({ code: 'import { foo, bar, bap } from "./re-export-default"', errors: ["bap not found in './re-export-default'"], }), - test({ - code: 'import { common } from "./re-export-default"', - // todo: better error message - errors: ["common not found via re-export-default.js -> common.js"], - }), + // #328: * exports do not include default test({ @@ -225,3 +214,20 @@ ruleTester.run('named', rule, { }), ], }) + +// #311: import of mismatched case +if (!CASE_SENSITIVE_FS) { + ruleTester.run('named (path case-insensitivity)', rule, { + valid: [ + test({ + code: 'import { b } from "./Named-Exports"', + }), + ], + invalid: [ + test({ + code: 'import { foo } from "./Named-Exports"', + errors: [`foo not found in './Named-Exports'`], + }), + ], + }) +} diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 3e4ef095c..96eebcc0e 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -27,8 +27,7 @@ const valid = [ ecmaFeatures: { jsx: true }, }, }), - test({ code: "import * as foo from './common';" - , settings: { 'import/ignore': ['common'] } }), + test({ code: "import * as foo from './common';" }), // destructuring namespaces test({ code: 'import * as names from "./named-exports";' + @@ -97,9 +96,6 @@ const valid = [ ] const invalid = [ - test({code: "import * as foo from './common';", - errors: ["No exported names found in module './common'."]}), - test({ code: "import * as names from './named-exports'; " + ' console.log(names.c);' , errors: [error('c', 'names')] }), diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index dd4d5a9c0..ed9d6de9b 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -1,7 +1,7 @@ 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 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 ruleTester = new RuleTester() @@ -21,7 +21,6 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parserOptions: { ecmaVersion: 6 } , }, "function x(){ require('baz'); }", - "a(require('b'), require('c'), require('d'));", `function foo() { switch (renderData.modalViewKey) { @@ -100,7 +99,35 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { code: "var foo = require('foo-module');\n\nvar a = 123;\n\nvar bar = require('bar-lib');", parserOptions: { sourceType: 'module' } - } + }, + { + code: ` + function foo() { + var foo = require('foo'); + foo(); + } + `, + parserOptions: { sourceType: 'module' }, + }, + { + code: ` + if (true) { + var foo = require('foo'); + foo(); + } + `, + parserOptions: { sourceType: 'module' }, + }, + { + code: ` + function a() { + var assign = Object.assign || require('object-assign'); + var foo = require('foo'); + var bar = 42; + } + `, + parserOptions: { sourceType: 'module' }, + }, ], invalid: [ @@ -183,14 +210,6 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { message: REQUIRE_ERROR_MESSAGE, } ] }, - { - code: "function a() {\nvar assign = Object.assign || require('object-assign');\nvar foo = require('foo');\nvar bar = 42; }", - errors: [ { - line: 3, - column: 1, - message: REQUIRE_ERROR_MESSAGE, - } ] - }, { code: "require('a');\nfoo(require('b'), require('c'), require('d'));\nrequire('d');\nvar foo = 'bar';", errors: [ diff --git a/tests/src/rules/no-internal-modules.js b/tests/src/rules/no-internal-modules.js index 26b164363..8ed1c623e 100644 --- a/tests/src/rules/no-internal-modules.js +++ b/tests/src/rules/no-internal-modules.js @@ -32,6 +32,10 @@ ruleTester.run('no-internal-modules', rule, { code: 'import get from "lodash.get"', filename: testFilePath('./internal-modules/plugins/plugin2/index.js'), }), + test({ + code: 'import b from "@org/package"', + filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), + }), test({ code: 'import b from "../../api/service"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), @@ -100,6 +104,17 @@ ruleTester.run('no-internal-modules', rule, { }, ], }), + test({ + code: 'import b from "@org/package/internal"', + filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), + errors: [ + { + message: 'Reaching to "@org/package/internal" is not allowed.', + line: 1, + column: 15, + }, + ], + }), test({ code: 'import get from "debug/node"', filename: testFilePath('./internal-modules/plugins/plugin.js'), diff --git a/tests/src/rules/no-unassigned-import.js b/tests/src/rules/no-unassigned-import.js new file mode 100644 index 000000000..86248d459 --- /dev/null +++ b/tests/src/rules/no-unassigned-import.js @@ -0,0 +1,43 @@ +import { test } from '../utils' +import * as path from 'path' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/no-unassigned-import') + +const error = { + ruleId: 'no-unassigned-import', + message: 'Imported module should be assigned' +} + +ruleTester.run('no-unassigned-import', rule, { + valid: [ + test({ code: 'import _ from "lodash"'}), + test({ code: 'import _, {foo} from "lodash"'}), + test({ code: 'import _, {foo as bar} from "lodash"'}), + test({ code: 'import {foo as bar} from "lodash"'}), + test({ code: 'import * as _ from "lodash"'}), + test({ code: 'import _ from "./"'}), + test({ code: 'const _ = require("lodash")'}), + test({ code: 'const {foo} = require("lodash")'}), + test({ code: 'const {foo: bar} = require("lodash")'}), + test({ code: 'const [a, b] = require("lodash")'}), + test({ code: 'const _ = require("lodash")'}), + test({ code: 'const _ = require("./")'}), + test({ code: 'foo(require("lodash"))'}), + test({ code: 'require("lodash").foo'}), + test({ code: 'require("lodash").foo()'}), + test({ code: 'require("lodash")()'}), + ], + invalid: [ + test({ + code: 'import "lodash"', + errors: [error], + }), + test({ + code: 'require("lodash")', + errors: [error], + }), + ], +}) diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index 1389a923f..5b4f6ae53 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -1,8 +1,9 @@ import * as path from 'path' -import assign from 'object-assign' import { test, SYNTAX_CASES } from '../utils' +import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve' + import { RuleTester } from 'eslint' var ruleTester = new RuleTester() @@ -12,7 +13,7 @@ function runResolverTests(resolver) { // redefine 'test' to set a resolver // thus 'rest'. needed something 4-chars-long for formatting simplicity function rest(specs) { - specs.settings = assign({}, + specs.settings = Object.assign({}, specs.settings, { 'import/resolver': resolver } ) @@ -131,10 +132,6 @@ function runResolverTests(resolver) { , errors: ["Unable to resolve path to module './does-not-exist'."], }), - rest({ code: 'import foo from "./jsx/MyUncoolComponent.jsx"' - , errors: ["Unable to resolve path to module './jsx/MyUncoolComponent.jsx'."] }), - - // commonjs setting rest({ code: 'var bar = require("./baz")', @@ -204,6 +201,30 @@ function runResolverTests(resolver) { }), ], }) + + if (!CASE_SENSITIVE_FS) { + ruleTester.run('case sensitivity', rule, { + valid: [ + rest({ // test with explicit flag + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + options: [{ caseSensitive: false }], + }), + ], + + invalid: [ + rest({ // test default + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + errors: [`Casing of ./jsx/MyUncoolComponent.jsx does not match the underlying filesystem.`], + }), + rest({ // test with explicit flag + code: 'import foo from "./jsx/MyUncoolComponent.jsx"', + options: [{ caseSensitive: true }], + errors: [`Casing of ./jsx/MyUncoolComponent.jsx does not match the underlying filesystem.`], + }), + ], + }) + } + } ['node', 'webpack'].forEach(runResolverTests) diff --git a/tests/src/rules/no-webpack-loader-syntax.js b/tests/src/rules/no-webpack-loader-syntax.js new file mode 100644 index 000000000..23a1190fb --- /dev/null +++ b/tests/src/rules/no-webpack-loader-syntax.js @@ -0,0 +1,74 @@ +import { test } from '../utils' + +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/no-webpack-loader-syntax') + +const message = 'Do not use import syntax to configure webpack loaders.' + +ruleTester.run('no-webpack-loader-syntax', rule, { + valid: [ + test({ code: 'import _ from "lodash"'}), + test({ code: 'import find from "lodash.find"'}), + test({ code: 'import foo from "./foo.css"'}), + test({ code: 'import data from "@scope/my-package/data.json"'}), + test({ code: 'var _ = require("lodash")'}), + test({ code: 'var find = require("lodash.find")'}), + test({ code: 'var foo = require("./foo")'}), + test({ code: 'var foo = require("../foo")'}), + test({ code: 'var foo = require("foo")'}), + test({ code: 'var foo = require("./")'}), + test({ code: 'var foo = require("@scope/foo")'}), + ], + invalid: [ + test({ + code: 'import _ from "babel!lodash"', + errors: [ + { message: `Unexpected '!' in 'babel!lodash'. ${message}` }, + ], + }), + test({ + code: 'import find from "-babel-loader!lodash.find"', + errors: [ + { message: `Unexpected '!' in '-babel-loader!lodash.find'. ${message}` }, + ], + }), + test({ + code: 'import foo from "style!css!./foo.css"', + errors: [ + { message: `Unexpected '!' in 'style!css!./foo.css'. ${message}` }, + ], + }), + test({ + code: 'import data from "json!@scope/my-package/data.json"', + errors: [ + { message: `Unexpected '!' in 'json!@scope/my-package/data.json'. ${message}` }, + ], + }), + test({ + code: 'var _ = require("babel!lodash")', + errors: [ + { message: `Unexpected '!' in 'babel!lodash'. ${message}` }, + ], + }), + test({ + code: 'var find = require("-babel-loader!lodash.find")', + errors: [ + { message: `Unexpected '!' in '-babel-loader!lodash.find'. ${message}` }, + ], + }), + test({ + code: 'var foo = require("style!css!./foo.css")', + errors: [ + { message: `Unexpected '!' in 'style!css!./foo.css'. ${message}` }, + ], + }), + test({ + code: 'var data = require("json!@scope/my-package/data.json")', + errors: [ + { message: `Unexpected '!' in 'json!@scope/my-package/data.json'. ${message}` }, + ], + }), + ], +}) diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index 452a26e4b..68f06c05f 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -209,6 +209,58 @@ ruleTester.run('order', rule, { }, ], }), + // Option: newlines-between: 'ignore' + test({ + code: ` + var fs = require('fs'); + + var index = require('./'); + var path = require('path'); + var sibling = require('./foo'); + + + var relParent1 = require('../foo'); + + var relParent3 = require('../'); + var async = require('async'); + `, + options: [ + { + groups: [ + ['builtin', 'index'], + ['sibling'], + ['parent', 'external'], + ], + 'newlines-between': 'ignore', + }, + ], + }), + // 'ignore' should be the default value for `newlines-between` + test({ + code: ` + var fs = require('fs'); + + var index = require('./'); + var path = require('path'); + var sibling = require('./foo'); + + + var relParent1 = require('../foo'); + + var relParent3 = require('../'); + + var async = require('async'); + `, + options: [ + { + groups: [ + ['builtin', 'index'], + ['sibling'], + ['parent', 'external'], + ], + }, + ], + }), // Option newlines-between: 'always' with multiline imports #1 test({ code: ` @@ -299,7 +351,7 @@ ruleTester.run('order', rule, { port: 4444, runner: { server_path: require('runner-binary').path, - + cli_args: { 'webdriver.chrome.driver': require('browser-binary').path } diff --git a/tests/src/rules/unambiguous.js b/tests/src/rules/unambiguous.js new file mode 100644 index 000000000..c1a89e829 --- /dev/null +++ b/tests/src/rules/unambiguous.js @@ -0,0 +1,56 @@ +import { RuleTester } from 'eslint' + +const ruleTester = new RuleTester() + , rule = require('rules/unambiguous') + +ruleTester.run('unambiguous', rule, { + valid: [ + 'function x() {}', + '"use strict"; function y() {}', + + { + code: 'import y from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'import * as y from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'import { y } from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'import z, { y } from "z"; function x() {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export {}', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export { x }', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export { y } from "z"', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'function x() {}; export * as y from "z"', + parser: 'babel-eslint', + parserOptions: { sourceType: 'module' }, + }, + { + code: 'export function x() {}', + parserOptions: { sourceType: 'module' }, + }, + ], + invalid: [ + { + code: 'function x() {}', + parserOptions: { sourceType: 'module' }, + errors: ['This module could be parsed as a valid script.'], + }, + ], +}) diff --git a/tests/src/utils.js b/tests/src/utils.js index aa599112b..144ae5498 100644 --- a/tests/src/utils.js +++ b/tests/src/utils.js @@ -1,5 +1,4 @@ import path from 'path' -import assign from 'object-assign' // warms up the module cache. this import takes a while (>500ms) import 'babel-eslint' @@ -11,7 +10,7 @@ export function testFilePath(relativePath) { export const FILENAME = testFilePath('foo.js') export function test(t) { - return assign({ + return Object.assign({ filename: FILENAME, parserOptions: { sourceType: 'module', diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js new file mode 100644 index 000000000..19e6a2122 --- /dev/null +++ b/utils/ModuleCache.js @@ -0,0 +1,47 @@ +"use strict" +exports.__esModule = true + +const log = require('debug')('eslint-module-utils:ModuleCache') + +class ModuleCache { + constructor(map) { + this.map = map || new Map() + } + + /** + * returns value for returning inline + * @param {[type]} cacheKey [description] + * @param {[type]} result [description] + */ + set(cacheKey, result) { + this.map.set(cacheKey, { result, lastSeen: Date.now() }) + log('setting entry for', cacheKey) + return result + } + + get(cacheKey, settings) { + if (this.map.has(cacheKey)) { + const f = this.map.get(cacheKey) + // check fresness + if (Date.now() - f.lastSeen < (settings.lifetime * 1000)) return f.result + } else log('cache miss for', cacheKey) + // cache miss + return undefined + } + +} + +ModuleCache.getSettings = function (settings) { + const cacheSettings = Object.assign({ + lifetime: 30, // seconds + }, settings['import/cache']) + + // parse infinity + if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { + cacheSettings.lifetime = Infinity + } + + return cacheSettings +} + +exports.default = ModuleCache diff --git a/src/core/declaredScope.js b/utils/declaredScope.js similarity index 72% rename from src/core/declaredScope.js rename to utils/declaredScope.js index 11575f4cb..2ef3d19a9 100644 --- a/src/core/declaredScope.js +++ b/utils/declaredScope.js @@ -1,4 +1,7 @@ -export default function declaredScope(context, name) { +"use strict" +exports.__esModule = true + +exports.default = function declaredScope(context, name) { let references = context.getScope().references , i for (i = 0; i < references.length; i++) { diff --git a/utils/hash.js b/utils/hash.js new file mode 100644 index 000000000..0b946a510 --- /dev/null +++ b/utils/hash.js @@ -0,0 +1,59 @@ +/** + * utilities for hashing config objects. + * basically iteratively updates hash with a JSON-like format + */ +"use strict" +exports.__esModule = true + +const createHash = require('crypto').createHash + +const stringify = JSON.stringify + +function hashify(value, hash) { + if (!hash) hash = createHash('sha256') + + if (value instanceof Array) { + hashArray(value, hash) + } else if (value instanceof Object) { + hashObject(value, hash) + } else { + hash.update(stringify(value) || 'undefined') + } + + return hash +} +exports.default = hashify + +function hashArray(array, hash) { + if (!hash) hash = createHash('sha256') + + hash.update('[') + for (let i = 0; i < array.length; i++) { + hashify(array[i], hash) + hash.update(',') + } + hash.update(']') + + return hash +} +hashify.array = hashArray +exports.hashArray = hashArray + +function hashObject(object, hash) { + if (!hash) hash = createHash('sha256') + + hash.update("{") + Object.keys(object).sort().forEach(key => { + hash.update(stringify(key)) + hash.update(':') + hashify(object[key], hash) + hash.update(",") + }) + hash.update('}') + + return hash +} +hashify.object = hashObject +exports.hashObject = hashObject + + diff --git a/utils/ignore.js b/utils/ignore.js new file mode 100644 index 000000000..88e4080dd --- /dev/null +++ b/utils/ignore.js @@ -0,0 +1,56 @@ +"use strict" +exports.__esModule = true + +const extname = require('path').extname + +const log = require('debug')('eslint-plugin-import:utils:ignore') + +// one-shot memoized +let cachedSet, lastSettings +function validExtensions(context) { + if (cachedSet && context.settings === lastSettings) { + return cachedSet + } + + lastSettings = context.settings + cachedSet = makeValidExtensionSet(context.settings) + return cachedSet +} + +function makeValidExtensionSet(settings) { + // start with explicit JS-parsed extensions + const exts = new Set(settings['import/extensions'] || [ '.js' ]) + + // all alternate parser extensions are also valid + if ('import/parsers' in settings) { + for (let parser in settings['import/parsers']) { + settings['import/parsers'][parser] + .forEach(ext => exts.add(ext)) + } + } + + return exts +} + +exports.default = function ignore(path, context) { + // check extension whitelist first (cheap) + if (!hasValidExtension(path, context)) return true + + if (!('import/ignore' in context.settings)) return false + const ignoreStrings = context.settings['import/ignore'] + + for (let i = 0; i < ignoreStrings.length; i++) { + const regex = new RegExp(ignoreStrings[i]) + if (regex.test(path)) { + log(`ignoring ${path}, matched pattern /${ignoreStrings[i]}/`) + return true + } + } + + return false +} + +function hasValidExtension(path, context) { + return validExtensions(context).has(extname(path)) +} +exports.hasValidExtension = hasValidExtension diff --git a/src/core/module-require.js b/utils/module-require.js similarity index 75% rename from src/core/module-require.js rename to utils/module-require.js index c940c7ae4..9b387ad1a 100644 --- a/src/core/module-require.js +++ b/utils/module-require.js @@ -1,15 +1,18 @@ -import Module from 'module' -import * as path from 'path' +"use strict" +exports.__esModule = true + +const Module = require('module') +const path = require('path') // borrowed from babel-eslint function createModule(filename) { - var mod = new Module(filename) + const mod = new Module(filename) mod.filename = filename mod.paths = Module._nodeModulePaths(path.dirname(filename)) return mod } -export default function moduleRequire(p) { +exports.default = function moduleRequire(p) { try { // attempt to get espree relative to eslint const eslintPath = require.resolve('eslint') diff --git a/utils/moduleVisitor.js b/utils/moduleVisitor.js new file mode 100644 index 000000000..4248317b6 --- /dev/null +++ b/utils/moduleVisitor.js @@ -0,0 +1,129 @@ +"use strict" +exports.__esModule = true + +/** + * Returns an object of node visitors that will call + * 'visitor' with every discovered module path. + * + * todo: correct function prototype for visitor + * @param {Function(String)} visitor [description] + * @param {[type]} options [description] + * @return {object} + */ +exports.default = function visitModules(visitor, options) { + // if esmodule is not explicitly disabled, it is assumed to be enabled + options = Object.assign({ esmodule: true }, options) + + let ignoreRegExps = [] + if (options.ignore != null) { + ignoreRegExps = options.ignore.map(p => new RegExp(p)) + } + + function checkSourceValue(source) { + if (source == null) return //? + + // handle ignore + if (ignoreRegExps.some(re => re.test(source.value))) return + + // fire visitor + visitor(source) + } + + // for import-y declarations + function checkSource(node) { + checkSourceValue(node.source) + } + + // for CommonJS `require` calls + // adapted from @mctep: http://git.io/v4rAu + function checkCommon(call) { + if (call.callee.type !== 'Identifier') return + if (call.callee.name !== 'require') return + if (call.arguments.length !== 1) return + + const modulePath = call.arguments[0] + if (modulePath.type !== 'Literal') return + if (typeof modulePath.value !== 'string') return + + checkSourceValue(modulePath) + } + + function checkAMD(call) { + if (call.callee.type !== 'Identifier') return + if (call.callee.name !== 'require' && + call.callee.name !== 'define') return + if (call.arguments.length !== 2) return + + const modules = call.arguments[0] + if (modules.type !== 'ArrayExpression') return + + for (let element of modules.elements) { + if (element.type !== 'Literal') continue + if (typeof element.value !== 'string') continue + + if (element.value === 'require' || + element.value === 'exports') continue // magic modules: http://git.io/vByan + + checkSourceValue(element) + } + } + + const visitors = {} + if (options.esmodule) { + Object.assign(visitors, { + 'ImportDeclaration': checkSource, + 'ExportNamedDeclaration': checkSource, + 'ExportAllDeclaration': checkSource, + }) + } + + if (options.commonjs || options.amd) { + visitors['CallExpression'] = function (call) { + if (options.commonjs) checkCommon(call) + if (options.amd) checkAMD(call) + } + } + + return visitors +} + +/** + * make an options schema for the module visitor, optionally + * adding extra fields. + + * @param {[type]} additionalProperties [description] + * @return {[type]} [description] + */ +function makeOptionsSchema(additionalProperties) { + const base = { + 'type': 'object', + 'properties': { + 'commonjs': { 'type': 'boolean' }, + 'amd': { 'type': 'boolean' }, + 'esmodule': { 'type': 'boolean' }, + 'ignore': { + 'type': 'array', + 'minItems': 1, + 'items': { 'type': 'string' }, + 'uniqueItems': true, + }, + }, + 'additionalProperties': false, + } + + if (additionalProperties){ + for (let key in additionalProperties) { + base.properties[key] = additionalProperties[key] + } + } + + return base +} +exports.makeOptionsSchema = makeOptionsSchema + +/** + * json schema object for options parameter. can be used to build + * rule options schema object. + * @type {Object} + */ +exports.optionsSchema = makeOptionsSchema() diff --git a/utils/package.json b/utils/package.json new file mode 100644 index 000000000..952a13195 --- /dev/null +++ b/utils/package.json @@ -0,0 +1,31 @@ +{ + "name": "eslint-module-utils", + "version": "1.0.0", + "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", + "engines": { + "node": ">=4" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/benmosher/eslint-plugin-import.git" + }, + "keywords": [ + "eslint-plugin-import", + "eslint", + "modules", + "esmodules" + ], + "author": "Ben Mosher ", + "license": "MIT", + "bugs": { + "url": "https://github.com/benmosher/eslint-plugin-import/issues" + }, + "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", + "dependencies": { + "debug": "2.2.0", + "pkg-dir": "^1.0.0" + } +} diff --git a/src/core/parse.js b/utils/parse.js similarity index 67% rename from src/core/parse.js rename to utils/parse.js index e793ecb7c..c93417a61 100644 --- a/src/core/parse.js +++ b/utils/parse.js @@ -1,22 +1,23 @@ -import moduleRequire from './module-require' -import assign from 'object-assign' -import { extname } from 'path' -import debug from 'debug' +"use strict" +exports.__esModule = true -const log = debug('eslint-plugin-import:parse') +const moduleRequire = require('./module-require').default +const extname = require('path').extname -export default function (path, content, context) { +const log = require('debug')('eslint-plugin-import:parse') + +exports.default = function parse(path, content, context) { if (context == null) throw new Error('need context to parse properly') - let { parserOptions } = context + let parserOptions = context.parserOptions const parserPath = getParserPath(path, context) if (!parserPath) throw new Error('parserPath is required!') // hack: espree blows up with frozen options - parserOptions = assign({}, parserOptions) - parserOptions.ecmaFeatures = assign({}, parserOptions.ecmaFeatures) + parserOptions = Object.assign({}, parserOptions) + parserOptions.ecmaFeatures = Object.assign({}, parserOptions.ecmaFeatures) // always attach comments parserOptions.attachComment = true diff --git a/src/core/resolve.js b/utils/resolve.js similarity index 60% rename from src/core/resolve.js rename to utils/resolve.js index 140633f25..1d6e164b1 100644 --- a/src/core/resolve.js +++ b/utils/resolve.js @@ -1,58 +1,48 @@ -import Map from 'es6-map' -import Set from 'es6-set' -import assign from 'object-assign' -import pkgDir from 'pkg-dir' +"use strict" +exports.__esModule = true -import fs from 'fs' -import * as path from 'path' +const pkgDir = require('pkg-dir') -export const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) +const fs = require('fs') +const path = require('path') -const fileExistsCache = new Map() +const hashObject = require('./hash').hashObject + , ModuleCache = require('./ModuleCache').default -function cachePath(cacheKey, result) { - fileExistsCache.set(cacheKey, { result, lastSeen: Date.now() }) -} +const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) +exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS -function checkCache(cacheKey, { lifetime }) { - if (fileExistsCache.has(cacheKey)) { - const { result, lastSeen } = fileExistsCache.get(cacheKey) - // check fresness - if (Date.now() - lastSeen < (lifetime * 1000)) return result - } - // cache miss - return undefined -} +const fileExistsCache = new ModuleCache() // http://stackoverflow.com/a/27382838 -function fileExistsWithCaseSync(filepath, cacheSettings) { +exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings) { // don't care if the FS is case-sensitive if (CASE_SENSITIVE_FS) return true // null means it resolved to a builtin if (filepath === null) return true + const parsedPath = path.parse(filepath) + , dir = parsedPath.dir - const dir = path.dirname(filepath) - - let result = checkCache(filepath, cacheSettings) + let result = fileExistsCache.get(filepath, cacheSettings) if (result != null) return result // base case - if (dir === '/' || dir === '.' || /^[A-Z]:\\$/i.test(dir)) { + if (dir === '' || parsedPath.root === filepath) { result = true } else { const filenames = fs.readdirSync(dir) - if (filenames.indexOf(path.basename(filepath)) === -1) { + if (filenames.indexOf(parsedPath.base) === -1) { result = false } else { result = fileExistsWithCaseSync(dir, cacheSettings) } } - cachePath(filepath, result) + fileExistsCache.set(filepath, result) return result } -export function relative(modulePath, sourceFile, settings) { +function relative(modulePath, sourceFile, settings) { return fullResolve(modulePath, sourceFile, settings).path } @@ -62,22 +52,15 @@ function fullResolve(modulePath, sourceFile, settings) { if (coreSet != null && coreSet.has(modulePath)) return { found: true, path: null } const sourceDir = path.dirname(sourceFile) - , cacheKey = sourceDir + hashObject(settings) + modulePath + , cacheKey = sourceDir + hashObject(settings).digest('hex') + modulePath - const cacheSettings = assign({ - lifetime: 30, // seconds - }, settings['import/cache']) + const cacheSettings = ModuleCache.getSettings(settings) - // parse infinity - if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { - cacheSettings.lifetime = Infinity - } - - const cachedPath = checkCache(cacheKey, cacheSettings) + const cachedPath = fileExistsCache.get(cacheKey, cacheSettings) if (cachedPath !== undefined) return { found: true, path: cachedPath } function cache(resolvedPath) { - cachePath(cacheKey, resolvedPath) + fileExistsCache.set(cacheKey, resolvedPath) } function withResolver(resolver, config) { @@ -111,25 +94,24 @@ function fullResolve(modulePath, sourceFile, settings) { const resolvers = resolverReducer(configResolvers, new Map()) - let resolved = { found: false } - resolvers.forEach(function (config, name) { - if (!resolved.found) { - const resolver = requireResolver(name, sourceFile) - resolved = withResolver(resolver, config) - if (resolved.found) { - // resolvers imply file existence, this double-check just ensures the case matches - if (fileExistsWithCaseSync(resolved.path, cacheSettings)) { - // else, counts - cache(resolved.path) - } else { - resolved = { found: false } - } - } - } - }) + for (let pair of resolvers) { + let name = pair[0] + , config = pair[1] + const resolver = requireResolver(name, sourceFile) + , resolved = withResolver(resolver, config) + + if (!resolved.found) continue + + // else, counts + cache(resolved.path) + return resolved + } - return resolved + // failed + // cache(undefined) + return { found: false } } +exports.relative = relative function resolverReducer(resolvers, map) { if (resolvers instanceof Array) { @@ -185,7 +167,7 @@ const erroredContexts = new Set() * null if package is core; * undefined if not found */ -export default function resolve(p, context) { +function resolve(p, context) { try { return relative( p , context.getFilename() @@ -195,18 +177,11 @@ export default function resolve(p, context) { if (!erroredContexts.has(context)) { context.report({ message: `Resolve error: ${err.message}`, - loc: { line: 1, col: 0 }, + loc: { line: 1, column: 0 }, }) erroredContexts.add(context) } } } resolve.relative = relative - - -import { createHash } from 'crypto' -function hashObject(object) { - const settingsShasum = createHash('sha1') - settingsShasum.update(JSON.stringify(object)) - return settingsShasum.digest('hex') -} +exports.default = resolve diff --git a/utils/unambiguous.js b/utils/unambiguous.js new file mode 100644 index 000000000..d4830a253 --- /dev/null +++ b/utils/unambiguous.js @@ -0,0 +1,29 @@ +'use strict' +exports.__esModule = true + +/** + * detect possible imports/exports without a full parse. + * used primarily to ignore the import/ignore setting, iif it looks like + * there might be something there (i.e., jsnext:main is set). + * + * A negative test means that a file is definitely _not_ a module. + * A positive test means it _could_ be. + * + * Not perfect, just a fast way to disqualify large non-ES6 modules and + * avoid a parse. + * @type {RegExp} + */ +exports.potentialModulePattern = + new RegExp(`(?:^|;)\s*(?:export|import)(?:(?:\s+\w)|(?:\s*[{*]))`) + +// future-/Babel-proof at the expense of being a little loose +const unambiguousNodeType = /^(Exp|Imp)ort.*Declaration$/ + +/** + * Given an AST, return true if the AST unambiguously represents a module. + * @param {Program node} ast + * @return {Boolean} + */ +exports.isModule = function isUnambiguousModule(ast) { + return ast.body.some(node => unambiguousNodeType.test(node.type)) +}