diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..93daf655e5 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +lib +coverage +.nyc_output +node_modules +tests/files/malformed.js +tests/files/with-syntax-error +resolvers/webpack/test/files +# we want to ignore "tests/files" here, but unfortunately doing so would +# interfere with unit test and fail it for some reason. +# tests/files diff --git a/.eslintrc.yml b/.eslintrc.yml index b54ed522fb..5ee1be595b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -24,6 +24,7 @@ rules: - 2 - single - allowTemplateLiterals: true + avoidEscape: true # dog fooding import/no-extraneous-dependencies: "error" diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..8fd7679907 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/eslint-plugin-import +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.gitignore b/.gitignore index 0bf60608a0..a01de720c3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,16 @@ coverage # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release +# Copied from ./LICENSE for the npm module releases +memo-parser/LICENSE +resolvers/node/LICENSE +resolvers/webpack/LICENSE +utils/LICENSE +memo-parser/.npmrc +resolvers/node/.npmrc +resolvers/webpack/.npmrc +utils/.npmrc + # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- diff --git a/.travis.yml b/.travis.yml index 606c355367..cd5650c65a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: node_js node_js: + - '13' - '12' - '10' - '8' @@ -18,6 +19,8 @@ env: # osx backlog is often deep, so to be polite we can just hit these highlights matrix: include: + - env: PACKAGE=resolvers/node + node_js: 13 - env: PACKAGE=resolvers/node node_js: 12 - env: PACKAGE=resolvers/node @@ -65,15 +68,16 @@ matrix: fast_finish: true allow_failures: - # issues with typescript deps in this version intersection + # issues with TypeScript deps in this version intersection - node_js: '4' env: ESLINT_VERSION=4 before_install: - 'nvm install-latest-npm' + - 'npm run copy-metafiles' - 'if [ -n "${PACKAGE-}" ]; then cd "${PACKAGE}"; fi' install: - - npm install + - 'npm install' - 'if [ -n "${ESLINT_VERSION}" ]; then ./tests/dep-time-travel.sh; fi' script: diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f76b79f9..e89efd1bee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,31 +1,84 @@ # Change Log + All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com). ## [Unreleased] +## [2.20.0] - 2020-01-10 +### Added +- [`order`]: added `caseInsensitive` as an additional option to `alphabetize` ([#1586], thanks [@dbrewer5]) +- [`no-restricted-paths`]: New `except` option per `zone`, allowing exceptions to be defined for a restricted zone ([#1238], thanks [@rsolomon]) + +### Fixed +- [`no-unused-modules`]: fix usage of `import/extensions` settings ([#1560], thanks [@stekycz]) +- [`import/extensions`]: ignore non-main modules ([#1563], thanks [@saschanaz]) +- TypeScript config: lookup for external modules in @types folder ([#1526], thanks [@joaovieira]) +- [`no-extraneous-dependencies`]: ensure `node.source` is truthy ([#1589], thanks [@ljharb]) +- [`extensions`]: Ignore query strings when checking for extensions ([#1572], thanks [@pcorpet]) + +### Docs +- [`extensions`]: improve `ignorePackages` docs ([#1248], thanks [@ivo-stefchev]) + +### Added +- [`order`]: add option pathGroupsExcludedImportTypes to allow ordering of external import types ([#1565], thanks [@Mairu]) + +## [2.19.1] - 2019-12-08 +### Fixed +- [`no-extraneous-dependencies`]: ensure `node.source` exists + +## [2.19.0] - 2019-12-08 +### Added +- [`internal-regex` setting]: regex pattern for marking packages "internal" ([#1491], thanks [@Librazy]) +- [`group-exports`]: make aggregate module exports valid ([#1472], thanks [@atikenny]) +- [`no-namespace`]: Make rule fixable ([#1401], thanks [@TrevorBurnham]) +- support `parseForESLint` from custom parser ([#1435], thanks [@JounQin]) +- [`no-extraneous-dependencies`]: Implement support for [bundledDependencies](https://npm.github.io/using-pkgs-docs/package-json/types/bundleddependencies.html) ([#1436], thanks [@schmidsi])) +- [`no-unused-modules`]: add flow type support ([#1542], thanks [@rfermann]) +- [`order`]: Adds support for pathGroups to allow ordering by defined patterns ([#795], [#1386], thanks [@Mairu]) +- [`no-duplicates`]: Add `considerQueryString` option : allow duplicate imports with different query strings ([#1107], thanks [@pcorpet]). +- [`order`]: Add support for alphabetical sorting of import paths within import groups ([#1360], [#1105], [#629], thanks [@duncanbeevers], [@stropho], [@luczsoma], [@randallreedjr]) +- [`no-commonjs`]: add `allowConditionalRequire` option ([#1439], thanks [@Pessimistress]) + +### Fixed +- [`default`]: make error message less confusing ([#1470], thanks [@golopot]) +- Improve performance of `ExportMap.for` by only loading paths when necessary. ([#1519], thanks [@brendo]) +- Support export of a merged TypeScript namespace declaration ([#1495], thanks [@benmunro]) +- [`order`]: fix autofix to not move imports across fn calls ([#1253], thanks [@tihonove]) +- [`prefer-default-export`]: fix false positive with type export ([#1506], thanks [@golopot]) +- [`extensions`]: Fix `ignorePackages` to produce errors ([#1521], thanks [@saschanaz]) +- [`no-unused-modules`]: fix crash due to `export *` ([#1496], thanks [@Taranys]) +- [`no-cycle`]: should not warn for Flow imports ([#1494], thanks [@maxmalov]) +- [`order`]: fix `@someModule` considered as `unknown` instead of `internal` ([#1493], thanks [@aamulumi]) +- [`no-extraneous-dependencies`]: Check `export from` ([#1049], thanks [@marcusdarmstrong]) + +### Docs +- [`no-useless-path-segments`]: add docs for option `commonjs` ([#1507], thanks [@golopot]) + +### Changed +- [`no-unused-modules`]/`eslint-module-utils`: Avoid superfluous calls and code ([#1551], thanks [@brettz9]) + ## [2.18.2] - 2019-07-19 - - Skip warning on type interfaces ([#1425], thanks [@lencioni]) +### Fixed +- Skip warning on type interfaces ([#1425], thanks [@lencioni]) ## [2.18.1] - 2019-07-18 - ### Fixed - - Improve parse perf when using `@typescript-eslint/parser` ([#1409], thanks [@bradzacher]) - - [`prefer-default-export`]: don't warn on TypeAlias & TSTypeAliasDeclaration ([#1377], thanks [@sharmilajesupaul]) - - [`no-unused-modules`]: Exclude package "main"/"bin"/"browser" entry points ([#1404], thanks [@rfermann]) - - [`export`]: false positive for typescript overloads ([#1412], thanks [@golopot]) +- Improve parse perf when using `@typescript-eslint/parser` ([#1409], thanks [@bradzacher]) +- [`prefer-default-export`]: don't warn on TypeAlias & TSTypeAliasDeclaration ([#1377], thanks [@sharmilajesupaul]) +- [`no-unused-modules`]: Exclude package "main"/"bin"/"browser" entry points ([#1404], thanks [@rfermann]) +- [`export`]: false positive for TypeScript overloads ([#1412], thanks [@golopot]) ### Refactors - - [`no-extraneous-dependencies`], `importType`: remove lodash ([#1419], thanks [@ljharb]) +- [`no-extraneous-dependencies`], `importType`: remove lodash ([#1419], thanks [@ljharb]) ## [2.18.0] - 2019-06-24 - ### Added - Support eslint v6 ([#1393], thanks [@sheepsteak]) - [`order`]: Adds support for correctly sorting unknown types into a single group ([#1375], thanks [@swernerx]) - [`order`]: add fixer for destructuring commonjs import ([#1372], thanks [@golopot]) -- typescript config: add TS def extensions + defer to TS over JS ([#1366], thanks [@benmosher]) +- TypeScript config: add TS def extensions + defer to TS over JS ([#1366], thanks [@benmosher]) ### Fixed - [`no-unused-modules`]: handle ClassDeclaration ([#1371], thanks [@golopot]) @@ -35,7 +88,6 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`no-named-as-default-member`]: update broken link ([#1389], thanks [@fooloomanzoo]) ## [2.17.3] - 2019-05-23 - ### Fixed - [`no-common-js`]: Also throw an error when assigning ([#1354], thanks [@charlessuh]) - [`no-unused-modules`]: don't crash when lint file outside src-folder ([#1347], thanks [@rfermann]) @@ -43,32 +95,28 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`no-unused-modules`]: make appveyor tests passing ([#1333], thanks [@rfermann]) - [`named`]: ignore Flow `typeof` imports and `type` exports ([#1345], thanks [@loganfsmyth]) - [refactor] fix eslint 6 compat by fixing imports (thank [@ljharb]) -- Improve support for Typescript declare structures ([#1356], thanks [@christophercurrie]) +- Improve support for TypeScript declare structures ([#1356], thanks [@christophercurrie]) ### Docs - add missing `no-unused-modules` in README ([#1358], thanks [@golopot]) - [`no-unused-modules`]: Indicates usage, plugin defaults to no-op, and add description to main README.md ([#1352], thanks [@johndevedu]) -[@christophercurrie]: https://github.com/christophercurrie - Document `env` option for `eslint-import-resolver-webpack` ([#1363], thanks [@kgregory]) ## [2.17.2] - 2019-04-16 - ### Fixed - [`no-unused-modules`]: avoid crash when using `ignoreExports`-option ([#1331], [#1323], thanks [@rfermann]) - [`no-unused-modules`]: make sure that rule with no options will not fail ([#1330], [#1334], thanks [@kiwka]) ## [2.17.1] - 2019-04-13 - ### Fixed - require v2.4 of `eslint-module-utils` ([#1322]) ## [2.17.0] - 2019-04-13 - ### Added - [`no-useless-path-segments`]: Add `noUselessIndex` option ([#1290], thanks [@timkraut]) - [`no-duplicates`]: Add autofix ([#1312], thanks [@lydell]) - Add [`no-unused-modules`] rule ([#1142], thanks [@rfermann]) -- support export type named exports from typescript ([#1304], thanks [@bradennapier] and [@schmod]) +- support export type named exports from TypeScript ([#1304], thanks [@bradennapier] and [@schmod]) ### Fixed - [`order`]: Fix interpreting some external modules being interpreted as internal modules ([#793], [#794] thanks [@ephys]) @@ -77,18 +125,17 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`namespace`]: add check for null ExportMap ([#1235], [#1144], thanks [@ljqx]) - [ExportMap] fix condition for checking if block comment ([#1234], [#1233], thanks [@ljqx]) - Fix overwriting of dynamic import() CallExpression ([`no-cycle`], [`no-relative-parent-import`], [`no-unresolved`], [`no-useless-path-segments`]) ([#1218], [#1166], [#1035], thanks [@vikr01]) -- [`export`]: false positives for typescript type + value export ([#1319], thanks [@bradzacher]) -- [`export`]: Support typescript namespaces ([#1320], [#1300], thanks [@bradzacher]) +- [`export`]: false positives for TypeScript type + value export ([#1319], thanks [@bradzacher]) +- [`export`]: Support TypeScript namespaces ([#1320], [#1300], thanks [@bradzacher]) ### Docs -- Update readme for Typescript ([#1256], [#1277], thanks [@kirill-konshin]) +- Update readme for TypeScript ([#1256], [#1277], thanks [@kirill-konshin]) - make rule names consistent ([#1112], thanks [@feychenie]) ### Tests - fix broken tests on master ([#1295], thanks [@jeffshaver] and [@ljharb]) - [`no-commonjs`]: add tests that show corner cases ([#1308], thanks [@TakeScoop]) - ## [2.16.0] - 2019-01-29 ### Added - `typescript` config ([#1257], thanks [@kirill-konshin]) @@ -105,13 +152,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`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]) @@ -120,7 +161,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - [`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]) +- [`order`]: fix autofixer when using typescript-eslint-parser ([#1137], thanks [@justinanastos]) - repeat fix from [#797] for [#717], in another place (thanks [@ljharb]) ### Refactors @@ -469,11 +510,10 @@ I'm seeing 62% improvement over my normal test codebase when executing only ## [1.1.0] - 2016-03-15 ### Added -- Added an [`ignore`](./docs/rules/no-unresolved.md#ignore) option to [`no-unresolved`] for those pesky files that no -resolver can find. (still prefer enhancing the Webpack and Node resolvers to -using it, though). See [#89] for details. +- Added an [`ignore`](./docs/rules/no-unresolved.md#ignore) option to [`no-unresolved`] for those pesky files that no resolver can find. (still prefer enhancing the Webpack and Node resolvers to using it, though). See [#89] for details. ## [1.0.4] - 2016-03-11 + ### Changed - respect hoisting for deep namespaces ([`namespace`]/[`no-deprecated`]) ([#211]) @@ -482,39 +522,41 @@ using it, though). See [#89] for details. - correct cache behavior in `eslint_d` for deep namespaces ([#200]) ## [1.0.3] - 2016-02-26 + ### Changed - no-deprecated follows deep namespaces ([#191]) ### Fixed -- [`namespace`] no longer flags modules with only a default export as having no -names. (ns.default is valid ES6) +- [`namespace`] no longer flags modules with only a default export as having no names. (ns.default is valid ES6) ## [1.0.2] - 2016-02-26 + ### Fixed - don't parse imports with no specifiers ([#192]) ## [1.0.1] - 2016-02-25 + ### Fixed - export `stage-0` shared config - documented [`no-deprecated`] - deep namespaces are traversed regardless of how they get imported ([#189]) ## [1.0.0] - 2016-02-24 + ### Added -- [`no-deprecated`]: WIP rule to let you know at lint time if you're using -deprecated functions, constants, classes, or modules. +- [`no-deprecated`]: WIP rule to let you know at lint time if you're using deprecated functions, constants, classes, or modules. ### Changed - [`namespace`]: support deep namespaces ([#119] via [#157]) ## [1.0.0-beta.0] - 2016-02-13 + ### Changed - support for (only) ESLint 2.x -- no longer needs/refers to `import/parser` or `import/parse-options`. Instead, -ESLint provides the configured parser + options to the rules, and they use that -to parse dependencies. +- no longer needs/refers to `import/parser` or `import/parse-options`. Instead, ESLint provides the configured parser + options to the rules, and they use that to parse dependencies. ### Removed + - `babylon` as default import parser (see Breaking) ## [0.13.0] - 2016-02-08 @@ -534,14 +576,11 @@ Unpublished from npm and re-released as 0.13.0. See [#170]. ## [0.12.0] - 2015-12-14 ### Changed -- Ignore [`import/ignore` setting] if exports are actually found in the parsed module. Does -this to support use of `jsnext:main` in `node_modules` without the pain of -managing an allow list or a nuanced deny list. +- Ignore [`import/ignore` setting] if exports are actually found in the parsed module. Does this to support use of `jsnext:main` in `node_modules` without the pain of managing an allow list or a nuanced deny list. ## [0.11.0] - 2015-11-27 ### Added -- Resolver plugins. Now the linter can read Webpack config, properly follow -aliases and ignore externals, dismisses inline loaders, etc. etc.! +- Resolver plugins. Now the linter can read Webpack config, properly follow aliases and ignore externals, dismisses inline loaders, etc. etc.! ## Earlier releases (0.10.1 and younger) See [GitHub release notes](https://github.com/benmosher/eslint-plugin-import/releases?after=v0.11.0) @@ -554,6 +593,7 @@ for info on changes for earlier releases. [`import/parsers` setting]: ./README.md#importparsers [`import/core-modules` setting]: ./README.md#importcore-modules [`import/external-module-folders` setting]: ./README.md#importexternal-module-folders +[`internal-regex` setting]: ./README.md#importinternal-regex [`default`]: ./docs/rules/default.md [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md @@ -598,19 +638,43 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#1589]: https://github.com/benmosher/eslint-plugin-import/issues/1589 +[#1586]: https://github.com/benmosher/eslint-plugin-import/pull/1586 +[#1572]: https://github.com/benmosher/eslint-plugin-import/pull/1572 +[#1563]: https://github.com/benmosher/eslint-plugin-import/pull/1563 +[#1560]: https://github.com/benmosher/eslint-plugin-import/pull/1560 +[#1551]: https://github.com/benmosher/eslint-plugin-import/pull/1551 +[#1542]: https://github.com/benmosher/eslint-plugin-import/pull/1542 +[#1526]: https://github.com/benmosher/eslint-plugin-import/pull/1526 +[#1521]: https://github.com/benmosher/eslint-plugin-import/pull/1521 +[#1519]: https://github.com/benmosher/eslint-plugin-import/pull/1519 +[#1507]: https://github.com/benmosher/eslint-plugin-import/pull/1507 +[#1506]: https://github.com/benmosher/eslint-plugin-import/pull/1506 +[#1496]: https://github.com/benmosher/eslint-plugin-import/pull/1496 +[#1495]: https://github.com/benmosher/eslint-plugin-import/pull/1495 +[#1494]: https://github.com/benmosher/eslint-plugin-import/pull/1494 +[#1493]: https://github.com/benmosher/eslint-plugin-import/pull/1493 +[#1472]: https://github.com/benmosher/eslint-plugin-import/pull/1472 +[#1470]: https://github.com/benmosher/eslint-plugin-import/pull/1470 +[#1439]: https://github.com/benmosher/eslint-plugin-import/pull/1439 +[#1436]: https://github.com/benmosher/eslint-plugin-import/pull/1436 +[#1435]: https://github.com/benmosher/eslint-plugin-import/pull/1435 [#1425]: https://github.com/benmosher/eslint-plugin-import/pull/1425 [#1419]: https://github.com/benmosher/eslint-plugin-import/pull/1419 [#1412]: https://github.com/benmosher/eslint-plugin-import/pull/1412 [#1409]: https://github.com/benmosher/eslint-plugin-import/pull/1409 [#1404]: https://github.com/benmosher/eslint-plugin-import/pull/1404 +[#1401]: https://github.com/benmosher/eslint-plugin-import/pull/1401 [#1393]: https://github.com/benmosher/eslint-plugin-import/pull/1393 [#1389]: https://github.com/benmosher/eslint-plugin-import/pull/1389 +[#1386]: https://github.com/benmosher/eslint-plugin-import/pull/1386 [#1377]: https://github.com/benmosher/eslint-plugin-import/pull/1377 [#1375]: https://github.com/benmosher/eslint-plugin-import/pull/1375 [#1372]: https://github.com/benmosher/eslint-plugin-import/pull/1372 [#1371]: https://github.com/benmosher/eslint-plugin-import/pull/1371 [#1370]: https://github.com/benmosher/eslint-plugin-import/pull/1370 [#1363]: https://github.com/benmosher/eslint-plugin-import/pull/1363 +[#1360]: https://github.com/benmosher/eslint-plugin-import/pull/1360 [#1358]: https://github.com/benmosher/eslint-plugin-import/pull/1358 [#1356]: https://github.com/benmosher/eslint-plugin-import/pull/1356 [#1354]: https://github.com/benmosher/eslint-plugin-import/pull/1354 @@ -633,6 +697,9 @@ for info on changes for earlier releases. [#1290]: https://github.com/benmosher/eslint-plugin-import/pull/1290 [#1277]: https://github.com/benmosher/eslint-plugin-import/pull/1277 [#1257]: https://github.com/benmosher/eslint-plugin-import/pull/1257 +[#1253]: https://github.com/benmosher/eslint-plugin-import/pull/1253 +[#1248]: https://github.com/benmosher/eslint-plugin-import/pull/1248 +[#1238]: https://github.com/benmosher/eslint-plugin-import/pull/1238 [#1235]: https://github.com/benmosher/eslint-plugin-import/pull/1235 [#1234]: https://github.com/benmosher/eslint-plugin-import/pull/1234 [#1232]: https://github.com/benmosher/eslint-plugin-import/pull/1232 @@ -648,10 +715,13 @@ for info on changes for earlier releases. [#1126]: https://github.com/benmosher/eslint-plugin-import/pull/1126 [#1122]: https://github.com/benmosher/eslint-plugin-import/pull/1122 [#1112]: https://github.com/benmosher/eslint-plugin-import/pull/1112 +[#1107]: https://github.com/benmosher/eslint-plugin-import/pull/1107 [#1106]: https://github.com/benmosher/eslint-plugin-import/pull/1106 +[#1105]: https://github.com/benmosher/eslint-plugin-import/pull/1105 [#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 +[#1049]: https://github.com/benmosher/eslint-plugin-import/pull/1049 [#1046]: https://github.com/benmosher/eslint-plugin-import/pull/1046 [#944]: https://github.com/benmosher/eslint-plugin-import/pull/944 [#912]: https://github.com/benmosher/eslint-plugin-import/pull/912 @@ -677,6 +747,7 @@ for info on changes for earlier releases. [#639]: https://github.com/benmosher/eslint-plugin-import/pull/639 [#632]: https://github.com/benmosher/eslint-plugin-import/pull/632 [#630]: https://github.com/benmosher/eslint-plugin-import/pull/630 +[#629]: https://github.com/benmosher/eslint-plugin-import/pull/629 [#628]: https://github.com/benmosher/eslint-plugin-import/pull/628 [#596]: https://github.com/benmosher/eslint-plugin-import/pull/596 [#586]: https://github.com/benmosher/eslint-plugin-import/pull/586 @@ -727,7 +798,7 @@ for info on changes for earlier releases. [#211]: https://github.com/benmosher/eslint-plugin-import/pull/211 [#164]: https://github.com/benmosher/eslint-plugin-import/pull/164 [#157]: https://github.com/benmosher/eslint-plugin-import/pull/157 - +[#1565]: https://github.com/benmosher/eslint-plugin-import/issues/1565 [#1366]: https://github.com/benmosher/eslint-plugin-import/issues/1366 [#1334]: https://github.com/benmosher/eslint-plugin-import/issues/1334 [#1323]: https://github.com/benmosher/eslint-plugin-import/issues/1323 @@ -747,6 +818,7 @@ for info on changes for earlier releases. [#863]: https://github.com/benmosher/eslint-plugin-import/issues/863 [#842]: https://github.com/benmosher/eslint-plugin-import/issues/842 [#839]: https://github.com/benmosher/eslint-plugin-import/issues/839 +[#795]: https://github.com/benmosher/eslint-plugin-import/issues/795 [#793]: https://github.com/benmosher/eslint-plugin-import/issues/793 [#720]: https://github.com/benmosher/eslint-plugin-import/issues/720 [#717]: https://github.com/benmosher/eslint-plugin-import/issues/717 @@ -811,7 +883,12 @@ 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.18.0...HEAD +[Unreleased]: https://github.com/benmosher/eslint-plugin-import/compare/v2.20.0...HEAD +[2.19.1]: https://github.com/benmosher/eslint-plugin-import/compare/v2.19.1...v2.20.0 +[2.19.1]: https://github.com/benmosher/eslint-plugin-import/compare/v2.19.0...v2.19.1 +[2.19.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.18.2...v2.19.0 +[2.18.2]: https://github.com/benmosher/eslint-plugin-import/compare/v2.18.1...v2.18.2 +[2.18.1]: https://github.com/benmosher/eslint-plugin-import/compare/v2.18.0...v2.18.1 [2.18.0]: https://github.com/benmosher/eslint-plugin-import/compare/v2.17.3...v2.18.0 [2.17.3]: https://github.com/benmosher/eslint-plugin-import/compare/v2.17.2...v2.17.3 [2.17.2]: https://github.com/benmosher/eslint-plugin-import/compare/v2.17.1...v2.17.2 @@ -871,7 +948,6 @@ for info on changes for earlier releases. [0.12.1]: https://github.com/benmosher/eslint-plugin-import/compare/v0.12.0...v0.12.1 [0.12.0]: https://github.com/benmosher/eslint-plugin-import/compare/v0.11.0...v0.12.0 [0.11.0]: https://github.com/benmosher/eslint-plugin-import/compare/v0.10.1...v0.11.0 - [@mathieudutour]: https://github.com/mathieudutour [@gausie]: https://github.com/gausie [@singles]: https://github.com/singles @@ -970,3 +1046,28 @@ for info on changes for earlier releases. [@sheepsteak]: https://github.com/sheepsteak [@sharmilajesupaul]: https://github.com/sharmilajesupaul [@lencioni]: https://github.com/lencioni +[@JounQin]: https://github.com/JounQin +[@atikenny]: https://github.com/atikenny +[@schmidsi]: https://github.com/schmidsi +[@TrevorBurnham]: https://github.com/TrevorBurnham +[@benmunro]: https://github.com/benmunro +[@tihonove]: https://github.com/tihonove +[@brendo]: https://github.com/brendo +[@saschanaz]: https://github.com/saschanaz +[@brettz9]: https://github.com/brettz9 +[@Taranys]: https://github.com/Taranys +[@maxmalov]: https://github.com/maxmalov +[@marcusdarmstrong]: https://github.com/marcusdarmstrong +[@Mairu]: https://github.com/Mairu +[@aamulumi]: https://github.com/aamulumi +[@pcorpet]: https://github.com/pcorpet +[@stropho]: https://github.com/stropho +[@luczsoma]: https://github.com/luczsoma +[@christophercurrie]: https://github.com/christophercurrie +[@randallreedjr]: https://github.com/randallreedjr +[@Pessimistress]: https://github.com/Pessimistress +[@stekycz]: https://github.com/stekycz +[@dbrewer5]: https://github.com/dbrewer5 +[@rsolomon]: https://github.com/rsolomon +[@joaovieira]: https://github.com/joaovieira +[@ivo-stefchev]: https://github.com/ivo-stefchev diff --git a/README.md b/README.md index ddc95d4f81..814d5fc28d 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a * Ensure all imports appear before other statements ([`first`]) * Ensure all exports appear after other statements ([`exports-last`]) * Report repeated import of the same module in multiple places ([`no-duplicates`]) -* Report namespace imports ([`no-namespace`]) +* Forbid namespace (a.k.a. "wildcard" `*`) imports ([`no-namespace`]) * Ensure consistent use of file extension within the import path ([`extensions`]) * Enforce a convention in module import order ([`order`]) * Enforce a newline after import statements ([`newline-after-import`]) @@ -112,6 +112,10 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a [`no-named-export`]: ./docs/rules/no-named-export.md [`dynamic-import-chunkname`]: ./docs/rules/dynamic-import-chunkname.md +## Support + +[Get supported eslint-plugin-import with the Tidelift Subscription](https://tidelift.com/subscription/pkg/npm-eslint-plugin-import?utm_source=npm-eslint-plugin-import&utm_medium=referral&utm_campaign=readme) + ## Installation ```sh @@ -148,7 +152,7 @@ rules: # etc... ``` -# Typescript +# TypeScript You may use the following shortcut or assemble your own config using the granular settings described below. @@ -398,6 +402,20 @@ settings: [`eslint_d`]: https://www.npmjs.com/package/eslint_d [`eslint-loader`]: https://www.npmjs.com/package/eslint-loader +#### `import/internal-regex` + +A regex for packages should be treated as internal. Useful when you are utilizing a monorepo setup or developing a set of packages that depend on each other. + +By default, any package referenced from [`import/external-module-folders`](#importexternal-module-folders) will be considered as "external", including packages in a monorepo like yarn workspace or lerna emvironentment. If you want to mark these packages as "internal" this will be useful. + +For example, if you pacakges in a monorepo are all in `@scope`, you can configure `import/internal-regex` like this + +```yaml +# .eslintrc.yml +settings: + import/internal-regex: ^@scope/ +``` + ## SublimeLinter-eslint diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..b155f54d59 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Latest major/minor version is supported only for security updates. + +## Reporting a Vulnerability + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/config/errors.js b/config/errors.js index 8f865b90f7..d99a9dacf0 100644 --- a/config/errors.js +++ b/config/errors.js @@ -9,6 +9,6 @@ module.exports = { , 'import/named': 2 , 'import/namespace': 2 , 'import/default': 2 - , 'import/export': 2 - } + , 'import/export': 2, + }, } diff --git a/config/recommended.js b/config/recommended.js index 70514eed3e..9970918933 100644 --- a/config/recommended.js +++ b/config/recommended.js @@ -16,7 +16,7 @@ module.exports = { // red flags (thus, warnings) 'import/no-named-as-default': 'warn', 'import/no-named-as-default-member': 'warn', - 'import/no-duplicates': 'warn' + 'import/no-duplicates': 'warn', }, // need all these for parsing dependencies (even if _your_ code doesn't need diff --git a/config/stage-0.js b/config/stage-0.js index 1a77784861..25ad75feb1 100644 --- a/config/stage-0.js +++ b/config/stage-0.js @@ -8,5 +8,5 @@ module.exports = { plugins: ['import'], rules: { 'import/no-deprecated': 1, - } + }, } diff --git a/config/typescript.js b/config/typescript.js index a8efe8e9a7..262e3c7999 100644 --- a/config/typescript.js +++ b/config/typescript.js @@ -2,20 +2,21 @@ * Adds `.jsx`, `.ts` and `.tsx` as an extension, and enables JSX/TSX parsing. */ -var allExtensions = ['.ts', '.tsx', '.d.ts', '.js', '.jsx']; +var allExtensions = ['.ts', '.tsx', '.d.ts', '.js', '.jsx'] module.exports = { settings: { 'import/extensions': allExtensions, + 'import/external-module-folders': ['node_modules', 'node_modules/@types'], 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx', '.d.ts'] + '@typescript-eslint/parser': ['.ts', '.tsx', '.d.ts'], }, 'import/resolver': { 'node': { - 'extensions': allExtensions - } - } - } + 'extensions': allExtensions, + }, + }, + }, } diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index b1757b4e52..4bcc5a98b1 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -11,7 +11,7 @@ You can also configure the regex format you'd like to accept for the webpackChun { "dynamic-import-chunkname": [2, { importFunctions: ["dynamicImport"], - webpackChunknameFormat: "[a-zA-Z0-57-9-/_]" + webpackChunknameFormat: "[a-zA-Z0-57-9-/_]+" }] } ``` diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 6e1d2b50a0..2f6d4a9c7a 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -29,13 +29,30 @@ By providing both a string and an object, the string will set the default settin , "never" | "always" | "ignorePackages", { - : "never" | "always" | "ignorePackages" + : "never" | "always" | "ignorePackages" } ] ``` For example, `["error", "never", { "svg": "always" }]` would require that all extensions are omitted, except for "svg". +`ignorePackages` can be set as a separate boolean option like this: +``` +"import/extensions": [ + , + "never" | "always" | "ignorePackages", + { + ignorePackages: true | false, + pattern: { + : "never" | "always" | "ignorePackages" + } + } +] +``` +In that case, if you still want to specify extensions, you can do so inside the **pattern** property. +Default value of `ignorePackages` is `false`. + + ### Exception When disallowing the use of certain extensions this rule makes an exception and allows the use of extension when the file would not be resolvable without extension. diff --git a/docs/rules/group-exports.md b/docs/rules/group-exports.md index b0d88f4a05..f61ff5306b 100644 --- a/docs/rules/group-exports.md +++ b/docs/rules/group-exports.md @@ -26,6 +26,12 @@ export { } ``` +```js +// Aggregating exports -> ok +export { default as module1 } from 'module-1' +export { default as module2 } from 'module-2' +``` + ```js // A single exports assignment -> ok module.exports = { @@ -63,6 +69,12 @@ export const first = true export const second = true ``` +```js +// Aggregating exports from the same module -> not ok! +export { module1 } from 'module-1' +export { module2 } from 'module-1' +``` + ```js // Multiple exports assignments -> not ok! exports.first = true diff --git a/docs/rules/no-commonjs.md b/docs/rules/no-commonjs.md index 4353886bf7..7be4bb3993 100644 --- a/docs/rules/no-commonjs.md +++ b/docs/rules/no-commonjs.md @@ -27,15 +27,30 @@ If `allowRequire` option is set to `true`, `require` calls are valid: ```js /*eslint no-commonjs: [2, { allowRequire: true }]*/ +var mod = require('./mod'); +``` + +but `module.exports` is reported as usual. + +### Allow conditional require + +By default, conditional requires are allowed: + +```js +var a = b && require("c") if (typeof window !== "undefined") { require('that-ugly-thing'); } + +var fs = null; +try { + fs = require("fs") +} catch (error) {} ``` -but `module.exports` is reported as usual. +If the `allowConditionalRequire` option is set to `false`, they will be reported. -This is useful for conditional requires. If you don't rely on synchronous module loading, check out [dynamic import](https://github.com/airbnb/babel-plugin-dynamic-import-node). ### Allow primitive modules diff --git a/docs/rules/no-duplicates.md b/docs/rules/no-duplicates.md index 0641e44186..f59b14d9cc 100644 --- a/docs/rules/no-duplicates.md +++ b/docs/rules/no-duplicates.md @@ -36,6 +36,31 @@ The motivation is that this is likely a result of two developers importing diffe names from the same module at different times (and potentially largely different locations in the file.) This rule brings both (or n-many) to attention. +### Query Strings + +By default, this rule ignores query strings (i.e. paths followed by a question mark), and thus imports from `./mod?a` and `./mod?b` will be considered as duplicates. However you can use the option `considerQueryString` to handle them as different (primarily because browsers will resolve those imports differently). + +Config: + +```json +"import/no-duplicates": ["error", {"considerQueryString": true}] +``` + +And then the following code becomes valid: +```js +import minifiedMod from './mod?minify' +import noCommentsMod from './mod?comments=0' +import originalMod from './mod' +``` + +It will still catch duplicates when using the same module and the exact same query string: +```js +import SomeDefaultClass from './mod?minify' + +// This is invalid, assuming `./mod` and `./mod.js` are the same target: +import * from './mod.js?minify' +``` + ## When Not To Use It If the core ESLint version is good enough (i.e. you're _not_ using Flow and you _are_ using [`import/extensions`](./extensions.md)), keep it and don't use this. diff --git a/docs/rules/no-extraneous-dependencies.md b/docs/rules/no-extraneous-dependencies.md index 2b66aa25c0..295590ccd0 100644 --- a/docs/rules/no-extraneous-dependencies.md +++ b/docs/rules/no-extraneous-dependencies.md @@ -1,6 +1,6 @@ # import/no-extraneous-dependencies: Forbid the use of extraneous packages -Forbid the import of external modules that are not declared in the `package.json`'s `dependencies`, `devDependencies`, `optionalDependencies` or `peerDependencies`. +Forbid the import of external modules that are not declared in the `package.json`'s `dependencies`, `devDependencies`, `optionalDependencies`, `peerDependencies`, or `bundledDependencies`. The closest parent `package.json` will be used. If no `package.json` is found, the rule will not lint anything. This behaviour can be changed with the rule option `packageDir`. Modules have to be installed for this rule to work. @@ -15,6 +15,8 @@ This rule supports the following options: `peerDependencies`: If set to `false`, then the rule will show an error when `peerDependencies` are imported. Defaults to `false`. +`bundledDependencies`: If set to `false`, then the rule will show an error when `bundledDependencies` are imported. Defaults to `true`. + You can set the options like this: ```js @@ -70,7 +72,10 @@ Given the following `package.json`: }, "peerDependencies": { "react": ">=15.0.0 <16.0.0" - } + }, + "bundledDependencies": [ + "@generated/foo", + ] } ``` @@ -90,6 +95,10 @@ var test = require('ava'); /* eslint import/no-extraneous-dependencies: ["error", {"optionalDependencies": false}] */ import isArray from 'lodash.isarray'; var isArray = require('lodash.isarray'); + +/* eslint import/no-extraneous-dependencies: ["error", {"bundledDependencies": false}] */ +import foo from '"@generated/foo"'; +var foo = require('"@generated/foo"'); ``` @@ -103,6 +112,7 @@ var foo = require('./foo'); import test from 'ava'; import find from 'lodash.find'; import isArray from 'lodash.isarray'; +import foo from '"@generated/foo"'; /* eslint import/no-extraneous-dependencies: ["error", {"peerDependencies": true}] */ import react from 'react'; diff --git a/docs/rules/no-namespace.md b/docs/rules/no-namespace.md index b308d66210..e0b0f0b967 100644 --- a/docs/rules/no-namespace.md +++ b/docs/rules/no-namespace.md @@ -1,6 +1,9 @@ # import/no-namespace -Reports if namespace import is used. +Enforce a convention of not using namespace (a.k.a. "wildcard" `*`) imports. + ++(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule, provided that the namespace object is only used for direct member access, e.g. `namespace.a`. +The `--fix` functionality for this rule requires ESLint 5 or newer. ## Rule Details @@ -12,10 +15,13 @@ import { a, b } from './bar' import defaultExport, { a, b } from './foobar' ``` -...whereas here imports will be reported: +Invalid: ```js import * as foo from 'foo'; +``` + +```js import defaultExport, * as foo from 'foo'; ``` diff --git a/docs/rules/no-restricted-paths.md b/docs/rules/no-restricted-paths.md index bad65ab8e1..3776699836 100644 --- a/docs/rules/no-restricted-paths.md +++ b/docs/rules/no-restricted-paths.md @@ -9,7 +9,7 @@ In order to prevent such scenarios this rule allows you to define restricted zon This rule has one option. The option is an object containing the definition of all restricted `zones` and the optional `basePath` which is used to resolve relative paths within. The default value for `basePath` is the current working directory. -Each zone consists of the `target` path and a `from` path. The `target` is the path where the restricted imports should be applied. The `from` path defines the folder that is not allowed to be used in an import. +Each zone consists of the `target` path and a `from` path. The `target` is the path where the restricted imports should be applied. The `from` path defines the folder that is not allowed to be used in an import. An optional `except` may be defined for a zone, allowing exception paths that would otherwise violate the related `from`. Note that `except` is relative to `from` and cannot backtrack to a parent directory. ### Examples @@ -37,3 +37,43 @@ The following patterns are not considered problems when configuration set to `{ ```js import baz from '../client/baz'; ``` + +--------------- + +Given the following folder structure: + +``` +my-project +├── client +│ └── foo.js +│ └── baz.js +└── server + ├── one + │ └── a.js + │ └── b.js + └── two +``` + +and the current file being linted is `my-project/server/one/a.js`. + +and the current configuration is set to: + +``` +{ "zones": [ { + "target": "./tests/files/restricted-paths/server/one", + "from": "./tests/files/restricted-paths/server", + "except": ["./one"] +} ] } +``` + +The following pattern is considered a problem: + +```js +import a from '../two/a' +``` + +The following pattern is not considered a problem: + +```js +import b from './b' +``` diff --git a/docs/rules/no-useless-path-segments.md b/docs/rules/no-useless-path-segments.md index 6a02eab9fa..19b7725855 100644 --- a/docs/rules/no-useless-path-segments.md +++ b/docs/rules/no-useless-path-segments.md @@ -73,3 +73,7 @@ import "./pages/index.js"; // should be "./pages" (auto-fixable) ``` Note: `noUselessIndex` only avoids ambiguous imports for `.js` files if you haven't specified other resolved file extensions. See [Settings: import/extensions](https://github.com/benmosher/eslint-plugin-import#importextensions) for details. + +### commonjs + +When set to `true`, this rule checks CommonJS imports. Default to `false`. diff --git a/docs/rules/order.md b/docs/rules/order.md index 88ddca46fb..667b63374f 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -94,8 +94,55 @@ You can set the options like this: "import/order": ["error", {"groups": ["index", "sibling", "parent", "internal", "external", "builtin"]}] ``` -### `newlines-between: [ignore|always|always-and-inside-groups|never]`: +### `pathGroups: [array of objects]`: + +To be able to group by paths mostly needed with aliases pathGroups can be defined. + +Properties of the objects + +| property | required | type | description | +|----------------|:--------:|--------|---------------| +| pattern | x | string | minimatch pattern for the paths to be in this group (will not be used for builtins or externals) | +| patternOptions | | object | options for minimatch, default: { nocomment: true } | +| group | x | string | one of the allowed groups, the pathGroup will be positioned relative to this group | +| position | | string | defines where around the group the pathGroup will be positioned, can be 'after' or 'before', if not provided pathGroup will be positioned like the group | + +```json +{ + "import/order": ["error", { + "pathGroups": [ + { + "pattern": "~/**", + "group": "external" + } + ] + }] +} +``` + +### `pathGroupsExcludedImportTypes: [array]`: + +This defines import types that are not handled by configured pathGroups. +This is mostly needed when you want to handle path groups that look like external imports. + +Example: +```json +{ + "import/order": ["error", { + "pathGroups": [ + { + "pattern": "@app/**", + "group": "external", + "position": "after" + } + ], + "pathGroupsExcludedImportTypes": ["builtin"] + }] +} +``` +The default value is `["builtin", "external"]`. +### `newlines-between: [ignore|always|always-and-inside-groups|never]`: Enforces or forbids new lines between import groups: @@ -164,8 +211,49 @@ import index from './'; import sibling from './foo'; ``` +### `alphabetize: {order: asc|desc|ignore, caseInsensitive: true|false}`: + +Sort the order within each group in alphabetical manner based on **import path**: + +- `order`: use `asc` to sort in ascending order, and `desc` to sort in descending order (default: `ignore`). +- `caseInsensitive`: use `true` to ignore case, and `false` to consider case (default: `false`). + +Example setting: +```js +alphabetize: { + order: 'asc', /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */ + caseInsensitive: true /* ignore case. Options: [true, false] */ +} +``` + +This will fail the rule check: + +```js +/* eslint import/order: ["error", {"alphabetize": true}] */ +import React, { PureComponent } from 'react'; +import aTypes from 'prop-types'; +import { compose, apply } from 'xcompose'; +import * as classnames from 'classnames'; +import blist from 'BList'; +``` + +While this will pass: + +```js +/* eslint import/order: ["error", {"alphabetize": true}] */ +import blist from 'BList'; +import * as classnames from 'classnames'; +import aTypes from 'prop-types'; +import React, { PureComponent } from 'react'; +import { compose, apply } from 'xcompose'; +``` + ## Related - [`import/external-module-folders`] setting +- [`import/internal-regex`] setting + [`import/external-module-folders`]: ../../README.md#importexternal-module-folders + +[`import/internal-regex`]: ../../README.md#importinternal-regex diff --git a/memo-parser/.eslintrc.yml b/memo-parser/.eslintrc.yml new file mode 100644 index 0000000000..e7e6b3d341 --- /dev/null +++ b/memo-parser/.eslintrc.yml @@ -0,0 +1,3 @@ +--- +rules: + import/no-extraneous-dependencies: 1 diff --git a/memo-parser/index.js b/memo-parser/index.js index 9fd74c33a9..b64f854219 100644 --- a/memo-parser/index.js +++ b/memo-parser/index.js @@ -1,4 +1,4 @@ -"use strict" +'use strict' const crypto = require('crypto') , moduleRequire = require('eslint-module-utils/module-require').default @@ -20,7 +20,7 @@ exports.parse = function parse(content, options) { options = Object.assign({}, options, parserOptions) if (!options.filePath) { - throw new Error("no file path provided!") + throw new Error('no file path provided!') } const keyHash = crypto.createHash('sha256') diff --git a/memo-parser/package.json b/memo-parser/package.json index fa7d12973e..69996d33eb 100644 --- a/memo-parser/package.json +++ b/memo-parser/package.json @@ -1,12 +1,13 @@ { "name": "memo-parser", - "version": "0.2.0", + "version": "0.2.1", "engines": { "node": ">=4" }, "description": "Memoizing wrapper for any ESLint-compatible parser module.", "main": "index.js", "scripts": { + "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -26,5 +27,8 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", "peerDependencies": { "eslint": ">=3.5.0" + }, + "dependencies": { + "eslint-module-utils": "^2.5.0" } } diff --git a/package.json b/package.json index b63f3af49c..906cf8fa46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "2.18.2", + "version": "2.20.0", "description": "Import with sanity.", "engines": { "node": ">=4" @@ -15,11 +15,13 @@ "memo-parser" ], "scripts": { - "build": "babel --quiet --out-dir lib src", "prebuild": "rimraf lib", + "build": "babel --quiet --out-dir lib src", + "postbuild": "npm run copy-metafiles", + "copy-metafiles": "for DIR in memo-parser resolvers/node resolvers/webpack utils; do cp LICENSE .npmrc \"${DIR}/\"; done", "watch": "npm run mocha -- --watch tests/src", "pretest": "linklocal", - "posttest": "eslint ./src", + "posttest": "eslint .", "mocha": "cross-env BABEL_ENV=test NODE_PATH=./src nyc -s mocha -R dot --recursive -t 5s", "test": "npm run mocha tests/src", "test-compiled": "npm run prepublish && NODE_PATH=./lib mocha --compilers js:babel-register --recursive tests/src", @@ -55,9 +57,9 @@ "babel-plugin-istanbul": "^4.1.6", "babel-preset-es2015-argon": "latest", "babel-register": "^6.26.0", - "babylon": "^6.15.0", + "babylon": "^6.18.0", "chai": "^4.2.0", - "coveralls": "^3.0.2", + "coveralls": "^3.0.6", "cross-env": "^4.0.0", "eslint": "2.x - 6.x", "eslint-import-resolver-node": "file:./resolvers/node", @@ -70,8 +72,8 @@ "mocha": "^3.5.3", "nyc": "^11.9.0", "redux": "^3.7.2", - "rimraf": "^2.6.3", - "semver": "^6.0.0", + "rimraf": "^2.7.1", + "semver": "^6.3.0", "sinon": "^2.4.1", "typescript": "~3.2.2", "typescript-eslint-parser": "^22.0.0" @@ -81,16 +83,17 @@ }, "dependencies": { "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", "contains-path": "^0.1.0", "debug": "^2.6.9", "doctrine": "1.5.0", "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", + "eslint-module-utils": "^2.4.1", "has": "^1.0.3", "minimatch": "^3.0.4", "object.values": "^1.1.0", "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" + "resolve": "^1.12.0" }, "nyc": { "require": [ diff --git a/resolvers/.eslintrc b/resolvers/.eslintrc.yml similarity index 100% rename from resolvers/.eslintrc rename to resolvers/.eslintrc.yml diff --git a/resolvers/node/.npmrc b/resolvers/node/.npmrc deleted file mode 100644 index 43c97e719a..0000000000 --- a/resolvers/node/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/resolvers/node/package.json b/resolvers/node/package.json index 76529084d6..22237d71ff 100644 --- a/resolvers/node/package.json +++ b/resolvers/node/package.json @@ -1,12 +1,13 @@ { "name": "eslint-import-resolver-node", - "version": "0.3.2", + "version": "0.3.3", "description": "Node default behavior import resolution plugin for eslint-plugin-import.", "main": "index.js", "files": [ "index.js" ], "scripts": { + "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", "test": "nyc mocha", "coveralls": "nyc report --reporter lcovonly && cd ../.. && coveralls < ./resolvers/node/coverage/lcov.info" }, @@ -29,7 +30,7 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import", "dependencies": { "debug": "^2.6.9", - "resolve": "^1.10.0" + "resolve": "^1.13.1" }, "devDependencies": { "chai": "^3.5.0", diff --git a/resolvers/webpack/.eslintrc.yml b/resolvers/webpack/.eslintrc.yml new file mode 100644 index 0000000000..febeb09cfa --- /dev/null +++ b/resolvers/webpack/.eslintrc.yml @@ -0,0 +1,4 @@ +--- +rules: + import/no-extraneous-dependencies: 1 + no-console: 1 diff --git a/resolvers/webpack/.npmrc b/resolvers/webpack/.npmrc deleted file mode 120000 index cba44bb384..0000000000 --- a/resolvers/webpack/.npmrc +++ /dev/null @@ -1 +0,0 @@ -../../.npmrc \ No newline at end of file diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index 204e0224ab..78e01348e4 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +## 0.12.0 - 2019-12-07 + +### Added +- [New] enable passing cwd as an option to `eslint-import-resolver-webpack` ([#1503], thanks [@Aghassi]) + ## 0.11.1 - 2019-04-13 ### Fixed @@ -117,6 +122,7 @@ 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]). +[#1503]: https://github.com/benmosher/eslint-plugin-import/pull/1503 [#1297]: https://github.com/benmosher/eslint-plugin-import/pull/1297 [#1261]: https://github.com/benmosher/eslint-plugin-import/pull/1261 [#1220]: https://github.com/benmosher/eslint-plugin-import/pull/1220 @@ -166,3 +172,4 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel [@mattkrick]: https://github.com/mattkrick [@idudinov]: https://github.com/idudinov [@keann]: https://github.com/keann +[@Aghassi]: https://github.com/Aghassi diff --git a/resolvers/webpack/README.md b/resolvers/webpack/README.md index a9869aec44..4fc3b0ffff 100644 --- a/resolvers/webpack/README.md +++ b/resolvers/webpack/README.md @@ -79,3 +79,7 @@ settings: NODE_ENV: 'local' production: true ``` + +## Support + +[Get supported eslint-import-resolver-webpack with the Tidelift Subscription](https://tidelift.com/subscription/pkg/npm-eslint-import-resolver-webpack?utm_source=npm-eslint-import-resolver-webpack&utm_medium=referral&utm_campaign=readme) diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index 0f75a28400..dd3fc7a38a 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -20,7 +20,15 @@ exports.interfaceVersion = 2 * resolveImport('./foo', '/Users/ben/bar.js') => '/Users/ben/foo.js' * @param {string} source - the module to resolve; i.e './some-module' * @param {string} file - the importing file's full path; i.e. '/usr/local/bin/file.js' - * TODO: take options as a third param, with webpack config file name + * @param {object} settings - the webpack config file name, as well as cwd + * @example + * options: { + * // Path to the webpack config + * config: 'webpack.config.js', + * // Path to be used to determine where to resolve webpack from + * // (may differ from the cwd in some cases) + * cwd: process.cwd() + * } * @return {string?} the resolved path to source, undefined if not resolved, or null * if resolved to a non-FS resource (i.e. script tag at page load) */ @@ -41,6 +49,11 @@ exports.resolve = function (source, file, settings) { var webpackConfig var configPath = get(settings, 'config') + /** + * Attempt to set the current working directory. + * If none is passed, default to the `cwd` where the config is located. + */ + , cwd = get(settings, 'cwd') , configIndex = get(settings, 'config-index') , env = get(settings, 'env') , argv = get(settings, 'argv', {}) @@ -114,7 +127,7 @@ exports.resolve = function (source, file, settings) { } // otherwise, resolve "normally" - var resolveSync = getResolveSync(configPath, webpackConfig) + var resolveSync = getResolveSync(configPath, webpackConfig, cwd) try { return { found: true, path: resolveSync(path.dirname(file), source) } @@ -130,13 +143,13 @@ exports.resolve = function (source, file, settings) { var MAX_CACHE = 10 var _cache = [] -function getResolveSync(configPath, webpackConfig) { +function getResolveSync(configPath, webpackConfig, cwd) { var cacheKey = { configPath: configPath, webpackConfig: webpackConfig } var cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey) }) if (!cached) { cached = { key: cacheKey, - value: createResolveSync(configPath, webpackConfig), + value: createResolveSync(configPath, webpackConfig, cwd), } // put in front and pop last item if (_cache.unshift(cached) > MAX_CACHE) { @@ -146,15 +159,18 @@ function getResolveSync(configPath, webpackConfig) { return cached.value } -function createResolveSync(configPath, webpackConfig) { +function createResolveSync(configPath, webpackConfig, cwd) { var webpackRequire , basedir = null if (typeof configPath === 'string') { - basedir = path.dirname(configPath) + // This can be changed via the settings passed in when defining the resolver + basedir = cwd || configPath + log(`Attempting to load webpack path from ${basedir}`) } try { + // Attempt to resolve webpack from the given `basedir` var webpackFilename = resolve.sync('webpack', { basedir, preserveSymlinks: false }) var webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false } diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 69a861c6ef..a8f8ae7102 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -1,9 +1,10 @@ { "name": "eslint-import-resolver-webpack", - "version": "0.11.1", + "version": "0.12.1", "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", "main": "index.js", "scripts": { + "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", "test": "nyc mocha -t 5s", "report": "nyc report --reporter=html", "coveralls": "nyc report --reporter lcovonly && cd ../.. && coveralls < ./resolvers/webpack/coverage/lcov.info" @@ -31,15 +32,15 @@ "homepage": "https://github.com/benmosher/eslint-plugin-import/tree/master/resolvers/webpack", "dependencies": { "array-find": "^1.0.0", - "debug": "^2.6.8", - "enhanced-resolve": "~0.9.0", + "debug": "^2.6.9", + "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", - "has": "^1.0.1", - "interpret": "^1.0.0", - "lodash": "^4.17.4", + "has": "^1.0.3", + "interpret": "^1.2.0", + "lodash": "^4.17.15", "node-libs-browser": "^1.0.0 || ^2.0.0", - "resolve": "^1.10.0", - "semver": "^5.3.0" + "resolve": "^1.13.1", + "semver": "^5.7.1" }, "peerDependencies": { "eslint-plugin-import": ">=1.4.0", diff --git a/resolvers/webpack/test/config.js b/resolvers/webpack/test/config.js index 07c6350c56..ff0c0bd669 100644 --- a/resolvers/webpack/test/config.js +++ b/resolvers/webpack/test/config.js @@ -119,8 +119,8 @@ describe("config", function () { var settings = { config: require(path.join(__dirname, './files/webpack.function.config.js')), argv: { - mode: 'test' - } + mode: 'test', + }, } expect(resolve('baz', file, settings)).to.have.property('path') @@ -130,7 +130,7 @@ describe("config", function () { 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 + argv: undefined, } expect(function () { resolve('baz', file, settings) }).to.not.throw(Error) diff --git a/resolvers/webpack/test/example.js b/resolvers/webpack/test/example.js new file mode 100644 index 0000000000..375f6b5a1e --- /dev/null +++ b/resolvers/webpack/test/example.js @@ -0,0 +1,9 @@ +var path = require('path') + +var resolve = require('../index').resolve + +var file = path.join(__dirname, 'files', 'src', 'dummy.js') + +var webpackDir = path.join(__dirname, "different-package-location") + +console.log(resolve('main-module', file, { config: "webpack.config.js", cwd: webpackDir})) diff --git a/resolvers/webpack/test/files/node_modules/webpack-resolver-plugin-test/index.js b/resolvers/webpack/test/files/node_modules/webpack-resolver-plugin-test/index.js index 2989f9bab3..f23d4af0c1 100644 --- a/resolvers/webpack/test/files/node_modules/webpack-resolver-plugin-test/index.js +++ b/resolvers/webpack/test/files/node_modules/webpack-resolver-plugin-test/index.js @@ -1,4 +1,4 @@ -var path = require('path'); +var path = require('path') /** * ResolverPlugin @@ -9,15 +9,15 @@ var path = require('path'); */ function ResolverPlugin(plugins, types) { - if(!Array.isArray(plugins)) plugins = [plugins]; - if(!types) types = ["normal"]; - else if(!Array.isArray(types)) types = [types]; + if(!Array.isArray(plugins)) plugins = [plugins] + if(!types) types = ["normal"] + else if(!Array.isArray(types)) types = [types] - this.plugins = plugins; - this.types = types; + this.plugins = plugins + this.types = types } -module.exports.ResolverPlugin = ResolverPlugin; +module.exports.ResolverPlugin = ResolverPlugin /** @@ -29,29 +29,29 @@ module.exports.ResolverPlugin = ResolverPlugin; */ function SimpleResolver(file, source) { - this.file = file; - this.source = source; + this.file = file + this.source = source } SimpleResolver.prototype.apply = function (resolver) { - var file = this.file; - var source = this.source; + var file = this.file + var source = this.source resolver.plugin('directory', function (request, done) { - var absolutePath = path.resolve(request.path, request.request); + var absolutePath = path.resolve(request.path, request.request) if (absolutePath === source) { resolver.doResolve('file', { request: file }, function (error, result) { - return done(undefined, result || undefined); - }); + return done(undefined, result || undefined) + }) } - return done(); + return done() - }); + }) } -module.exports.SimpleResolver = SimpleResolver; +module.exports.SimpleResolver = SimpleResolver diff --git a/resolvers/webpack/test/files/webpack.config.js b/resolvers/webpack/test/files/webpack.config.js index 7c7c8b3c89..38a4c888bd 100644 --- a/resolvers/webpack/test/files/webpack.config.js +++ b/resolvers/webpack/test/files/webpack.config.js @@ -23,10 +23,10 @@ module.exports = { 'bootstrap', function (context, request, callback) { if (request === 'underscore') { - return callback(null, 'underscore'); - }; - callback(); - } + return callback(null, 'underscore') + } + callback() + }, ], plugins: [ @@ -34,7 +34,7 @@ module.exports = { new pluginsTest.SimpleResolver( path.join(__dirname, 'some', 'bar', 'bar.js'), path.join(__dirname, 'some', 'bar') - ) - ]) - ] + ), + ]), + ], } diff --git a/resolvers/webpack/test/root.js b/resolvers/webpack/test/root.js index 4839f3b894..4365720091 100644 --- a/resolvers/webpack/test/root.js +++ b/resolvers/webpack/test/root.js @@ -6,6 +6,7 @@ var resolve = require('../index').resolve var file = path.join(__dirname, 'files', 'src', 'dummy.js') +var webpackDir = path.join(__dirname, "different-package-location") describe("root", function () { it("works", function () { @@ -32,5 +33,13 @@ describe("root", function () { .property('path') .to.equal(path.join(__dirname, 'files', 'bower_components', 'typeahead.js')) }) - + it("supports passing a different directory to load webpack from", function () { + // Webpack should still be able to resolve the config here + expect(resolve('main-module', file, { config: "webpack.config.js", cwd: webpackDir})) + .property('path') + .to.equal(path.join(__dirname, 'files', 'src', 'main-module.js')) + expect(resolve('typeahead', file, { config: "webpack.config.js", cwd: webpackDir})) + .property('path') + .to.equal(path.join(__dirname, 'files', 'bower_components', 'typeahead.js')) + }) }) diff --git a/src/.eslintrc b/src/.eslintrc.yml similarity index 100% rename from src/.eslintrc rename to src/.eslintrc.yml diff --git a/src/ExportMap.js b/src/ExportMap.js index ebeb4fad7f..ba455e3685 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -310,11 +310,18 @@ ExportMap.for = function (context) { return null } + // check for and cache ignore + if (isIgnored(path, context)) { + log('ignored path due to ignore settings:', path) + exportCache.set(cacheKey, null) + return null + } + const content = fs.readFileSync(path, { encoding: 'utf8' }) - // check for and cache ignore - if (isIgnored(path, context) || !unambiguous.test(content)) { - log('ignored path due to unambiguous regex or ignore settings:', path) + // check for and cache unambigious modules + if (!unambiguous.test(content)) { + log('ignored path due to unambiguous regex:', path) exportCache.set(cacheKey, null) return null } @@ -397,19 +404,27 @@ ExportMap.parse = function (path, content, context) { function captureDependency(declaration) { if (declaration.source == null) return null + if (declaration.importKind === 'type') return null // skip Flow type imports const importedSpecifiers = new Set() const supportedTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']) + let hasImportedType = false if (declaration.specifiers) { declaration.specifiers.forEach(specifier => { - if (supportedTypes.has(specifier.type)) { + const isType = specifier.importKind === 'type' + hasImportedType = hasImportedType || isType + + if (supportedTypes.has(specifier.type) && !isType) { importedSpecifiers.add(specifier.type) } - if (specifier.type === 'ImportSpecifier') { + if (specifier.type === 'ImportSpecifier' && !isType) { importedSpecifiers.add(specifier.imported.name) } }) } + // only Flow types were imported + if (hasImportedType && importedSpecifiers.size === 0) return null + const p = remotePath(declaration.source.value) if (p == null) return null const existing = m.imports.get(p) @@ -514,30 +529,32 @@ ExportMap.parse = function (path, content, context) { // This doesn't declare anything, but changes what's being exported. if (n.type === 'TSExportAssignment') { - const moduleDecl = ast.body.find((bodyNode) => + const moduleDecls = ast.body.filter((bodyNode) => bodyNode.type === 'TSModuleDeclaration' && bodyNode.id.name === n.expression.name ) - if (moduleDecl && moduleDecl.body && moduleDecl.body.body) { - moduleDecl.body.body.forEach((moduleBlockNode) => { - // Export-assignment exports all members in the namespace, explicitly exported or not. - const exportedDecl = moduleBlockNode.type === 'ExportNamedDeclaration' ? - moduleBlockNode.declaration : - moduleBlockNode - - if (exportedDecl.type === 'VariableDeclaration') { - exportedDecl.declarations.forEach((decl) => - recursivePatternCapture(decl.id,(id) => m.namespace.set( - id.name, - captureDoc(source, docStyleParsers, decl, exportedDecl, moduleBlockNode)) + moduleDecls.forEach((moduleDecl) => { + if (moduleDecl && moduleDecl.body && moduleDecl.body.body) { + moduleDecl.body.body.forEach((moduleBlockNode) => { + // Export-assignment exports all members in the namespace, explicitly exported or not. + const exportedDecl = moduleBlockNode.type === 'ExportNamedDeclaration' ? + moduleBlockNode.declaration : + moduleBlockNode + + if (exportedDecl.type === 'VariableDeclaration') { + exportedDecl.declarations.forEach((decl) => + recursivePatternCapture(decl.id,(id) => m.namespace.set( + id.name, + captureDoc(source, docStyleParsers, decl, exportedDecl, moduleBlockNode)) + ) ) - ) - } else { - m.namespace.set( - exportedDecl.id.name, - captureDoc(source, docStyleParsers, moduleBlockNode)) - } - }) - } + } else { + m.namespace.set( + exportedDecl.id.name, + captureDoc(source, docStyleParsers, moduleBlockNode)) + } + }) + } + }) } }) diff --git a/src/core/importType.js b/src/core/importType.js index b948ea2bb9..8ec01c29e8 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -34,7 +34,7 @@ function isExternalPath(path, name, settings) { } const externalModuleRegExp = /^\w/ -function isExternalModule(name, settings, path) { +export function isExternalModule(name, settings, path) { return externalModuleRegExp.test(name) && isExternalPath(path, name, settings) } @@ -43,8 +43,8 @@ export function isExternalModuleMain(name, settings, path) { return externalModuleMainRegExp.test(name) && isExternalPath(path, name, settings) } -const scopedRegExp = /^@[^/]+\/[^/]+/ -function isScoped(name) { +const scopedRegExp = /^@[^/]+\/?[^/]+/ +export function isScoped(name) { return scopedRegExp.test(name) } @@ -54,8 +54,9 @@ export function isScopedMain(name) { } function isInternalModule(name, settings, path) { + const internalScope = (settings && settings['import/internal-regex']) const matchesScopedOrExternalRegExp = scopedRegExp.test(name) || externalModuleRegExp.test(name) - return (matchesScopedOrExternalRegExp && !isExternalPath(path, name, settings)) + return (matchesScopedOrExternalRegExp && (internalScope && new RegExp(internalScope).test(name) || !isExternalPath(path, name, settings))) } function isRelativeToParent(name) { @@ -83,6 +84,10 @@ function typeTest(name, settings, path) { return 'unknown' } +export function isScopedModule(name) { + return name.indexOf('@') === 0 +} + export default function resolveImportType(name, context) { return typeTest(name, context.settings, resolve(name, context)) } diff --git a/src/rules/default.js b/src/rules/default.js index 7e07800dae..a524dcdc72 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -13,14 +13,9 @@ module.exports = { function checkDefault(specifierType, node) { - // poor man's Array.find - let defaultSpecifier - node.specifiers.some((n) => { - if (n.type === specifierType) { - defaultSpecifier = n - return true - } - }) + const defaultSpecifier = node.specifiers.find( + specifier => specifier.type === specifierType + ) if (!defaultSpecifier) return var imports = Exports.get(node.source.value, context) @@ -29,7 +24,10 @@ module.exports = { if (imports.errors.length) { imports.reportErrors(context, node) } else if (imports.get('default') === undefined) { - context.report(defaultSpecifier, 'No default export found in module.') + context.report({ + node: defaultSpecifier, + message: `No default export found in imported module "${node.source.value}".`, + }) } } diff --git a/src/rules/export.js b/src/rules/export.js index a9fba849e0..9402bc9d87 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -3,7 +3,7 @@ import docsUrl from '../docsUrl' import includes from 'array-includes' /* -Notes on Typescript namespaces aka TSModuleDeclaration: +Notes on TypeScript namespaces aka TSModuleDeclaration: There are two forms: - active namespaces: namespace Foo {} / module Foo {} @@ -86,7 +86,7 @@ module.exports = { if (node.declaration == null) return const parent = getParent(node) - // support for old typescript versions + // support for old TypeScript versions const isTypeVariableDecl = node.declaration.kind === 'type' if (node.declaration.id != null) { diff --git a/src/rules/extensions.js b/src/rules/extensions.js index b72c91bad0..fd9d177adf 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -1,7 +1,7 @@ import path from 'path' import resolve from 'eslint-module-utils/resolve' -import { isBuiltIn, isExternalModuleMain, isScopedMain } from '../core/importType' +import { isBuiltIn, isExternalModule, isScoped, isScopedModule } from '../core/importType' import docsUrl from '../docsUrl' const enumValues = { enum: [ 'always', 'ignorePackages', 'never' ] } @@ -50,6 +50,11 @@ function buildProperties(context) { } }) + if (result.defaultConfig === 'ignorePackages') { + result.defaultConfig = 'always' + result.ignorePackages = true + } + return result } @@ -105,8 +110,8 @@ module.exports = { return props.pattern[extension] || props.defaultConfig } - function isUseOfExtensionRequired(extension, isPackageMain) { - return getModifier(extension) === 'always' && (!props.ignorePackages || !isPackageMain) + function isUseOfExtensionRequired(extension, isPackage) { + return getModifier(extension) === 'always' && (!props.ignorePackages || !isPackage) } function isUseOfExtensionForbidden(extension) { @@ -121,16 +126,30 @@ module.exports = { return resolvedFileWithoutExtension === resolve(file, context) } + function isExternalRootModule(file) { + const slashCount = file.split('/').length - 1 + + if (isScopedModule(file) && slashCount <= 1) return true + if (isExternalModule(file, context, resolve(file, context)) && !slashCount) return true + return false + } + function checkFileExtension(node) { const { source } = node // bail if the declaration doesn't have a source, e.g. "export { foo };" if (!source) return - const importPath = source.value + const importPathWithQueryString = source.value // don't enforce anything on builtins - if (isBuiltIn(importPath, context.settings)) return + if (isBuiltIn(importPathWithQueryString, context.settings)) return + + const importPath = importPathWithQueryString.replace(/\?(.*)$/, '') + + // don't enforce in root external packages as they may have names with `.js`. + // Like `import Decimal from decimal.js`) + if (isExternalRootModule(importPath)) return const resolvedPath = resolve(importPath, context) @@ -139,24 +158,24 @@ module.exports = { const extension = path.extname(resolvedPath || importPath).substring(1) // determine if this is a module - const isPackageMain = isExternalModuleMain(importPath, context.settings) - || isScopedMain(importPath) + const isPackage = isExternalModule(importPath, context.settings) + || isScoped(importPath) if (!extension || !importPath.endsWith(`.${extension}`)) { - const extensionRequired = isUseOfExtensionRequired(extension, isPackageMain) + const extensionRequired = isUseOfExtensionRequired(extension, isPackage) const extensionForbidden = isUseOfExtensionForbidden(extension) if (extensionRequired && !extensionForbidden) { context.report({ node: source, message: - `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPath}"`, + `Missing file extension ${extension ? `"${extension}" ` : ''}for "${importPathWithQueryString}"`, }) } } else if (extension) { if (isUseOfExtensionForbidden(extension) && isResolvableWithoutExtension(importPath)) { context.report({ node: source, - message: `Unexpected use of file extension "${extension}" for "${importPath}"`, + message: `Unexpected use of file extension "${extension}" for "${importPathWithQueryString}"`, }) } } diff --git a/src/rules/group-exports.js b/src/rules/group-exports.js index d650fff877..cd7fc992dd 100644 --- a/src/rules/group-exports.js +++ b/src/rules/group-exports.js @@ -1,4 +1,6 @@ import docsUrl from '../docsUrl' +import values from 'object.values' +import flat from 'array.prototype.flat' const meta = { type: 'suggestion', @@ -46,11 +48,18 @@ function create(context) { const nodes = { modules: new Set(), commonjs: new Set(), + sources: {}, } return { ExportNamedDeclaration(node) { - nodes.modules.add(node) + if (!node.source) { + nodes.modules.add(node) + } else if (Array.isArray(nodes.sources[node.source.value])) { + nodes.sources[node.source.value].push(node) + } else { + nodes.sources[node.source.value] = [node] + } }, AssignmentExpression(node) { @@ -86,6 +95,16 @@ function create(context) { }) } + // Report multiple `aggregated exports` from the same module (ES2015 modules) + flat(values(nodes.sources) + .filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1)) + .forEach((node) => { + context.report({ + node, + message: errors[node.type], + }) + }) + // Report multiple `module.exports` assignments (CommonJS) if (nodes.commonjs.size > 1) { nodes.commonjs.forEach(node => { diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index 261654bbf2..456f030f42 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -25,6 +25,26 @@ function allowRequire(node, options) { return options.allowRequire } +function allowConditionalRequire(node, options) { + return options.allowConditionalRequire !== false +} + +function validateScope(scope) { + return scope.variableScope.type === 'module' +} + +// https://github.com/estree/estree/blob/master/es5.md +function isConditional(node) { + if ( + node.type === 'IfStatement' + || node.type === 'TryStatement' + || node.type === 'LogicalExpression' + || node.type === 'ConditionalExpression' + ) return true + if (node.parent) return isConditional(node.parent) + return false +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -35,6 +55,7 @@ const schemaObject = { properties: { allowPrimitiveModules: { 'type': 'boolean' }, allowRequire: { 'type': 'boolean' }, + allowConditionalRequire: { 'type': 'boolean' }, }, additionalProperties: false, } @@ -87,12 +108,7 @@ module.exports = { }, 'CallExpression': function (call) { - if (context.getScope().type !== 'module') return - if ( - call.parent.type !== 'ExpressionStatement' - && call.parent.type !== 'VariableDeclarator' - && call.parent.type !== 'AssignmentExpression' - ) return + if (!validateScope(context.getScope())) return if (call.callee.type !== 'Identifier') return if (call.callee.name !== 'require') return @@ -105,6 +121,8 @@ module.exports = { if (allowRequire(call, options)) return + if (allowConditionalRequire(call, options) && isConditional(call.parent)) return + // keeping it simple: all 1-string-arg `require` calls are reported context.report({ node: call.callee, diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index 33e3357482..1334a12582 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -230,15 +230,38 @@ module.exports = { url: docsUrl('no-duplicates'), }, fixable: 'code', + schema: [ + { + type: 'object', + properties: { + considerQueryString: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], }, create: function (context) { + // Prepare the resolver from options. + const considerQueryStringOption = context.options[0] && + context.options[0]['considerQueryString'] + const defaultResolver = sourcePath => resolve(sourcePath, context) || sourcePath + const resolver = considerQueryStringOption ? (sourcePath => { + const parts = sourcePath.match(/^([^?]*)\?(.*)$/) + if (!parts) { + return defaultResolver(sourcePath) + } + return defaultResolver(parts[1]) + '?' + parts[2] + }) : defaultResolver + 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 resolvedPath = resolver(n.source.value) const importMap = n.importKind === 'type' ? typesImported : imported if (importMap.has(resolvedPath)) { diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index 647481a374..7746f489ec 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -11,12 +11,19 @@ function hasKeys(obj = {}) { return Object.keys(obj).length > 0 } +function arrayOrKeys(arrayOrObject) { + return Array.isArray(arrayOrObject) ? arrayOrObject : Object.keys(arrayOrObject) +} + function extractDepFields(pkg) { return { dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {}, optionalDependencies: pkg.optionalDependencies || {}, peerDependencies: pkg.peerDependencies || {}, + // BundledDeps should be in the form of an array, but object notation is also supported by + // `npm`, so we convert it to an array if it is an object + bundledDependencies: arrayOrKeys(pkg.bundleDependencies || pkg.bundledDependencies || []), } } @@ -28,6 +35,7 @@ function getDependencies(context, packageDir) { devDependencies: {}, optionalDependencies: {}, peerDependencies: {}, + bundledDependencies: [], } if (packageDir && packageDir.length > 0) { @@ -63,6 +71,7 @@ function getDependencies(context, packageDir) { packageContent.devDependencies, packageContent.optionalDependencies, packageContent.peerDependencies, + packageContent.bundledDependencies, ].some(hasKeys)) { return null } @@ -121,11 +130,13 @@ function reportIfMissing(context, deps, depsOptions, node, name) { const isInDevDeps = deps.devDependencies[packageName] !== undefined const isInOptDeps = deps.optionalDependencies[packageName] !== undefined const isInPeerDeps = deps.peerDependencies[packageName] !== undefined + const isInBundledDeps = deps.bundledDependencies.indexOf(packageName) !== -1 if (isInDeps || (depsOptions.allowDevDeps && isInDevDeps) || (depsOptions.allowPeerDeps && isInPeerDeps) || - (depsOptions.allowOptDeps && isInOptDeps) + (depsOptions.allowOptDeps && isInOptDeps) || + (depsOptions.allowBundledDeps && isInBundledDeps) ) { return } @@ -169,6 +180,7 @@ module.exports = { 'devDependencies': { 'type': ['boolean', 'array'] }, 'optionalDependencies': { 'type': ['boolean', 'array'] }, 'peerDependencies': { 'type': ['boolean', 'array'] }, + 'bundledDependencies': { 'type': ['boolean', 'array'] }, 'packageDir': { 'type': ['string', 'array'] }, }, 'additionalProperties': false, @@ -185,12 +197,25 @@ module.exports = { allowDevDeps: testConfig(options.devDependencies, filename) !== false, allowOptDeps: testConfig(options.optionalDependencies, filename) !== false, allowPeerDeps: testConfig(options.peerDependencies, filename) !== false, + allowBundledDeps: testConfig(options.bundledDependencies, filename) !== false, } // todo: use module visitor from module-utils core return { ImportDeclaration: function (node) { - reportIfMissing(context, deps, depsOptions, node, node.source.value) + if (node.source) { + reportIfMissing(context, deps, depsOptions, node, node.source.value) + } + }, + ExportNamedDeclaration: function (node) { + if (node.source) { + reportIfMissing(context, deps, depsOptions, node, node.source.value) + } + }, + ExportAllDeclaration: function (node) { + if (node.source) { + reportIfMissing(context, deps, depsOptions, node, node.source.value) + } }, CallExpression: function handleRequires(node) { if (isStaticRequire(node)) { diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 3dbedca500..a3a6913646 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -16,13 +16,143 @@ module.exports = { docs: { url: docsUrl('no-namespace'), }, + fixable: 'code', }, create: function (context) { return { 'ImportNamespaceSpecifier': function (node) { - context.report(node, `Unexpected namespace import.`) + const scopeVariables = context.getScope().variables + const namespaceVariable = scopeVariables.find((variable) => + variable.defs[0].node === node + ) + const namespaceReferences = namespaceVariable.references + const namespaceIdentifiers = namespaceReferences.map(reference => reference.identifier) + const canFix = namespaceIdentifiers.length > 0 && !usesNamespaceAsObject(namespaceIdentifiers) + + context.report({ + node, + message: `Unexpected namespace import.`, + fix: canFix && (fixer => { + const scopeManager = context.getSourceCode().scopeManager + const fixes = [] + + // Pass 1: Collect variable names that are already in scope for each reference we want + // to transform, so that we can be sure that we choose non-conflicting import names + const importNameConflicts = {} + namespaceIdentifiers.forEach((identifier) => { + const parent = identifier.parent + if (parent && parent.type === 'MemberExpression') { + const importName = getMemberPropertyName(parent) + const localConflicts = getVariableNamesInScope(scopeManager, parent) + if (!importNameConflicts[importName]) { + importNameConflicts[importName] = localConflicts + } else { + localConflicts.forEach((c) => importNameConflicts[importName].add(c)) + } + } + }) + + // Choose new names for each import + const importNames = Object.keys(importNameConflicts) + const importLocalNames = generateLocalNames( + importNames, + importNameConflicts, + namespaceVariable.name + ) + + // Replace the ImportNamespaceSpecifier with a list of ImportSpecifiers + const namedImportSpecifiers = importNames.map((importName) => + importName === importLocalNames[importName] + ? importName + : `${importName} as ${importLocalNames[importName]}` + ) + fixes.push(fixer.replaceText(node, `{ ${namedImportSpecifiers.join(', ')} }`)) + + // Pass 2: Replace references to the namespace with references to the named imports + namespaceIdentifiers.forEach((identifier) => { + const parent = identifier.parent + if (parent && parent.type === 'MemberExpression') { + const importName = getMemberPropertyName(parent) + fixes.push(fixer.replaceText(parent, importLocalNames[importName])) + } + }) + + return fixes + }), + }) }, } }, } + +/** + * @param {Identifier[]} namespaceIdentifiers + * @returns {boolean} `true` if the namespace variable is more than just a glorified constant + */ +function usesNamespaceAsObject(namespaceIdentifiers) { + return !namespaceIdentifiers.every((identifier) => { + const parent = identifier.parent + + // `namespace.x` or `namespace['x']` + return ( + parent && parent.type === 'MemberExpression' && + (parent.property.type === 'Identifier' || parent.property.type === 'Literal') + ) + }) +} + +/** + * @param {MemberExpression} memberExpression + * @returns {string} the name of the member in the object expression, e.g. the `x` in `namespace.x` + */ +function getMemberPropertyName(memberExpression) { + return memberExpression.property.type === 'Identifier' + ? memberExpression.property.name + : memberExpression.property.value +} + +/** + * @param {ScopeManager} scopeManager + * @param {ASTNode} node + * @return {Set} + */ +function getVariableNamesInScope(scopeManager, node) { + let currentNode = node + let scope = scopeManager.acquire(currentNode) + while (scope == null) { + currentNode = currentNode.parent + scope = scopeManager.acquire(currentNode, true) + } + return new Set([ + ...scope.variables.map(variable => variable.name), + ...scope.upper.variables.map(variable => variable.name), + ]) +} + +/** + * + * @param {*} names + * @param {*} nameConflicts + * @param {*} namespaceName + */ +function generateLocalNames(names, nameConflicts, namespaceName) { + const localNames = {} + names.forEach((name) => { + let localName + if (!nameConflicts[name].has(name)) { + localName = name + } else if (!nameConflicts[name].has(`${namespaceName}_${name}`)) { + localName = `${namespaceName}_${name}` + } else { + for (let i = 1; i < Infinity; i++) { + if (!nameConflicts[name].has(`${namespaceName}_${name}_${i}`)) { + localName = `${namespaceName}_${name}_${i}` + break + } + } + } + localNames[name] = localName + }) + return localNames +} diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 0d906f6318..221457b1c9 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -4,6 +4,7 @@ import path from 'path' import resolve from 'eslint-module-utils/resolve' import isStaticRequire from '../core/staticRequire' import docsUrl from '../docsUrl' +import importType from '../core/importType' module.exports = { meta: { @@ -24,6 +25,13 @@ module.exports = { properties: { target: { type: 'string' }, from: { type: 'string' }, + except: { + type: 'array', + items: { + type: 'string', + }, + uniqueItems: true, + }, }, additionalProperties: false, }, @@ -46,6 +54,19 @@ module.exports = { return containsPath(currentFilename, targetPath) }) + function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) { + const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath) + + return importType(relativeExceptionPath, context) !== 'parent' + } + + function reportInvalidExceptionPath(node) { + context.report({ + node, + message: 'Restricted path exceptions must be descendants of the configured `from` path for that zone.', + }) + } + function checkForRestrictedImportPath(importPath, node) { const absoluteImportPath = resolve(importPath, context) @@ -54,14 +75,36 @@ module.exports = { } matchingZones.forEach((zone) => { + const exceptionPaths = zone.except || [] const absoluteFrom = path.resolve(basePath, zone.from) - if (containsPath(absoluteImportPath, absoluteFrom)) { - context.report({ - node, - message: `Unexpected path "${importPath}" imported in restricted zone.`, - }) + if (!containsPath(absoluteImportPath, absoluteFrom)) { + return + } + + const absoluteExceptionPaths = exceptionPaths.map((exceptionPath) => + path.resolve(absoluteFrom, exceptionPath) + ) + const hasValidExceptionPaths = absoluteExceptionPaths + .every((absoluteExceptionPath) => isValidExceptionPath(absoluteFrom, absoluteExceptionPath)) + + if (!hasValidExceptionPaths) { + reportInvalidExceptionPath(node) + return + } + + const pathIsExcepted = absoluteExceptionPaths + .some((absoluteExceptionPath) => containsPath(absoluteImportPath, absoluteExceptionPath)) + + if (pathIsExcepted) { + return } + + context.report({ + node, + message: `Unexpected path "{{importPath}}" imported in restricted zone.`, + data: { importPath }, + }) }) } diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index 47cd11c0df..5c6a73d828 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -5,6 +5,7 @@ */ import Exports from '../ExportMap' +import { getFileExtensions } from 'eslint-module-utils/ignore' import resolve from 'eslint-module-utils/resolve' import docsUrl from '../docsUrl' import { dirname, join } from 'path' @@ -16,19 +17,40 @@ import includes from 'array-includes' // and has been moved to eslint/lib/cli-engine/file-enumerator in version 6 let listFilesToProcess try { - var FileEnumerator = require('eslint/lib/cli-engine/file-enumerator').FileEnumerator - listFilesToProcess = function (src) { - var e = new FileEnumerator() + const FileEnumerator = require('eslint/lib/cli-engine/file-enumerator').FileEnumerator + listFilesToProcess = function (src, extensions) { + const e = new FileEnumerator({ + extensions: extensions, + }) return Array.from(e.iterateFiles(src), ({ filePath, ignored }) => ({ ignored, filename: filePath, })) } } catch (e1) { + // Prevent passing invalid options (extensions array) to old versions of the function. + // https://github.com/eslint/eslint/blob/v5.16.0/lib/util/glob-utils.js#L178-L280 + // https://github.com/eslint/eslint/blob/v5.2.0/lib/util/glob-util.js#L174-L269 + let originalListFilesToProcess try { - listFilesToProcess = require('eslint/lib/util/glob-utils').listFilesToProcess + originalListFilesToProcess = require('eslint/lib/util/glob-utils').listFilesToProcess + listFilesToProcess = function (src, extensions) { + return originalListFilesToProcess(src, { + extensions: extensions, + }) + } } catch (e2) { - listFilesToProcess = require('eslint/lib/util/glob-util').listFilesToProcess + originalListFilesToProcess = require('eslint/lib/util/glob-util').listFilesToProcess + + listFilesToProcess = function (src, extensions) { + const patterns = src.reduce((carry, pattern) => { + return carry.concat(extensions.map((extension) => { + return /\*\*|\*\./.test(pattern) ? pattern : `${pattern}/**/*${extension}` + })) + }, src.slice()) + + return originalListFilesToProcess(patterns) + } } } @@ -42,11 +64,12 @@ const VARIABLE_DECLARATION = 'VariableDeclaration' const FUNCTION_DECLARATION = 'FunctionDeclaration' const CLASS_DECLARATION = 'ClassDeclaration' const DEFAULT = 'default' +const TYPE_ALIAS = 'TypeAlias' -let preparationDone = false const importList = new Map() const exportList = new Map() const ignoredFiles = new Set() +const filesOutsideSrc = new Set() const isNodeModule = path => { return /\/(node_modules)\//.test(path) @@ -57,12 +80,14 @@ const isNodeModule = path => { * * return all files matching src pattern, which are not matching the ignoreExports pattern */ -const resolveFiles = (src, ignoreExports) => { +const resolveFiles = (src, ignoreExports, context) => { + const extensions = Array.from(getFileExtensions(context.settings)) + const srcFiles = new Set() - const srcFileList = listFilesToProcess(src) + const srcFileList = listFilesToProcess(src, extensions) // prepare list of ignored files - const ignoredFilesList = listFilesToProcess(ignoreExports) + const ignoredFilesList = listFilesToProcess(ignoreExports, extensions) ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename)) // prepare list of source files, don't consider files from node_modules @@ -86,8 +111,13 @@ const prepareImportsAndExports = (srcFiles, context) => { // dependencies === export * from const currentExportAll = new Set() - dependencies.forEach(value => { - currentExportAll.add(value().path) + dependencies.forEach(getDependency => { + const dependency = getDependency() + if (dependency === null) { + return + } + + currentExportAll.add(dependency.path) }) exportAll.set(file, currentExportAll) @@ -192,11 +222,27 @@ const getSrc = src => { * prepare the lists of existing imports and exports - should only be executed once at * the start of a new eslint run */ +let srcFiles +let lastPrepareKey const doPreparation = (src, ignoreExports, context) => { - const srcFiles = resolveFiles(getSrc(src), ignoreExports) + const prepareKey = JSON.stringify({ + src: (src || []).sort(), + ignoreExports: (ignoreExports || []).sort(), + extensions: Array.from(getFileExtensions(context.settings)).sort(), + }) + if (prepareKey === lastPrepareKey) { + return + } + + importList.clear() + exportList.clear() + ignoredFiles.clear() + filesOutsideSrc.clear() + + srcFiles = resolveFiles(getSrc(src), ignoreExports, context) prepareImportsAndExports(srcFiles, context) determineUsage() - preparationDone = true + lastPrepareKey = prepareKey } const newNamespaceImportExists = specifiers => @@ -332,7 +378,7 @@ module.exports = { unusedExports, } = context.options[0] || {} - if (unusedExports && !preparationDone) { + if (unusedExports) { doPreparation(src, ignoreExports, context) } @@ -353,7 +399,7 @@ module.exports = { exportCount.delete(EXPORT_ALL_DECLARATION) exportCount.delete(IMPORT_NAMESPACE_SPECIFIER) - if (missingExports && exportCount.size < 1) { + if (exportCount.size < 1) { // node.body[0] === 'undefined' only happens, if everything is commented out in the file // being linted context.report(node.body[0] ? node.body[0] : node, 'No exports found') @@ -375,12 +421,17 @@ module.exports = { return } - // refresh list of source files - const srcFiles = resolveFiles(getSrc(src), ignoreExports) + if (filesOutsideSrc.has(file)) { + return + } // make sure file to be linted is included in source files if (!srcFiles.has(file)) { - return + srcFiles = resolveFiles(getSrc(src), ignoreExports, context) + if (!srcFiles.has(file)) { + filesOutsideSrc.add(file) + return + } } exports = exportList.get(file) @@ -456,7 +507,8 @@ module.exports = { if (declaration) { if ( declaration.type === FUNCTION_DECLARATION || - declaration.type === CLASS_DECLARATION + declaration.type === CLASS_DECLARATION || + declaration.type === TYPE_ALIAS ) { newExportIdentifiers.add(declaration.id.name) } @@ -781,7 +833,8 @@ module.exports = { if (node.declaration) { if ( node.declaration.type === FUNCTION_DECLARATION || - node.declaration.type === CLASS_DECLARATION + node.declaration.type === CLASS_DECLARATION || + node.declaration.type === TYPE_ALIAS ) { checkUsage(node, node.declaration.id.name) } diff --git a/src/rules/order.js b/src/rules/order.js index 3d3e1b96b7..fcdf12bda5 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -1,5 +1,6 @@ 'use strict' +import minimatch from 'minimatch' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' import docsUrl from '../docsUrl' @@ -162,8 +163,10 @@ function canCrossNodeWhileReorder(node) { function canReorderItems(firstNode, secondNode) { const parent = firstNode.parent - const firstIndex = parent.body.indexOf(firstNode) - const secondIndex = parent.body.indexOf(secondNode) + const [firstIndex, secondIndex] = [ + parent.body.indexOf(firstNode), + parent.body.indexOf(secondNode), + ].sort() const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1) for (var nodeBetween of nodesBetween) { if (!canCrossNodeWhileReorder(nodeBetween)) { @@ -177,12 +180,12 @@ function fixOutOfOrder(context, firstNode, secondNode, order) { const sourceCode = context.getSourceCode() const firstRoot = findRootNode(firstNode.node) - let firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot) + const firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot) const firstRootEnd = findEndOfLineWithComments(sourceCode, firstRoot) const secondRoot = findRootNode(secondNode.node) - let secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot) - let secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot) + const secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot) + const secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot) const canFix = canReorderItems(firstRoot, secondRoot) let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd) @@ -240,15 +243,93 @@ function makeOutOfOrderReport(context, imported) { reportOutOfOrder(context, imported, outOfOrder, 'before') } +function importsSorterAsc(importA, importB) { + if (importA < importB) { + return -1 + } + + if (importA > importB) { + return 1 + } + + return 0 +} + +function importsSorterDesc(importA, importB) { + if (importA < importB) { + return 1 + } + + if (importA > importB) { + return -1 + } + + return 0 +} + +function mutateRanksToAlphabetize(imported, alphabetizeOptions) { + const groupedByRanks = imported.reduce(function(acc, importedItem) { + if (!Array.isArray(acc[importedItem.rank])) { + acc[importedItem.rank] = [] + } + acc[importedItem.rank].push(importedItem.name) + return acc + }, {}) + + const groupRanks = Object.keys(groupedByRanks) + + const sorterFn = alphabetizeOptions.order === 'asc' ? importsSorterAsc : importsSorterDesc + const comparator = alphabetizeOptions.caseInsensitive ? (a, b) => sorterFn(String(a).toLowerCase(), String(b).toLowerCase()) : (a, b) => sorterFn(a, b) + // sort imports locally within their group + groupRanks.forEach(function(groupRank) { + groupedByRanks[groupRank].sort(comparator) + }) + + // assign globally unique rank to each import + let newRank = 0 + const alphabetizedRanks = groupRanks.sort().reduce(function(acc, groupRank) { + groupedByRanks[groupRank].forEach(function(importedItemName) { + acc[importedItemName] = newRank + newRank += 1 + }) + return acc + }, {}) + + // mutate the original group-rank with alphabetized-rank + imported.forEach(function(importedItem) { + importedItem.rank = alphabetizedRanks[importedItem.name] + }) +} + // DETECTING -function computeRank(context, ranks, name, type) { - return ranks[importType(name, context)] + - (type === 'import' ? 0 : 100) +function computePathRank(ranks, pathGroups, path, maxPosition) { + for (let i = 0, l = pathGroups.length; i < l; i++) { + const { pattern, patternOptions, group, position = 1 } = pathGroups[i] + if (minimatch(path, pattern, patternOptions || { nocomment: true })) { + return ranks[group] + (position / maxPosition) + } + } +} + +function computeRank(context, ranks, name, type, excludedImportTypes) { + const impType = importType(name, context) + let rank + if (!excludedImportTypes.has(impType)) { + rank = computePathRank(ranks.groups, ranks.pathGroups, name, ranks.maxPosition) + } + if (!rank) { + rank = ranks.groups[impType] + } + if (type !== 'import') { + rank += 100 + } + + return rank } -function registerNode(context, node, name, type, ranks, imported) { - const rank = computeRank(context, ranks, name, type) +function registerNode(context, node, name, type, ranks, imported, excludedImportTypes) { + const rank = computeRank(context, ranks, name, type, excludedImportTypes) if (rank !== -1) { imported.push({name, rank, node}) } @@ -292,6 +373,49 @@ function convertGroupsToRanks(groups) { }, rankObject) } +function convertPathGroupsForRanks(pathGroups) { + const after = {} + const before = {} + + const transformed = pathGroups.map((pathGroup, index) => { + const { group, position: positionString } = pathGroup + let position = 0 + if (positionString === 'after') { + if (!after[group]) { + after[group] = 1 + } + position = after[group]++ + } else if (positionString === 'before') { + if (!before[group]) { + before[group] = [] + } + before[group].push(index) + } + + return Object.assign({}, pathGroup, { position }) + }) + + let maxPosition = 1 + + Object.keys(before).forEach((group) => { + const groupLength = before[group].length + before[group].forEach((groupIndex, index) => { + transformed[groupIndex].position = -1 * (groupLength - index) + }) + maxPosition = Math.max(maxPosition, groupLength) + }) + + Object.keys(after).forEach((key) => { + const groupNextPosition = after[key] + maxPosition = Math.max(maxPosition, groupNextPosition - 1) + }) + + return { + pathGroups: transformed, + maxPosition: maxPosition > 10 ? Math.pow(10, Math.ceil(Math.log10(maxPosition))) : 10, + } +} + function fixNewLineAfterImport(context, previousImport) { const prevRoot = findRootNode(previousImport.node) const tokensToEndOfLine = takeTokensAfterWhile( @@ -338,7 +462,7 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) { context.report({ node: previousImport.node, message: 'There should be at least one empty line between import groups', - fix: fixNewLineAfterImport(context, previousImport, currentImport), + fix: fixNewLineAfterImport(context, previousImport), }) } else if (currentImport.rank === previousImport.rank && emptyLinesBetween > 0 @@ -361,6 +485,14 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) { }) } +function getAlphabetizeConfig(options) { + const alphabetize = options.alphabetize || {} + const order = alphabetize.order || 'ignore' + const caseInsensitive = alphabetize.caseInsensitive || false + + return {order, caseInsensitive} +} + module.exports = { meta: { type: 'suggestion', @@ -376,6 +508,32 @@ module.exports = { groups: { type: 'array', }, + pathGroupsExcludedImportTypes: { + type: 'array', + }, + pathGroups: { + type: 'array', + items: { + type: 'object', + properties: { + pattern: { + type: 'string', + }, + patternOptions: { + type: 'object', + }, + group: { + type: 'string', + enum: types, + }, + position: { + type: 'string', + enum: ['after', 'before'], + }, + }, + required: ['pattern', 'group'], + }, + }, 'newlines-between': { enum: [ 'ignore', @@ -384,6 +542,20 @@ module.exports = { 'never', ], }, + alphabetize: { + type: 'object', + properties: { + caseInsensitive: { + type: 'boolean', + default: false, + }, + order: { + enum: ['ignore', 'asc', 'desc'], + default: 'ignore', + }, + }, + additionalProperties: false, + }, }, additionalProperties: false, }, @@ -393,10 +565,17 @@ module.exports = { create: function importOrderRule (context) { const options = context.options[0] || {} const newlinesBetweenImports = options['newlines-between'] || 'ignore' + const pathGroupsExcludedImportTypes = new Set(options['pathGroupsExcludedImportTypes'] || ['builtin', 'external']) + const alphabetize = getAlphabetizeConfig(options) let ranks try { - ranks = convertGroupsToRanks(options.groups || defaultGroups) + const { pathGroups, maxPosition } = convertPathGroupsForRanks(options.pathGroups || []) + ranks = { + groups: convertGroupsToRanks(options.groups || defaultGroups), + pathGroups, + maxPosition, + } } catch (error) { // Malformed configuration return { @@ -419,7 +598,15 @@ module.exports = { ImportDeclaration: function handleImports(node) { if (node.specifiers.length) { // Ignoring unassigned imports const name = node.source.value - registerNode(context, node, name, 'import', ranks, imported) + registerNode( + context, + node, + name, + 'import', + ranks, + imported, + pathGroupsExcludedImportTypes + ) } }, CallExpression: function handleRequires(node) { @@ -427,15 +614,27 @@ module.exports = { return } const name = node.arguments[0].value - registerNode(context, node, name, 'require', ranks, imported) + registerNode( + context, + node, + name, + 'require', + ranks, + imported, + pathGroupsExcludedImportTypes + ) }, 'Program:exit': function reportAndReset() { - makeOutOfOrderReport(context, imported) - if (newlinesBetweenImports !== 'ignore') { makeNewlinesBetweenReport(context, imported, newlinesBetweenImports) } + if (alphabetize.order !== 'ignore') { + mutateRanksToAlphabetize(imported, alphabetize) + } + + makeOutOfOrderReport(context, imported) + imported = [] }, FunctionDeclaration: incrementLevel, diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index 59c26d11ee..17a07688c3 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -14,6 +14,7 @@ module.exports = { let specifierExportCount = 0 let hasDefaultExport = false let hasStarExport = false + let hasTypeExport = false let namedExportNode = null function captureDeclaration(identifierOrPattern) { @@ -23,7 +24,10 @@ module.exports = { .forEach(function(property) { captureDeclaration(property.value) }) - } else { + } else if (identifierOrPattern.type === 'ArrayPattern') { + identifierOrPattern.elements + .forEach(captureDeclaration) + } else { // assume it's a single standard identifier specifierExportCount++ } @@ -47,9 +51,6 @@ module.exports = { // if there are specifiers, node.declaration should be null if (!node.declaration) return - // don't warn on single type aliases, declarations, or interfaces - if (node.exportKind === 'type') return - const { type } = node.declaration if ( @@ -58,6 +59,8 @@ module.exports = { type === 'TSInterfaceDeclaration' || type === 'InterfaceDeclaration' ) { + specifierExportCount++ + hasTypeExport = true return } @@ -83,7 +86,7 @@ module.exports = { }, 'Program:exit': function() { - if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport) { + if (specifierExportCount === 1 && !hasDefaultExport && !hasStarExport && !hasTypeExport) { context.report(namedExportNode, 'Prefer default export.') } }, diff --git a/tests/.eslintrc b/tests/.eslintrc deleted file mode 100644 index 700a3d6883..0000000000 --- a/tests/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ ---- -env: - mocha: true -rules: - no-unused-expressions: 0 - max-len: 0 diff --git a/tests/.eslintrc.yml b/tests/.eslintrc.yml new file mode 100644 index 0000000000..92b917ed62 --- /dev/null +++ b/tests/.eslintrc.yml @@ -0,0 +1,8 @@ +--- +parserOptions: + ecmaVersion: 8 +env: + mocha: true +rules: + max-len: 0 + import/default: 0 diff --git a/tests/dep-time-travel.sh b/tests/dep-time-travel.sh index 996ed0b1cf..078d9059b8 100755 --- a/tests/dep-time-travel.sh +++ b/tests/dep-time-travel.sh @@ -4,13 +4,13 @@ npm install --no-save eslint@$ESLINT_VERSION --ignore-scripts || true -# completely remove the new typescript parser for ESLint < v5 +# completely remove the new TypeScript parser for ESLint < v5 if [[ "$ESLINT_VERSION" -lt "5" ]]; then echo "Removing @typescript-eslint/parser..." npm uninstall --no-save @typescript-eslint/parser fi -# use these alternate typescript dependencies for ESLint < v4 +# 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 diff --git a/tests/files/.eslintrc b/tests/files/.eslintrc index 5970c5fa16..6d36c133b3 100644 --- a/tests/files/.eslintrc +++ b/tests/files/.eslintrc @@ -1,5 +1,285 @@ ---- -parser: 'babel-eslint' -parserOptions: - ecmaFeatures: - jsx: true +{ + "parser": "babel-eslint", + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 8 + }, + "rules": { + "accessor-pairs": 0, + "array-bracket-newline": 0, + "array-bracket-spacing": 0, + "array-callback-return": 0, + "array-element-newline": 0, + "arrow-body-style": 0, + "arrow-parens": 0, + "arrow-spacing": 0, + "block-scoped-var": 0, + "block-spacing": 0, + "brace-style": 0, + "callback-return": 0, + "camelcase": 0, + "capitalized-comments": 0, + "class-methods-use-this": 0, + "comma-dangle": 0, + "comma-spacing": 0, + "comma-style": 0, + "complexity": 0, + "computed-property-spacing": 0, + "consistent-return": 0, + "consistent-this": 0, + "constructor-super": 0, + "curly": 0, + "default-case": 0, + "dot-location": 0, + "dot-notation": 0, + "eol-last": 0, + "eqeqeq": 0, + "for-direction": 0, + "func-call-spacing": 0, + "func-name-matching": 0, + "func-names": 0, + "func-style": 0, + "function-paren-newline": 0, + "generator-star-spacing": 0, + "getter-return": 0, + "global-require": 0, + "guard-for-in": 0, + "handle-callback-err": 0, + "id-blacklist": 0, + "id-length": 0, + "id-match": 0, + "implicit-arrow-linebreak": 0, + "indent": 0, + "indent-legacy": 0, + "init-declarations": 0, + "jsx-quotes": 0, + "key-spacing": 0, + "keyword-spacing": 0, + "line-comment-position": 0, + "linebreak-style": 0, + "lines-around-comment": 0, + "lines-around-directive": 0, + "lines-between-class-members": 0, + "max-classes-per-file": 0, + "max-depth": 0, + "max-len": 0, + "max-lines": 0, + "max-lines-per-function": 0, + "max-nested-callbacks": 0, + "max-params": 0, + "max-statements": 0, + "max-statements-per-line": 0, + "multiline-comment-style": 0, + "multiline-ternary": 0, + "new-cap": 0, + "new-parens": 0, + "newline-after-var": 0, + "newline-before-return": 0, + "newline-per-chained-call": 0, + "no-alert": 0, + "no-array-constructor": 0, + "no-async-promise-executor": 0, + "no-await-in-loop": 0, + "no-bitwise": 0, + "no-buffer-constructor": 0, + "no-caller": 0, + "no-case-declarations": 0, + "no-catch-shadow": 0, + "no-class-assign": 0, + "no-compare-neg-zero": 0, + "no-cond-assign": 0, + "no-confusing-arrow": 0, + "no-console": 0, + "no-const-assign": 0, + "no-constant-condition": 0, + "no-continue": 0, + "no-control-regex": 0, + "no-debugger": 0, + "no-delete-var": 0, + "no-div-regex": 0, + "no-dupe-args": 0, + "no-dupe-class-members": 0, + "no-dupe-keys": 0, + "no-duplicate-case": 0, + "no-duplicate-imports": 0, + "no-else-return": 0, + "no-empty": 0, + "no-empty-character-class": 0, + "no-empty-function": 0, + "no-empty-pattern": 0, + "no-eq-null": 0, + "no-eval": 0, + "no-ex-assign": 0, + "no-extend-native": 0, + "no-extra-bind": 0, + "no-extra-boolean-cast": 0, + "no-extra-label": 0, + "no-extra-parens": 0, + "no-extra-semi": 0, + "no-fallthrough": 0, + "no-floating-decimal": 0, + "no-func-assign": 0, + "no-global-assign": 0, + "no-implicit-coercion": 0, + "no-implicit-globals": 0, + "no-implied-eval": 0, + "no-inline-comments": 0, + "no-inner-declarations": 0, + "no-invalid-regexp": 0, + "no-invalid-this": 0, + "no-irregular-whitespace": 0, + "no-iterator": 0, + "no-label-var": 0, + "no-labels": 0, + "no-lone-blocks": 0, + "no-lonely-if": 0, + "no-loop-func": 0, + "no-magic-numbers": 0, + "no-misleading-character-class": 0, + "no-mixed-operators": 0, + "no-mixed-requires": 0, + "no-mixed-spaces-and-tabs": 0, + "no-multi-assign": 0, + "no-multi-spaces": 0, + "no-multi-str": 0, + "no-multiple-empty-lines": 0, + "no-native-reassign": 0, + "no-negated-condition": 0, + "no-negated-in-lhs": 0, + "no-nested-ternary": 0, + "no-new": 0, + "no-new-func": 0, + "no-new-object": 0, + "no-new-require": 0, + "no-new-symbol": 0, + "no-new-wrappers": 0, + "no-obj-calls": 0, + "no-octal": 0, + "no-octal-escape": 0, + "no-param-reassign": 0, + "no-path-concat": 0, + "no-plusplus": 0, + "no-process-env": 0, + "no-process-exit": 0, + "no-proto": 0, + "no-prototype-builtins": 0, + "no-redeclare": 0, + "no-regex-spaces": 0, + "no-restricted-globals": 0, + "no-restricted-imports": 0, + "no-restricted-modules": 0, + "no-restricted-properties": 0, + "no-restricted-syntax": 0, + "no-return-assign": 0, + "no-return-await": 0, + "no-script-url": 0, + "no-self-assign": 0, + "no-self-compare": 0, + "no-sequences": 0, + "no-shadow": 0, + "no-shadow-restricted-names": 0, + "no-spaced-func": 0, + "no-sparse-arrays": 0, + "no-sync": 0, + "no-tabs": 0, + "no-template-curly-in-string": 0, + "no-ternary": 0, + "no-this-before-super": 0, + "no-throw-literal": 0, + "no-trailing-spaces": 0, + "no-undef": 0, + "no-undef-init": 0, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unexpected-multiline": 0, + "no-unmodified-loop-condition": 0, + "no-unneeded-ternary": 0, + "no-unreachable": 0, + "no-unsafe-finally": 0, + "no-unsafe-negation": 0, + "no-unused-expressions": 0, + "no-unused-labels": 0, + "no-unused-vars": 0, + "no-use-before-define": 0, + "no-useless-call": 0, + "no-useless-catch": 0, + "no-useless-computed-key": 0, + "no-useless-concat": 0, + "no-useless-constructor": 0, + "no-useless-escape": 0, + "no-useless-rename": 0, + "no-useless-return": 0, + "no-var": 0, + "no-void": 0, + "no-warning-comments": 0, + "no-whitespace-before-property": 0, + "no-with": 0, + "nonblock-statement-body-position": 0, + "object-curly-newline": 0, + "object-curly-spacing": 0, + "object-property-newline": 0, + "object-shorthand": 0, + "one-var": 0, + "one-var-declaration-per-line": 0, + "operator-assignment": 0, + "operator-linebreak": 0, + "padded-blocks": 0, + "padding-line-between-statements": 0, + "prefer-arrow-callback": 0, + "prefer-const": 0, + "prefer-destructuring": 0, + "prefer-named-capture-group": 0, + "prefer-numeric-literals": 0, + "prefer-object-spread": 0, + "prefer-promise-reject-errors": 0, + "prefer-reflect": 0, + "prefer-rest-params": 0, + "prefer-spread": 0, + "prefer-template": 0, + "quote-props": 0, + "quotes": 0, + "radix": 0, + "require-atomic-updates": 0, + "require-await": 0, + "require-jsdoc": 0, + "require-unicode-regexp": 0, + "require-yield": 0, + "rest-spread-spacing": 0, + "semi": 0, + "semi-spacing": 0, + "semi-style": 0, + "sort-imports": 0, + "sort-keys": 0, + "sort-vars": 0, + "space-before-blocks": 0, + "space-before-function-paren": 0, + "space-in-parens": 0, + "space-infix-ops": 0, + "space-unary-ops": 0, + "spaced-comment": 0, + "strict": 0, + "switch-colon-spacing": 0, + "symbol-description": 0, + "template-curly-spacing": 0, + "template-tag-spacing": 0, + "unicode-bom": 0, + "use-isnan": 0, + "valid-jsdoc": 0, + "valid-typeof": 0, + "vars-on-top": 0, + "wrap-iife": 0, + "wrap-regex": 0, + "yield-star-spacing": 0, + "yoda": 0, + "import/no-unresolved": 0, + "import/named": 0, + "import/namespace": 0, + "import/default": 0, + "import/export": 0, + "import/no-named-as-default": 0, + "import/no-named-as-default-member": 0, + "import/no-duplicates": 0, + "import/no-extraneous-dependencies": 0, + "import/unambiguous": 0 + } +} diff --git a/tests/files/bundled-dependencies/as-array-bundle-deps/node_modules/@generated/bar/index.js b/tests/files/bundled-dependencies/as-array-bundle-deps/node_modules/@generated/bar/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/bundled-dependencies/as-array-bundle-deps/node_modules/@generated/foo/index.js b/tests/files/bundled-dependencies/as-array-bundle-deps/node_modules/@generated/foo/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/bundled-dependencies/as-array-bundle-deps/package.json b/tests/files/bundled-dependencies/as-array-bundle-deps/package.json new file mode 100644 index 0000000000..ef9c675edb --- /dev/null +++ b/tests/files/bundled-dependencies/as-array-bundle-deps/package.json @@ -0,0 +1,4 @@ +{ + "dummy": true, + "bundleDependencies": ["@generated/foo"] +} diff --git a/tests/files/bundled-dependencies/as-object/node_modules/@generated/bar/index.js b/tests/files/bundled-dependencies/as-object/node_modules/@generated/bar/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/bundled-dependencies/as-object/node_modules/@generated/foo/index.js b/tests/files/bundled-dependencies/as-object/node_modules/@generated/foo/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/bundled-dependencies/as-object/package.json b/tests/files/bundled-dependencies/as-object/package.json new file mode 100644 index 0000000000..1a5baff5a9 --- /dev/null +++ b/tests/files/bundled-dependencies/as-object/package.json @@ -0,0 +1,4 @@ +{ + "dummy": true, + "bundledDependencies": {"@generated/foo": "latest"} +} diff --git a/tests/files/bundled-dependencies/race-condition/node_modules/@generated/bar/index.js b/tests/files/bundled-dependencies/race-condition/node_modules/@generated/bar/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/bundled-dependencies/race-condition/node_modules/@generated/foo/index.js b/tests/files/bundled-dependencies/race-condition/node_modules/@generated/foo/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/bundled-dependencies/race-condition/package.json b/tests/files/bundled-dependencies/race-condition/package.json new file mode 100644 index 0000000000..827ecc583e --- /dev/null +++ b/tests/files/bundled-dependencies/race-condition/package.json @@ -0,0 +1,5 @@ +{ + "dummy": true, + "bundledDependencies": {"@generated/bar": "latest"}, + "bundleDependencies": ["@generated/foo"] +} diff --git a/tests/files/cycles/flow-types-depth-one.js b/tests/files/cycles/flow-types-depth-one.js new file mode 100644 index 0000000000..f8a7a4b47c --- /dev/null +++ b/tests/files/cycles/flow-types-depth-one.js @@ -0,0 +1,6 @@ +// @flow + +import type { FooType } from './flow-types-depth-two'; +import { type BarType, bar } from './flow-types-depth-two'; + +export { bar } diff --git a/tests/files/cycles/flow-types-depth-two.js b/tests/files/cycles/flow-types-depth-two.js new file mode 100644 index 0000000000..9058840ac6 --- /dev/null +++ b/tests/files/cycles/flow-types-depth-two.js @@ -0,0 +1 @@ +import { foo } from './depth-one' diff --git a/tests/files/cycles/flow-types.js b/tests/files/cycles/flow-types.js new file mode 100644 index 0000000000..fbfb69f309 --- /dev/null +++ b/tests/files/cycles/flow-types.js @@ -0,0 +1,6 @@ +// @flow + +import type { FooType } from './flow-types-depth-two'; +import { type BarType } from './flow-types-depth-two'; + +export const bar = 1; diff --git a/tests/files/internal-modules/package.json b/tests/files/internal-modules/package.json index e69de29bb2..0967ef424b 100644 --- a/tests/files/internal-modules/package.json +++ b/tests/files/internal-modules/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/files/load-error-resolver.js b/tests/files/load-error-resolver.js index aa9d33010d..7cf7cfa1bc 100644 --- a/tests/files/load-error-resolver.js +++ b/tests/files/load-error-resolver.js @@ -1 +1 @@ -throw new Error('TEST ERROR') +throw new SyntaxError('TEST SYNTAX ERROR') diff --git a/tests/files/no-unused-modules/cjs.js b/tests/files/no-unused-modules/cjs.js new file mode 100644 index 0000000000..d5d7fbb98d --- /dev/null +++ b/tests/files/no-unused-modules/cjs.js @@ -0,0 +1,7 @@ +// Simple import extracted from 'redux-starter-kit' compiled file + +function isPlain(val) { + return true; +} + +exports.isPlain = isPlain; diff --git a/tests/files/no-unused-modules/filte-r.js b/tests/files/no-unused-modules/filte-r.js new file mode 100644 index 0000000000..c5b0dbbfeb --- /dev/null +++ b/tests/files/no-unused-modules/filte-r.js @@ -0,0 +1 @@ +export * from './cjs' diff --git a/tests/files/no-unused-modules/flow-0.js b/tests/files/no-unused-modules/flow-0.js new file mode 100644 index 0000000000..46bda68794 --- /dev/null +++ b/tests/files/no-unused-modules/flow-0.js @@ -0,0 +1 @@ +import { type FooType } from './flow-2'; diff --git a/tests/files/no-unused-modules/flow-1.js b/tests/files/no-unused-modules/flow-1.js new file mode 100644 index 0000000000..bb7266d3ce --- /dev/null +++ b/tests/files/no-unused-modules/flow-1.js @@ -0,0 +1,2 @@ +// @flow strict +export type Bar = number; diff --git a/tests/files/no-unused-modules/flow-2.js b/tests/files/no-unused-modules/flow-2.js new file mode 100644 index 0000000000..0cbb836a6d --- /dev/null +++ b/tests/files/no-unused-modules/flow-2.js @@ -0,0 +1,2 @@ +// @flow strict +export type FooType = string; diff --git a/tests/files/no-unused-modules/jsx/file-jsx-a.jsx b/tests/files/no-unused-modules/jsx/file-jsx-a.jsx new file mode 100644 index 0000000000..1de6d020c8 --- /dev/null +++ b/tests/files/no-unused-modules/jsx/file-jsx-a.jsx @@ -0,0 +1,3 @@ +import {b} from './file-jsx-b'; + +export const a = b + 1; diff --git a/tests/files/no-unused-modules/jsx/file-jsx-b.jsx b/tests/files/no-unused-modules/jsx/file-jsx-b.jsx new file mode 100644 index 0000000000..202103085c --- /dev/null +++ b/tests/files/no-unused-modules/jsx/file-jsx-b.jsx @@ -0,0 +1 @@ +export const b = 2; diff --git a/tests/files/no-unused-modules/typescript/file-ts-a.ts b/tests/files/no-unused-modules/typescript/file-ts-a.ts new file mode 100644 index 0000000000..a4272256e6 --- /dev/null +++ b/tests/files/no-unused-modules/typescript/file-ts-a.ts @@ -0,0 +1,3 @@ +import {b} from './file-ts-b'; + +export const a = b + 1; diff --git a/tests/files/no-unused-modules/typescript/file-ts-b.ts b/tests/files/no-unused-modules/typescript/file-ts-b.ts new file mode 100644 index 0000000000..202103085c --- /dev/null +++ b/tests/files/no-unused-modules/typescript/file-ts-b.ts @@ -0,0 +1 @@ +export const b = 2; diff --git a/tests/files/node_modules/@generated/bar/index.js b/tests/files/node_modules/@generated/bar/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/node_modules/@generated/foo/index.js b/tests/files/node_modules/@generated/foo/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/node_modules/eslint-import-resolver-foo/index.js b/tests/files/node_modules/eslint-import-resolver-foo/index.js new file mode 120000 index 0000000000..d194dba0df --- /dev/null +++ b/tests/files/node_modules/eslint-import-resolver-foo/index.js @@ -0,0 +1 @@ +../../foo-bar-resolver-v2.js \ No newline at end of file diff --git a/tests/files/package.json b/tests/files/package.json index 0a60f28d36..0ca8e77737 100644 --- a/tests/files/package.json +++ b/tests/files/package.json @@ -15,5 +15,6 @@ }, "optionalDependencies": { "lodash.isarray": "^4.0.0" - } + }, + "bundledDependencies": ["@generated/foo"] } diff --git a/tests/files/restricted-paths/server/one/a.js b/tests/files/restricted-paths/server/one/a.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/restricted-paths/server/one/b.js b/tests/files/restricted-paths/server/one/b.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/restricted-paths/server/two/a.js b/tests/files/restricted-paths/server/two/a.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/files/typescript-export-assign-merged.d.ts b/tests/files/typescript-export-assign-merged.d.ts new file mode 100644 index 0000000000..377a10d20c --- /dev/null +++ b/tests/files/typescript-export-assign-merged.d.ts @@ -0,0 +1,41 @@ +export = AssignedNamespace; + +declare namespace AssignedNamespace { + type MyType = string + enum MyEnum { + Foo, + Bar, + Baz + } +} + +declare namespace AssignedNamespace { + interface Foo { + native: string | number + typedef: MyType + enum: MyEnum + } + + abstract class Bar { + abstract foo(): Foo + + method(); + } + + export function getFoo() : MyType; + + export module MyModule { + export function ModuleFunction(); + } + + export namespace MyNamespace { + export function NamespaceFunction(); + + export module NSModule { + export function NSModuleFunction(); + } + } + + // Export-assignment exports all members in the namespace, explicitly exported or not. + // interface NotExported {} +} diff --git a/tests/src/config/typescript.js b/tests/src/config/typescript.js new file mode 100644 index 0000000000..d5e3ec8507 --- /dev/null +++ b/tests/src/config/typescript.js @@ -0,0 +1,14 @@ +import path from 'path' +import { expect } from 'chai' + +const config = require(path.join(__dirname, '..', '..', '..', 'config', 'typescript')) + +describe('config typescript', () => { + // https://github.com/benmosher/eslint-plugin-import/issues/1525 + it('should mark @types paths as external', () => { + const externalModuleFolders = config.settings['import/external-module-folders'] + expect(externalModuleFolders).to.exist + expect(externalModuleFolders).to.contain('node_modules') + expect(externalModuleFolders).to.contain('node_modules/@types') + }) +}) diff --git a/tests/src/core/eslintParser.js b/tests/src/core/eslintParser.js new file mode 100644 index 0000000000..3870ccc6e4 --- /dev/null +++ b/tests/src/core/eslintParser.js @@ -0,0 +1,7 @@ +module.exports = { + parseForESLint: function() { + return { + ast: {}, + } + }, +} diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 44edcf6292..d61544e7a9 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -85,7 +85,7 @@ describe('ExportMap', function () { var imports = ExportMap.parse( path, contents, - { parserPath: 'babel-eslint', settings: {} } + { parserPath: 'babel-eslint', settings: {} }, ) expect(imports, 'imports').to.exist diff --git a/tests/src/core/importType.js b/tests/src/core/importType.js index f60063991d..034b3cbbcf 100644 --- a/tests/src/core/importType.js +++ b/tests/src/core/importType.js @@ -30,6 +30,7 @@ describe('importType(name)', function () { }) it("should return 'external' for scopes packages", function() { + expect(importType('@cycle/', context)).to.equal('external') expect(importType('@cycle/core', context)).to.equal('external') expect(importType('@cycle/dom', context)).to.equal('external') expect(importType('@some-thing/something', context)).to.equal('external') @@ -43,18 +44,19 @@ describe('importType(name)', function () { }) it("should return 'internal' for non-builtins resolved outside of node_modules", function () { - const pathContext = testContext({ "import/resolver": { node: { paths: [pathToTestFiles] } } }) + const pathContext = testContext({ 'import/resolver': { node: { paths: [pathToTestFiles] } } }) expect(importType('importType', pathContext)).to.equal('internal') }) it.skip("should return 'internal' for scoped packages resolved outside of node_modules", function () { - const pathContext = testContext({ "import/resolver": { node: { paths: [pathToTestFiles] } } }) + const pathContext = testContext({ 'import/resolver': { node: { paths: [pathToTestFiles] } } }) expect(importType('@importType/index', pathContext)).to.equal('internal') }) - + it("should return 'internal' for internal modules that are referenced by aliases", function () { const pathContext = testContext({ 'import/resolver': { node: { paths: [pathToTestFiles] } } }) expect(importType('@my-alias/fn', pathContext)).to.equal('internal') + expect(importType('@importType', pathContext)).to.equal('internal') }) it("should return 'internal' for aliased internal modules that look like core modules (node resolver)", function () { @@ -96,7 +98,6 @@ describe('importType(name)', function () { }) it("should return 'unknown' for any unhandled cases", function() { - expect(importType('@malformed', context)).to.equal('unknown') expect(importType(' /malformed', context)).to.equal('unknown') expect(importType(' foo', context)).to.equal('unknown') }) @@ -130,6 +131,16 @@ describe('importType(name)', function () { expect(importType('resolve', foldersContext)).to.equal('internal') }) + it("should return 'internal' for module from 'node_modules' if its name matched 'internal-regex'", function() { + const foldersContext = testContext({ 'import/internal-regex': '^@org' }) + expect(importType('@org/foobar', foldersContext)).to.equal('internal') + }) + + it("should return 'external' for module from 'node_modules' if its name did not match 'internal-regex'", function() { + const foldersContext = testContext({ 'import/internal-regex': '^@bar' }) + expect(importType('@org/foobar', foldersContext)).to.equal('external') + }) + it("should return 'external' for module from 'node_modules' if 'node_modules' contained in 'external-module-folders'", function() { const foldersContext = testContext({ 'import/external-module-folders': ['node_modules'] }) expect(importType('resolve', foldersContext)).to.equal('external') diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 4d8b5ab58c..72d232730d 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -9,6 +9,8 @@ describe('parse(content, { settings, ecmaFeatures })', function () { const path = getFilename('jsx.js') const parseStubParser = require('./parseStubParser') const parseStubParserPath = require.resolve('./parseStubParser') + const eslintParser = require('./eslintParser') + const eslintParserPath = require.resolve('./eslintParser') let content before((done) => @@ -43,6 +45,15 @@ describe('parse(content, { settings, ecmaFeatures })', function () { 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) }) + it('passes with custom `parseForESLint` parser', function () { + const parseForESLintSpy = sinon.spy(eslintParser, 'parseForESLint') + const parseSpy = sinon.spy() + eslintParser.parse = parseSpy + parse(path, content, { settings: {}, parserPath: eslintParserPath }) + expect(parseForESLintSpy.callCount, 'custom `parseForESLint` parser to be called once').to.equal(1) + expect(parseSpy.callCount, '`parseForESLint` takes higher priority than `parse`').to.equal(0) + }) + it('throws on context == null', function () { expect(parse.bind(null, path, content, null)).to.throw(Error) }) diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index b9a9063243..1aa2071db6 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -8,6 +8,11 @@ import * as fs from 'fs' import * as utils from '../utils' describe('resolve', function () { + // We don't want to test for a specific stack, just that it was there in the error message. + function replaceErrorStackForTest(str) { + return typeof str === 'string' ? str.replace(/(\n\s+at .+:\d+\)?)+$/, '\n') : str + } + it('throws on bad parameters', function () { expect(resolve.bind(null, null, null)).to.throw(Error) }) @@ -16,15 +21,15 @@ describe('resolve', function () { const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-v1' }) expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), )).to.equal(utils.testFilePath('./bar.jsx')) expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }), )).to.equal(undefined) expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('not-found.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('not-found.js') } }), )).to.equal(undefined) }) @@ -32,15 +37,15 @@ describe('resolve', function () { const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-no-version' }) expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), )).to.equal(utils.testFilePath('./bar.jsx')) expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }), )).to.equal(undefined) expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('not-found.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('not-found.js') } }), )).to.equal(undefined) }) @@ -52,20 +57,20 @@ describe('resolve', function () { } expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), )).to.equal(utils.testFilePath('./bar.jsx')) testContextReports.length = 0 expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }), )).to.equal(undefined) expect(testContextReports[0]).to.be.an('object') - expect(testContextReports[0].message).to.equal('Resolve error: foo-bar-resolver-v2 resolve test exception') + expect(replaceErrorStackForTest(testContextReports[0].message)).to.equal('Resolve error: foo-bar-resolver-v2 resolve test exception\n') expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 }) testContextReports.length = 0 expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('not-found.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('not-found.js') } }), )).to.equal(undefined) expect(testContextReports.length).to.equal(0) }) @@ -74,7 +79,7 @@ describe('resolve', function () { const testContext = utils.testContext({ 'import/resolver': [ './foo-bar-resolver-v2', './foo-bar-resolver-v1' ] }) expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), )).to.equal(utils.testFilePath('./bar.jsx')) }) @@ -82,7 +87,7 @@ describe('resolve', function () { const testContext = utils.testContext({ 'import/resolver': { './foo-bar-resolver-v2': {} } }) expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), )).to.equal(utils.testFilePath('./bar.jsx')) }) @@ -90,7 +95,15 @@ describe('resolve', function () { const testContext = utils.testContext({ 'import/resolver': [ { './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} } ] }) expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), + )).to.equal(utils.testFilePath('./bar.jsx')) + }) + + it('finds resolvers from the source files rather than eslint-module-utils', function () { + const testContext = utils.testContext({ 'import/resolver': { 'foo': {} } }) + + expect(resolve( '../files/foo' + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }), )).to.equal(utils.testFilePath('./bar.jsx')) }) @@ -103,7 +116,7 @@ describe('resolve', function () { testContextReports.length = 0 expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('foo.js') } }) + , 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: invalid resolver config') @@ -111,15 +124,15 @@ describe('resolve', function () { }) it('reports loaded resolver with invalid interface', function () { - const resolverName = './foo-bar-resolver-invalid'; - const testContext = utils.testContext({ 'import/resolver': resolverName }); + 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') } }) + , 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`) @@ -130,7 +143,7 @@ describe('resolve', function () { const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] }}) expect(resolve( './jsx/MyCoolComponent' - , testContext + , testContext, )).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx')) }) @@ -142,10 +155,10 @@ describe('resolve', function () { } expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }) + , Object.assign({}, testContext, { getFilename: function () { return utils.getFilename('exception.js') } }), )).to.equal(undefined) expect(testContextReports[0]).to.be.an('object') - expect(testContextReports[0].message).to.equal('Resolve error: TEST ERROR') + expect(replaceErrorStackForTest(testContextReports[0].message)).to.equal('Resolve error: SyntaxError: TEST SYNTAX ERROR\n') expect(testContextReports[0].loc).to.eql({ line: 1, column: 0 }) }) diff --git a/tests/src/rules/default.js b/tests/src/rules/default.js index c02b364489..c21f1fd8c2 100644 --- a/tests/src/rules/default.js +++ b/tests/src/rules/default.js @@ -102,7 +102,7 @@ ruleTester.run('default', rule, { test({ code: 'import baz from "./named-exports";', - errors: [{ message: 'No default export found in module.' + errors: [{ message: 'No default export found in imported module "./named-exports".' , type: 'ImportDefaultSpecifier'}]}), test({ @@ -114,29 +114,29 @@ ruleTester.run('default', rule, { test({ code: 'export baz from "./named-exports"', parser: require.resolve('babel-eslint'), - errors: ['No default export found in module.'], + errors: ['No default export found in imported module "./named-exports".'], }), test({ code: 'export baz, { bar } from "./named-exports"', parser: require.resolve('babel-eslint'), - errors: ['No default export found in module.'], + errors: ['No default export found in imported module "./named-exports".'], }), test({ code: 'export baz, * as names from "./named-exports"', parser: require.resolve('babel-eslint'), - errors: ['No default export found in module.'], + errors: ['No default export found in imported module "./named-exports".'], }), // exports default from a module with no default test({ code: 'import twofer from "./broken-trampoline"', parser: require.resolve('babel-eslint'), - errors: ['No default export found in module.'], + errors: ['No default export found in imported module "./broken-trampoline".'], }), // #328: * exports do not include default test({ code: 'import barDefault from "./re-export"', - errors: [`No default export found in module.`], + errors: ['No default export found in imported module "./re-export".'], }), ], }) @@ -152,7 +152,7 @@ if (!CASE_SENSITIVE_FS) { invalid: [ test({ code: 'import bar from "./Named-Exports"', - errors: ['No default export found in module.'], + errors: ['No default export found in imported module "./Named-Exports".'], }), ], }) diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index a858250e2b..c7f303c4dd 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -108,7 +108,7 @@ ruleTester.run('export', rule, { }) -context('Typescript', function () { +context('TypeScript', function () { getTSParsers().forEach((parser) => { const parserConfig = { parser: parser, diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index 871c62e85c..770e123d31 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -8,8 +8,8 @@ const ruleTester = new RuleTester() const error = type => ({ ruleId: 'exports-last', message: 'Export statements should appear at the end of the file', - type -}); + type, +}) ruleTester.run('exports-last', rule, { valid: [ diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index d7b97bea0b..562802eef3 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -63,7 +63,7 @@ ruleTester.run('extensions', rule, { code: ` import foo from './foo.js' import bar from './bar.json' - import Component from './Component' + import Component from './Component.jsx' import express from 'express' `, options: [ 'ignorePackages' ], @@ -116,6 +116,35 @@ ruleTester.run('extensions', rule, { ].join('\n'), options: [ 'never' ], }), + + // Root packages should be ignored and they are names not files + test({ + code: [ + 'import lib from "pkg.js"', + 'import lib2 from "pgk/package"', + 'import lib3 from "@name/pkg.js"', + ].join('\n'), + options: [ 'never' ], + }), + + // Query strings. + test({ + code: 'import bare from "./foo?a=True.ext"', + options: [ 'never' ], + }), + test({ + code: 'import bare from "./foo.js?a=True"', + options: [ 'always' ], + }), + + test({ + code: [ + 'import lib from "pkg"', + 'import lib2 from "pgk/package.js"', + 'import lib3 from "@name/pkg"', + ].join('\n'), + options: [ 'always' ], + }), ], invalid: [ @@ -127,15 +156,6 @@ ruleTester.run('extensions', rule, { column: 15, } ], }), - test({ - code: 'import a from "a"', - options: [ 'always' ], - errors: [ { - message: 'Missing file extension "js" for "a"', - line: 1, - column: 15, - } ], - }), test({ code: 'import dot from "./file.with.dot"', options: [ 'always' ], @@ -275,11 +295,35 @@ ruleTester.run('extensions', rule, { ], }), test({ - code: 'import thing from "non-package"', + code: 'import thing from "non-package/test"', options: [ 'always' ], errors: [ { - message: 'Missing file extension for "non-package"', + message: 'Missing file extension for "non-package/test"', + line: 1, + column: 19, + }, + ], + }), + + test({ + code: 'import thing from "@name/pkg/test"', + options: [ 'always' ], + errors: [ + { + message: 'Missing file extension for "@name/pkg/test"', + line: 1, + column: 19, + }, + ], + }), + + test({ + code: 'import thing from "@name/pkg/test.js"', + options: [ 'never' ], + errors: [ + { + message: 'Unexpected use of file extension "js" for "@name/pkg/test.js"', line: 1, column: 19, }, @@ -293,6 +337,7 @@ ruleTester.run('extensions', rule, { import bar from './bar.json' import Component from './Component' import baz from 'foo/baz' + import baw from '@scoped/baw/import' import express from 'express' `, options: [ 'always', {ignorePackages: true} ], @@ -301,10 +346,25 @@ ruleTester.run('extensions', rule, { message: 'Missing file extension for "./Component"', line: 4, column: 31, - }, { - message: 'Missing file extension for "foo/baz"', - line: 5, - column: 25, + }, + ], + }), + + test({ + code: ` + import foo from './foo.js' + import bar from './bar.json' + import Component from './Component' + import baz from 'foo/baz' + import baw from '@scoped/baw/import' + import express from 'express' + `, + options: [ 'ignorePackages' ], + errors: [ + { + message: 'Missing file extension for "./Component"', + line: 4, + column: 31, }, ], }), @@ -359,5 +419,29 @@ ruleTester.run('extensions', rule, { }, ], }), + + // Query strings. + test({ + code: 'import withExtension from "./foo.js?a=True"', + options: [ 'never' ], + errors: [ + { + message: 'Unexpected use of file extension "js" for "./foo.js?a=True"', + line: 1, + column: 27, + }, + ], + }), + test({ + code: 'import withoutExtension from "./foo?a=True.ext"', + options: [ 'always' ], + errors: [ + { + message: 'Missing file extension for "./foo?a=True.ext"', + line: 1, + column: 30, + }, + ], + }), ], }) diff --git a/tests/src/rules/first.js b/tests/src/rules/first.js index 6a0fcdd649..55367cf43c 100644 --- a/tests/src/rules/first.js +++ b/tests/src/rules/first.js @@ -22,7 +22,7 @@ ruleTester.run('first', rule, { , errors: 1 , output: "import { x } from './foo';\ import { y } from './bar';\ - export { x };" + export { x };", }) , test({ code: "import { x } from './foo';\ export { x };\ @@ -32,11 +32,11 @@ ruleTester.run('first', rule, { , output: "import { x } from './foo';\ import { y } from './bar';\ import { z } from './baz';\ - export { x };" + export { x };", }) , test({ code: "import { x } from './foo'; import { y } from 'bar'" , options: ['absolute-first'] - , errors: 1 + , errors: 1, }) , test({ code: "import { x } from 'foo';\ 'use directive';\ @@ -44,7 +44,7 @@ ruleTester.run('first', rule, { , errors: 1 , output: "import { x } from 'foo';\ import { y } from 'bar';\ - 'use directive';" + 'use directive';", }) , test({ code: "var a = 1;\ import { y } from './bar';\ @@ -56,12 +56,12 @@ ruleTester.run('first', rule, { var a = 1;\ if (true) { x() };\ import { x } from './foo';\ - import { z } from './baz';" + import { z } from './baz';", }) , test({ code: "if (true) { console.log(1) }import a from 'b'" , errors: 1 - , output: "import a from 'b'\nif (true) { console.log(1) }" + , output: "import a from 'b'\nif (true) { console.log(1) }", }) , - ] + ], }) diff --git a/tests/src/rules/group-exports.js b/tests/src/rules/group-exports.js index 3b08997e33..9a0c2c1ba7 100644 --- a/tests/src/rules/group-exports.js +++ b/tests/src/rules/group-exports.js @@ -45,6 +45,10 @@ ruleTester.run('group-exports', rule, { // test export default {} ` }), + test({ code: ` + export { default as module1 } from './module-1' + export { default as module2 } from './module-2' + ` }), test({ code: 'module.exports = {} '}), test({ code: ` module.exports = { test: true, @@ -111,6 +115,16 @@ ruleTester.run('group-exports', rule, { errors.named, ], }), + test({ + code: ` + export { method1 } from './module-1' + export { method2 } from './module-1' + `, + errors: [ + errors.named, + errors.named, + ], + }), test({ code: ` module.exports = {} diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index ec8a1dbecd..8318066496 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -197,7 +197,7 @@ ruleTester.run('named', rule, { test({ code: 'import { baz } from "./broken-trampoline"', parser: require.resolve('babel-eslint'), - errors: ["baz not found via broken-trampoline.js -> named-exports.js"], + errors: ['baz not found via broken-trampoline.js -> named-exports.js'], }), // parse errors @@ -282,9 +282,9 @@ ruleTester.run('named (export *)', rule, { }) -context('Typescript', function () { +context('TypeScript', function () { getTSParsers().forEach((parser) => { - ['typescript', 'typescript-declare', 'typescript-export-assign'].forEach((source) => { + ['typescript', 'typescript-declare', 'typescript-export-assign', 'typescript-export-assign-merged'].forEach((source) => { ruleTester.run(`named`, rule, { valid: [ test({ diff --git a/tests/src/rules/no-commonjs.js b/tests/src/rules/no-commonjs.js index 8ca8fde509..1bcbc65ab3 100644 --- a/tests/src/rules/no-commonjs.js +++ b/tests/src/rules/no-commonjs.js @@ -56,6 +56,13 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { { code: 'module.exports = function () {}', options: [{ allowPrimitiveModules: true }] }, { code: 'module.exports = "foo"', options: ['allow-primitive-modules'] }, { code: 'module.exports = "foo"', options: [{ allowPrimitiveModules: true }] }, + + { code: 'if (typeof window !== "undefined") require("x")', options: [{ allowRequire: true }] }, + { code: 'if (typeof window !== "undefined") require("x")', options: [{ allowRequire: false }] }, + { code: 'if (typeof window !== "undefined") { require("x") }', options: [{ allowRequire: true }] }, + { code: 'if (typeof window !== "undefined") { require("x") }', options: [{ allowRequire: false }] }, + + { code: 'try { require("x") } catch (error) {}' }, ], invalid: [ @@ -65,6 +72,19 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { { code: 'var x = require("x")', errors: [ { message: IMPORT_MESSAGE }] }, { code: 'x = require("x")', errors: [ { message: IMPORT_MESSAGE }] }, { code: 'require("x")', errors: [ { message: IMPORT_MESSAGE }] }, + + { code: 'if (typeof window !== "undefined") require("x")', + options: [{ allowConditionalRequire: false }], + errors: [ { message: IMPORT_MESSAGE }], + }, + { code: 'if (typeof window !== "undefined") { require("x") }', + options: [{ allowConditionalRequire: false }], + errors: [ { message: IMPORT_MESSAGE }], + }, + { code: 'try { require("x") } catch (error) {}', + options: [{ allowConditionalRequire: false }], + errors: [ { message: IMPORT_MESSAGE }], + }, ]), // exports diff --git a/tests/src/rules/no-cycle.js b/tests/src/rules/no-cycle.js index 18fe88af18..df1e6d1433 100644 --- a/tests/src/rules/no-cycle.js +++ b/tests/src/rules/no-cycle.js @@ -53,6 +53,10 @@ ruleTester.run('no-cycle', rule, { code: 'import type { FooType, BarType } from "./depth-one"', parser: require.resolve('babel-eslint'), }), + test({ + code: 'import { bar } from "./flow-types"', + parser: require.resolve('babel-eslint'), + }), ], invalid: [ test({ @@ -120,6 +124,11 @@ ruleTester.run('no-cycle', rule, { errors: [error(`Dependency cycle via ./depth-two:1=>./depth-one:1`)], parser: require.resolve('babel-eslint'), }), + test({ + code: 'import { bar } from "./flow-types-depth-one"', + parser: require.resolve('babel-eslint'), + errors: [error(`Dependency cycle via ./flow-types-depth-two:4=>./depth-one:1`)], + }), ], }) // }) diff --git a/tests/src/rules/no-deprecated.js b/tests/src/rules/no-deprecated.js index 28b8734f7e..36a137f7ad 100644 --- a/tests/src/rules/no-deprecated.js +++ b/tests/src/rules/no-deprecated.js @@ -15,16 +15,16 @@ ruleTester.run('no-deprecated', rule, { test({ code: "import { fn } from './deprecated'", - settings: { 'import/docstyle': ['tomdoc'] } + settings: { 'import/docstyle': ['tomdoc'] }, }), test({ code: "import { fine } from './tomdoc-deprecated'", - settings: { 'import/docstyle': ['tomdoc'] } + settings: { 'import/docstyle': ['tomdoc'] }, }), test({ code: "import { _undocumented } from './tomdoc-deprecated'", - settings: { 'import/docstyle': ['tomdoc'] } + settings: { 'import/docstyle': ['tomdoc'] }, }), // naked namespace is fine @@ -70,7 +70,7 @@ ruleTester.run('no-deprecated', rule, { test({ code: "import { fn } from './tomdoc-deprecated'", settings: { 'import/docstyle': ['tomdoc'] }, - errors: ["Deprecated: This function is terrible."], + errors: ['Deprecated: This function is terrible.'], }), test({ @@ -198,7 +198,7 @@ ruleTester.run('no-deprecated: hoisting', rule, { ], }) -describe('Typescript', function () { +describe('TypeScript', function () { getTSParsers().forEach((parser) => { const parserConfig = { parser: parser, diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index 29b080466f..a4c41f677a 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -25,6 +25,18 @@ ruleTester.run('no-duplicates', rule, { code: "import { x } from './foo'; import type { y } from './foo'", parser: require.resolve('babel-eslint'), }), + + // #1107: Using different query strings that trigger different webpack loaders. + test({ + code: "import x from './bar?optionX'; import y from './bar?optionY';", + options: [{'considerQueryString': true}], + settings: { 'import/resolver': 'webpack' }, + }), + test({ + code: "import x from './foo'; import y from './bar';", + options: [{'considerQueryString': true}], + settings: { 'import/resolver': 'webpack' }, + }), ], invalid: [ test({ @@ -45,11 +57,31 @@ ruleTester.run('no-duplicates', rule, { output: "import { x , y } from './bar'; ", settings: { 'import/resolve': { paths: [path.join( process.cwd() - , 'tests', 'files' + , 'tests', 'files', )] }}, errors: 2, // path ends up hardcoded }), + // #1107: Using different query strings that trigger different webpack loaders. + test({ + code: "import x from './bar.js?optionX'; import y from './bar?optionX';", + settings: { 'import/resolver': 'webpack' }, + errors: 2, // path ends up hardcoded + }), + test({ + code: "import x from './bar?optionX'; import y from './bar?optionY';", + settings: { 'import/resolver': 'webpack' }, + errors: 2, // path ends up hardcoded + }), + + // #1107: Using same query strings that trigger the same loader. + test({ + code: "import x from './bar?optionX'; import y from './bar.js?optionX';", + options: [{'considerQueryString': true}], + settings: { 'import/resolver': 'webpack' }, + errors: 2, // path ends up hardcoded + }), + // #86: duplicate unresolved modules should be flagged test({ code: "import foo from 'non-existent'; import bar from 'non-existent';", diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index b9d24580ec..114a733af7 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -18,6 +18,9 @@ const packageDirWithFlowTyped = path.join(__dirname, '../../files/with-flow-type const packageDirMonoRepoRoot = path.join(__dirname, '../../files/monorepo') const packageDirMonoRepoWithNested = path.join(__dirname, '../../files/monorepo/packages/nested-package') const packageDirWithEmpty = path.join(__dirname, '../../files/empty') +const packageDirBundleDeps = path.join(__dirname, '../../files/bundled-dependencies/as-array-bundle-deps') +const packageDirBundledDepsAsObject = path.join(__dirname, '../../files/bundled-dependencies/as-object') +const packageDirBundledDepsRaceCondition = path.join(__dirname, '../../files/bundled-dependencies/race-condition') ruleTester.run('no-extraneous-dependencies', rule, { valid: [ @@ -106,6 +109,25 @@ ruleTester.run('no-extraneous-dependencies', rule, { code: 'import rightpad from "right-pad";', options: [{packageDir: [packageDirMonoRepoRoot, packageDirMonoRepoWithNested]}], }), + test({ code: 'import foo from "@generated/foo"'}), + test({ + code: 'import foo from "@generated/foo"', + options: [{packageDir: packageDirBundleDeps}], + }), + test({ + code: 'import foo from "@generated/foo"', + options: [{packageDir: packageDirBundledDepsAsObject}], + }), + test({ + code: 'import foo from "@generated/foo"', + options: [{packageDir: packageDirBundledDepsRaceCondition}], + }), + test({ code: 'export { foo } from "lodash.cond"' }), + test({ code: 'export * from "lodash.cond"' }), + test({ code: 'export function getToken() {}' }), + test({ code: 'export class Component extends React.Component {}' }), + test({ code: 'export function Component() {}' }), + test({ code: 'export const Component = () => {}' }), ], invalid: [ test({ @@ -289,5 +311,33 @@ 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 bar from "@generated/bar"', + errors: ["'@generated/bar' should be listed in the project's dependencies. Run 'npm i -S @generated/bar' to add it"], + }), + test({ + code: 'import foo from "@generated/foo"', + options: [{bundledDependencies: false}], + errors: ["'@generated/foo' should be listed in the project's dependencies. Run 'npm i -S @generated/foo' to add it"], + }), + test({ + code: 'import bar from "@generated/bar"', + options: [{packageDir: packageDirBundledDepsRaceCondition}], + errors: ["'@generated/bar' should be listed in the project's dependencies. Run 'npm i -S @generated/bar' to add it"], + }), + test({ + code: 'export { foo } from "not-a-dependency";', + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: '\'not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it', + }], + }), + test({ + code: 'export * from "not-a-dependency";', + errors: [{ + ruleId: 'no-extraneous-dependencies', + message: '\'not-a-dependency\' should be listed in the project\'s dependencies. Run \'npm i -S not-a-dependency\' to add it', + }], + }), ], }) diff --git a/tests/src/rules/no-namespace.js b/tests/src/rules/no-namespace.js index d9ef3423c2..a7cb4dd21f 100644 --- a/tests/src/rules/no-namespace.js +++ b/tests/src/rules/no-namespace.js @@ -1,44 +1,113 @@ import { RuleTester } from 'eslint' +import eslintPkg from 'eslint/package.json' +import semver from 'semver' +import { test } from '../utils' const ERROR_MESSAGE = 'Unexpected namespace import.' const ruleTester = new RuleTester() +// --fix functionality requires ESLint 5+ +const FIX_TESTS = semver.satisfies(eslintPkg.version, '>5.0.0') ? [ + test({ + code: ` + import * as foo from './foo'; + florp(foo.bar); + florp(foo['baz']); + `.trim(), + output: ` + import { bar, baz } from './foo'; + florp(bar); + florp(baz); + `.trim(), + errors: [ { + line: 1, + column: 8, + message: ERROR_MESSAGE, + }], + }), + test({ + code: ` + import * as foo from './foo'; + const bar = 'name conflict'; + const baz = 'name conflict'; + const foo_baz = 'name conflict'; + florp(foo.bar); + florp(foo['baz']); + `.trim(), + output: ` + import { bar as foo_bar, baz as foo_baz_1 } from './foo'; + const bar = 'name conflict'; + const baz = 'name conflict'; + const foo_baz = 'name conflict'; + florp(foo_bar); + florp(foo_baz_1); + `.trim(), + errors: [ { + line: 1, + column: 8, + message: ERROR_MESSAGE, + }], + }), + test({ + code: ` + import * as foo from './foo'; + function func(arg) { + florp(foo.func); + florp(foo['arg']); + } + `.trim(), + output: ` + import { func as foo_func, arg as foo_arg } from './foo'; + function func(arg) { + florp(foo_func); + florp(foo_arg); + } + `.trim(), + errors: [ { + line: 1, + column: 8, + message: ERROR_MESSAGE, + }], + }), +] : [] + ruleTester.run('no-namespace', require('rules/no-namespace'), { valid: [ - { code: "import { a, b } from 'foo';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, - { code: "import { a, b } from './foo';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, - { code: "import bar from 'bar';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, - { code: "import bar from './bar';", parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, + { code: 'import { a, b } from \'foo\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, + { code: 'import { a, b } from \'./foo\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, + { code: 'import bar from \'bar\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, + { code: 'import bar from \'./bar\';', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, ], invalid: [ - { - code: "import * as foo from 'foo';", + test({ + code: 'import * as foo from \'foo\';', + output: 'import * as foo from \'foo\';', errors: [ { line: 1, column: 8, message: ERROR_MESSAGE, } ], - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: "import defaultExport, * as foo from 'foo';", + }), + test({ + code: 'import defaultExport, * as foo from \'foo\';', + output: 'import defaultExport, * as foo from \'foo\';', errors: [ { line: 1, column: 23, message: ERROR_MESSAGE, } ], - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, - { - code: "import * as foo from './foo';", + }), + test({ + code: 'import * as foo from \'./foo\';', + output: 'import * as foo from \'./foo\';', errors: [ { line: 1, column: 8, message: ERROR_MESSAGE, } ], - parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - }, + }), + ...FIX_TESTS, ], }) diff --git a/tests/src/rules/no-restricted-paths.js b/tests/src/rules/no-restricted-paths.js index 13f8472cb1..1c3edb3dae 100644 --- a/tests/src/rules/no-restricted-paths.js +++ b/tests/src/rules/no-restricted-paths.js @@ -28,6 +28,28 @@ ruleTester.run('no-restricted-paths', rule, { zones: [ { target: './tests/files/restricted-paths/client', from: './tests/files/restricted-paths/other' } ], } ], }), + test({ + code: 'import a from "./a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['./one'], + } ], + } ], + }), + test({ + code: 'import a from "../two/a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['./two'], + } ], + } ], + }), // irrelevant function calls @@ -37,14 +59,14 @@ ruleTester.run('no-restricted-paths', rule, { filename: testFilePath('./restricted-paths/client/a.js'), options: [ { zones: [ { target: './tests/files/restricted-paths/client', from: './tests/files/restricted-paths/server' } ], - } ], }), + } ] }), // no config test({ code: 'require("../server/b.js")' }), test({ code: 'import b from "../server/b.js"' }), // builtin (ignore) - test({ code: 'require("os")' }) + test({ code: 'require("os")' }), ], invalid: [ @@ -107,5 +129,38 @@ ruleTester.run('no-restricted-paths', rule, { column: 19, } ], }), + test({ + code: 'import b from "../two/a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['./one'], + } ], + } ], + errors: [ { + message: 'Unexpected path "../two/a.js" imported in restricted zone.', + line: 1, + column: 15, + } ], + }), + test({ + code: 'import b from "../two/a.js"', + filename: testFilePath('./restricted-paths/server/one/a.js'), + options: [ { + zones: [ { + target: './tests/files/restricted-paths/server/one', + from: './tests/files/restricted-paths/server', + except: ['../client/a'], + } ], + } ], + errors: [ { + message: 'Restricted path exceptions must be descendants of the configured ' + + '`from` path for that zone.', + line: 1, + column: 15, + } ], + }), ], }) diff --git a/tests/src/rules/no-unassigned-import.js b/tests/src/rules/no-unassigned-import.js index 92b2769998..97be736134 100644 --- a/tests/src/rules/no-unassigned-import.js +++ b/tests/src/rules/no-unassigned-import.js @@ -8,7 +8,7 @@ const ruleTester = new RuleTester() const error = { ruleId: 'no-unassigned-import', - message: 'Imported module should be assigned' + message: 'Imported module should be assigned', } ruleTester.run('no-unassigned-import', rule, { diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index 124ac84830..cc3aca8844 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -15,7 +15,7 @@ function runResolverTests(resolver) { function rest(specs) { specs.settings = Object.assign({}, specs.settings, - { 'import/resolver': resolver } + { 'import/resolver': resolver }, ) return test(specs) @@ -302,34 +302,34 @@ ruleTester.run('no-unresolved ignore list', rule, { valid: [ test({ code: 'import "./malformed.js"', - options: [{ ignore: ['\.png$', '\.gif$']}], + options: [{ ignore: ['.png$', '.gif$']}], }), test({ code: 'import "./test.giffy"', - options: [{ ignore: ['\.png$', '\.gif$']}], + options: [{ ignore: ['.png$', '.gif$']}], }), test({ code: 'import "./test.gif"', - options: [{ ignore: ['\.png$', '\.gif$']}], + options: [{ ignore: ['.png$', '.gif$']}], }), test({ code: 'import "./test.png"', - options: [{ ignore: ['\.png$', '\.gif$']}], + options: [{ ignore: ['.png$', '.gif$']}], }), ], invalid:[ test({ code: 'import "./test.gif"', - options: [{ ignore: ['\.png$']}], + options: [{ ignore: ['.png$']}], errors: [ "Unable to resolve path to module './test.gif'." ], }), test({ code: 'import "./test.png"', - options: [{ ignore: ['\.gif$']}], + options: [{ ignore: ['.gif$']}], errors: [ "Unable to resolve path to module './test.png'." ], }), ], @@ -343,9 +343,9 @@ ruleTester.run('no-unresolved unknown resolver', rule, { // logs resolver load error test({ code: 'import "./malformed.js"', - settings: { 'import/resolver': 'foo' }, + settings: { 'import/resolver': 'doesnt-exist' }, errors: [ - `Resolve error: unable to load resolver "foo".`, + `Resolve error: unable to load resolver "doesnt-exist".`, `Unable to resolve path to module './malformed.js'.`, ], }), @@ -353,9 +353,9 @@ ruleTester.run('no-unresolved unknown resolver', rule, { // only logs resolver message once test({ code: 'import "./malformed.js"; import "./fake.js"', - settings: { 'import/resolver': 'foo' }, + settings: { 'import/resolver': 'doesnt-exist' }, errors: [ - `Resolve error: unable to load resolver "foo".`, + `Resolve error: unable to load resolver "doesnt-exist".`, `Unable to resolve path to module './malformed.js'.`, `Unable to resolve path to module './fake.js'.`, ], diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index 8050b56935..cb3d4c103d 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -1,10 +1,13 @@ import { test, testFilePath } from '../utils' +import jsxConfig from '../../../config/react' +import typescriptConfig from '../../../config/typescript' import { RuleTester } from 'eslint' -import { expect } from 'chai' import fs from 'fs' const ruleTester = new RuleTester() + , typescriptRuleTester = new RuleTester(typescriptConfig) + , jsxRuleTester = new RuleTester(jsxConfig) , rule = require('rules/no-unused-modules') const error = message => ({ ruleId: 'no-unused-modules', message }) @@ -19,6 +22,18 @@ const unusedExportsOptions = [{ ignoreExports: [testFilePath('./no-unused-modules/*ignored*.js')], }] +const unusedExportsTypescriptOptions = [{ + unusedExports: true, + src: [testFilePath('./no-unused-modules/typescript')], + ignoreExports: undefined, +}] + +const unusedExportsJsxOptions = [{ + unusedExports: true, + src: [testFilePath('./no-unused-modules/jsx')], + ignoreExports: undefined, +}] + // tests for missing exports ruleTester.run('no-unused-modules', rule, { valid: [ @@ -148,7 +163,7 @@ ruleTester.run('no-unused-modules', rule, { ], }) -// // test for export from +// // test for export from ruleTester.run('no-unused-modules', rule, { valid: [], invalid: [ @@ -454,6 +469,11 @@ describe('test behaviour for new file', () => { test({ options: unusedExportsOptions, code: `export * from '${testFilePath('./no-unused-modules/file-added-0.js')}'`, filename: testFilePath('./no-unused-modules/file-0.js')}), + // Test export * from 'external-compiled-library' + test({ options: unusedExportsOptions, + code: `export * from 'external-compiled-library'`, + filename: testFilePath('./no-unused-modules/file-r.js'), + }), ], invalid: [ test({ options: unusedExportsOptions, @@ -639,3 +659,94 @@ describe('do not report unused export for files mentioned in package.json', () = ], }) }) + +describe('correctly report flow types', () => { + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ + options: unusedExportsOptions, + code: 'import { type FooType } from "./flow-2";', + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/flow-0.js'), + }), + test({ + options: unusedExportsOptions, + code: `// @flow strict + export type FooType = string;`, + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/flow-2.js'), + }), + ], + invalid: [ + test({ + options: unusedExportsOptions, + code: `// @flow strict + export type Bar = string;`, + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/flow-1.js'), + errors: [ + error(`exported declaration 'Bar' not used within other modules`), + ], + }), + ], + }) +}) + +describe('Avoid errors if re-export all from umd compiled library', () => { + ruleTester.run('no-unused-modules', rule, { + valid: [ + test({ options: unusedExportsOptions, + code: `export * from '${testFilePath('./no-unused-modules/bin.js')}'`, + filename: testFilePath('./no-unused-modules/main/index.js')}), + ], + invalid: [], + }) +}) + +describe('correctly work with Typescript only files', () => { + typescriptRuleTester.run('no-unused-modules', rule, { + valid: [ + test({ + options: unusedExportsTypescriptOptions, + code: 'import a from "file-ts-a";', + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), + }), + ], + invalid: [ + test({ + options: unusedExportsTypescriptOptions, + code: `export const b = 2;`, + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/typescript/file-ts-b.ts'), + errors: [ + error(`exported declaration 'b' not used within other modules`), + ], + }), + ], + }) +}) + +describe('correctly work with JSX only files', () => { + jsxRuleTester.run('no-unused-modules', rule, { + valid: [ + test({ + options: unusedExportsJsxOptions, + code: 'import a from "file-jsx-a";', + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/jsx/file-jsx-a.jsx'), + }), + ], + invalid: [ + test({ + options: unusedExportsJsxOptions, + code: `export const b = 2;`, + parser: require.resolve('babel-eslint'), + filename: testFilePath('./no-unused-modules/jsx/file-jsx-b.jsx'), + errors: [ + error(`exported declaration 'b' not used within other modules`), + ], + }), + ], + }) +}) diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index 426c40a104..8ba8b9f1eb 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -1,4 +1,4 @@ -import { test, testVersion, getTSParsers } from '../utils' +import { test, getTSParsers } from '../utils' import { RuleTester } from 'eslint' @@ -164,7 +164,7 @@ ruleTester.run('order', rule, { var index = require('./'); `, }), - // Addijg unknown import types (e.g. using an resolver alias via babel) to the groups. + // Adding unknown import types (e.g. using a resolver alias via babel) to the groups. test({ code: ` import fs from 'fs'; @@ -175,7 +175,7 @@ ruleTester.run('order', rule, { groups: ['builtin', 'external', 'unknown', 'parent', 'sibling', 'index'], }], }), - // Using unknown import types (e.g. using an resolver alias via babel) with + // Using unknown import types (e.g. using a resolver alias via babel) with // an alternative custom group list. test({ code: ` @@ -187,7 +187,7 @@ ruleTester.run('order', rule, { groups: [ 'unknown', 'builtin', 'external', 'parent', 'sibling', 'index' ], }], }), - // Using unknown import types (e.g. using an resolver alias via babel) + // Using unknown import types (e.g. using a resolver alias via babel) // Option: newlines-between: 'always' test({ code: ` @@ -204,6 +204,101 @@ ruleTester.run('order', rule, { }, ], }), + + // Using pathGroups to customize ordering, position 'after' + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { Input } from '~/components/Input'; + import { Button } from '#/components/Button'; + import { add } from './helper';`, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '#/**', group: 'external', position: 'after' }, + ], + }], + }), + // pathGroup without position means "equal" with group + test({ + code: ` + import fs from 'fs'; + import { Input } from '~/components/Input'; + import async from 'async'; + import { Button } from '#/components/Button'; + import _ from 'lodash'; + import { add } from './helper';`, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external' }, + { pattern: '#/**', group: 'external' }, + ], + }], + }), + // Using pathGroups to customize ordering, position 'before' + test({ + code: ` + import fs from 'fs'; + + import { Input } from '~/components/Input'; + + import { Button } from '#/components/Button'; + + import _ from 'lodash'; + + import { add } from './helper';`, + options: [{ + 'newlines-between': 'always', + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'before' }, + { pattern: '#/**', group: 'external', position: 'before' }, + ], + }], + }), + // Using pathGroups to customize ordering, with patternOptions + test({ + code: ` + import fs from 'fs'; + + import _ from 'lodash'; + + import { Input } from '~/components/Input'; + + import { Button } from '!/components/Button'; + + import { add } from './helper';`, + options: [{ + 'newlines-between': 'always', + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '!/**', patternOptions: { nonegate: true }, group: 'external', position: 'after' }, + ], + }], + }), + // Using pathGroups to customize ordering for imports that are recognized as 'external' + // by setting pathGroupsExcludedImportTypes without 'external' + test({ + code: ` + import fs from 'fs'; + + import { Input } from '@app/components/Input'; + + import { Button } from '@app2/components/Button'; + + import _ from 'lodash'; + + import { add } from './helper';`, + options: [{ + 'newlines-between': 'always', + pathGroupsExcludedImportTypes: ['builtin'], + pathGroups: [ + { pattern: '@app/**', group: 'external', position: 'before' }, + { pattern: '@app2/**', group: 'external', position: 'before' }, + ], + }], + }), + // Option: newlines-between: 'always' test({ code: ` @@ -319,7 +414,7 @@ ruleTester.run('order', rule, { } from 'bar'; import external from 'external' `, - options: [{ 'newlines-between': 'always' }] + options: [{ 'newlines-between': 'always' }], }), // Option newlines-between: 'always' with multiline imports #2 test({ @@ -330,7 +425,7 @@ ruleTester.run('order', rule, { import external from 'external' `, - options: [{ 'newlines-between': 'always' }] + options: [{ 'newlines-between': 'always' }], }), // Option newlines-between: 'always' with multiline imports #3 test({ @@ -341,7 +436,7 @@ ruleTester.run('order', rule, { import bar from './sibling'; `, - options: [{ 'newlines-between': 'always' }] + options: [{ 'newlines-between': 'always' }], }), // Option newlines-between: 'always' with not assigned import #1 test({ @@ -353,7 +448,7 @@ ruleTester.run('order', rule, { import _ from 'lodash'; `, - options: [{ 'newlines-between': 'always' }] + options: [{ 'newlines-between': 'always' }], }), // Option newlines-between: 'never' with not assigned import #2 test({ @@ -363,7 +458,7 @@ ruleTester.run('order', rule, { import 'something-else'; import _ from 'lodash'; `, - options: [{ 'newlines-between': 'never' }] + options: [{ 'newlines-between': 'never' }], }), // Option newlines-between: 'always' with not assigned require #1 test({ @@ -375,7 +470,7 @@ ruleTester.run('order', rule, { var _ = require('lodash'); `, - options: [{ 'newlines-between': 'always' }] + options: [{ 'newlines-between': 'always' }], }), // Option newlines-between: 'never' with not assigned require #2 test({ @@ -385,7 +480,7 @@ ruleTester.run('order', rule, { require('something-else'); var _ = require('lodash'); `, - options: [{ 'newlines-between': 'never' }] + options: [{ 'newlines-between': 'never' }], }), // Option newlines-between: 'never' should ignore nested require statement's #1 test({ @@ -402,7 +497,7 @@ ruleTester.run('order', rule, { } } `, - options: [{ 'newlines-between': 'never' }] + options: [{ 'newlines-between': 'never' }], }), // Option newlines-between: 'always' should ignore nested require statement's #2 test({ @@ -418,7 +513,7 @@ ruleTester.run('order', rule, { } } `, - options: [{ 'newlines-between': 'always' }] + options: [{ 'newlines-between': 'always' }], }), // Option: newlines-between: 'always-and-inside-groups' test({ @@ -446,6 +541,62 @@ ruleTester.run('order', rule, { }, ], }), + // Option alphabetize: {order: 'ignore'} + test({ + code: ` + import a from 'foo'; + import b from 'bar'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'ignore'}, + }], + }), + // Option alphabetize: {order: 'asc'} + test({ + code: ` + import c from 'Bar'; + import b from 'bar'; + import a from 'foo'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'asc'}, + }], + }), + // Option alphabetize: {order: 'desc'} + test({ + code: ` + import a from 'foo'; + import b from 'bar'; + import c from 'Bar'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'desc'}, + }], + }), + // Option alphabetize with newlines-between: {order: 'asc', newlines-between: 'always'} + test({ + code: ` + import b from 'Bar'; + import c from 'bar'; + import a from 'foo'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'asc'}, + 'newlines-between': 'always', + }], + }), ], invalid: [ // builtin before external module (require) @@ -573,7 +724,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], }), - // fix order of multile import + // fix order of multiline import test({ code: ` var async = require('async'); @@ -1265,6 +1416,283 @@ ruleTester.run('order', rule, { }, ], }), + // reorder fix cannot cross function call on moving below #1 + test({ + code: ` + const local = require('./local'); + + fn_call(); + + const global1 = require('global1'); + const global2 = require('global2'); + + fn_call(); + `, + output: ` + const local = require('./local'); + + fn_call(); + + const global1 = require('global1'); + const global2 = require('global2'); + + fn_call(); + `, + errors: [{ + ruleId: 'order', + message: '`./local` import should occur after import of `global2`', + }], + }), + // reorder fix cannot cross function call on moving below #2 + test({ + code: ` + const local = require('./local'); + fn_call(); + const global1 = require('global1'); + const global2 = require('global2'); + + fn_call(); + `, + output: ` + const local = require('./local'); + fn_call(); + const global1 = require('global1'); + const global2 = require('global2'); + + fn_call(); + `, + errors: [{ + ruleId: 'order', + message: '`./local` import should occur after import of `global2`', + }], + }), + // reorder fix cannot cross function call on moving below #3 + test({ + code: ` + const local1 = require('./local1'); + const local2 = require('./local2'); + const local3 = require('./local3'); + const local4 = require('./local4'); + fn_call(); + const global1 = require('global1'); + const global2 = require('global2'); + const global3 = require('global3'); + const global4 = require('global4'); + const global5 = require('global5'); + fn_call(); + `, + output: ` + const local1 = require('./local1'); + const local2 = require('./local2'); + const local3 = require('./local3'); + const local4 = require('./local4'); + fn_call(); + const global1 = require('global1'); + const global2 = require('global2'); + const global3 = require('global3'); + const global4 = require('global4'); + const global5 = require('global5'); + fn_call(); + `, + errors: [ + '`./local1` import should occur after import of `global5`', + '`./local2` import should occur after import of `global5`', + '`./local3` import should occur after import of `global5`', + '`./local4` import should occur after import of `global5`', + ], + }), + // reorder fix cannot cross function call on moving below + test(withoutAutofixOutput({ + code: ` + const local = require('./local'); + const global1 = require('global1'); + const global2 = require('global2'); + fn_call(); + const global3 = require('global3'); + + fn_call(); + `, + errors: [{ + ruleId: 'order', + message: '`./local` import should occur after import of `global3`', + }], + })), + // reorder fix cannot cross function call on moving below + // fix imports that not crosses function call only + test({ + code: ` + const local1 = require('./local1'); + const global1 = require('global1'); + const global2 = require('global2'); + fn_call(); + const local2 = require('./local2'); + const global3 = require('global3'); + const global4 = require('global4'); + + fn_call(); + `, + output: ` + const local1 = require('./local1'); + const global1 = require('global1'); + const global2 = require('global2'); + fn_call(); + const global3 = require('global3'); + const global4 = require('global4'); + const local2 = require('./local2'); + + fn_call(); + `, + errors: [ + '`./local1` import should occur after import of `global4`', + '`./local2` import should occur after import of `global4`', + ], + }), + + // pathGroup with position 'after' + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { add } from './helper'; + import { Input } from '~/components/Input'; + `, + output: ` + import fs from 'fs'; + import _ from 'lodash'; + import { Input } from '~/components/Input'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + ], + }], + errors: [{ + ruleId: 'order', + message: '`~/components/Input` import should occur before import of `./helper`', + }], + }), + // pathGroup without position + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { add } from './helper'; + import { Input } from '~/components/Input'; + import async from 'async'; + `, + output: ` + import fs from 'fs'; + import _ from 'lodash'; + import { Input } from '~/components/Input'; + import async from 'async'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external' }, + ], + }], + errors: [{ + ruleId: 'order', + message: '`./helper` import should occur after import of `async`', + }], + }), + // pathGroup with position 'before' + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { add } from './helper'; + import { Input } from '~/components/Input'; + `, + output: ` + import fs from 'fs'; + import { Input } from '~/components/Input'; + import _ from 'lodash'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'before' }, + ], + }], + errors: [{ + ruleId: 'order', + message: '`~/components/Input` import should occur before import of `lodash`', + }], + }), + // multiple pathGroup with different positions for same group, fix for 'after' + test({ + code: ` + import fs from 'fs'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Output } from '~/components/Output'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + import { Export } from '-/components/Export'; + `, + output: ` + import fs from 'fs'; + import { Export } from '-/components/Export'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Output } from '~/components/Output'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '#/**', group: 'external', position: 'after' }, + { pattern: '-/**', group: 'external', position: 'before' }, + { pattern: '$/**', group: 'external', position: 'before' }, + ], + }], + errors: [ + { + ruleId: 'order', + message: '`-/components/Export` import should occur before import of `$/components/Import`', + }, + ], + }), + + // multiple pathGroup with different positions for same group, fix for 'before' + test({ + code: ` + import fs from 'fs'; + import { Export } from '-/components/Export'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + import { Output } from '~/components/Output'; + `, + output: ` + import fs from 'fs'; + import { Export } from '-/components/Export'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Output } from '~/components/Output'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '#/**', group: 'external', position: 'after' }, + { pattern: '-/**', group: 'external', position: 'before' }, + { pattern: '$/**', group: 'external', position: 'before' }, + ], + }], + errors: [ + { + ruleId: 'order', + message: '`~/components/Output` import should occur before import of `#/components/Input`', + }, + ], + }), // reorder fix cannot cross non import or require test(withoutAutofixOutput({ @@ -1278,6 +1706,33 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), + // reorder fix cannot cross function call on moving below (from #1252) + test({ + code: ` + const env = require('./config'); + + Object.keys(env); + + const http = require('http'); + const express = require('express'); + + http.createServer(express()); + `, + output: ` + const env = require('./config'); + + Object.keys(env); + + const http = require('http'); + const express = require('express'); + + http.createServer(express()); + `, + errors: [{ + ruleId: 'order', + message: '`./config` import should occur after import of `express`', + }], + }), // reorder cannot cross non plain requires test(withoutAutofixOutput({ code: ` @@ -1312,7 +1767,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), - // cannot require in case of not assignement require + // cannot require in case of not assignment require test(withoutAutofixOutput({ code: ` var async = require('async'); @@ -1336,7 +1791,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), - // reorder cannot cross variable assignemet (import statement) + // reorder cannot cross variable assignment (import statement) test(withoutAutofixOutput({ code: ` import async from 'async'; @@ -1360,7 +1815,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), - // cannot reorder in case of not assignement import + // cannot reorder in case of not assignment import test(withoutAutofixOutput({ code: ` import async from 'async'; @@ -1387,5 +1842,101 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), + // Option alphabetize: {order: 'asc'} + test({ + code: ` + import b from 'bar'; + import c from 'Bar'; + import a from 'foo'; + + import index from './'; + `, + output: ` + import c from 'Bar'; + import b from 'bar'; + import a from 'foo'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'asc'}, + }], + errors: [{ + ruleID: 'order', + message: '`Bar` import should occur before import of `bar`', + }], + }), + // Option alphabetize: {order: 'desc'} + test({ + code: ` + import a from 'foo'; + import c from 'Bar'; + import b from 'bar'; + + import index from './'; + `, + output: ` + import a from 'foo'; + import b from 'bar'; + import c from 'Bar'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'desc'}, + }], + errors: [{ + ruleID: 'order', + message: '`bar` import should occur before import of `Bar`', + }], + }), + // Option alphabetize {order: 'asc': caseInsensitive: true} + test({ + code: ` + import b from 'foo'; + import a from 'Bar'; + + import index from './'; + `, + output: ` + import a from 'Bar'; + import b from 'foo'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'asc', caseInsensitive: true}, + }], + errors: [{ + ruleID: 'order', + message: '`Bar` import should occur before import of `foo`', + }], + }), + // Option alphabetize {order: 'desc': caseInsensitive: true} + test({ + code: ` + import a from 'Bar'; + import b from 'foo'; + + import index from './'; + `, + output: ` + import b from 'foo'; + import a from 'Bar'; + + import index from './'; + `, + options: [{ + groups: ['external', 'index'], + alphabetize: {order: 'desc', caseInsensitive: true}, + }], + errors: [{ + ruleID: 'order', + message: '`foo` import should occur before import of `Bar`', + }], + }), ].filter((t) => !!t), }) diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js index ae630b4476..19aef41e0b 100644 --- a/tests/src/rules/prefer-default-export.js +++ b/tests/src/rules/prefer-default-export.js @@ -3,7 +3,7 @@ import { test, getNonDefaultParsers } from '../utils' import { RuleTester } from 'eslint' const ruleTester = new RuleTester() - , rule = require('rules/prefer-default-export') + , rule = require('../../../src/rules/prefer-default-export') ruleTester.run('prefer-default-export', rule, { valid: [ @@ -43,6 +43,10 @@ ruleTester.run('prefer-default-export', rule, { code: ` export const { foo: { bar, baz } } = item;`, }), + test({ + code: ` + export const [a, b] = item;`, + }), test({ code: ` let item; @@ -127,10 +131,18 @@ ruleTester.run('prefer-default-export', rule, { message: 'Prefer default export.', }], }), + test({ + code: ` + export const [a] = ["foo"]`, + errors: [{ + ruleId: 'ExportNamedDeclaration', + message: 'Prefer default export.', + }], + }), ], -}); +}) -context('Typescript', function() { +context('TypeScript', function() { getNonDefaultParsers().forEach((parser) => { const parserConfig = { parser: parser, @@ -138,7 +150,7 @@ context('Typescript', function() { 'import/parsers': { [parser]: ['.ts'] }, 'import/resolver': { 'eslint-import-resolver-typescript': true }, }, - }; + } ruleTester.run('prefer-default-export', rule, { valid: [ @@ -182,8 +194,15 @@ context('Typescript', function() { }, parserConfig, ), + test ( + { + code: 'export interface foo { bar: string; }; export function goo() {}', + parser, + }, + parserConfig, + ), ], invalid: [], - }); - }); -}); + }) + }) +}) diff --git a/tests/src/utils.js b/tests/src/utils.js index 9e0c4a1e05..4bc8f0119a 100644 --- a/tests/src/utils.js +++ b/tests/src/utils.js @@ -10,19 +10,19 @@ export function testFilePath(relativePath) { } export function getTSParsers() { - const parsers = []; + const parsers = [] if (semver.satisfies(eslintPkg.version, '>=4.0.0 <6.0.0')) { - parsers.push(require.resolve('typescript-eslint-parser')); + parsers.push(require.resolve('typescript-eslint-parser')) } if (semver.satisfies(eslintPkg.version, '>5.0.0')) { - parsers.push(require.resolve('@typescript-eslint/parser')); + parsers.push(require.resolve('@typescript-eslint/parser')) } - return parsers; + return parsers } export function getNonDefaultParsers() { - return getTSParsers().concat(require.resolve('babel-eslint')); + return getTSParsers().concat(require.resolve('babel-eslint')) } export const FILENAME = testFilePath('foo.js') diff --git a/utils/.eslintrc.yml b/utils/.eslintrc.yml new file mode 100644 index 0000000000..d30c264659 --- /dev/null +++ b/utils/.eslintrc.yml @@ -0,0 +1,3 @@ +--- +rules: + no-console: 1 diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index d0b2128edc..4573f794bc 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,11 +5,23 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel ## Unreleased +### Fixed +- Uses createRequireFromPath to resolve loaders ([#1591], thanks [@arcanis]) +- report the error stack on a resolution error ([#599], thanks [@sompylasar]) + +## v2.5.0 - 2019-12-07 + +### Added +- support `parseForESLint` from custom parser ([#1435], thanks [@JounQin]) + +### Changed + - Avoid superfluous calls and code ([#1551], thanks [@brettz9]) + ## v2.4.1 - 2019-07-19 ### Fixed - Improve parse perf when using `@typescript-eslint/parser` ([#1409], thanks [@bradzacher]) - - Improve support for Typescript declare structures ([#1356], thanks [@christophercurrie]) + - Improve support for TypeScript declare structures ([#1356], thanks [@christophercurrie]) ## v2.4.0 - 2019-04-13 @@ -51,7 +63,9 @@ Yanked due to critical issue with cache key resulting from #839. - `unambiguous.test()` regex is now properly in multiline mode - +[#1591]: https://github.com/benmosher/eslint-plugin-import/pull/1591 +[#1551]: https://github.com/benmosher/eslint-plugin-import/pull/1551 +[#1435]: https://github.com/benmosher/eslint-plugin-import/pull/1435 [#1409]: https://github.com/benmosher/eslint-plugin-import/pull/1409 [#1356]: https://github.com/benmosher/eslint-plugin-import/pull/1356 [#1290]: https://github.com/benmosher/eslint-plugin-import/pull/1290 @@ -59,9 +73,14 @@ Yanked due to critical issue with cache key resulting from #839. [#1166]: https://github.com/benmosher/eslint-plugin-import/issues/1166 [#1160]: https://github.com/benmosher/eslint-plugin-import/pull/1160 [#1035]: https://github.com/benmosher/eslint-plugin-import/issues/1035 +[#599]: https://github.com/benmosher/eslint-plugin-import/pull/599 [@hulkish]: https://github.com/hulkish [@timkraut]: https://github.com/timkraut [@vikr01]: https://github.com/vikr01 [@bradzacher]: https://github.com/bradzacher [@christophercurrie]: https://github.com/christophercurrie +[@brettz9]: https://github.com/brettz9 +[@JounQin]: https://github.com/JounQin +[@arcanis]: https://github.com/arcanis +[@sompylasar]: https://github.com/sompylasar diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js index eba86d2585..ab0266fe58 100644 --- a/utils/ModuleCache.js +++ b/utils/ModuleCache.js @@ -1,4 +1,4 @@ -"use strict" +'use strict' exports.__esModule = true const log = require('debug')('eslint-module-utils:ModuleCache') diff --git a/utils/declaredScope.js b/utils/declaredScope.js index 2ef3d19a97..904279ad79 100644 --- a/utils/declaredScope.js +++ b/utils/declaredScope.js @@ -1,4 +1,4 @@ -"use strict" +'use strict' exports.__esModule = true exports.default = function declaredScope(context, name) { diff --git a/utils/hash.js b/utils/hash.js index 0b946a5106..d69dd4df5f 100644 --- a/utils/hash.js +++ b/utils/hash.js @@ -2,7 +2,7 @@ * utilities for hashing config objects. * basically iteratively updates hash with a JSON-like format */ -"use strict" +'use strict' exports.__esModule = true const createHash = require('crypto').createHash @@ -42,12 +42,12 @@ exports.hashArray = hashArray function hashObject(object, hash) { if (!hash) hash = createHash('sha256') - hash.update("{") + hash.update('{') Object.keys(object).sort().forEach(key => { hash.update(stringify(key)) hash.update(':') hashify(object[key], hash) - hash.update(",") + hash.update(',') }) hash.update('}') diff --git a/utils/module-require.js b/utils/module-require.js index 9b387ad1a6..689450658c 100644 --- a/utils/module-require.js +++ b/utils/module-require.js @@ -1,4 +1,4 @@ -"use strict" +'use strict' exports.__esModule = true const Module = require('module') diff --git a/utils/package.json b/utils/package.json index eaad9b2544..8bf67a9252 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,11 +1,12 @@ { "name": "eslint-module-utils", - "version": "2.4.1", + "version": "2.5.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" }, "scripts": { + "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -25,7 +26,7 @@ }, "homepage": "https://github.com/benmosher/eslint-plugin-import#readme", "dependencies": { - "debug": "^2.6.8", + "debug": "^2.6.9", "pkg-dir": "^2.0.0" } } diff --git a/utils/parse.js b/utils/parse.js index 99e5a9334e..fa2ff14259 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -1,4 +1,4 @@ -"use strict" +'use strict' exports.__esModule = true const moduleRequire = require('./module-require').default @@ -36,12 +36,30 @@ exports.default = function parse(path, content, context) { // "project" or "projects" in parserOptions. Removing these options means the parser will // only parse one file in isolate mode, which is much, much faster. // https://github.com/benmosher/eslint-plugin-import/issues/1408#issuecomment-509298962 - delete parserOptions.project; - delete parserOptions.projects; + delete parserOptions.project + delete parserOptions.projects // require the parser relative to the main module (i.e., ESLint) const parser = moduleRequire(parserPath) + if (typeof parser.parseForESLint === 'function') { + let ast + try { + ast = parser.parseForESLint(content, parserOptions).ast + } catch (e) { + // + } + if (!ast || typeof ast !== 'object') { + console.warn( + '`parseForESLint` from parser `' + + parserPath + + '` is invalid and will just be ignored' + ) + } else { + return ast + } + } + return parser.parse(content, parserOptions) } diff --git a/utils/resolve.js b/utils/resolve.js index 87a1eaea81..3dc15f022a 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -1,9 +1,10 @@ -"use strict" +'use strict' exports.__esModule = true const pkgDir = require('pkg-dir') const fs = require('fs') +const Module = require('module') const path = require('path') const hashObject = require('./hash').hashObject @@ -12,20 +13,37 @@ const hashObject = require('./hash').hashObject const CASE_SENSITIVE_FS = !fs.existsSync(path.join(__dirname, 'reSOLVE.js')) exports.CASE_SENSITIVE_FS = CASE_SENSITIVE_FS +const ERROR_NAME = 'EslintPluginImportResolveError' + const fileExistsCache = new ModuleCache() -function tryRequire(target) { - let resolved; +// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) +const createRequireFromPath = Module.createRequireFromPath || function (filename) { + const mod = new Module(filename, null) + mod.filename = filename + mod.paths = Module._nodeModulePaths(path.dirname(filename)) + + mod._compile(`module.exports = require;`, filename) + + return mod.exports +} + +function tryRequire(target, sourceFile) { + let resolved try { // Check if the target exists - resolved = require.resolve(target); + if (sourceFile != null) { + resolved = createRequireFromPath(sourceFile).resolve(target) + } else { + resolved = require.resolve(target) + } } catch(e) { // If the target does not exist then just return undefined - return undefined; + return undefined } // If the target exists then return the loaded module - return require(resolved); + return require(resolved) } // http://stackoverflow.com/a/27382838 @@ -64,7 +82,7 @@ function relative(modulePath, sourceFile, settings) { function fullResolve(modulePath, sourceFile, settings) { // check if this is a bonus core module const coreSet = new Set(settings['import/core-modules']) - if (coreSet != null && coreSet.has(modulePath)) return { found: true, path: null } + if (coreSet.has(modulePath)) return { found: true, path: null } const sourceDir = path.dirname(sourceFile) , cacheKey = sourceDir + hashObject(settings).digest('hex') + modulePath @@ -146,7 +164,9 @@ function resolverReducer(resolvers, map) { return map } - throw new Error('invalid resolver config') + const err = new Error('invalid resolver config') + err.name = ERROR_NAME + throw err } function getBaseDir(sourceFile) { @@ -154,15 +174,19 @@ function getBaseDir(sourceFile) { } function requireResolver(name, sourceFile) { // Try to resolve package with conventional name - let resolver = tryRequire(`eslint-import-resolver-${name}`) || - tryRequire(name) || + let resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile) || + tryRequire(name, sourceFile) || tryRequire(path.resolve(getBaseDir(sourceFile), name)) if (!resolver) { - throw new Error(`unable to load resolver "${name}".`) + const err = new Error(`unable to load resolver "${name}".`) + err.name = ERROR_NAME + throw err } if (!isResolverValid(resolver)) { - throw new Error(`${name} with invalid interface loaded as resolver`) + const err = new Error(`${name} with invalid interface loaded as resolver`) + err.name = ERROR_NAME + throw err } return resolver @@ -194,8 +218,14 @@ function resolve(p, context) { ) } catch (err) { if (!erroredContexts.has(context)) { + // The `err.stack` string starts with `err.name` followed by colon and `err.message`. + // We're filtering out the default `err.name` because it adds little value to the message. + let errMessage = err.message + if (err.name !== ERROR_NAME && err.stack) { + errMessage = err.stack.replace(/^Error: /, '') + } context.report({ - message: `Resolve error: ${err.message}`, + message: `Resolve error: ${errMessage}`, loc: { line: 1, column: 0 }, }) erroredContexts.add(context)