diff --git a/.travis.yml b/.travis.yml index ea8c60a593..e8eaf9d967 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ matrix: node_js: 6 - env: PACKAGE=resolvers/webpack node_js: 4 + - os: osx env: ESLINT_VERSION=5 node_js: 10 @@ -44,16 +45,23 @@ matrix: - os: osx env: ESLINT_VERSION=2 node_js: 4 + exclude: - node_js: '4' env: ESLINT_VERSION=5 + + fast_finish: true + allow_failures: + # issues with typescript deps in this version intersection + - node_js: '4' + env: ESLINT_VERSION=4 before_install: - 'nvm install-latest-npm' - 'if [ -n "${PACKAGE-}" ]; then cd "${PACKAGE}"; fi' install: - npm install - - npm install --no-save eslint@$ESLINT_VERSION --ignore-scripts || true + - 'if [ -n "${ESLINT_VERSION}" ]; then ./tests/dep-time-travel.sh; fi' script: - 'npm test' diff --git a/CHANGELOG.md b/CHANGELOG.md index d384d5c390..d93839d76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## [Unreleased] + +## [2.16.0] - 2019-01-29 +### Added +- `typescript` config ([#1257], thanks [@kirill-konshin]) + +### Fixed +- Memory leak of `SourceCode` objects for all parsed dependencies, resolved. (issue [#1266], thanks [@asapach] and [@sergei-startsev] for digging in) + +## [2.15.0] - 2019-01-22 +### Added +- new rule: [`no-named-export`] ([#1157], thanks [@fsmaia]) + +### Fixed +- [`no-extraneous-dependencies`]: `packageDir` option with array value was clobbering package deps instead of merging them ([#1175]/[#1176], thanks [@aravindet] & [@pzhine]) +- [`dynamic-import-chunkname`]: Add proper webpack comment parsing ([#1163], thanks [@st-sloth]) +- [`named`]: fix destructuring assignment ([#1232], thanks [@ljqx]) + + +## [2.14.0] - 2018-08-13 +* 69e0187 (HEAD -> master, source/master, origin/master, origin/HEAD) Merge pull request #1151 from jf248/jsx +|\ +| * e30a757 (source/pr/1151, fork/jsx) Add JSX check to namespace rule +|/ +* 8252344 (source/pr/1148) Add error to output when module loaded as resolver has invalid API +### Added +- [`no-useless-path-segments`]: add commonJS (CJS) support ([#1128], thanks [@1pete]) +- [`namespace`]: add JSX check ([#1151], thanks [@jf248]) + +### Fixed +- [`no-cycle`]: ignore Flow imports ([#1126], thanks [@gajus]) +- fix Flow type imports ([#1106], thanks [@syymza]) +- [`no-relative-parent-imports`]: resolve paths ([#1135], thanks [@chrislloyd]) +- [`import/order`]: fix autofixer when using typescript-eslint-parser ([#1137], thanks [@justinanastos]) +- repeat fix from [#797] for [#717], in another place (thanks [@ljharb]) + +### Refactors +- add explicit support for RestElement alongside ExperimentalRestProperty (thanks [@ljharb]) + ## [2.13.0] - 2018-06-24 ### Added - Add ESLint 5 support ([#1122], thanks [@ai] and [@ljharb]) @@ -470,10 +508,23 @@ for info on changes for earlier releases. [`no-default-export`]: ./docs/rules/no-default-export.md [`no-useless-path-segments`]: ./docs/rules/no-useless-path-segments.md [`no-cycle`]: ./docs/rules/no-cycle.md +[`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md +[`no-named-export`]: ./docs/rules/no-named-export.md [`memo-parser`]: ./memo-parser/README.md +[#1257]: https://github.com/benmosher/eslint-plugin-import/pull/1257 +[#1232]: https://github.com/benmosher/eslint-plugin-import/pull/1232 +[#1176]: https://github.com/benmosher/eslint-plugin-import/pull/1176 +[#1163]: https://github.com/benmosher/eslint-plugin-import/pull/1163 +[#1157]: https://github.com/benmosher/eslint-plugin-import/pull/1157 +[#1151]: https://github.com/benmosher/eslint-plugin-import/pull/1151 +[#1137]: https://github.com/benmosher/eslint-plugin-import/pull/1137 +[#1135]: https://github.com/benmosher/eslint-plugin-import/pull/1135 +[#1128]: https://github.com/benmosher/eslint-plugin-import/pull/1128 +[#1126]: https://github.com/benmosher/eslint-plugin-import/pull/1126 [#1122]: https://github.com/benmosher/eslint-plugin-import/pull/1122 +[#1106]: https://github.com/benmosher/eslint-plugin-import/pull/1106 [#1093]: https://github.com/benmosher/eslint-plugin-import/pull/1093 [#1085]: https://github.com/benmosher/eslint-plugin-import/pull/1085 [#1068]: https://github.com/benmosher/eslint-plugin-import/pull/1068 @@ -486,6 +537,7 @@ for info on changes for earlier releases. [#858]: https://github.com/benmosher/eslint-plugin-import/pull/858 [#843]: https://github.com/benmosher/eslint-plugin-import/pull/843 [#871]: https://github.com/benmosher/eslint-plugin-import/pull/871 +[#797]: https://github.com/benmosher/eslint-plugin-import/pull/797 [#744]: https://github.com/benmosher/eslint-plugin-import/pull/744 [#742]: https://github.com/benmosher/eslint-plugin-import/pull/742 [#737]: https://github.com/benmosher/eslint-plugin-import/pull/737 @@ -551,6 +603,8 @@ for info on changes for earlier releases. [#314]: https://github.com/benmosher/eslint-plugin-import/pull/314 [#912]: https://github.com/benmosher/eslint-plugin-import/pull/912 +[#1266]: https://github.com/benmosher/eslint-plugin-import/issues/1266 +[#1175]: https://github.com/benmosher/eslint-plugin-import/issues/1175 [#1058]: https://github.com/benmosher/eslint-plugin-import/issues/1058 [#931]: https://github.com/benmosher/eslint-plugin-import/issues/931 [#886]: https://github.com/benmosher/eslint-plugin-import/issues/886 @@ -558,6 +612,7 @@ for info on changes for earlier releases. [#842]: https://github.com/benmosher/eslint-plugin-import/issues/842 [#839]: https://github.com/benmosher/eslint-plugin-import/issues/839 [#720]: https://github.com/benmosher/eslint-plugin-import/issues/720 +[#717]: https://github.com/benmosher/eslint-plugin-import/issues/717 [#686]: https://github.com/benmosher/eslint-plugin-import/issues/686 [#671]: https://github.com/benmosher/eslint-plugin-import/issues/671 [#660]: https://github.com/benmosher/eslint-plugin-import/issues/660 @@ -619,7 +674,11 @@ for info on changes for earlier releases. [#119]: https://github.com/benmosher/eslint-plugin-import/issues/119 [#89]: https://github.com/benmosher/eslint-plugin-import/issues/89 -[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v2.12.0...HEAD +[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v2.16.0...HEAD +[2.16.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.15.0...v2.16.0 +[2.15.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.14.0...v2.15.0 +[2.14.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.13.0...v2.14.0 +[2.13.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.12.0...v2.13.0 [2.12.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.11.0...v2.12.0 [2.11.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.10.0...v2.11.0 [2.10.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.9.0...v2.10.0 @@ -733,3 +792,15 @@ for info on changes for earlier releases. [@hulkish]: https://github.com/hulkish [@chrislloyd]: https://github.com/chrislloyd [@ai]: https://github.com/ai +[@syymza]: https://github.com/syymza +[@justinanastos]: https://github.com/justinanastos +[@1pete]: https://github.com/1pete +[@gajus]: https://github.com/gajus +[@jf248]: https://github.com/jf248 +[@aravindet]: https://github.com/aravindet +[@pzhine]: https://github.com/pzhine +[@st-sloth]: https://github.com/st-sloth +[@ljqx]: https://github.com/ljqx +[@kirill-konshin]: https://github.com/kirill-konshin +[@asapach]: https://github.com/asapach +[@sergei-startsev]: https://github.com/sergei-startsev diff --git a/README.md b/README.md index 53b2640627..6f826a85b7 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Forbid unassigned imports ([`no-unassigned-import`]) * Forbid named default exports ([`no-named-default`]) * Forbid default exports ([`no-default-export`]) +* Forbid named exports ([`no-named-export`]) * Forbid anonymous values as default exports ([`no-anonymous-default-export`]) * Prefer named exports to be grouped together in a single export declaration ([`group-exports`]) * Enforce a leading comment with the webpackChunkName for dynamic imports ([`dynamic-import-chunkname`]) @@ -104,6 +105,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-anonymous-default-export`]: ./docs/rules/no-anonymous-default-export.md [`group-exports`]: ./docs/rules/group-exports.md [`no-default-export`]: ./docs/rules/no-default-export.md +[`no-named-export`]: ./docs/rules/no-named-export.md [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md ## Installation diff --git a/appveyor.yml b/appveyor.yml index 0176e12545..bb435695f5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,13 +3,14 @@ environment: matrix: - nodejs_version: "10" - nodejs_version: "8" - - nodejs_version: "6" - - nodejs_version: "4" + # - nodejs_version: "6" + # - nodejs_version: "4" matrix: fast_finish: true - allow_failures: - - nodejs_version: "4" # for eslint 5 + + # allow_failures: + # - nodejs_version: "4" # for eslint 5 # platform: # - x86 diff --git a/config/typescript.js b/config/typescript.js new file mode 100644 index 0000000000..d2d707c589 --- /dev/null +++ b/config/typescript.js @@ -0,0 +1,22 @@ +/** + * Adds `.jsx`, `.ts` and `.tsx` as an extension, and enables JSX/TSX parsing. + */ +var jsExtensions = ['.js', '.jsx']; +var tsExtensions = ['.ts', '.tsx']; +var allExtensions = jsExtensions.concat(tsExtensions); + +module.exports = { + + settings: { + 'import/extensions': allExtensions, + 'import/parsers': { + 'typescript-eslint-parser': tsExtensions + }, + 'import/resolver': { + 'node': { + 'extensions': allExtensions + } + } + } + +} diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index 98b98871e8..b1757b4e52 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -28,6 +28,10 @@ import( /*webpackChunkName:"someModule"*/ 'someModule', ); +import( + /* webpackChunkName : "someModule" */ + 'someModule', +); // chunkname contains a 6 (forbidden by rule config) import( @@ -41,6 +45,12 @@ import( 'someModule', ); +// invalid syntax for webpack comment +import( + /* totally not webpackChunkName: "someModule" */ + 'someModule', +); + // single-line comment, not a block-style comment import( // webpackChunkName: "someModule" @@ -59,6 +69,15 @@ The following patterns are valid: /* webpackChunkName: "someOtherModule12345789" */ 'someModule', ); + import( + /* webpackChunkName: "someModule" */ + /* webpackPrefetch: true */ + 'someModule', + ); + import( + /* webpackChunkName: "someModule", webpackPrefetch: true */ + 'someModule', + ); ``` ## When Not To Use It diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md index 7583651f31..fae7d8daf2 100644 --- a/docs/rules/no-deprecated.md +++ b/docs/rules/no-deprecated.md @@ -1,9 +1,5 @@ # import/no-deprecated -**Stage: 0** - -**NOTE**: this rule is currently a work in progress. There may be "breaking" changes: most likely, additional cases that are flagged. - Reports use of a deprecated name, as indicated by a JSDoc block with a `@deprecated` tag or TomDoc `Deprecated: ` comment. diff --git a/docs/rules/no-extraneous-dependencies.md b/docs/rules/no-extraneous-dependencies.md index 5c3542ebd4..2b66aa25c0 100644 --- a/docs/rules/no-extraneous-dependencies.md +++ b/docs/rules/no-extraneous-dependencies.md @@ -29,10 +29,14 @@ You can also use an array of globs instead of literal booleans: When using an array of globs, the setting will be set to `true` (no errors reported) if the name of the file being linted matches a single glob in the array, and `false` otherwise. -Also there is one more option called `packageDir`, this option is to specify the path to the folder containing package.json and is relative to the current working directory. +Also there is one more option called `packageDir`, this option is to specify the path to the folder containing package.json. + +If provided as a relative path string, will be computed relative to the current working directory at linter execution time. If this is not ideal (does not work with some editor integrations), consider using `__dirname` to provide a path relative to your configuration. ```js "import/no-extraneous-dependencies": ["error", {"packageDir": './some-dir/'}] +// or +"import/no-extraneous-dependencies": ["error", {"packageDir": path.join(__dirname, 'some-dir')}] ``` It may also be an array of multiple paths, to support monorepos or other novel project diff --git a/docs/rules/no-named-export.md b/docs/rules/no-named-export.md new file mode 100644 index 0000000000..b3a0ef8d63 --- /dev/null +++ b/docs/rules/no-named-export.md @@ -0,0 +1,77 @@ +# no-named-export + +Prohibit named exports. Mostly an inverse of [`no-default-export`]. + +[`no-default-export`]: ./no-default-export.md + +## Rule Details + +The following patterns are considered warnings: + +```javascript +// bad1.js + +// There is only a single module export and it's a named export. +export const foo = 'foo'; +``` + +```javascript +// bad2.js + +// There is more than one named export in the module. +export const foo = 'foo'; +export const bar = 'bar'; +``` + +```javascript +// bad3.js + +// There is more than one named export in the module. +const foo = 'foo'; +const bar = 'bar'; +export { foo, bar } +``` + +```javascript +// bad4.js + +// There is more than one named export in the module. +export * from './other-module' +``` + +```javascript +// bad5.js + +// There is a default and a named export. +export const foo = 'foo'; +const bar = 'bar'; +export default 'bar'; +``` + +The following patterns are not warnings: + +```javascript +// good1.js + +// There is only a single module export and it's a default export. +export default 'bar'; +``` + +```javascript +// good2.js + +// There is only a single module export and it's a default export. +const foo = 'foo'; +export { foo as default } +``` + +```javascript +// good3.js + +// There is only a single module export and it's a default export. +export default from './other-module'; +``` + +## When Not To Use It + +If you don't care if named imports are used, or if you prefer named imports over default imports. diff --git a/docs/rules/no-useless-path-segments.md b/docs/rules/no-useless-path-segments.md index d0891ee187..b2ae82a3a7 100644 --- a/docs/rules/no-useless-path-segments.md +++ b/docs/rules/no-useless-path-segments.md @@ -1,6 +1,6 @@ # import/no-useless-path-segments -Use this rule to prevent unnecessary path segemnts in import and require statements. +Use this rule to prevent unnecessary path segments in import and require statements. ## Rule Details diff --git a/package.json b/package.json index 7d39395a76..79f52e5930 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "2.13.0", + "version": "2.16.0", "description": "Import with sanity.", "engines": { "node": ">=4" @@ -44,13 +44,13 @@ }, "homepage": "https://github.com/benmosher/eslint-plugin-import", "devDependencies": { - "babel-eslint": "8.0.x", + "babel-eslint": "^8.2.6", "babel-plugin-istanbul": "^4.1.6", "babel-preset-es2015-argon": "latest", "babel-register": "^6.26.0", - "babylon": "6.15.0", + "babylon": "^6.15.0", "chai": "^3.5.0", - "coveralls": "^3.0.0", + "coveralls": "^3.0.2", "cross-env": "^4.0.0", "eslint": "2.x - 5.x", "eslint-import-resolver-node": "file:./resolvers/node", @@ -58,31 +58,31 @@ "eslint-import-resolver-webpack": "file:./resolvers/webpack", "eslint-module-utils": "file:./utils", "eslint-plugin-import": "2.x", - "gulp": "^3.9.0", + "gulp": "^3.9.1", "gulp-babel": "6.1.2", - "linklocal": "^2.6.0", + "linklocal": "^2.8.2", "mocha": "^3.5.3", - "nyc": "^11.7.1", - "redux": "^3.0.4", - "rimraf": "^2.6.2", - "sinon": "^2.3.2", - "typescript": "^2.6.2", - "typescript-eslint-parser": "^15.0.0" + "nyc": "^11.9.0", + "redux": "^3.7.2", + "rimraf": "^2.6.3", + "sinon": "^2.4.1", + "typescript": "^3.2.2", + "typescript-eslint-parser": "^21.0.2" }, "peerDependencies": { "eslint": "2.x - 5.x" }, "dependencies": { "contains-path": "^0.1.0", - "debug": "^2.6.8", + "debug": "^2.6.9", "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.1", - "eslint-module-utils": "^2.2.0", - "has": "^1.0.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.3", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.3.0", + "has": "^1.0.3", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", "read-pkg-up": "^2.0.0", - "resolve": "^1.6.0" + "resolve": "^1.9.0" }, "nyc": { "require": [ diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index 93246bf0b7..5526ca5401 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -5,6 +5,15 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased + +## 0.11.0 - 2018-01-22 + +### Added +- support for `argv` parameter when config is a function. ([#1261], thanks [@keann]) + +### Fixed +- crash when webpack config is an array of functions ([#1219]/[#1220] by [@idudinov]) + ## 0.10.1 - 2018-06-24 ### Fixed - log a useful error in a module bug arises ([#768]/[#767], thanks [@mattkrick]) @@ -104,6 +113,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - `interpret` configs (such as `.babel.js`). Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). +[#1261]: https://github.com/benmosher/eslint-plugin-import/pull/1261 +[#1220]: https://github.com/benmosher/eslint-plugin-import/pull/1220 [#1091]: https://github.com/benmosher/eslint-plugin-import/pull/1091 [#969]: https://github.com/benmosher/eslint-plugin-import/pull/969 [#968]: https://github.com/benmosher/eslint-plugin-import/pull/968 @@ -121,6 +132,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel [#181]: https://github.com/benmosher/eslint-plugin-import/pull/181 [#164]: https://github.com/benmosher/eslint-plugin-import/pull/164 +[#1219]: https://github.com/benmosher/eslint-plugin-import/issues/1219 [#788]: https://github.com/benmosher/eslint-plugin-import/issues/788 [#767]: https://github.com/benmosher/eslint-plugin-import/issues/767 [#681]: https://github.com/benmosher/eslint-plugin-import/issues/681 @@ -147,3 +159,5 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel [@SkeLLLa]: https://github.com/SkeLLLa [@graingert]: https://github.com/graingert [@mattkrick]: https://github.com/mattkrick +[@idudinov]: https://github.com/idudinov +[@keann]: https://github.com/keann diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index 4c9abc61d8..146213a0ca 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -47,6 +47,7 @@ exports.resolve = function (source, file, settings) { var configPath = get(settings, 'config') , configIndex = get(settings, 'config-index') , env = get(settings, 'env') + , argv = get(settings, 'argv', {}) , packageDir log('Config path from settings:', configPath) @@ -87,10 +88,18 @@ exports.resolve = function (source, file, settings) { } if (typeof webpackConfig === 'function') { - webpackConfig = webpackConfig(env) + webpackConfig = webpackConfig(env, argv) } if (Array.isArray(webpackConfig)) { + webpackConfig = webpackConfig.map(cfg => { + if (typeof cfg === 'function') { + return cfg(env, argv) + } + + return cfg + }) + if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { webpackConfig = webpackConfig[configIndex] } diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index a36a78e474..5985babe6c 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-webpack", - "version": "0.10.1", + "version": "0.11.0", "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", "main": "index.js", "scripts": { diff --git a/resolvers/webpack/test/config.js b/resolvers/webpack/test/config.js index 2519daf8a8..07c6350c56 100644 --- a/resolvers/webpack/test/config.js +++ b/resolvers/webpack/test/config.js @@ -103,4 +103,36 @@ describe("config", function () { .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'bar.js')) }) + it('finds the config at option env when config is an array of functions', function() { + var settings = { + config: require(path.join(__dirname, './files/webpack.function.config.multiple.js')), + env: { + dummy: true, + }, + } + + expect(resolve('bar', file, settings)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'some', 'goofy', 'path', 'bar.js')) + }) + + it('passes argv to config when it is a function', function() { + var settings = { + config: require(path.join(__dirname, './files/webpack.function.config.js')), + argv: { + mode: 'test' + } + } + + expect(resolve('baz', file, settings)).to.have.property('path') + .and.equal(path.join(__dirname, 'files', 'some', 'bar', 'bar.js')) + }) + + it('passes a default empty argv object to config when it is a function', function() { + var settings = { + config: require(path.join(__dirname, './files/webpack.function.config.js')), + argv: undefined + } + + expect(function () { resolve('baz', file, settings) }).to.not.throw(Error) + }) }) diff --git a/resolvers/webpack/test/files/webpack.function.config.js b/resolvers/webpack/test/files/webpack.function.config.js index ce87dd1b11..0dad14e067 100644 --- a/resolvers/webpack/test/files/webpack.function.config.js +++ b/resolvers/webpack/test/files/webpack.function.config.js @@ -1,12 +1,13 @@ var path = require('path') var pluginsTest = require('webpack-resolver-plugin-test') -module.exports = function(env) { +module.exports = function(env, argv) { return { resolve: { alias: { 'foo': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), 'bar': env ? path.join(__dirname, 'some', 'goofy', 'path', 'bar.js') : undefined, + 'baz': argv.mode === 'test' ? path.join(__dirname, 'some', 'bar', 'bar.js') : undefined, 'some-alias': path.join(__dirname, 'some'), }, modules: [ diff --git a/resolvers/webpack/test/files/webpack.function.config.multiple.js b/resolvers/webpack/test/files/webpack.function.config.multiple.js new file mode 100644 index 0000000000..4dbc94bbc9 --- /dev/null +++ b/resolvers/webpack/test/files/webpack.function.config.multiple.js @@ -0,0 +1,43 @@ +var path = require('path') +var pluginsTest = require('webpack-resolver-plugin-test') + +module.exports = [function(env) { + return { + resolve: { + alias: { + 'foo': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), + 'bar': env ? path.join(__dirname, 'some', 'goofy', 'path', 'bar.js') : undefined, + 'some-alias': path.join(__dirname, 'some'), + }, + modules: [ + path.join(__dirname, 'src'), + path.join(__dirname, 'fallback'), + 'node_modules', + 'bower_components', + ], + modulesDirectories: ['node_modules', 'bower_components'], + root: path.join(__dirname, 'src'), + fallback: path.join(__dirname, 'fallback'), + }, + + externals: [ + { 'jquery': 'jQuery' }, + 'bootstrap', + function (context, request, callback) { + if (request === 'underscore') { + return callback(null, 'underscore') + } + callback() + }, + ], + + plugins: [ + new pluginsTest.ResolverPlugin([ + new pluginsTest.SimpleResolver( + path.join(__dirname, 'some', 'bar', 'bar.js'), + path.join(__dirname, 'some', 'bar') + ), + ]), + ], + } +}] diff --git a/src/ExportMap.js b/src/ExportMap.js index 1cb5dc3e9c..07120754c6 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -4,6 +4,8 @@ import doctrine from 'doctrine' import debug from 'debug' +import SourceCode from 'eslint/lib/util/source-code' + import parse from 'eslint-module-utils/parse' import resolve from 'eslint-module-utils/resolve' import isIgnored, { hasValidExtension } from 'eslint-module-utils/ignore' @@ -38,7 +40,12 @@ export default class ExportMap { get size() { let size = this.namespace.size + this.reexports.size - this.dependencies.forEach(dep => size += dep().size) + this.dependencies.forEach(dep => { + const d = dep() + // CJS / ignored dependencies won't exist (#717) + if (d == null) return + size += d.size + }) return size } @@ -185,25 +192,36 @@ export default class ExportMap { /** * parse docs from the first node that has leading comments - * @param {...[type]} nodes [description] - * @return {{doc: object}} */ -function captureDoc(docStyleParsers) { +function captureDoc(source, docStyleParsers, ...nodes) { const metadata = {} - , nodes = Array.prototype.slice.call(arguments, 1) // 'some' short-circuits on first 'true' nodes.some(n => { - if (!n.leadingComments) return false + try { - for (let name in docStyleParsers) { - const doc = docStyleParsers[name](n.leadingComments) - if (doc) { - metadata.doc = doc + let leadingComments + + // n.leadingComments is legacy `attachComments` behavior + if ('leadingComments' in n) { + leadingComments = n.leadingComments + } else if (n.range) { + leadingComments = source.getCommentsBefore(n) } - } - return true + if (!leadingComments || leadingComments.length === 0) return false + + for (let name in docStyleParsers) { + const doc = docStyleParsers[name](leadingComments) + if (doc) { + metadata.doc = doc + } + } + + return true + } catch (err) { + return false + } }) return metadata @@ -385,7 +403,7 @@ ExportMap.parse = function (path, content, context) { const existing = m.imports.get(p) if (existing != null) return existing.getter - const getter = () => ExportMap.for(childContext(p, context)) + const getter = thunkFor(p, context) m.imports.set(p, { getter, source: { // capturing actual node reference holds full AST in memory! @@ -396,11 +414,12 @@ ExportMap.parse = function (path, content, context) { return getter } + const source = makeSourceCode(content, ast) ast.body.forEach(function (n) { if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(docStyleParsers, n) + const exportMeta = captureDoc(source, docStyleParsers, n) if (n.declaration.type === 'Identifier') { addNamespace(exportMeta, n.declaration) } @@ -436,12 +455,12 @@ ExportMap.parse = function (path, content, context) { case 'TSInterfaceDeclaration': case 'TSAbstractClassDeclaration': case 'TSModuleDeclaration': - m.namespace.set(n.declaration.id.name, captureDoc(docStyleParsers, n)) + m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)) break case 'VariableDeclaration': n.declaration.declarations.forEach((d) => recursivePatternCapture(d.id, - id => m.namespace.set(id.name, captureDoc(docStyleParsers, d, n)))) + id => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)))) break } } @@ -481,6 +500,15 @@ ExportMap.parse = function (path, content, context) { return m } +/** + * The creation of this closure is isolated from other scopes + * to avoid over-retention of unrelated variables, which has + * caused memory leaks. See #1266. + */ +function thunkFor(p, context) { + return () => ExportMap.for(childContext(p, context)) +} + /** * Traverse a pattern/identifier node, calling 'callback' @@ -507,6 +535,10 @@ export function recursivePatternCapture(pattern, callback) { recursivePatternCapture(element, callback) }) break + + case 'AssignmentPattern': + callback(pattern.left) + break } } @@ -522,3 +554,17 @@ function childContext(path, context) { path, } } + + +/** + * sometimes legacy support isn't _that_ hard... right? + */ +function makeSourceCode(text, ast) { + if (SourceCode.length > 1) { + // ESLint 3 + return new SourceCode(text, ast) + } else { + // ESLint 4, 5 + return new SourceCode({ text, ast }) + } +} diff --git a/src/core/importType.js b/src/core/importType.js index 62519d175d..89b162aad3 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -57,7 +57,7 @@ function isInternalModule(name, settings, path) { } function isRelativeToParent(name) { - return name.indexOf('../') === 0 + return /^\.\.[\\/]/.test(name) } const indexFiles = ['.', './', './index', './index.js'] @@ -66,7 +66,7 @@ function isIndex(name) { } function isRelativeToSibling(name) { - return name.indexOf('./') === 0 + return /^\.[\\/]/.test(name) } const typeTest = cond([ diff --git a/src/index.js b/src/index.js index 7df67867f5..6cbe0a6428 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,7 @@ export const rules = { 'newline-after-import': require('./rules/newline-after-import'), 'prefer-default-export': require('./rules/prefer-default-export'), 'no-default-export': require('./rules/no-default-export'), + 'no-named-export': require('./rules/no-named-export'), 'no-dynamic-require': require('./rules/no-dynamic-require'), 'unambiguous': require('./rules/unambiguous'), 'no-unassigned-import': require('./rules/no-unassigned-import'), @@ -61,4 +62,5 @@ export const configs = { 'react': require('../config/react'), 'react-native': require('../config/react-native'), 'electron': require('../config/electron'), + 'typescript': require('../config/typescript'), } diff --git a/src/rules/default.js b/src/rules/default.js index 83c0ea95e3..7e07800dae 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -3,6 +3,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('default'), }, diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index 867808f0b0..44fb5611cc 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -1,7 +1,9 @@ +import vm from 'vm' import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('dynamic-import-chunkname'), }, @@ -27,8 +29,10 @@ module.exports = { const { importFunctions = [] } = config || {} const { webpackChunknameFormat = '[0-9a-zA-Z-_/.]+' } = config || {} - const commentFormat = ` webpackChunkName: "${webpackChunknameFormat}" ` - const commentRegex = new RegExp(commentFormat) + const paddedCommentRegex = /^ (\S[\s\S]+\S) $/ + const commentStyleRegex = /^( \w+: ("[^"]*"|\d+|false|true),?)+ $/ + const chunkSubstrFormat = ` webpackChunkName: "${webpackChunknameFormat}",? ` + const chunkSubstrRegex = new RegExp(chunkSubstrFormat) return { CallExpression(node) { @@ -40,7 +44,7 @@ module.exports = { const arg = node.arguments[0] const leadingComments = sourceCode.getComments(arg).leading - if (!leadingComments || leadingComments.length !== 1) { + if (!leadingComments || leadingComments.length === 0) { context.report({ node, message: 'dynamic imports require a leading comment with the webpack chunkname', @@ -48,20 +52,56 @@ module.exports = { return } - const comment = leadingComments[0] - if (comment.type !== 'Block') { - context.report({ - node, - message: 'dynamic imports require a /* foo */ style comment, not a // foo comment', - }) - return + let isChunknamePresent = false + + for (const comment of leadingComments) { + if (comment.type !== 'Block') { + context.report({ + node, + message: 'dynamic imports require a /* foo */ style comment, not a // foo comment', + }) + return + } + + if (!paddedCommentRegex.test(comment.value)) { + context.report({ + node, + message: `dynamic imports require a block comment padded with spaces - /* foo */`, + }) + return + } + + try { + // just like webpack itself does + vm.runInNewContext(`(function(){return {${comment.value}}})()`) + } + catch (error) { + context.report({ + node, + message: `dynamic imports require a "webpack" comment with valid syntax`, + }) + return + } + + if (!commentStyleRegex.test(comment.value)) { + context.report({ + node, + message: + `dynamic imports require a leading comment in the form /*${chunkSubstrFormat}*/`, + }) + return + } + + if (chunkSubstrRegex.test(comment.value)) { + isChunknamePresent = true + } } - const webpackChunkDefinition = comment.value - if (!webpackChunkDefinition.match(commentRegex)) { + if (!isChunknamePresent) { context.report({ node, - message: `dynamic imports require a leading comment in the form /*${commentFormat}*/`, + message: + `dynamic imports require a leading comment in the form /*${chunkSubstrFormat}*/`, }) } }, diff --git a/src/rules/export.js b/src/rules/export.js index f6adf0ae83..db5c8c3c1b 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -3,6 +3,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('export'), }, diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js index 2d74ab5f31..fc40cc8271 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.js @@ -8,6 +8,7 @@ function isNonExportStatement({ type }) { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('exports-last'), }, diff --git a/src/rules/extensions.js b/src/rules/extensions.js index d50bd0ce8b..b72c91bad0 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -55,6 +55,7 @@ function buildProperties(context) { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('extensions'), }, diff --git a/src/rules/first.js b/src/rules/first.js index 7af7f330b3..7bcd1fa22e 100644 --- a/src/rules/first.js +++ b/src/rules/first.js @@ -2,6 +2,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('first'), }, @@ -105,7 +106,7 @@ module.exports = { insertSourceCode = insertSourceCode.trim() + insertSourceCode.match(/^(\s+)/)[0] } - insertFixer = lastLegalImp ? + insertFixer = lastLegalImp ? fixer.insertTextAfter(lastLegalImp, insertSourceCode) : fixer.insertTextBefore(body[0], insertSourceCode) const fixers = [insertFixer].concat(removeFixers) diff --git a/src/rules/group-exports.js b/src/rules/group-exports.js index 96fff24fed..d650fff877 100644 --- a/src/rules/group-exports.js +++ b/src/rules/group-exports.js @@ -1,6 +1,7 @@ import docsUrl from '../docsUrl' const meta = { + type: 'suggestion', docs: { url: docsUrl('group-exports'), }, diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js index 9af8f7912e..7e1fdb1011 100644 --- a/src/rules/max-dependencies.js +++ b/src/rules/max-dependencies.js @@ -16,6 +16,7 @@ const countDependencies = (dependencies, lastNode, context) => { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('max-dependencies'), }, diff --git a/src/rules/named.js b/src/rules/named.js index 8c2acd714e..b1f261f32b 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -4,6 +4,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('named'), }, @@ -30,6 +31,9 @@ module.exports = { node.specifiers.forEach(function (im) { if (im.type !== type) return + // ignore type imports + if (im.importKind === 'type') return + const deepLookup = imports.hasDeep(im[key].name) if (!deepLookup.found) { diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 93e5891594..598b530d02 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -5,6 +5,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('namespace'), }, @@ -44,7 +45,7 @@ module.exports = { return { // pick up all imports at body entry time, to properly respect hoisting - 'Program': function ({ body }) { + Program: function ({ body }) { function processBodyStatement(declaration) { if (declaration.type !== 'ImportDeclaration') return @@ -58,7 +59,7 @@ module.exports = { return } - for (let specifier of declaration.specifiers) { + for (const specifier of declaration.specifiers) { switch (specifier.type) { case 'ImportNamespaceSpecifier': if (!imports.size) { @@ -83,7 +84,7 @@ module.exports = { }, // same as above, but does not add names to local map - 'ExportNamespaceSpecifier': function (namespace) { + ExportNamespaceSpecifier: function (namespace) { var declaration = importDeclaration(context) var imports = Exports.get(declaration.source.value, context) @@ -102,7 +103,7 @@ module.exports = { // todo: check for possible redefinition - 'MemberExpression': function (dereference) { + MemberExpression: function (dereference) { if (dereference.object.type !== 'Identifier') return if (!namespaces.has(dereference.object.name)) return @@ -146,7 +147,7 @@ module.exports = { }, - 'VariableDeclarator': function ({ id, init }) { + VariableDeclarator: function ({ id, init }) { if (init == null) return if (init.type !== 'Identifier') return if (!namespaces.has(init.name)) return @@ -160,8 +161,12 @@ module.exports = { if (pattern.type !== 'ObjectPattern') return - for (let property of pattern.properties) { - if (property.type === 'ExperimentalRestProperty' || !property.key) { + for (const property of pattern.properties) { + if ( + property.type === 'ExperimentalRestProperty' + || property.type === 'RestElement' + || !property.key + ) { continue } @@ -189,6 +194,17 @@ module.exports = { testKey(id, namespaces.get(init.name)) }, + + JSXMemberExpression: function({object, property}) { + if (!namespaces.has(object.name)) return + var namespace = namespaces.get(object.name) + if (!namespace.has(property.name)) { + context.report({ + node: property, + message: makeMessage(property, [object.name]), + }) + } + }, } }, } diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index fda1bc7634..f5724ef4a3 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -45,6 +45,7 @@ function isClassWithDecorator(node) { module.exports = { meta: { + type: 'layout', docs: { url: docsUrl('newline-after-import'), }, diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index b66b8b203f..4b7a8fcc2a 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -4,6 +4,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-absolute-path'), }, diff --git a/src/rules/no-amd.js b/src/rules/no-amd.js index 3ccb2129de..bb7c8ed826 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.js @@ -10,34 +10,33 @@ import docsUrl from '../docsUrl' //------------------------------------------------------------------------------ module.exports = { - meta: { - docs: { - url: docsUrl('no-amd'), - }, + meta: { + type: 'suggestion', + docs: { + url: docsUrl('no-amd'), }, + }, - create: function (context) { + create: function (context) { + return { + '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-anonymous-default-export.js b/src/rules/no-anonymous-default-export.js index 34128a914a..1557404507 100644 --- a/src/rules/no-anonymous-default-export.js +++ b/src/rules/no-anonymous-default-export.js @@ -72,6 +72,7 @@ const defaults = Object.keys(defs) module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-anonymous-default-export'), }, diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index 22939aa7bf..b6f11a7f0b 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -41,6 +41,7 @@ const schemaObject = { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-commonjs'), }, diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index bbc251e388..f769b862cc 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -10,6 +10,7 @@ import docsUrl from '../docsUrl' // todo: cache cycles / deep relationships for faster repeat evaluation module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-cycle') }, schema: [makeOptionsSchema({ maxDepth:{ @@ -30,6 +31,14 @@ module.exports = { function checkSourceValue(sourceNode, importer) { const imported = Exports.get(sourceNode.value, context) + if (sourceNode.parent && sourceNode.parent.importKind === 'type') { + return // no Flow import resolution + } + + if (sourceNode._babelType === 'Literal') { + return // no Flow import resolution, workaround for ESLint < 5.x + } + if (imported == null) { return // no-unresolved territory } diff --git a/src/rules/no-default-export.js b/src/rules/no-default-export.js index 8d240ed6a1..e1c687c9f7 100644 --- a/src/rules/no-default-export.js +++ b/src/rules/no-default-export.js @@ -1,5 +1,6 @@ module.exports = { meta: { + type: 'suggestion', docs: {}, }, diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index ef96f41633..7a3130b20c 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -17,6 +17,7 @@ function getDeprecation(metadata) { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-deprecated'), }, diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 72b305e677..4632ea0ec9 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -13,6 +13,7 @@ function checkImports(imported, context) { module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('no-duplicates'), }, diff --git a/src/rules/no-dynamic-require.js b/src/rules/no-dynamic-require.js index 5726d72ca3..b9ccad27b3 100644 --- a/src/rules/no-dynamic-require.js +++ b/src/rules/no-dynamic-require.js @@ -15,6 +15,7 @@ function isStaticValue(arg) { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-dynamic-require'), }, diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index 9d51018e9a..d2c7cac6ee 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -42,9 +42,12 @@ function getDependencies(context, packageDir) { if (!isEmpty(paths)) { // use rule config to find package.json paths.forEach(dir => { - Object.assign(packageContent, extractDepFields( + const _packageContent = extractDepFields( JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')) - )) + ) + Object.keys(packageContent).forEach(depsKey => + Object.assign(packageContent[depsKey], _packageContent[depsKey]) + ) }) } else { // use closest package.json @@ -155,6 +158,7 @@ function testConfig(config, filename) { module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('no-extraneous-dependencies'), }, @@ -176,11 +180,7 @@ module.exports = { create: function (context) { const options = context.options[0] || {} const filename = context.getFilename() - const deps = getDependencies(context, options.packageDir) - - if (!deps) { - return {} - } + const deps = getDependencies(context, options.packageDir) || extractDepFields({}) const depsOptions = { allowDevDeps: testConfig(options.devDependencies, filename) !== false, diff --git a/src/rules/no-internal-modules.js b/src/rules/no-internal-modules.js index 3e28554faa..9987dfd5c5 100644 --- a/src/rules/no-internal-modules.js +++ b/src/rules/no-internal-modules.js @@ -7,6 +7,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-internal-modules'), }, diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index 6bd6941a79..0908162bd1 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -2,6 +2,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-mutable-exports'), }, diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index 17af25a6fe..b7c3c75827 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -14,6 +14,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-named-as-default-member'), }, diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index eb9769513f..ad6a8ee6d1 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -4,6 +4,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('no-named-as-default'), }, diff --git a/src/rules/no-named-default.js b/src/rules/no-named-default.js index e25cd49509..86f24ef6d1 100644 --- a/src/rules/no-named-default.js +++ b/src/rules/no-named-default.js @@ -2,6 +2,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-named-default'), }, diff --git a/src/rules/no-named-export.js b/src/rules/no-named-export.js new file mode 100644 index 0000000000..2fa6392014 --- /dev/null +++ b/src/rules/no-named-export.js @@ -0,0 +1,34 @@ +import docsUrl from '../docsUrl' + +module.exports = { + meta: { + type: 'suggestion', + docs: { url: docsUrl('no-named-export') }, + }, + + create(context) { + // ignore non-modules + if (context.parserOptions.sourceType !== 'module') { + return {} + } + + const message = 'Named exports are not allowed.' + + return { + ExportAllDeclaration(node) { + context.report({node, message}) + }, + + ExportNamedDeclaration(node) { + if (node.specifiers.length === 0) { + return context.report({node, message}) + } + + const someNamed = node.specifiers.some(specifier => specifier.exported.name !== 'default') + if (someNamed) { + context.report({node, message}) + } + }, + } + }, +} diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 76a11f92dc..3dbedca500 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -12,6 +12,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-namespace'), }, diff --git a/src/rules/no-nodejs-modules.js b/src/rules/no-nodejs-modules.js index e73ed379d0..125bb5f3f1 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.js @@ -10,6 +10,7 @@ function reportIfMissing(context, node, allowed, name) { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-nodejs-modules'), }, diff --git a/src/rules/no-relative-parent-imports.js b/src/rules/no-relative-parent-imports.js index 3153eeb784..544525755e 100644 --- a/src/rules/no-relative-parent-imports.js +++ b/src/rules/no-relative-parent-imports.js @@ -1,11 +1,13 @@ import moduleVisitor, { makeOptionsSchema } from 'eslint-module-utils/moduleVisitor' import docsUrl from '../docsUrl' -import { basename } from 'path' +import { basename, dirname, relative } from 'path' +import resolve from 'eslint-module-utils/resolve' import importType from '../core/importType' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-relative-parent-imports'), }, @@ -14,11 +16,24 @@ module.exports = { create: function noRelativePackages(context) { const myPath = context.getFilename() - if (myPath === '') return {} // can't cycle-check a non-file + if (myPath === '') return {} // can't check a non-file function checkSourceValue(sourceNode) { const depPath = sourceNode.value - if (importType(depPath, context) === 'parent') { + + if (importType(depPath, context) === 'external') { // ignore packages + return + } + + const absDepPath = resolve(depPath, context) + + if (!absDepPath) { // unable to resolve path + return + } + + const relDepPath = relative(dirname(myPath), absDepPath) + + if (importType(relDepPath, context) === 'parent') { context.report({ node: sourceNode, message: 'Relative imports from parent directories are not allowed. ' + diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 5b20c40d84..0d906f6318 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -7,6 +7,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('no-restricted-paths'), }, diff --git a/src/rules/no-self-import.js b/src/rules/no-self-import.js index 8a8620c9ae..b869d46e06 100644 --- a/src/rules/no-self-import.js +++ b/src/rules/no-self-import.js @@ -21,6 +21,7 @@ function isImportingSelf(context, node, requireName) { module.exports = { meta: { + type: 'problem', docs: { description: 'Forbid a module from importing itself', recommended: true, diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js index ad081bd1be..5ea637e67b 100644 --- a/src/rules/no-unassigned-import.js +++ b/src/rules/no-unassigned-import.js @@ -54,6 +54,7 @@ function create(context) { module.exports = { create, meta: { + type: 'suggestion', docs: { url: docsUrl('no-unassigned-import'), }, diff --git a/src/rules/no-unresolved.js b/src/rules/no-unresolved.js index 2a5232a1cd..8436e4c92b 100644 --- a/src/rules/no-unresolved.js +++ b/src/rules/no-unresolved.js @@ -10,6 +10,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('no-unresolved'), }, diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.js index b9c4eedda0..2ad207fada 100644 --- a/src/rules/no-useless-path-segments.js +++ b/src/rules/no-useless-path-segments.js @@ -35,10 +35,21 @@ const countRelParent = x => sumBy(x, v => v === '..') module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('no-useless-path-segments'), }, + schema: [ + { + type: 'object', + properties: { + commonjs: { type: 'boolean' }, + }, + additionalProperties: false, + }, + ], + fixable: 'code', }, diff --git a/src/rules/no-webpack-loader-syntax.js b/src/rules/no-webpack-loader-syntax.js index e89fc9c35c..723f472692 100644 --- a/src/rules/no-webpack-loader-syntax.js +++ b/src/rules/no-webpack-loader-syntax.js @@ -11,6 +11,7 @@ function reportIfNonStandard(context, node, name) { module.exports = { meta: { + type: 'problem', docs: { url: docsUrl('no-webpack-loader-syntax'), }, diff --git a/src/rules/order.js b/src/rules/order.js index 81babd7fde..5c68f1b310 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -97,8 +97,8 @@ function findRootNode(node) { function findEndOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node)) let endOfTokens = tokensToEndOfLine.length > 0 - ? tokensToEndOfLine[tokensToEndOfLine.length - 1].end - : node.end + ? tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1] + : node.range[1] let result = endOfTokens for (let i = endOfTokens; i < sourceCode.text.length; i++) { if (sourceCode.text[i] === '\n') { @@ -121,7 +121,7 @@ function commentOnSameLineAs(node) { function findStartOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node)) - let startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].start : node.start + let startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0] let result = startOfTokens for (let i = startOfTokens - 1; i > 0; i--) { if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t') { @@ -296,11 +296,11 @@ function fixNewLineAfterImport(context, previousImport) { const tokensToEndOfLine = takeTokensAfterWhile( context.getSourceCode(), prevRoot, commentOnSameLineAs(prevRoot)) - let endOfLine = prevRoot.end + let endOfLine = prevRoot.range[1] if (tokensToEndOfLine.length > 0) { - endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].end + endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1] } - return (fixer) => fixer.insertTextAfterRange([prevRoot.start, endOfLine], '\n') + return (fixer) => fixer.insertTextAfterRange([prevRoot.range[0], endOfLine], '\n') } function removeNewLineAfterImport(context, currentImport, previousImport) { @@ -362,6 +362,7 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) { module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('order'), }, diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index f9cec8bf0b..0e31346f3b 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -4,6 +4,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('prefer-default-export'), }, diff --git a/src/rules/unambiguous.js b/src/rules/unambiguous.js index f89ebad9cf..7ec38c2cb2 100644 --- a/src/rules/unambiguous.js +++ b/src/rules/unambiguous.js @@ -8,6 +8,7 @@ import docsUrl from '../docsUrl' module.exports = { meta: { + type: 'suggestion', docs: { url: docsUrl('unambiguous'), }, diff --git a/tests/dep-time-travel.sh b/tests/dep-time-travel.sh new file mode 100755 index 0000000000..eae24998df --- /dev/null +++ b/tests/dep-time-travel.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# expected: ESLINT_VERSION numeric env var + +npm install --no-save eslint@$ESLINT_VERSION --ignore-scripts || true + +# use these alternate typescript dependencies for ESLint < v4 +if [[ "$ESLINT_VERSION" -lt "4" ]]; then + echo "Downgrading babel-eslint..." + npm i --no-save babel-eslint@8.0.3 + + echo "Downgrading TypeScript dependencies..." + npm i --no-save typescript-eslint-parser@15 typescript@2.8.1 +fi + +# typescript-eslint-parser 1.1.1+ is not compatible with node 6 +if [[ "$TRAVIS_NODE_VERSION" -lt "8" ]]; then + echo "Downgrading eslint-import-resolver-typescript..." + npm i --no-save eslint-import-resolver-typescript@1.0.2 +fi diff --git a/tests/files/deprecated.js b/tests/files/deprecated.js index 10e81dc912..f5229f59b8 100644 --- a/tests/files/deprecated.js +++ b/tests/files/deprecated.js @@ -27,18 +27,18 @@ export const MY_TERRIBLE_ACTION = "ugh" * @deprecated this chain is awful * @type {String} */ -export const CHAIN_A = "a" +export const CHAIN_A = "a", /** * @deprecated so awful * @type {String} */ - , CHAIN_B = "b" + CHAIN_B = "b", /** * @deprecated still terrible * @type {String} */ - , CHAIN_C = "C" + CHAIN_C = "C" /** * this one is fine diff --git a/tests/files/empty/package.json b/tests/files/empty/package.json new file mode 100644 index 0000000000..da86787ad3 --- /dev/null +++ b/tests/files/empty/package.json @@ -0,0 +1,4 @@ +{ + "name": "foo", + "version": "1.0.0" +} diff --git a/tests/files/flowtypes.js b/tests/files/flowtypes.js index 7ada3482b1..2df2471475 100644 --- a/tests/files/flowtypes.js +++ b/tests/files/flowtypes.js @@ -10,3 +10,6 @@ export type MyType = { export interface MyInterface {} export class MyClass {} + +export opaque type MyOpaqueType: string = string; + diff --git a/tests/files/foo-bar-resolver-invalid.js b/tests/files/foo-bar-resolver-invalid.js new file mode 100644 index 0000000000..a6213d6678 --- /dev/null +++ b/tests/files/foo-bar-resolver-invalid.js @@ -0,0 +1 @@ +exports = {}; diff --git a/tests/files/monorepo/package.json b/tests/files/monorepo/package.json index 3ed889ddf5..cf0b87ffa6 100644 --- a/tests/files/monorepo/package.json +++ b/tests/files/monorepo/package.json @@ -1,5 +1,8 @@ { "private": true, + "dependencies": { + "right-pad": "^1.0.1" + }, "devDependencies": { "left-pad": "^1.2.0" } diff --git a/tests/files/named-exports.js b/tests/files/named-exports.js index 752092e0ad..f2881c10c5 100644 --- a/tests/files/named-exports.js +++ b/tests/files/named-exports.js @@ -14,6 +14,8 @@ export class ExportedClass { // destructuring exports export var { destructuredProp } = {} + , { destructingAssign = null } = {} + , { destructingAssign: destructingRenamedAssign = null } = {} , [ arrayKeyProp ] = [] , [ { deepProp } ] = [] , { arr: [ ,, deepSparseElement ] } = {} diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 3423fe3e11..8e01f62acf 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -273,7 +273,7 @@ describe('ExportMap', function () { context('#size', function () { it('counts the names', () => expect(ExportMap.get('./named-exports', fakeContext)) - .to.have.property('size', 8)) + .to.have.property('size', 10)) it('includes exported namespace size', () => expect(ExportMap.get('./export-all', fakeContext)) .to.have.property('size', 1)) @@ -318,6 +318,7 @@ describe('ExportMap', function () { ] configs.forEach(([description, parserConfig]) => { + describe(description, function () { const context = Object.assign({}, fakeContext, { settings: { diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 9cc153ae3c..4b0f12c626 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -38,6 +38,8 @@ describe('parse(content, { settings, ecmaFeatures })', function () { .that.is.eql(parserOptions.ecmaFeatures) .and.is.not.equal(parserOptions.ecmaFeatures) expect(parseSpy.args[0][1], 'custom parser to get parserOptions.attachComment equal to true').to.have.property('attachComment', true) + expect(parseSpy.args[0][1], 'custom parser to get parserOptions.tokens equal to true').to.have.property('tokens', true) + expect(parseSpy.args[0][1], 'custom parser to get parserOptions.range equal to true').to.have.property('range', true) expect(parseSpy.args[0][1], 'custom parser to get parserOptions.filePath equal to the full path of the source file').to.have.property('filePath', path) }) diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 3c15303edf..b9a9063243 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -110,6 +110,22 @@ describe('resolve', function () { expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 }) }) + it('reports loaded resolver with invalid interface', function () { + const resolverName = './foo-bar-resolver-invalid'; + const testContext = utils.testContext({ 'import/resolver': resolverName }); + const testContextReports = [] + testContext.report = function (reportInfo) { + testContextReports.push(reportInfo) + } + testContextReports.length = 0 + expect(resolve( '../files/foo' + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + )).to.equal(undefined) + expect(testContextReports[0]).to.be.an('object') + expect(testContextReports[0].message).to.equal(`Resolve error: ${resolverName} with invalid interface loaded as resolver`) + expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 }) + }) + it('respects import/resolve extensions', function () { const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) @@ -119,7 +135,6 @@ describe('resolve', function () { }) it('reports load exception in a user resolver', function () { - const testContext = utils.testContext({ 'import/resolver': './load-error-resolver' }) const testContextReports = [] testContext.report = function (reportInfo) { diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index 329401106a..6b0d448da9 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -18,8 +18,10 @@ const parser = 'babel-eslint' const noLeadingCommentError = 'dynamic imports require a leading comment with the webpack chunkname' const nonBlockCommentError = 'dynamic imports require a /* foo */ style comment, not a // foo comment' -const commentFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: "${commentFormat}" */` -const pickyCommentFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: "${pickyCommentFormat}" */` +const noPaddingCommentError = 'dynamic imports require a block comment padded with spaces - /* foo */' +const invalidSyntaxCommentError = 'dynamic imports require a "webpack" comment with valid syntax' +const commentFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: "${commentFormat}",? */` +const pickyCommentFormatError = `dynamic imports require a leading comment in the form /* webpackChunkName: "${pickyCommentFormat}",? */` ruleTester.run('dynamic-import-chunkname', rule, { valid: [ @@ -79,6 +81,56 @@ ruleTester.run('dynamic-import-chunkname', rule, { options, parser, }, + { + code: `import( + /* webpackChunkName: "someModule", webpackPrefetch: true */ + 'test' + )`, + options, + parser, + }, + { + code: `import( + /* webpackChunkName: "someModule", webpackPrefetch: true, */ + 'test' + )`, + options, + parser, + }, + { + code: `import( + /* webpackPrefetch: true, webpackChunkName: "someModule" */ + 'test' + )`, + options, + parser, + }, + { + code: `import( + /* webpackPrefetch: true, webpackChunkName: "someModule", */ + 'test' + )`, + options, + parser, + }, + { + code: `import( + /* webpackPrefetch: true */ + /* webpackChunkName: "someModule" */ + 'test' + )`, + options, + parser, + }, + { + code: `import( + /* webpackChunkName: "someModule" */ + /* webpackPrefetch: true */ + 'test' + )`, + options, + parser, + }, { code: `import( /* webpackChunkName: "someModule" */ @@ -124,7 +176,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { options, parser, errors: [{ - message: commentFormatError, + message: invalidSyntaxCommentError, type: 'CallExpression', }], }, @@ -148,7 +200,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { options, parser, errors: [{ - message: commentFormatError, + message: invalidSyntaxCommentError, type: 'CallExpression', }], }, @@ -164,6 +216,79 @@ ruleTester.run('dynamic-import-chunkname', rule, { type: 'CallExpression', }], }, + { + code: `import( + /*webpackChunkName: "someModule"*/ + 'someModule' + )`, + options, + parser, + errors: [{ + message: noPaddingCommentError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName : "someModule" */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackChunkName: "someModule" ; */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: invalidSyntaxCommentError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* totally not webpackChunkName: "someModule" */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: invalidSyntaxCommentError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackPrefetch: true */ + /* webpackChunk: "someModule" */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, + { + code: `import( + /* webpackPrefetch: true, webpackChunk: "someModule" */ + 'someModule' + )`, + options, + parser, + errors: [{ + message: commentFormatError, + type: 'CallExpression', + }], + }, { code: `import( /* webpackChunkName: "someModule123" */ @@ -183,7 +308,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options: multipleImportFunctionOptions, errors: [{ - message: commentFormatError, + message: invalidSyntaxCommentError, type: 'CallExpression', }], }, @@ -194,7 +319,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options: multipleImportFunctionOptions, errors: [{ - message: commentFormatError, + message: invalidSyntaxCommentError, type: 'CallExpression', }], }, @@ -224,7 +349,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options, errors: [{ - message: commentFormatError, + message: invalidSyntaxCommentError, type: 'CallExpression', }], }, @@ -246,7 +371,7 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options, errors: [{ - message: commentFormatError, + message: invalidSyntaxCommentError, type: 'CallExpression', }], }, diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 4fdd3434f9..92b3d9163e 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -22,6 +22,8 @@ ruleTester.run('named', rule, { test({code: 'import bar, { foo } from "./bar.js"'}), test({code: 'import {a, b, d} from "./named-exports"'}), test({code: 'import {ExportedClass} from "./named-exports"'}), + test({code: 'import { destructingAssign } from "./named-exports"'}), + test({code: 'import { destructingRenamedAssign } from "./named-exports"'}), test({code: 'import { ActionTypes } from "./qc"'}), test({code: 'import {a, b, c, d} from "./re-export"'}), @@ -72,69 +74,13 @@ ruleTester.run('named', rule, { code: 'import type { MissingType } from "./flowtypes"', parser: 'babel-eslint', }), - - // TypeScript - test({ - code: 'import { MyType } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { Foo } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { Bar } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { getFoo } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), - test({ - code: 'import { MyEnum } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - }), test({ - code: ` - import { MyModule } from "./typescript" - MyModule.ModuleFunction() - `, - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, + code: 'import type { MyOpaqueType } from "./flowtypes"', + parser: 'babel-eslint', }), test({ - code: ` - import { MyNamespace } from "./typescript" - MyNamespace.NSModule.NSModuleFunction() - `, - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, + code: 'import { type MyOpaqueType, MyClass } from "./flowtypes"', + parser: 'babel-eslint', }), // jsnext @@ -236,30 +182,10 @@ ruleTester.run('named', rule, { // }], // }), - // TypeScript test({ - code: 'import { MissingType } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - errors: [{ - message: "MissingType not found in './typescript'", - type: 'Identifier', - }], - }), - test({ - code: 'import { NotExported } from "./typescript"', - parser: 'typescript-eslint-parser', - settings: { - 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, - 'import/resolver': { 'eslint-import-resolver-typescript': true }, - }, - errors: [{ - message: "NotExported not found in './typescript'", - type: 'Identifier', - }], + code: 'import { type MyOpaqueType, MyMissingClass } from "./flowtypes"', + parser: 'babel-eslint', + errors: ["MyMissingClass not found in './flowtypes'"], }), // jsnext @@ -326,3 +252,101 @@ ruleTester.run('named (export *)', rule, { }), ], }) + + +context("Typescript", function () { + // Typescript + ruleTester.run("named", rule, { + valid: [ + test({ + code: 'import { MyType } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { Foo } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { Bar } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { getFoo } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: 'import { MyEnum } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: ` + import { MyModule } from "./typescript" + MyModule.ModuleFunction() + `, + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + test({ + code: ` + import { MyNamespace } from "./typescript" + MyNamespace.NSModule.NSModuleFunction() + `, + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + }), + ], + + invalid: [ + test({ + code: 'import { MissingType } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + errors: [{ + message: "MissingType not found in './typescript'", + type: 'Identifier', + }], + }), + test({ + code: 'import { NotExported } from "./typescript"', + parser: 'typescript-eslint-parser', + settings: { + 'import/parsers': { 'typescript-eslint-parser': ['.ts'] }, + 'import/resolver': { 'eslint-import-resolver-typescript': true }, + }, + errors: [{ + message: "NotExported not found in './typescript'", + type: 'Identifier', + }], + }), + ] + }) +}) diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 1cfee2b54d..7fa8cfcdb9 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -104,6 +104,16 @@ const valid = [ parser: 'babel-eslint', }), + // JSX + test({ + code: 'import * as Names from "./named-exports"; const Foo = ', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }), + ...SYNTAX_CASES, ] @@ -185,6 +195,17 @@ const invalid = [ errors: [`'default' not found in imported namespace 'ree'.`], }), + // JSX + test({ + code: 'import * as Names from "./named-exports"; const Foo = ', + errors: [error('e', 'Names')], + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }), + ] /////////////////////// diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index 00ebfa432b..730cef3636 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -153,8 +153,9 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { }, { code: `//issue 592 + export default @SomeDecorator(require('./some-file')) - export default class App {} + class App {} `, parserOptions: { sourceType: 'module' }, parser: 'babel-eslint', diff --git a/tests/src/rules/no-cycle.js b/tests/src/rules/no-cycle.js index ae45ba36ec..4ee4daacb6 100644 --- a/tests/src/rules/no-cycle.js +++ b/tests/src/rules/no-cycle.js @@ -36,6 +36,10 @@ ruleTester.run('no-cycle', rule, { code: 'import { foo } from "./depth-two"', options: [{ maxDepth: 1 }], }), + test({ + code: 'import type { FooType } from "./depth-one"', + parser: 'babel-eslint', + }), ], invalid: [ test({ diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index 381b392cbf..c29c8e5bd0 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -17,6 +17,7 @@ const packageFileWithSyntaxErrorMessage = (() => { const packageDirWithFlowTyped = path.join(__dirname, '../../files/with-flow-typed') const packageDirMonoRepoRoot = path.join(__dirname, '../../files/monorepo') const packageDirMonoRepoWithNested = path.join(__dirname, '../../files/monorepo/packages/nested-package') +const packageDirWithEmpty = path.join(__dirname, '../../files/empty') ruleTester.run('no-extraneous-dependencies', rule, { valid: [ @@ -89,6 +90,22 @@ ruleTester.run('no-extraneous-dependencies', rule, { code: 'import leftpad from "left-pad";', options: [{packageDir: packageDirMonoRepoRoot}], }), + test({ + code: 'import react from "react";', + options: [{packageDir: [packageDirMonoRepoRoot, packageDirMonoRepoWithNested]}], + }), + test({ + code: 'import leftpad from "left-pad";', + options: [{packageDir: [packageDirMonoRepoRoot, packageDirMonoRepoWithNested]}], + }), + test({ + code: 'import leftpad from "left-pad";', + options: [{packageDir: [packageDirMonoRepoWithNested, packageDirMonoRepoRoot]}], + }), + test({ + code: 'import rightpad from "right-pad";', + options: [{packageDir: [packageDirMonoRepoRoot, packageDirMonoRepoWithNested]}], + }), ], invalid: [ test({ @@ -263,5 +280,14 @@ ruleTester.run('no-extraneous-dependencies', rule, { message: "'react' should be listed in the project's dependencies. Run 'npm i -S react' to add it", }], }), + test({ + code: 'import "react";', + filename: path.join(packageDirWithEmpty, 'index.js'), + options: [{packageDir: packageDirWithEmpty}], + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: "'react' should be listed in the project's dependencies. Run 'npm i -S react' to add it", + }], + }), ] }) diff --git a/tests/src/rules/no-named-export.js b/tests/src/rules/no-named-export.js new file mode 100644 index 0000000000..d8748d3f6b --- /dev/null +++ b/tests/src/rules/no-named-export.js @@ -0,0 +1,179 @@ +import { RuleTester } from 'eslint' +import { test } from '../utils' + +const ruleTester = new RuleTester() + , rule = require('rules/no-named-export') + +ruleTester.run('no-named-export', rule, { + valid: [ + test({ + code: 'export default function bar() {};', + }), + test({ + code: 'export { foo as default }', + }), + test({ + code: 'export default from "foo.js"', + parser: 'babel-eslint', + }), + + // no exports at all + test({ + code: `import * as foo from './foo';`, + }), + test({ + code: `import foo from './foo';`, + }), + test({ + code: `import {default as foo} from './foo';`, + }), + ], + invalid: [ + test({ + code: ` + export const foo = 'foo'; + export const bar = 'bar'; + `, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }, { + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: ` + export const foo = 'foo'; + export default bar;`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: ` + export const foo = 'foo'; + export function bar() {}; + `, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }, { + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export const foo = 'foo';`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: ` + const foo = 'foo'; + export { foo }; + `, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export { foo, bar }`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export const { foo, bar } = item;`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export const { foo, bar: baz } = item;`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export const { foo: { bar, baz } } = item;`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: ` + export const foo = item; + export { item }; + `, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }, { + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export * from './foo';`, + errors: [{ + ruleId: 'ExportAllDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export const { foo } = { foo: "bar" };`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export const { foo: { bar } } = { foo: { bar: "baz" } };`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: 'export { a, b } from "foo.js"', + parser: 'babel-eslint', + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export type UserId = number;`, + parser: 'babel-eslint', + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: 'export foo from "foo.js"', + parser: 'babel-eslint', + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + test({ + code: `export Memory, { MemoryValue } from './Memory'`, + parser: 'babel-eslint', + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Named exports are not allowed.', + }], + }), + ], +}) diff --git a/tests/src/rules/no-relative-parent-imports.js b/tests/src/rules/no-relative-parent-imports.js index 6d7a2c2fae..8978230904 100644 --- a/tests/src/rules/no-relative-parent-imports.js +++ b/tests/src/rules/no-relative-parent-imports.js @@ -38,9 +38,18 @@ ruleTester.run('no-relative-parent-imports', rule, { test({ code: 'import("./app/index.js")', }), + test({ + code: 'import(".")', + }), + test({ + code: 'import("path")', + }), test({ code: 'import("package")', }), + test({ + code: 'import("@scope/package")', + }), ], invalid: [ @@ -69,5 +78,21 @@ ruleTester.run('no-relative-parent-imports', rule, { column: 8, } ], }), + test({ + code: 'import foo from "./../plugin.js"', + errors: [ { + message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `./../plugin.js` or consider making `./../plugin.js` a package.', + line: 1, + column: 17 + }] + }), + test({ + code: 'import foo from "../../api/service"', + errors: [ { + message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../../api/service` or consider making `../../api/service` a package.', + line: 1, + column: 17 + }] + }) ], }) diff --git a/tests/src/rules/no-useless-path-segments.js b/tests/src/rules/no-useless-path-segments.js index 1f4229f5ee..ed20440012 100644 --- a/tests/src/rules/no-useless-path-segments.js +++ b/tests/src/rules/no-useless-path-segments.js @@ -7,6 +7,10 @@ const rule = require('rules/no-useless-path-segments') function runResolverTests(resolver) { ruleTester.run(`no-useless-path-segments (${resolver})`, rule, { valid: [ + // commonjs with default options + test({ code: 'require("./../files/malformed.js")' }), + + // esmodule test({ code: 'import "./malformed.js"' }), test({ code: 'import "./test-module"' }), test({ code: 'import "./bar/"' }), @@ -16,6 +20,49 @@ function runResolverTests(resolver) { ], invalid: [ + // commonjs + test({ + code: 'require("./../files/malformed.js")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], + }), + test({ + code: 'require("./../files/malformed")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "./../files/malformed", should be "../files/malformed"'], + }), + test({ + code: 'require("../files/malformed.js")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "../files/malformed.js", should be "./malformed.js"'], + }), + test({ + code: 'require("../files/malformed")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "../files/malformed", should be "./malformed"'], + }), + test({ + code: 'require("./test-module/")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "./test-module/", should be "./test-module"'], + }), + test({ + code: 'require("./")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "./", should be "."'], + }), + test({ + code: 'require("../")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "../", should be ".."'], + }), + test({ + code: 'require("./deep//a")', + options: [{ commonjs: true }], + errors: [ 'Useless path segments for "./deep//a", should be "./deep/a"'], + }), + + // esmodule test({ code: 'import "./../files/malformed.js"', errors: [ 'Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index fb3b788448..23487dac91 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -1260,5 +1260,21 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), + // fix incorrect order with typescript-eslint-parser + test({ + code: ` + var async = require('async'); + var fs = require('fs'); + `, + output: ` + var fs = require('fs'); + var async = require('async'); + `, + parser: 'typescript-eslint-parser', + errors: [{ + ruleId: 'order', + message: '`fs` import should occur before import of `async`', + }], + }), ], }) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 018fd30669..cb86dc2259 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +## v2.3.0 - 2019-01-22 +### Fixed +- use `process.hrtime()` for cache dates ([#1160], thanks [@hulkish]) + ## v2.2.0 - 2018-03-29 ### Changed - `parse`: attach node locations by default. @@ -30,3 +34,9 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode + + + +[#1160]: https://github.com/benmosher/eslint-plugin-import/pull/1160 + +[@hulkish]: https://github.com/hulkish diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js index 19e6a21226..eba86d2585 100644 --- a/utils/ModuleCache.js +++ b/utils/ModuleCache.js @@ -14,7 +14,7 @@ class ModuleCache { * @param {[type]} result [description] */ set(cacheKey, result) { - this.map.set(cacheKey, { result, lastSeen: Date.now() }) + this.map.set(cacheKey, { result, lastSeen: process.hrtime() }) log('setting entry for', cacheKey) return result } @@ -23,7 +23,7 @@ class ModuleCache { if (this.map.has(cacheKey)) { const f = this.map.get(cacheKey) // check fresness - if (Date.now() - f.lastSeen < (settings.lifetime * 1000)) return f.result + if (process.hrtime(f.lastSeen)[0] < settings.lifetime) return f.result } else log('cache miss for', cacheKey) // cache miss return undefined diff --git a/utils/ignore.js b/utils/ignore.js index 88e4080dda..91cc731a81 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -24,8 +24,11 @@ function makeValidExtensionSet(settings) { // 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)) + const parserSettings = settings['import/parsers'][parser] + if (!Array.isArray(parserSettings)) { + throw new TypeError('"settings" for ' + parser + ' must be an array') + } + parserSettings.forEach(ext => exts.add(ext)) } } diff --git a/utils/package.json b/utils/package.json index d955c53680..c380b6e2b1 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,6 +1,6 @@ { "name": "eslint-module-utils", - "version": "2.2.0", + "version": "2.3.0", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" @@ -26,6 +26,6 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", "dependencies": { "debug": "^2.6.8", - "pkg-dir": "^1.0.0" + "pkg-dir": "^2.0.0" } } diff --git a/utils/parse.js b/utils/parse.js index 5bafdba495..2946047ad5 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -19,12 +19,14 @@ exports.default = function parse(path, content, context) { parserOptions = Object.assign({}, parserOptions) parserOptions.ecmaFeatures = Object.assign({}, parserOptions.ecmaFeatures) - // always include and attach comments + // always include comments and tokens (for doc parsing) parserOptions.comment = true - parserOptions.attachComment = true + parserOptions.attachComment = true // keeping this for backward-compat with older parsers + parserOptions.tokens = true // attach node locations parserOptions.loc = true + parserOptions.range = true // provide the `filePath` like eslint itself does, in `parserOptions` // https://github.com/eslint/eslint/blob/3ec436ee/lib/linter.js#L637 diff --git a/utils/resolve.js b/utils/resolve.js index b280ca2cfa..87a1eaea81 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -160,8 +160,19 @@ function requireResolver(name, sourceFile) { if (!resolver) { throw new Error(`unable to load resolver "${name}".`) + } + if (!isResolverValid(resolver)) { + throw new Error(`${name} with invalid interface loaded as resolver`) + } + + return resolver +} + +function isResolverValid(resolver) { + if (resolver.interfaceVersion === 2) { + return resolver.resolve && typeof resolver.resolve === 'function' } else { - return resolver; + return resolver.resolveImport && typeof resolver.resolveImport === 'function' } }