diff --git a/.eslintrc b/.eslintrc index a90ba1d4be..224ffcb0e3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,12 +12,19 @@ "env": { "node": true, "es6": true, + "es2017": true, }, "parserOptions": { "sourceType": "module", "ecmaVersion": 2020, }, "rules": { + "array-bracket-spacing": [2, "never"], + "arrow-body-style": [2, "as-needed"], + "arrow-parens": [2, "always"], + "arrow-spacing": [2, { "before": true, "after": true }], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], "comma-dangle": ["error", { "arrays": "always-multiline", "objects": "always-multiline", @@ -25,12 +32,47 @@ "exports": "always-multiline", "functions": "always-multiline", }], + "comma-spacing": [2, { "before": false, "after": true }], "comma-style": [2, "last"], - "curly": [2, "multi-line"], + "computed-property-spacing": [2, "never"], + "curly": [2, "all"], + "default-case": [2, { "commentPattern": "(?:)" }], + "default-case-last": [2], + "default-param-last": [2], + "dot-location": [2, "property"], + "dot-notation": [2, { "allowKeywords": true, "allowPattern": "throws" }], "eol-last": [2, "always"], "eqeqeq": [2, "allow-null"], - "func-call-spacing": 2, - "indent": [2, 2], + "for-direction": [2], + "function-call-argument-newline": [2, "consistent"], + "func-call-spacing": [2, "never"], + "implicit-arrow-linebreak": [2, "beside"], + "indent": [2, 2, { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + }, + "CallExpression": { + "arguments": 1 + }, + "ArrayExpression": 1, + "ObjectExpression": 1, + "ImportDeclaration": 1, + "flatTernaryExpressions": false, + }], + "jsx-quotes": [2, "prefer-double"], + "key-spacing": [2, { + "beforeColon": false, + "afterColon": true, + "mode": "strict", + }], "keyword-spacing": ["error", { "before": true, "after": true, @@ -40,27 +82,69 @@ "case": { "after": true } } }], + "linebreak-style": [2, "unix"], + "lines-around-directive": [2, { + "before": "always", + "after": "always", + }], "max-len": 0, + "new-parens": 2, + "no-array-constructor": 2, + "no-compare-neg-zero": 2, "no-cond-assign": [2, "always"], + "no-extra-parens": 2, + "no-multiple-empty-lines": [2, { "max": 1, "maxEOF": 1, "maxBOF": 0 }], "no-return-assign": [2, "always"], + "no-trailing-spaces": 2, + "no-use-before-define": [2, { "functions": true, "classes": true, "variables": true }], "no-var": 2, "object-curly-spacing": [2, "always"], "object-shorthand": ["error", "always", { "ignoreConstructors": false, - "avoidQuotes": true, + "avoidQuotes": false, + "avoidExplicitReturnArrows": true, }], "one-var": [2, "never"], + "operator-linebreak": [2, "none", { + "overrides": { + "?": "before", + ":": "before", + "&&": "before", + "||": "before", + }, + }], "prefer-const": 2, + "prefer-object-spread": 2, + "prefer-rest-params": 2, + "prefer-template": 2, + "quote-props": [2, "as-needed", { "keywords": false }], "quotes": [2, "single", { "allowTemplateLiterals": true, "avoidEscape": true, }], + "rest-spread-spacing": [2, "never"], "semi": [2, "always"], + "semi-spacing": [2, { "before": false, "after": true }], + "semi-style": [2, "last"], + "space-before-blocks": [2, { "functions": "always", "keywords": "always", "classes": "always" }], "space-before-function-paren": ["error", { "anonymous": "always", "named": "never", "asyncArrow": "always", }], + "space-in-parens": [2, "never"], + "space-infix-ops": [2], + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "switch-colon-spacing": [2, { "after": true, "before": false }], + "template-curly-spacing": [2, "never"], + "template-tag-spacing": [2, "never"], + "unicode-bom": [2, "never"], + "use-isnan": [2, { "enforceForSwitchCase": true }], + "valid-typeof": [2], + "wrap-iife": [2, "outside", { "functionPrototypeMethods": true }], + "wrap-regex": [2], + "yield-star-spacing": [2, { "before": false, "after": true }], + "yoda": [2, "never", { "exceptRange": true, "onlyEquality": false }], "eslint-plugin/consistent-output": [ "error", @@ -76,7 +160,16 @@ "eslint-plugin/require-meta-type": "error", // dog fooding - "import/no-extraneous-dependencies": "error", + "import/no-extraneous-dependencies": ["error", { + "devDependencies": [ + "tests/**", + "resolvers/*/test/**", + "scripts/**" + ], + "optionalDependencies": false, + "peerDependencies": true, + "bundledDependencies": false, + }], "import/unambiguous": "off", }, @@ -98,19 +191,58 @@ }, }, { - "files": "resolvers/**", + "files": [ + "resolvers/**", + "utils/**", + ], "env": { "es6": false, }, + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2016, + }, + "rules": { + "comma-dangle": ["error", { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "never" + }], + "prefer-destructuring": "off", + "prefer-object-spread": "off", + "prefer-rest-params": "off", + "prefer-spread": "off", + "prefer-template": "off", + } }, { - "files": "resolvers/webpack/**", + "files": [ + "resolvers/webpack/**", + "utils/**", + ], "rules": { - "import/no-extraneous-dependencies": 1, "no-console": 1, }, - "env": { - "es6": true, + }, + { + "files": [ + "utils/**", // TODO + "src/ExportMap.js", // TODO + ], + "rules": { + "no-use-before-define": "off", + }, + }, + { + "files": [ + "resolvers/webpack/index.js", + "resolvers/webpack/test/example.js", + "utils/parse.js", + ], + "rules": { + "no-console": "off", }, }, { @@ -122,22 +254,6 @@ "es6": false }, }, - { - "files": "utils/**", - "parserOptions": { - "ecmaVersion": 6, - }, - "rules": { - "comma-dangle": ["error", { - "arrays": "always-multiline", - "objects": "always-multiline", - "imports": "always-multiline", - "exports": "always-multiline", - "functions": "never" - }], - "no-console": 1, - }, - }, { "files": "tests/**", "env": { diff --git a/.gitignore b/.gitignore index 8e2f6da1ee..587dbd9280 100644 --- a/.gitignore +++ b/.gitignore @@ -25,13 +25,9 @@ resolvers/node/LICENSE resolvers/webpack/LICENSE utils/LICENSE memo-parser/.npmrc -memo-parser/.nycrc resolvers/node/.npmrc -resolvers/node/.nycrc resolvers/webpack/.npmrc -resolvers/webpack/.nycrc utils/.npmrc -utils/.nycrc # Dependency directory # Commenting this out is preferred by some people, see diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000000..d179615f45 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "line-length": false, + "ul-indent": { + "start_indent": 1, + "start_indented": true + }, + "ul-style": { + "style": "dash" + } +} diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000000..6ed5b5b6ee --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,2 @@ +CHANGELOG.md +node_modules diff --git a/.travis.yml b/.travis.yml index f57222a8ff..21a7070fb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,6 @@ matrix: fast_finish: true before_install: - - 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash && . $NVM_DIR/nvm.sh' - 'nvm install-latest-npm' - 'NPM_CONFIG_LEGACY_PEER_DEPS=true npm install' - 'npm run copy-metafiles' diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8e889241..9552774dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,85 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## [Unreleased] +### Added +- [`dynamic-import-chunkname`]: add `allowEmpty` option to allow empty leading comments ([#2942], thanks [@JiangWeixian]) + +### Changed +- [Docs] `no-extraneous-dependencies`: Make glob pattern description more explicit ([#2944], thanks [@mulztob]) +- [`no-unused-modules`]: add console message to help debug [#2866] +- [Refactor] `ExportMap`: make procedures static instead of monkeypatching exportmap ([#2982], thanks [@soryy708]) + +## [2.29.1] - 2023-12-14 + +### Fixed +- [`no-extraneous-dependencies`]: ignore `export type { ... } from '...'` when `includeTypes` is `false` ([#2919], thanks [@Pandemic1617]) +- [`no-unused-modules`]: support export patterns with array destructuring ([#2930], thanks [@ljharb]) +- [Deps] update `tsconfig-paths` ([#2447], thanks [@domdomegg]) + +## [2.29.0] - 2023-10-22 + +### Added +- TypeScript config: add .cts and .mts extensions ([#2851], thanks [@Zamiell]) +- [`newline-after-import`]: new option `exactCount` and docs update ([#1933], thanks [@anikethsaha] and [@reosarevok]) +- [`newline-after-import`]: fix `exactCount` with `considerComments` false positive, when there is a leading comment ([#2884], thanks [@kinland]) + +## [2.28.1] - 2023-08-18 + +### Fixed +- [`order`]: revert breaking change to single nested group ([#2854], thanks [@yndajas]) + +### Changed +- [Docs] remove duplicate fixable notices in docs ([#2850], thanks [@bmish]) + +## [2.28.0] - 2023-07-27 + +### Fixed +- [`no-duplicates`]: remove duplicate identifiers in duplicate imports ([#2577], thanks [@joe-matsec]) +- [`consistent-type-specifier-style`]: fix accidental removal of comma in certain cases ([#2754], thanks [@bradzacher]) +- [Perf] `ExportMap`: Improve `ExportMap.for` performance on larger codebases ([#2756], thanks [@leipert]) +- [`no-extraneous-dependencies`]/TypeScript: do not error when importing inline type from dev dependencies ([#1820], thanks [@andyogo]) +- [`newline-after-import`]/TypeScript: do not error when re-exporting a namespaced import ([#2832], thanks [@laurens-dg]) +- [`order`]: partial fix for [#2687] (thanks [@ljharb]) +- [`no-duplicates`]: Detect across type and regular imports ([#2835], thanks [@benkrejci]) +- [`extensions`]: handle `.` and `..` properly ([#2778], thanks [@benasher44]) +- [`no-unused-modules`]: improve schema (thanks [@ljharb]) +- [`no-unused-modules`]: report error on binding instead of parent export ([#2842], thanks [@Chamion]) + +### Changed +- [Docs] [`no-duplicates`]: fix example schema ([#2684], thanks [@simmo]) +- [Docs] [`group-exports`]: fix syntax highlighting ([#2699], thanks [@devinrhode2]) +- [Docs] [`extensions`]: reference node ESM behavior ([#2748], thanks [@xM8WVqaG]) +- [Refactor] [`exports-last`]: use `array.prototype.findlastindex` (thanks [@ljharb]) +- [Refactor] [`no-anonymous-default-export`]: use `object.fromentries` (thanks [@ljharb]) +- [Refactor] [`no-unused-modules`]: use `array.prototype.flatmap` (thanks [@ljharb]) + +## [2.27.5] - 2023-01-16 + +### Fixed +- [`order]`: Fix group ranks order when alphabetizing ([#2674], thanks [@Pearce-Ropion]) + +## [2.27.4] - 2023-01-11 + +### Fixed +- `semver` should be a prod dep ([#2668]) + +## [2.27.3] - 2023-01-11 + +### Fixed +- [`no-empty-named-blocks`]: rewrite rule to only check import declarations ([#2666]) + +## [2.27.2] - 2023-01-11 + +### Fixed +- [`no-duplicates`]: do not unconditionally require `typescript` ([#2665]) + +## [2.27.1] - 2023-01-11 + +### Fixed +- `array.prototype.flatmap` should be a prod dep ([#2664], thanks [@cristobal]) + +## [2.27.0] - 2023-01-11 + ### Added - [`newline-after-import`]: add `considerComments` option ([#2399], thanks [@pri1311]) - [`no-cycle`]: add `allowUnsafeDynamicCyclicDependency` option ([#2387], thanks [@GerkinDev]) @@ -34,6 +113,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange - `ExportMap`: add missing param to function ([#2589], thanks [@Fdawgs]) - [`no-unused-modules`]: `checkPkgFieldObject` filters boolean fields from checks ([#2598], thanks [@mpint]) - [`no-cycle`]: accept Flow `typeof` imports, just like `type` ([#2608], thanks [@gnprice]) +- [`no-import-module-exports`]: avoid a false positive for import variables ([#2315], thanks [@BarryThePenguin]) ### Changed - [Tests] [`named`]: Run all TypeScript test ([#2427], thanks [@ProdigySim]) @@ -1029,6 +1109,25 @@ for info on changes for earlier releases. [`memo-parser`]: ./memo-parser/README.md +[#2982]: https://github.com/import-js/eslint-plugin-import/pull/2982 +[#2944]: https://github.com/import-js/eslint-plugin-import/pull/2944 +[#2942]: https://github.com/import-js/eslint-plugin-import/pull/2942 +[#2919]: https://github.com/import-js/eslint-plugin-import/pull/2919 +[#2884]: https://github.com/import-js/eslint-plugin-import/pull/2884 +[#2866]: https://github.com/import-js/eslint-plugin-import/pull/2866 +[#2854]: https://github.com/import-js/eslint-plugin-import/pull/2854 +[#2851]: https://github.com/import-js/eslint-plugin-import/pull/2851 +[#2850]: https://github.com/import-js/eslint-plugin-import/pull/2850 +[#2842]: https://github.com/import-js/eslint-plugin-import/pull/2842 +[#2835]: https://github.com/import-js/eslint-plugin-import/pull/2835 +[#2832]: https://github.com/import-js/eslint-plugin-import/pull/2832 +[#2778]: https://github.com/import-js/eslint-plugin-import/pull/2778 +[#2756]: https://github.com/import-js/eslint-plugin-import/pull/2756 +[#2754]: https://github.com/import-js/eslint-plugin-import/pull/2754 +[#2748]: https://github.com/import-js/eslint-plugin-import/pull/2748 +[#2735]: https://github.com/import-js/eslint-plugin-import/pull/2735 +[#2699]: https://github.com/import-js/eslint-plugin-import/pull/2699 +[#2664]: https://github.com/import-js/eslint-plugin-import/pull/2664 [#2613]: https://github.com/import-js/eslint-plugin-import/pull/2613 [#2608]: https://github.com/import-js/eslint-plugin-import/pull/2608 [#2605]: https://github.com/import-js/eslint-plugin-import/pull/2605 @@ -1074,6 +1173,7 @@ for info on changes for earlier releases. [#2332]: https://github.com/import-js/eslint-plugin-import/pull/2332 [#2334]: https://github.com/import-js/eslint-plugin-import/pull/2334 [#2330]: https://github.com/import-js/eslint-plugin-import/pull/2330 +[#2315]: https://github.com/import-js/eslint-plugin-import/pull/2315 [#2305]: https://github.com/import-js/eslint-plugin-import/pull/2305 [#2299]: https://github.com/import-js/eslint-plugin-import/pull/2299 [#2297]: https://github.com/import-js/eslint-plugin-import/pull/2297 @@ -1351,6 +1451,16 @@ for info on changes for earlier releases. [#211]: https://github.com/import-js/eslint-plugin-import/pull/211 [#164]: https://github.com/import-js/eslint-plugin-import/pull/164 [#157]: https://github.com/import-js/eslint-plugin-import/pull/157 + +[#2930]: https://github.com/import-js/eslint-plugin-import/issues/2930 +[#2687]: https://github.com/import-js/eslint-plugin-import/issues/2687 +[#2684]: https://github.com/import-js/eslint-plugin-import/issues/2684 +[#2674]: https://github.com/import-js/eslint-plugin-import/issues/2674 +[#2668]: https://github.com/import-js/eslint-plugin-import/issues/2668 +[#2666]: https://github.com/import-js/eslint-plugin-import/issues/2666 +[#2665]: https://github.com/import-js/eslint-plugin-import/issues/2665 +[#2577]: https://github.com/import-js/eslint-plugin-import/issues/2577 +[#2447]: https://github.com/import-js/eslint-plugin-import/issues/2447 [#2444]: https://github.com/import-js/eslint-plugin-import/issues/2444 [#2412]: https://github.com/import-js/eslint-plugin-import/issues/2412 [#2392]: https://github.com/import-js/eslint-plugin-import/issues/2392 @@ -1469,7 +1579,17 @@ for info on changes for earlier releases. [#119]: https://github.com/import-js/eslint-plugin-import/issues/119 [#89]: https://github.com/import-js/eslint-plugin-import/issues/89 -[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.26.0...HEAD +[Unreleased]: https://github.com/import-js/eslint-plugin-import/compare/v2.29.1...HEAD +[2.29.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.29.0...v2.29.1 +[2.29.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.28.1...v2.29.0 +[2.28.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.28.0...v2.28.1 +[2.28.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.27.5...v2.28.0 +[2.27.5]: https://github.com/import-js/eslint-plugin-import/compare/v2.27.4...v2.27.5 +[2.27.4]: https://github.com/import-js/eslint-plugin-import/compare/v2.27.3...v2.27.4 +[2.27.3]: https://github.com/import-js/eslint-plugin-import/compare/v2.27.2...v2.27.3 +[2.27.2]: https://github.com/import-js/eslint-plugin-import/compare/v2.27.1...v2.27.2 +[2.27.1]: https://github.com/import-js/eslint-plugin-import/compare/v2.27.0...v2.27.1 +[2.27.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.26.0...v2.27.0 [2.26.0]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.4...v2.26.0 [2.25.4]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.3...v2.25.4 [2.25.3]: https://github.com/import-js/eslint-plugin-import/compare/v2.25.2...v2.25.3 @@ -1569,6 +1689,7 @@ for info on changes for earlier releases. [@alexgorbatchev]: https://github.com/alexgorbatchev [@andreubotella]: https://github.com/andreubotella [@AndrewLeedham]: https://github.com/AndrewLeedham +[@andyogo]: https://github.com/andyogo [@aravindet]: https://github.com/aravindet [@arvigeus]: https://github.com/arvigeus [@asapach]: https://github.com/asapach @@ -1578,8 +1699,11 @@ for info on changes for earlier releases. [@atos1990]: https://github.com/atos1990 [@azyzz228]: https://github.com/azyzz228 [@barbogast]: https://github.com/barbogast +[@BarryThePenguin]: https://github.com/BarryThePenguin [@be5invis]: https://github.com/be5invis [@beatrizrezener]: https://github.com/beatrizrezener +[@benasher44]: https://github.com/benasher44 +[@benkrejci]: https://github.com/benkrejci [@benmosher]: https://github.com/benmosher [@benmunro]: https://github.com/benmunro [@BenoitZugmeyer]: https://github.com/BenoitZugmeyer @@ -1592,19 +1716,23 @@ for info on changes for earlier releases. [@bradzacher]: https://github.com/bradzacher [@brendo]: https://github.com/brendo [@brettz9]: https://github.com/brettz9 +[@Chamion]: https://github.com/Chamion [@charlessuh]: https://github.com/charlessuh [@charpeni]: https://github.com/charpeni [@cherryblossom000]: https://github.com/cherryblossom000 [@chrislloyd]: https://github.com/chrislloyd [@christianvuerings]: https://github.com/christianvuerings [@christophercurrie]: https://github.com/christophercurrie +[@cristobal]: https://github.com/cristobal [@DamienCassou]: https://github.com/DamienCassou [@danny-andrews]: https://github.com/dany-andrews [@darkartur]: https://github.com/darkartur [@davidbonnet]: https://github.com/davidbonnet [@dbrewer5]: https://github.com/dbrewer5 +[@devinrhode2]: https://github.com/devinrhode2 [@devongovett]: https://github.com/devongovett [@dmnd]: https://github.com/dmnd +[@domdomegg]: https://github.com/domdomegg [@duncanbeevers]: https://github.com/duncanbeevers [@dwardu]: https://github.com/dwardu [@echenley]: https://github.com/echenley @@ -1654,9 +1782,11 @@ for info on changes for earlier releases. [@jeffshaver]: https://github.com/jeffshaver [@jf248]: https://github.com/jf248 [@jfmengels]: https://github.com/jfmengels +[@JiangWeixian]: https://github.com/JiangWeixian [@jimbolla]: https://github.com/jimbolla [@jkimbo]: https://github.com/jkimbo [@joaovieira]: https://github.com/joaovieira +[@joe-matsec]: https://github.com/joe-matsec [@johndevedu]: https://github.com/johndevedu [@johnthagen]: https://github.com/johnthagen [@jonboiser]: https://github.com/jonboiser @@ -1670,6 +1800,7 @@ for info on changes for earlier releases. [@kentcdodds]: https://github.com/kentcdodds [@kevin940726]: https://github.com/kevin940726 [@kgregory]: https://github.com/kgregory +[@kinland]: https://github.com/kinland [@kirill-konshin]: https://github.com/kirill-konshin [@kiwka]: https://github.com/kiwka [@klimashkin]: https://github.com/klimashkin @@ -1677,8 +1808,10 @@ for info on changes for earlier releases. [@knpwrs]: https://github.com/knpwrs [@KostyaZgara]: https://github.com/KostyaZgara [@kylemh]: https://github.com/kylemh +[@laurens-dg]: https://github.com/laurens-dg [@laysent]: https://github.com/laysent [@le0nik]: https://github.com/le0nik +[@leipert]: https://github.com/leipert [@lemonmade]: https://github.com/lemonmade [@lencioni]: https://github.com/lencioni [@leonardodino]: https://github.com/leonardodino @@ -1715,6 +1848,7 @@ for info on changes for earlier releases. [@mplewis]: https://github.com/mplewis [@mrmckeb]: https://github.com/mrmckeb [@msvab]: https://github.com/msvab +[@mulztob]: https://github.com/mulztob [@mx-bernhard]: https://github.com/mx-bernhard [@Nfinished]: https://github.com/Nfinished [@nickofthyme]: https://github.com/nickofthyme @@ -1725,6 +1859,7 @@ for info on changes for earlier releases. [@ombene]: https://github.com/ombene [@ota-meshi]: https://github.com/ota-meshi [@OutdatedVersion]: https://github.com/OutdatedVersion +[@Pandemic1617]: https://github.com/Pandemic1617 [@panrafal]: https://github.com/panrafal [@paztis]: https://github.com/paztis [@pcorpet]: https://github.com/pcorpet @@ -1758,6 +1893,7 @@ for info on changes for earlier releases. [@sheepsteak]: https://github.com/sheepsteak [@silviogutierrez]: https://github.com/silviogutierrez [@SimenB]: https://github.com/SimenB +[@simmo]: https://github.com/simmo [@sindresorhus]: https://github.com/sindresorhus [@singles]: https://github.com/singles [@skozin]: https://github.com/skozin @@ -1795,6 +1931,9 @@ for info on changes for earlier releases. [@wKich]: https://github.com/wKich [@wschurman]: https://github.com/wschurman [@wtgtybhertgeghgtwtg]: https://github.com/wtgtybhertgeghgtwtg +[@xM8WVqaG]: https://github.com/xM8WVqaG [@xpl]: https://github.com/xpl +[@yndajas]: https://github.com/yndajas [@yordis]: https://github.com/yordis +[@Zamiell]: https://github.com/Zamiell [@zloirock]: https://github.com/zloirock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eba44f51ac..871e90ad8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,18 +6,27 @@ Thanks for your interest in helping out! Here are a **few** _weird_ tricks to ~~ When opening an [issue](#issues): -- [ ] search open/closed issues -- [ ] discuss bug/enhancement in new or old issue + - [ ] search open/closed issues + - [ ] discuss bug/enhancement in new or old issue [PR](#prs) time: -- [ ] write tests -- [ ] implement feature/fix bug -- [ ] update docs -- [ ] make a note in change log + - [ ] write tests + - [ ] implement feature/fix bug + - [ ] update docs + - [ ] make a note in change log Remember, you don't need to do it all yourself; any of these are helpful! 😎 +## How to get started + +If you are new to `eslint`, below are a few resources that will help you to familiarize yourself with the project. + + - Watch [this presentation](https://www.youtube.com/watch?v=2W9tUnALrLg) to learn the fundamental concept of Abstract Syntax Trees (AST) and the way `eslint` works under the hood. + - Familiarize yourself with the [AST explorer](https://astexplorer.net/) tool. Look into rules in `docs/rules`, create patterns in the rules, then analyze its AST. + - Explore the blog posts on how to create a custom rule. [One blog post](https://blog.yonatan.dev/writing-a-custom-eslint-rule-to-spot-undeclared-props/). [Second blog post](https://betterprogramming.pub/creating-custom-eslint-rules-cdc579694608). + - Read the official `eslint` [developer guide](https://eslint.org/docs/latest/developer-guide/architecture/). + ## Issues ### Search open + closed issues for similar cases diff --git a/README.md b/README.md index 5c6f1a3211..d6f107d1c9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a ⌨️ Set in the `typescript` configuration.\ 🚸 Set in the `warnings` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\ -💡 Manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\ +💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).\ ❌ Deprecated. ### Helpful warnings @@ -131,7 +131,7 @@ rules: # etc... ``` -# TypeScript +## TypeScript You may use the following snippet or assemble your own config using the granular settings described below it. @@ -154,7 +154,7 @@ settings: [`@typescript-eslint/parser`]: https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser [`eslint-import-resolver-typescript`]: https://github.com/import-js/eslint-import-resolver-typescript -# Resolvers +## Resolvers With the advent of module bundlers and the current state of modules and module syntax specs, it's not always obvious where `import x from 'module'` should look @@ -175,7 +175,7 @@ resolvers are just npm packages, so [third party packages are supported](https:/ You can reference resolvers in several ways (in order of precedence): -- as a conventional `eslint-import-resolver` name, like `eslint-import-resolver-foo`: + - as a conventional `eslint-import-resolver` name, like `eslint-import-resolver-foo`: ```yaml # .eslintrc.yml @@ -195,7 +195,7 @@ module.exports = { } ``` -- with a full npm module name, like `my-awesome-npm-module`: + - with a full npm module name, like `my-awesome-npm-module`: ```yaml # .eslintrc.yml @@ -214,7 +214,7 @@ module.exports = { } ``` -- with a filesystem path to resolver, defined in this example as a `computed property` name: + - with a filesystem path to resolver, defined in this example as a `computed property` name: ```js // .eslintrc.js @@ -336,11 +336,11 @@ If you are using `yarn` PnP as your package manager, add the `.yarn` folder and Each item in this array is either a folder's name, its subpath, or its absolute prefix path: -- `jspm_modules` will match any file or folder named `jspm_modules` or which has a direct or non-direct parent named `jspm_modules`, e.g. `/home/me/project/jspm_modules` or `/home/me/project/jspm_modules/some-pkg/index.js`. + - `jspm_modules` will match any file or folder named `jspm_modules` or which has a direct or non-direct parent named `jspm_modules`, e.g. `/home/me/project/jspm_modules` or `/home/me/project/jspm_modules/some-pkg/index.js`. -- `packages/core` will match any path that contains these two segments, for example `/home/me/project/packages/core/src/utils.js`. + - `packages/core` will match any path that contains these two segments, for example `/home/me/project/packages/core/src/utils.js`. -- `/home/me/project/packages` will only match files and directories inside this directory, and the directory itself. + - `/home/me/project/packages` will only match files and directories inside this directory, and the directory itself. Please note that incomplete names are not allowed here so `components` won't match `bower_components` and `packages/ui` won't match `packages/ui-utils` (but will match `packages/ui/utils`). @@ -355,7 +355,7 @@ directly using webpack, for example: # .eslintrc.yml settings: import/parsers: - @typescript-eslint/parser: [ .ts, .tsx ] + "@typescript-eslint/parser": [ .ts, .tsx ] ``` In this case, [`@typescript-eslint/parser`](https://www.npmjs.com/package/@typescript-eslint/parser) diff --git a/appveyor.yml b/appveyor.yml index dbeb0132d6..e50ab87d2a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,6 +26,7 @@ matrix: allow_failures: - nodejs_version: "4" # for eslint 5 + - configuration: WSL platform: - x86 diff --git a/config/typescript.js b/config/typescript.js index ed03fb3f6c..ff7d0795c8 100644 --- a/config/typescript.js +++ b/config/typescript.js @@ -1,23 +1,26 @@ /** - * Adds `.jsx`, `.ts` and `.tsx` as an extension, and enables JSX/TSX parsing. + * This config: + * 1) adds `.jsx`, `.ts`, `.cts`, `.mts`, and `.tsx` as an extension + * 2) enables JSX/TSX parsing */ // Omit `.d.ts` because 1) TypeScript compilation already confirms that // types are resolved, and 2) it would mask an unresolved // `.ts`/`.tsx`/`.js`/`.jsx` implementation. -const allExtensions = ['.ts', '.tsx', '.js', '.jsx']; +const typeScriptExtensions = ['.ts', '.cts', '.mts', '.tsx']; -module.exports = { +const allExtensions = [...typeScriptExtensions, '.js', '.jsx']; +module.exports = { settings: { 'import/extensions': allExtensions, 'import/external-module-folders': ['node_modules', 'node_modules/@types'], 'import/parsers': { - '@typescript-eslint/parser': ['.ts', '.tsx'], + '@typescript-eslint/parser': typeScriptExtensions, }, 'import/resolver': { - 'node': { - 'extensions': allExtensions, + node: { + extensions: allExtensions, }, }, }, diff --git a/docs/rules/consistent-type-specifier-style.md b/docs/rules/consistent-type-specifier-style.md index 54c09049ef..41d98e4e1f 100644 --- a/docs/rules/consistent-type-specifier-style.md +++ b/docs/rules/consistent-type-specifier-style.md @@ -37,8 +37,8 @@ This rule includes a fixer that will automatically convert your specifiers to th The rule accepts a single string option which may be one of: -- `'prefer-inline'` - enforces that named type-only specifiers are only ever written with an inline marker; and never as part of a top-level, type-only import. -- `'prefer-top-level'` - enforces that named type-only specifiers only ever written as part of a top-level, type-only import; and never with an inline marker. + - `'prefer-inline'` - enforces that named type-only specifiers are only ever written with an inline marker; and never as part of a top-level, type-only import. + - `'prefer-top-level'` - enforces that named type-only specifiers only ever written as part of a top-level, type-only import; and never with an inline marker. By default the rule will use the `prefer-inline` option. diff --git a/docs/rules/default.md b/docs/rules/default.md index ffbbdc166a..9f8c919bf3 100644 --- a/docs/rules/default.md +++ b/docs/rules/default.md @@ -19,7 +19,6 @@ A module path that is [ignored] or not [unambiguously an ES module] will not be [ignored]: ../README.md#importignore [unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar - ## Rule Details Given: @@ -54,7 +53,6 @@ import bar from './bar' // no default export found in ./bar import baz from './baz' // no default export found in ./baz ``` - ## When Not To Use It If you are using CommonJS and/or modifying the exported namespace of any module at @@ -65,10 +63,9 @@ either, so such a situation will be reported in the importing module. ## Further Reading -- Lee Byron's [ES7] export proposal -- [`import/ignore`] setting -- [`jsnext:main`] (Rollup) - + - Lee Byron's [ES7] export proposal + - [`import/ignore`] setting + - [`jsnext:main`] (Rollup) [ES7]: https://github.com/leebyron/ecmascript-more-export-from [`import/ignore`]: ../../README.md#importignore diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md index 472a366485..dd526c8913 100644 --- a/docs/rules/dynamic-import-chunkname.md +++ b/docs/rules/dynamic-import-chunkname.md @@ -7,18 +7,22 @@ This rule reports any dynamic imports without a webpackChunkName specified in a This rule enforces naming of webpack chunks in dynamic imports. When you don't explicitly name chunks, webpack will autogenerate chunk names that are not consistent across builds, which prevents long-term browser caching. ## Rule Details + This rule runs against `import()` by default, but can be configured to also run against an alternative dynamic-import function, e.g. 'dynamicImport.' You can also configure the regex format you'd like to accept for the webpackChunkName - for example, if we don't want the number 6 to show up in our chunk names: + ```javascript { "dynamic-import-chunkname": [2, { importFunctions: ["dynamicImport"], - webpackChunknameFormat: "[a-zA-Z0-57-9-/_]+" + webpackChunknameFormat: "[a-zA-Z0-57-9-/_]+", + allowEmpty: false }] } ``` ### invalid + The following patterns are invalid: ```javascript @@ -53,7 +57,9 @@ import( 'someModule', ); ``` + ### valid + The following patterns are valid: ```javascript @@ -82,6 +88,38 @@ The following patterns are valid: ); ``` +### `allowEmpty: true` + +If you want to allow dynamic imports without a webpackChunkName, you can set `allowEmpty: true` in the rule config. This will allow dynamic imports without a leading comment, or with a leading comment that does not contain a webpackChunkName. + +Given `{ "allowEmpty": true }`: + + +### valid + +The following patterns are valid: + +```javascript +import('someModule'); + +import( + /* webpackChunkName: "someModule" */ + 'someModule', +); +``` + +### invalid + +The following patterns are invalid: + +```javascript +// incorrectly formatted comment +import( + /*webpackChunkName:"someModule"*/ + 'someModule', +); +``` + ## When Not To Use It If you don't care that webpack will autogenerate chunk names and may blow up browser caches and bundle size reports. diff --git a/docs/rules/export.md b/docs/rules/export.md index 115d2d8b29..54a8a39cf3 100644 --- a/docs/rules/export.md +++ b/docs/rules/export.md @@ -17,6 +17,7 @@ export default makeClass // Multiple default exports. ``` or + ```js export const foo = function () { /*...*/ } // Multiple exports of name 'foo'. @@ -31,6 +32,6 @@ intent to rename, etc. ## Further Reading -- Lee Byron's [ES7] export proposal + - Lee Byron's [ES7] export proposal [ES7]: https://github.com/leebyron/ecmascript-more-export-from diff --git a/docs/rules/exports-last.md b/docs/rules/exports-last.md index 6b3e4bac40..56e947e94b 100644 --- a/docs/rules/exports-last.md +++ b/docs/rules/exports-last.md @@ -4,7 +4,6 @@ This rule enforces that all exports are declared at the bottom of the file. This rule will report any export declarations that comes before any non-export statements. - ## This will be reported ```JS @@ -45,7 +44,7 @@ export const str = 'foo' If you don't mind exports being sprinkled throughout a file, you may not want to enable this rule. -#### ES6 exports only +### ES6 exports only The exports-last rule is currently only working on ES6 exports. You may not want to enable this rule if you're using CommonJS exports. diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 9e78b8c70f..946ccb7bf8 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -2,7 +2,7 @@ -Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default. Depending on the resolver you can configure more extensions to get resolved automatically. +Some file resolve algorithms allow you to omit the file extension within the import source path. For example the `node` resolver (which does not yet support ESM/`import`) can resolve `./foo/bar` to the absolute path `/User/someone/foo/bar.js` because the `.js` extension is resolved automatically by default in CJS. Depending on the resolver you can configure more extensions to get resolved automatically. In order to provide a consistent use of file extensions across your code base, this rule can enforce or disallow the use of certain file extensions. @@ -10,13 +10,13 @@ In order to provide a consistent use of file extensions across your code base, t This rule either takes one string option, one object option, or a string and an object option. If it is the string `"never"` (the default value), then the rule forbids the use for any extension. If it is the string `"always"`, then the rule enforces the use of extensions for all import statements. If it is the string `"ignorePackages"`, then the rule enforces the use of extensions for all import statements except package imports. -``` +```json "import/extensions": [, "never" | "always" | "ignorePackages"] ``` By providing an object you can configure each extension separately. -``` +```json "import/extensions": [, { : "never" | "always" | "ignorePackages" }] @@ -26,7 +26,7 @@ By providing an object you can configure each extension separately. By providing both a string and an object, the string will set the default setting for all extensions, and the object can be used to set granular overrides for specific extensions. -``` +```json "import/extensions": [ , "never" | "always" | "ignorePackages", @@ -40,7 +40,7 @@ For example, `["error", "never", { "svg": "always" }]` would require that all ex `ignorePackages` can be set as a separate boolean option like this: -``` +```json "import/extensions": [ , "never" | "always" | "ignorePackages", @@ -62,7 +62,7 @@ When disallowing the use of certain extensions this rule makes an exception and For example, given the following folder structure: -``` +```pt ├── foo │   ├── bar.js │   ├── bar.json @@ -170,3 +170,5 @@ import foo from '@/foo'; ## When Not To Use It If you are not concerned about a consistent usage of file extension. + +In the future, when this rule supports native node ESM resolution, and the plugin is configured to use native rather than transpiled ESM (a config option that is not yet available) - setting this to `always` will have no effect. diff --git a/docs/rules/first.md b/docs/rules/first.md index 21904e2fa1..c765a29739 100644 --- a/docs/rules/first.md +++ b/docs/rules/first.md @@ -57,6 +57,7 @@ Given that, see [#255] for the reasoning. ### With Fixer This rule contains a fixer to reorder in-body import to top, the following criteria applied: + 1. Never re-order relative to each other, even if `absolute-first` is set. 2. If an import creates an identifier, and that identifier is referenced at module level *before* the import itself, that won't be re-ordered. @@ -67,8 +68,8 @@ enable this rule. ## Further Reading -- [`import/order`]: a major step up from `absolute-first` -- Issue [#255] + - [`import/order`]: a major step up from `absolute-first` + - Issue [#255] [`import/order`]: ./order.md [#255]: https://github.com/import-js/eslint-plugin-import/issues/255 diff --git a/docs/rules/group-exports.md b/docs/rules/group-exports.md index c5a23cd218..9fb212de6a 100644 --- a/docs/rules/group-exports.md +++ b/docs/rules/group-exports.md @@ -62,7 +62,7 @@ test.another = true module.exports = test ``` -```flow js +```ts const first = true; type firstType = boolean @@ -71,7 +71,6 @@ export {first} export type {firstType} ``` - ### Invalid ```js @@ -105,7 +104,7 @@ module.exports.first = true module.exports.second = true ``` -```flow js +```ts type firstType = boolean type secondType = any diff --git a/docs/rules/max-dependencies.md b/docs/rules/max-dependencies.md index 1da74f6818..1ecbca64d3 100644 --- a/docs/rules/max-dependencies.md +++ b/docs/rules/max-dependencies.md @@ -47,6 +47,7 @@ Ignores `type` imports. Type imports are a feature released in TypeScript 3.8, y Given `{"max": 2, "ignoreTypeImports": true}`: + ### Fail ```ts @@ -55,6 +56,7 @@ import b from './b'; import c from './c'; ``` + ### Pass ```ts diff --git a/docs/rules/named.md b/docs/rules/named.md index 6d376defac..44f8dc6584 100644 --- a/docs/rules/named.md +++ b/docs/rules/named.md @@ -18,7 +18,6 @@ A module path that is [ignored] or not [unambiguously an ES module] will not be [unambiguously an ES module]: https://github.com/bmeck/UnambiguousJavaScriptGrammar [Flow]: https://flow.org/ - ## Rule Details Given: @@ -94,10 +93,9 @@ runtime, you will likely see false positives with this rule. ## Further Reading -- [`import/ignore`] setting -- [`jsnext:main`] deprecation -- [`pkg.module`] (Rollup) - + - [`import/ignore`] setting + - [`jsnext:main`] deprecation + - [`pkg.module`] (Rollup) [`jsnext:main`]: https://github.com/jsforum/jsforum/issues/5 [`pkg.module`]: https://github.com/rollup/rollup/wiki/pkg.module diff --git a/docs/rules/namespace.md b/docs/rules/namespace.md index 5ac25b750d..1a177f5819 100644 --- a/docs/rules/namespace.md +++ b/docs/rules/namespace.md @@ -30,6 +30,7 @@ redefinition of the namespace in an intermediate scope. Adherence to the ESLint For [ES7], reports if an exported namespace would be empty (no names exported from the referenced module.) Given: + ```js // @module ./named-exports export const a = 1 @@ -44,7 +45,9 @@ export class ExportedClass { } // ES7 export * as deep from './deep' ``` + and: + ```js // @module ./deep export const e = "MC2" @@ -94,9 +97,9 @@ still can't be statically analyzed any further. ## Further Reading -- Lee Byron's [ES7] export proposal -- [`import/ignore`] setting -- [`jsnext:main`](Rollup) + - Lee Byron's [ES7] export proposal + - [`import/ignore`] setting + - [`jsnext:main`](Rollup) [ES7]: https://github.com/leebyron/ecmascript-more-export-from [`import/ignore`]: ../../README.md#importignore diff --git a/docs/rules/newline-after-import.md b/docs/rules/newline-after-import.md index ed0a5b678f..ef5aeed767 100644 --- a/docs/rules/newline-after-import.md +++ b/docs/rules/newline-after-import.md @@ -5,77 +5,126 @@ Enforces having one or more empty lines after the last top-level import statement or require call. -+(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule. ## Rule Details -This rule supports the following options: -- `count` which sets the number of newlines that are enforced after the last top-level import statement or require call. This option defaults to `1`. +This rule supports the following options: -- `considerComments` which enforces the rule on comments after the last import-statement as well when set to true. This option defaults to `false`. + - `count` which sets the number of newlines that are enforced after the last top-level import statement or require call. This option defaults to `1`. + + - `exactCount` which enforce the exact numbers of newlines that is mentioned in `count`. This option defaults to `false`. + + - `considerComments` which enforces the rule on comments after the last import-statement as well when set to true. This option defaults to `false`. Valid: ```js -import defaultExport from './foo' +import defaultExport from './foo'; -const FOO = 'BAR' +const FOO = 'BAR'; ``` ```js -import defaultExport from './foo' -import { bar } from 'bar-lib' +import defaultExport from './foo'; +import { bar } from 'bar-lib'; -const FOO = 'BAR' +const FOO = 'BAR'; ``` ```js -const FOO = require('./foo') -const BAR = require('./bar') +const FOO = require('./foo'); +const BAR = require('./bar'); -const BAZ = 1 +const BAZ = 1; ``` Invalid: ```js import * as foo from 'foo' -const FOO = 'BAR' +const FOO = 'BAR'; ``` ```js -import * as foo from 'foo' -const FOO = 'BAR' +import * as foo from 'foo'; +const FOO = 'BAR'; -import { bar } from 'bar-lib' +import { bar } from 'bar-lib'; ``` ```js -const FOO = require('./foo') -const BAZ = 1 -const BAR = require('./bar') +const FOO = require('./foo'); +const BAZ = 1; +const BAR = require('./bar'); ``` With `count` set to `2` this will be considered valid: ```js -import defaultExport from './foo' +import defaultExport from './foo'; -const FOO = 'BAR' +const FOO = 'BAR'; +``` + +```js +import defaultExport from './foo'; + + + +const FOO = 'BAR'; ``` With `count` set to `2` these will be considered invalid: ```js -import defaultExport from './foo' -const FOO = 'BAR' +import defaultExport from './foo'; +const FOO = 'BAR'; ``` ```js -import defaultExport from './foo' +import defaultExport from './foo'; -const FOO = 'BAR' +const FOO = 'BAR'; +``` + +With `count` set to `2` and `exactCount` set to `true` this will be considered valid: + +```js +import defaultExport from './foo'; + + +const FOO = 'BAR'; +``` + +With `count` set to `2` and `exactCount` set to `true` these will be considered invalid: + +```js +import defaultExport from './foo'; +const FOO = 'BAR'; +``` + +```js +import defaultExport from './foo'; + +const FOO = 'BAR'; +``` + +```js +import defaultExport from './foo'; + + + +const FOO = 'BAR'; +``` + +```js +import defaultExport from './foo'; + + + + +const FOO = 'BAR'; ``` With `considerComments` set to `false` this will be considered valid: @@ -104,6 +153,7 @@ const FOO = 'BAR' ``` ## Example options usage + ```json { "rules": { @@ -112,7 +162,6 @@ const FOO = 'BAR' } ``` - ## When Not To Use It If you like to visually group module imports with its usage, you don't want to use this rule. diff --git a/docs/rules/no-absolute-path.md b/docs/rules/no-absolute-path.md index a796f9d574..48fb9532bd 100644 --- a/docs/rules/no-absolute-path.md +++ b/docs/rules/no-absolute-path.md @@ -38,9 +38,9 @@ By default, only ES6 imports and CommonJS `require` calls will have this rule en You may provide an options object providing true/false for any of -- `esmodule`: defaults to `true` -- `commonjs`: defaults to `true` -- `amd`: defaults to `false` + - `esmodule`: defaults to `true` + - `commonjs`: defaults to `true` + - `amd`: defaults to `false` If `{ amd: true }` is provided, dependency paths for AMD-style `define` and `require` calls will be resolved: diff --git a/docs/rules/no-amd.md b/docs/rules/no-amd.md index 155c19b3ca..6e592ba758 100644 --- a/docs/rules/no-amd.md +++ b/docs/rules/no-amd.md @@ -33,5 +33,5 @@ Special thanks to @xjamundx for donating his no-define rule as a start to this. ## Further Reading -- [`no-commonjs`](./no-commonjs.md): report CommonJS `require` and `exports` -- Source: https://github.com/xjamundx/eslint-plugin-modules + - [`no-commonjs`](./no-commonjs.md): report CommonJS `require` and `exports` + - Source: diff --git a/docs/rules/no-anonymous-default-export.md b/docs/rules/no-anonymous-default-export.md index d3c88f94e0..70efb84501 100644 --- a/docs/rules/no-anonymous-default-export.md +++ b/docs/rules/no-anonymous-default-export.md @@ -28,6 +28,7 @@ The complete default configuration looks like this. ## Rule Details ### Fail + ```js export default [] @@ -48,6 +49,7 @@ export default new Foo() ``` ### Pass + ```js const foo = 123 export default foo diff --git a/docs/rules/no-commonjs.md b/docs/rules/no-commonjs.md index 09a6b44010..4dc9c8c5d9 100644 --- a/docs/rules/no-commonjs.md +++ b/docs/rules/no-commonjs.md @@ -86,12 +86,11 @@ don't want this rule. It is also fairly noisy if you have a larger codebase that is being transitioned from CommonJS to ES6 modules. - ## Contributors Special thanks to @xjamundx for donating the module.exports and exports.* bits. ## Further Reading -- [`no-amd`](./no-amd.md): report on AMD `require`, `define` -- Source: https://github.com/xjamundx/eslint-plugin-modules + - [`no-amd`](./no-amd.md): report on AMD `require`, `define` + - Source: diff --git a/docs/rules/no-cycle.md b/docs/rules/no-cycle.md index 6635ba73f0..76e96f95f2 100644 --- a/docs/rules/no-cycle.md +++ b/docs/rules/no-cycle.md @@ -24,7 +24,6 @@ for that, see [`no-self-import`]. This rule ignores type-only imports in Flow and TypeScript syntax (`import type` and `import typeof`), which have no runtime effect. - ## Rule Details ### Options @@ -103,9 +102,9 @@ this rule enabled. ## Further Reading -- [Original inspiring issue](https://github.com/import-js/eslint-plugin-import/issues/941) -- Rule to detect that module imports itself: [`no-self-import`] -- [`import/external-module-folders`] setting + - [Original inspiring issue](https://github.com/import-js/eslint-plugin-import/issues/941) + - Rule to detect that module imports itself: [`no-self-import`] + - [`import/external-module-folders`] setting [`no-self-import`]: ./no-self-import.md diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md index 641fc1a8f4..a647d77ad5 100644 --- a/docs/rules/no-deprecated.md +++ b/docs/rules/no-deprecated.md @@ -3,7 +3,7 @@ Reports use of a deprecated name, as indicated by a JSDoc block with a `@deprecated` -tag or TomDoc `Deprecated: ` comment. +tag or TomDoc `Deprecated:` comment. using a JSDoc `@deprecated` tag: @@ -45,19 +45,18 @@ export function multiply(six, nine) { Only JSDoc is enabled by default. Other documentation styles can be enabled with the `import/docstyle` setting. - ```yaml # .eslintrc.yml settings: import/docstyle: ['jsdoc', 'tomdoc'] ``` -### Worklist +## Worklist -- [x] report explicit imports on the import node -- [x] support namespaces - - [x] should bubble up through deep namespaces (#157) -- [x] report explicit imports at reference time (at the identifier) similar to namespace -- [x] mark module deprecated if file JSDoc has a @deprecated tag? -- [ ] don't flag redeclaration of imported, deprecated names -- [ ] flag destructuring + - [x] report explicit imports on the import node + - [x] support namespaces + - [x] should bubble up through deep namespaces (#157) + - [x] report explicit imports at reference time (at the identifier) similar to namespace + - [x] mark module deprecated if file JSDoc has a @deprecated tag? + - [ ] don't flag redeclaration of imported, deprecated names + - [ ] flag destructuring diff --git a/docs/rules/no-duplicates.md b/docs/rules/no-duplicates.md index 553fbbcc34..29c16f15d1 100644 --- a/docs/rules/no-duplicates.md +++ b/docs/rules/no-duplicates.md @@ -7,7 +7,6 @@ Reports if a resolved path is imported more than once. -+(fixable) The `--fix` option on the [command line] automatically fixes some problems reported by this rule. ESLint core has a similar rule ([`no-duplicate-imports`](https://eslint.org/docs/rules/no-duplicate-imports)), but this version is different in two key ways: @@ -18,6 +17,7 @@ is different in two key ways: ## Rule Details Valid: + ```js import SomeDefaultClass, * as names from './mod' // Flow `type` import from same module is fine @@ -53,6 +53,7 @@ Config: ``` And then the following code becomes valid: + ```js import minifiedMod from './mod?minify' import noCommentsMod from './mod?comments=0' @@ -60,6 +61,7 @@ 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' @@ -79,17 +81,22 @@ Config: -❌ Invalid `["error", "prefer-inline"]` +❌ Invalid `["error", {"prefer-inline": true}]` ```js import { AValue, type AType } from './mama-mia' import type { BType } from './mama-mia' + +import { CValue } from './papa-mia' +import type { CType } from './papa-mia' ``` -✅ Valid with `["error", "prefer-inline"]` +✅ Valid with `["error", {"prefer-inline": true}]` ```js import { AValue, type AType, type BType } from './mama-mia' + +import { CValue, type CType } from './papa-mia' ``` diff --git a/docs/rules/no-empty-named-blocks.md b/docs/rules/no-empty-named-blocks.md index 7bf4d695cf..ad83c535f8 100644 --- a/docs/rules/no-empty-named-blocks.md +++ b/docs/rules/no-empty-named-blocks.md @@ -1,6 +1,6 @@ # import/no-empty-named-blocks -🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). +🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions). @@ -9,35 +9,41 @@ Reports the use of empty named import blocks. ## Rule Details ### Valid + ```js import { mod } from 'mod' import Default, { mod } from 'mod' ``` When using typescript + ```js import type { mod } from 'mod' ``` When using flow + ```js import typeof { mod } from 'mod' ``` ### Invalid + ```js import {} from 'mod' import Default, {} from 'mod' ``` When using typescript + ```js import type Default, {} from 'mod' import type {} from 'mod' ``` When using flow + ```js import typeof {} from 'mod' import typeof Default, {} from 'mod' -``` \ No newline at end of file +``` diff --git a/docs/rules/no-extraneous-dependencies.md b/docs/rules/no-extraneous-dependencies.md index 68cd4b154f..848d5bb0da 100644 --- a/docs/rules/no-extraneous-dependencies.md +++ b/docs/rules/no-extraneous-dependencies.md @@ -7,11 +7,12 @@ The closest parent `package.json` will be used. If no `package.json` is found, t Modules have to be installed for this rule to work. -### Options +## Options This rule supports the following options: `devDependencies`: If set to `false`, then the rule will show an error when `devDependencies` are imported. Defaults to `true`. +Type imports are ignored by default. `optionalDependencies`: If set to `false`, then the rule will show an error when `optionalDependencies` are imported. Defaults to `true`. @@ -31,7 +32,7 @@ You can also use an array of globs instead of literal booleans: "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js"]}] ``` -When using an array of globs, the setting will be set to `true` (no errors reported) if the name of the file being linted matches a single glob in the array, and `false` otherwise. +When using an array of globs, the setting will be set to `true` (no errors reported) if the name of the file being linted (i.e. not the imported file/module) matches a single glob in the array, and `false` otherwise. There are 2 boolean options to opt into checking extra imports that are normally ignored: `includeInternal`, which enables the checking of internal modules, and `includeTypes`, which enables checking of type imports in TypeScript. @@ -59,6 +60,7 @@ folder layouts: ## Rule Details Given the following `package.json`: + ```json { "name": "my-project", @@ -87,7 +89,6 @@ Given the following `package.json`: } ``` - ## Fail ```js @@ -116,7 +117,6 @@ var foo = require('./foo'); import type { MyType } from 'foo'; ``` - ## Pass ```js @@ -134,7 +134,6 @@ import type { MyType } from 'foo'; import react from 'react'; ``` - ## When Not To Use It If you do not have a `package.json` file in your project. diff --git a/docs/rules/no-import-module-exports.md b/docs/rules/no-import-module-exports.md index 08aacfcc34..1c57226495 100644 --- a/docs/rules/no-import-module-exports.md +++ b/docs/rules/no-import-module-exports.md @@ -12,7 +12,8 @@ If you have multiple entry points or are using `js:next` this rule includes an ## Options -#### `exceptions` +### `exceptions` + - An array of globs. The rule will be omitted from any file that matches a glob in the options array. For example, the following setting will omit the rule in the `some-file.js` file. @@ -43,6 +44,7 @@ console.log(baz) ``` ### Pass + Given the following package.json: ```json @@ -75,4 +77,5 @@ module.exports = foo; ``` ### Further Reading + - [webpack issue #4039](https://github.com/webpack/webpack/issues/4039) diff --git a/docs/rules/no-internal-modules.md b/docs/rules/no-internal-modules.md index 47f7490da6..433b55140d 100644 --- a/docs/rules/no-internal-modules.md +++ b/docs/rules/no-internal-modules.md @@ -8,14 +8,14 @@ Use this rule to prevent importing the submodules of other modules. This rule has two mutally exclusive options that are arrays of [minimatch/glob patterns](https://github.com/isaacs/node-glob#glob-primer) patterns: -- `allow` that include paths and import statements that can be imported with reaching. -- `forbid` that exclude paths and import statements that can be imported with reaching. + - `allow` that include paths and import statements that can be imported with reaching. + - `forbid` that exclude paths and import statements that can be imported with reaching. ### Examples Given the following folder structure: -``` +```pt my-project ├── actions │ └── getUser.js @@ -33,7 +33,8 @@ my-project ``` And the .eslintrc file: -``` + +```json { ... "rules": { @@ -76,7 +77,7 @@ export { settings } from '../app'; Given the following folder structure: -``` +```pt my-project ├── actions │ └── getUser.js @@ -94,7 +95,8 @@ my-project ``` And the .eslintrc file: -``` + +```json { ... "rules": { diff --git a/docs/rules/no-mutable-exports.md b/docs/rules/no-mutable-exports.md index f0a6251c19..ce51627858 100644 --- a/docs/rules/no-mutable-exports.md +++ b/docs/rules/no-mutable-exports.md @@ -43,8 +43,8 @@ export function getCount() {} // reported here: exported function is reassigned To prevent general reassignment of these identifiers, exported or not, you may want to enable the following core ESLint rules: -- [no-func-assign] -- [no-class-assign] + - [no-func-assign] + - [no-class-assign] [no-func-assign]: https://eslint.org/docs/rules/no-func-assign [no-class-assign]: https://eslint.org/docs/rules/no-class-assign diff --git a/docs/rules/no-named-as-default-member.md b/docs/rules/no-named-as-default-member.md index 5e0f5069e9..e8935fb7df 100644 --- a/docs/rules/no-named-as-default-member.md +++ b/docs/rules/no-named-as-default-member.md @@ -17,13 +17,12 @@ Furthermore, [in Babel 5 this is actually how things worked][blog]. This was fixed in Babel 6. Before upgrading an existing codebase to Babel 6, it can be useful to run this lint rule. - [blog]: https://kentcdodds.com/blog/misunderstanding-es6-modules-upgrading-babel-tears-and-a-solution - ## Rule Details Given: + ```js // foo.js export default 'foo'; @@ -31,11 +30,13 @@ export const bar = 'baz'; ``` ...this would be valid: + ```js import foo, {bar} from './foo.js'; ``` ...and the following would be reported: + ```js // Caution: `foo` also has a named export `bar`. // Check if you meant to write `import {bar} from './foo.js'` instead. diff --git a/docs/rules/no-named-as-default.md b/docs/rules/no-named-as-default.md index b3715e6c44..043d699424 100644 --- a/docs/rules/no-named-as-default.md +++ b/docs/rules/no-named-as-default.md @@ -8,12 +8,13 @@ Reports use of an exported name as the locally imported name of a default export Rationale: using an exported name as the name of the default export is likely... -- *misleading*: others familiar with `foo.js` probably expect the name to be `foo` -- *a mistake*: only needed to import `bar` and forgot the brackets (the case that is prompting this) + - _misleading_: others familiar with `foo.js` probably expect the name to be `foo` + - _a mistake_: only needed to import `bar` and forgot the brackets (the case that is prompting this) ## Rule Details Given: + ```js // foo.js export default 'foo'; @@ -21,11 +22,13 @@ export const bar = 'baz'; ``` ...this would be valid: + ```js import foo from './foo.js'; ``` ...and this would be reported: + ```js // message: Using exported name 'bar' as identifier for default export. import bar from './foo.js'; @@ -43,8 +46,8 @@ export bar from './foo.js'; ## Further Reading -- ECMAScript Proposal: [export ns from] -- ECMAScript Proposal: [export default from] + - ECMAScript Proposal: [export ns from] + - ECMAScript Proposal: [export default from] [export ns from]: https://github.com/leebyron/ecmascript-export-ns-from [export default from]: https://github.com/leebyron/ecmascript-export-default-from diff --git a/docs/rules/no-named-default.md b/docs/rules/no-named-default.md index 2f3d54b807..05860cde1e 100644 --- a/docs/rules/no-named-default.md +++ b/docs/rules/no-named-default.md @@ -13,6 +13,7 @@ Note that type imports, as used by [Flow], are always ignored. ## Rule Details Given: + ```js // foo.js export default 'foo'; @@ -20,12 +21,14 @@ export const bar = 'baz'; ``` ...these would be valid: + ```js import foo from './foo.js'; import foo, { bar } from './foo.js'; ``` ...and these would be reported: + ```js // message: Using exported name 'bar' as identifier for default export. import { default as foo } from './foo.js'; diff --git a/docs/rules/no-namespace.md b/docs/rules/no-namespace.md index 5545bce229..c7346515a5 100644 --- a/docs/rules/no-namespace.md +++ b/docs/rules/no-namespace.md @@ -6,14 +6,13 @@ 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. +The rule is auto-fixable when the namespace object is only used for direct member access, e.g. `namespace.a`. -### Options +## Options This rule supports the following options: -- `ignore`: array of glob strings for modules that should be ignored by the rule. + - `ignore`: array of glob strings for modules that should be ignored by the rule. ## Rule Details diff --git a/docs/rules/no-nodejs-modules.md b/docs/rules/no-nodejs-modules.md index 624c27e059..5cbc907286 100644 --- a/docs/rules/no-nodejs-modules.md +++ b/docs/rules/no-nodejs-modules.md @@ -4,11 +4,11 @@ Forbid the use of Node.js builtin modules. Can be useful for client-side web projects that do not have access to those modules. -### Options +## Options This rule supports the following options: -- `allow`: Array of names of allowed modules. Defaults to an empty array. + - `allow`: Array of names of allowed modules. Defaults to an empty array. ## Rule Details diff --git a/docs/rules/no-relative-packages.md b/docs/rules/no-relative-packages.md index 4919de94e5..4014ed9859 100644 --- a/docs/rules/no-relative-packages.md +++ b/docs/rules/no-relative-packages.md @@ -9,13 +9,11 @@ Use this rule to prevent importing packages through relative paths. It's useful in Yarn/Lerna workspaces, were it's possible to import a sibling package using `../package` relative path, while direct `package` is the correct one. -+(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule. - -### Examples +## Examples Given the following folder structure: -``` +```pt my-project ├── packages │ ├── foo @@ -28,7 +26,8 @@ my-project ``` And the .eslintrc file: -``` + +```json { ... "rules": { diff --git a/docs/rules/no-relative-parent-imports.md b/docs/rules/no-relative-parent-imports.md index e5684eb1cf..c1f9784871 100644 --- a/docs/rules/no-relative-parent-imports.md +++ b/docs/rules/no-relative-parent-imports.md @@ -8,7 +8,7 @@ This rule is useful for enforcing tree-like folder structures instead of complex To fix violations of this rule there are three general strategies. Given this example: -``` +```pt numbers └── three.js add.js @@ -32,51 +32,51 @@ You can, 1. Move the file to be in a sibling folder (or higher) of the dependency. -`three.js` could be be in the same folder as `add.js`: + `three.js` could be be in the same folder as `add.js`: -``` -three.js -add.js -``` + ```pt + three.js + add.js + ``` -or since `add` doesn't have any imports, it could be in it's own directory (namespace): + or since `add` doesn't have any imports, it could be in it's own directory (namespace): -``` -math -└── add.js -three.js -``` + ```pt + math + └── add.js + three.js + ``` 2. Pass the dependency as an argument at runtime (dependency injection) -```js -// three.js -export default function three(add) { - return add([1, 2]); -} + ```js + // three.js + export default function three(add) { + return add([1, 2]); + } -// somewhere else when you use `three.js`: -import add from './add'; -import three from './numbers/three'; -console.log(three(add)); -``` + // somewhere else when you use `three.js`: + import add from './add'; + import three from './numbers/three'; + console.log(three(add)); + ``` 3. Make the dependency a package so it's globally available to all files in your project: -```js -import add from 'add'; // from https://www.npmjs.com/package/add -export default function three() { - return add([1,2]); -} -``` + ```js + import add from 'add'; // from https://www.npmjs.com/package/add + export default function three() { + return add([1,2]); + } + ``` These are (respectively) static, dynamic & global solutions to graph-like dependency resolution. -### Examples +## Examples Given the following folder structure: -``` +```pt my-project ├── lib │ ├── a.js @@ -85,7 +85,8 @@ my-project ``` And the .eslintrc file: -``` + +```json { ... "rules": { diff --git a/docs/rules/no-restricted-paths.md b/docs/rules/no-restricted-paths.md index 344c33bb67..293f3ba009 100644 --- a/docs/rules/no-restricted-paths.md +++ b/docs/rules/no-restricted-paths.md @@ -13,25 +13,26 @@ This rule has one option. The option is an object containing the definition of a The default value for `basePath` is the current working directory. Each zone consists of the `target` paths, a `from` paths, and an optional `except` and `message` attribute. -- `target` contains the paths where the restricted imports should be applied. It can be expressed by - - directory string path that matches all its containing files - - glob pattern matching all the targeted files - - an array of multiple of the two types above -- `from` paths define the folders that are not allowed to be used in an import. It can be expressed by - - directory string path that matches all its containing files - - glob pattern matching all the files restricted to be imported - - an array of multiple directory string path - - an array of multiple glob patterns -- `except` may be defined for a zone, allowing exception paths that would otherwise violate the related `from`. Note that it does not alter the behaviour of `target` in any way. - - in case `from` contains only glob patterns, `except` must be an array of glob patterns as well - - in case `from` contains only directory path, `except` is relative to `from` and cannot backtrack to a parent directory -- `message` - will be displayed in case of the rule violation. + + - `target` contains the paths where the restricted imports should be applied. It can be expressed by + - directory string path that matches all its containing files + - glob pattern matching all the targeted files + - an array of multiple of the two types above + - `from` paths define the folders that are not allowed to be used in an import. It can be expressed by + - directory string path that matches all its containing files + - glob pattern matching all the files restricted to be imported + - an array of multiple directory string path + - an array of multiple glob patterns + - `except` may be defined for a zone, allowing exception paths that would otherwise violate the related `from`. Note that it does not alter the behaviour of `target` in any way. + - in case `from` contains only glob patterns, `except` must be an array of glob patterns as well + - in case `from` contains only directory path, `except` is relative to `from` and cannot backtrack to a parent directory + - `message` - will be displayed in case of the rule violation. ### Examples Given the following folder structure: -``` +```pt my-project ├── client │ └── foo.js @@ -58,7 +59,7 @@ import baz from '../client/baz'; Given the following folder structure: -``` +```pt my-project ├── client │ └── foo.js @@ -74,7 +75,7 @@ and the current file being linted is `my-project/server/one/a.js`. and the current configuration is set to: -``` +```json { "zones": [ { "target": "./tests/files/restricted-paths/server/one", "from": "./tests/files/restricted-paths/server", @@ -99,7 +100,7 @@ import b from './b' Given the following folder structure: -``` +```pt my-project ├── client └── foo.js @@ -111,7 +112,7 @@ my-project and the current configuration is set to: -``` +```json { "zones": [ { "target": "./tests/files/restricted-paths/client/!(sub-module)/**/*", "from": "./tests/files/restricted-paths/client/sub-module/**/*", @@ -134,7 +135,7 @@ import b from './baz' Given the following folder structure: -``` +```pt my-project └── one └── a.js @@ -149,7 +150,7 @@ my-project and the current configuration is set to: -``` +```json { "zones": [ { @@ -195,4 +196,3 @@ import a from '../one/a' ```js import a from './a' ``` - diff --git a/docs/rules/no-unassigned-import.md b/docs/rules/no-unassigned-import.md index 6f763e9737..617395e2c3 100644 --- a/docs/rules/no-unassigned-import.md +++ b/docs/rules/no-unassigned-import.md @@ -3,12 +3,13 @@ With both CommonJS' `require` and the ES6 modules' `import` syntax, it is possible to import a module but not to use its result. This can be done explicitly by not assigning the module to as variable. Doing so can mean either of the following things: -- The module is imported but not used -- The module has side-effects (like [`should`](https://www.npmjs.com/package/should)). Having side-effects, makes it hard to know whether the module is actually used or can be removed. It can also make it harder to test or mock parts of your application. + + - The module is imported but not used + - The module has side-effects (like [`should`](https://www.npmjs.com/package/should)). Having side-effects, makes it hard to know whether the module is actually used or can be removed. It can also make it harder to test or mock parts of your application. This rule aims to remove modules with side-effects by reporting when a module is imported but not assigned. -### Options +## Options This rule supports the following option: @@ -16,7 +17,6 @@ This rule supports the following option: Note that the globs start from the where the linter is executed (usually project root), but not from each file that includes the source. Learn more in both the pass and fail examples below. - ## Fail ```js @@ -28,7 +28,6 @@ import '../styles/app.css' // {"allow": ["styles/*.css"]} ``` - ## Pass ```js diff --git a/docs/rules/no-unresolved.md b/docs/rules/no-unresolved.md index 13f7928877..ca1da39c00 100644 --- a/docs/rules/no-unresolved.md +++ b/docs/rules/no-unresolved.md @@ -50,6 +50,7 @@ const { default: x } = require('./foo') // ignored ``` Both may be provided, too: + ```js /*eslint import/no-unresolved: [2, { commonjs: true, amd: true }]*/ const { default: x } = require('./foo') // reported if './foo' is not found @@ -84,7 +85,6 @@ const { default: x } = require('./foo') // reported if './foo' is actually './Fo The `caseSensitive` option does not detect case for the current working directory. The `caseSensitiveStrict` option allows checking `cwd` in resolved path. By default, the option is disabled. - ```js /*eslint import/no-unresolved: [2, { caseSensitiveStrict: true }]*/ @@ -102,9 +102,9 @@ If you're using a module bundler other than Node or Webpack, you may end up with ## Further Reading -- [Resolver plugins](../../README.md#resolvers) -- [Node resolver](https://npmjs.com/package/eslint-import-resolver-node) (default) -- [Webpack resolver](https://npmjs.com/package/eslint-import-resolver-webpack) -- [`import/ignore`] global setting + - [Resolver plugins](../../README.md#resolvers) + - [Node resolver](https://npmjs.com/package/eslint-import-resolver-node) (default) + - [Webpack resolver](https://npmjs.com/package/eslint-import-resolver-webpack) + - [`import/ignore`] global setting [`import/ignore`]: ../../README.md#importignore diff --git a/docs/rules/no-unused-modules.md b/docs/rules/no-unused-modules.md index 5cd24bef41..53c2479272 100644 --- a/docs/rules/no-unused-modules.md +++ b/docs/rules/no-unused-modules.md @@ -3,19 +3,20 @@ Reports: - - modules without any exports - - individual exports not being statically `import`ed or `require`ed from other modules in the same project - - dynamic imports are supported if argument is a literal string + - modules without any exports + - individual exports not being statically `import`ed or `require`ed from other modules in the same project + - dynamic imports are supported if argument is a literal string ## Rule Details ### Usage -In order for this plugin to work, one of the options `missingExports` or `unusedExports` must be enabled (see "Options" section below). In the future, these options will be enabled by default (see https://github.com/import-js/eslint-plugin-import/issues/1324) +In order for this plugin to work, at least one of the options `missingExports` or `unusedExports` must be enabled (see "Options" section below). In the future, these options will be enabled by default (see ) Example: -``` + +```json "rules: { ...otherRules, "import/no-unused-modules": [1, {"unusedExports": true}] @@ -26,14 +27,15 @@ Example: This rule takes the following option: -- **`missingExports`**: if `true`, files without any exports are reported (defaults to `false`) -- **`unusedExports`**: if `true`, exports without any static usage within other modules are reported (defaults to `false`) -- `src`: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided -- `ignoreExports`: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package) - + - **`missingExports`**: if `true`, files without any exports are reported (defaults to `false`) + - **`unusedExports`**: if `true`, exports without any static usage within other modules are reported (defaults to `false`) + - `src`: an array with files/paths to be analyzed. It only applies to unused exports. Defaults to `process.cwd()`, if not provided + - `ignoreExports`: an array with files/paths for which unused exports will not be reported (e.g module entry points in a published package) ### Example for missing exports + #### The following will be reported + ```js const class MyClass { /*...*/ } @@ -45,18 +47,23 @@ function makeClass() { return new MyClass(...arguments) } ```js export default function () { /*...*/ } ``` + ```js export const foo = function () { /*...*/ } ``` + ```js export { foo, bar } ``` + ```js export { foo as bar } ``` ### Example for unused exports + given file-f: + ```js import { e } from 'file-a' import { f } from 'file-b' @@ -65,24 +72,32 @@ export { default, i0 } from 'file-d' // both will be reported export const j = 99 // will be reported ``` + and file-d: + ```js export const i0 = 9 // will not be reported export const i1 = 9 // will be reported export default () => {} // will not be reported ``` + and file-c: + ```js export const h = 8 // will not be reported export default () => {} // will be reported, as export * only considers named exports and ignores default exports ``` + and file-b: + ```js import two, { b, c, doAnything } from 'file-a' export const f = 6 // will not be reported ``` + and file-a: + ```js const b = 2 const c = 3 @@ -102,6 +117,7 @@ export default 5 // will not be reported ``` #### Important Note + Exports from files listed as a main file (`main`, `browser`, or `bin` fields in `package.json`) will be ignored by default. This only applies if the `package.json` is not set to `private: true` ## When not to use diff --git a/docs/rules/no-useless-path-segments.md b/docs/rules/no-useless-path-segments.md index c8dc67727a..22c4bf965b 100644 --- a/docs/rules/no-useless-path-segments.md +++ b/docs/rules/no-useless-path-segments.md @@ -10,7 +10,7 @@ Use this rule to prevent unnecessary path segments in import and require stateme Given the following folder structure: -``` +```pt my-project ├── app.js ├── footer.js @@ -62,6 +62,7 @@ import fs from "fs"; ### noUselessIndex If you want to detect unnecessary `/index` or `/index.js` (depending on the specified file extensions, see below) imports in your paths, you can enable the option `noUselessIndex`. By default it is set to `false`: + ```js "import/no-useless-path-segments": ["error", { noUselessIndex: true, diff --git a/docs/rules/no-webpack-loader-syntax.md b/docs/rules/no-webpack-loader-syntax.md index e1b7a4bd9d..291b1c058a 100644 --- a/docs/rules/no-webpack-loader-syntax.md +++ b/docs/rules/no-webpack-loader-syntax.md @@ -5,6 +5,7 @@ Forbid Webpack loader syntax in imports. [Webpack](https://webpack.js.org) allows specifying the [loaders](https://webpack.js.org/concepts/loaders/) to use in the import source string using a special syntax like this: + ```js var moduleWithOneLoader = require("my-loader!./my-awesome-module"); ``` diff --git a/docs/rules/order.md b/docs/rules/order.md index fc3aa9bca5..24692f2d21 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -5,7 +5,6 @@ Enforce a convention in the order of `require()` / `import` statements. -+(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule. With the [`groups`](#groups-array) option set to `["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"]` the order is as shown in the following example: @@ -37,7 +36,6 @@ Unassigned imports are ignored, as the order they are imported in may be importa Statements using the ES6 `import` syntax must appear before any `require()` statements. - ## Fail ```ts @@ -55,7 +53,6 @@ var path = require('path'); import foo from './foo'; // `import` statements must be before `require` statement ``` - ## Pass ```ts @@ -103,11 +100,12 @@ import 'format2.css'; // OK This rule supports the following options: -### `groups: [array]`: +### `groups: [array]` How groups are defined, and the order to respect. `groups` must be an array of `string` or [`string`]. The only allowed `string`s are: `"builtin"`, `"external"`, `"internal"`, `"unknown"`, `"parent"`, `"sibling"`, `"index"`, `"object"`, `"type"`. The enforced order is the same as the order of each element in a group. Omitted types are implicitly grouped together as the last element. Example: + ```ts [ 'builtin', // Built-in types are first @@ -117,6 +115,7 @@ The enforced order is the same as the order of each element in a group. Omitted // Then the rest: internal and external type ] ``` + The default value is `["builtin", "external", "parent", "sibling", "index"]`. You can set the options like this: @@ -139,7 +138,7 @@ You can set the options like this: ] ``` -### `pathGroups: [array of objects]`: +### `pathGroups: [array of objects]` To be able to group by paths mostly needed with aliases pathGroups can be defined. @@ -165,7 +164,7 @@ Properties of the objects } ``` -### `distinctGroup: [boolean]`: +### `distinctGroup: [boolean]` This changes how `pathGroups[].position` affects grouping. The property is most useful when `newlines-between` is set to `always` and at least 1 `pathGroups` entry has a `position` property set. @@ -174,6 +173,7 @@ By default, in the context of a particular `pathGroup` entry, when setting `posi Note that currently, `distinctGroup` defaults to `true`. However, in a later update, the default will change to `false` Example: + ```json { "import/order": ["error", { @@ -190,12 +190,13 @@ Example: } ``` -### `pathGroupsExcludedImportTypes: [array]`: +### `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", { @@ -214,6 +215,7 @@ Example: You can also use `patterns`(e.g., `react`, `react-router-dom`, etc). Example: + ```json { "import/order": [ @@ -231,16 +233,17 @@ Example: ] } ``` + The default value is `["builtin", "external", "object"]`. -### `newlines-between: [ignore|always|always-and-inside-groups|never]`: +### `newlines-between: [ignore|always|always-and-inside-groups|never]` Enforces or forbids new lines between import groups: -- If set to `ignore`, no errors related to new lines between import groups will be reported. -- If set to `always`, at least one new line between each group will be enforced, and new lines inside a group will be forbidden. To prevent multiple lines between imports, core `no-multiple-empty-lines` rule can be used. -- If set to `always-and-inside-groups`, it will act like `always` except newlines are allowed inside import groups. -- If set to `never`, no new lines are allowed in the entire import section. + - If set to `ignore`, no errors related to new lines between import groups will be reported. + - If set to `always`, at least one new line between each group will be enforced, and new lines inside a group will be forbidden. To prevent multiple lines between imports, core `no-multiple-empty-lines` rule can be used. + - If set to `always-and-inside-groups`, it will act like `always` except newlines are allowed inside import groups. + - If set to `never`, no new lines are allowed in the entire import section. The default value is `"ignore"`. @@ -304,15 +307,16 @@ import index from './'; import sibling from './foo'; ``` -### `alphabetize: {order: asc|desc|ignore, orderImportKind: asc|desc|ignore, caseInsensitive: true|false}`: +### `alphabetize: {order: asc|desc|ignore, orderImportKind: 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`). -- `orderImportKind`: use `asc` to sort in ascending order various import kinds, e.g. imports prefixed with `type` or `typeof`, with same import path. Use `desc` to sort in descending order (default: `ignore`). -- `caseInsensitive`: use `true` to ignore case, and `false` to consider case (default: `false`). + - `order`: use `asc` to sort in ascending order, and `desc` to sort in descending order (default: `ignore`). + - `orderImportKind`: use `asc` to sort in ascending order various import kinds, e.g. imports prefixed with `type` or `typeof`, with same import path. Use `desc` to sort in descending order (default: `ignore`). + - `caseInsensitive`: use `true` to ignore case, and `false` to consider case (default: `false`). Example setting: + ```ts alphabetize: { order: 'asc', /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */ @@ -342,9 +346,9 @@ import React, { PureComponent } from 'react'; import { compose, apply } from 'xcompose'; ``` -### `warnOnUnassignedImports: true|false`: +### `warnOnUnassignedImports: true|false` -* default: `false` + - default: `false` Warns when unassigned imports are out of order. These warning will not be fixed with `--fix` because unassigned imports are used for side-effects and changing the @@ -371,9 +375,9 @@ import './styles.css'; ## Related -- [`import/external-module-folders`] setting + - [`import/external-module-folders`] setting -- [`import/internal-regex`] setting + - [`import/internal-regex`] setting [`import/external-module-folders`]: ../../README.md#importexternal-module-folders diff --git a/docs/rules/prefer-default-export.md b/docs/rules/prefer-default-export.md index 5d335f4c12..e2a7bacd73 100644 --- a/docs/rules/prefer-default-export.md +++ b/docs/rules/prefer-default-export.md @@ -6,12 +6,12 @@ In exporting files, this rule checks if there is default export or not. ## Rule Details -##### rule schema: +### rule schema ```javascript "import/prefer-default-export": [ ( "off" | "warn" | "error" ), - { "target": "single" | "any" } // default is "single" + { "target": "single" | "any" } // default is "single" ] ``` @@ -109,7 +109,6 @@ How to setup config file for this rule: } ``` - The following patterns are *not* considered warnings: ```javascript diff --git a/docs/rules/unambiguous.md b/docs/rules/unambiguous.md index da77a7453f..e9e5bf73da 100644 --- a/docs/rules/unambiguous.md +++ b/docs/rules/unambiguous.md @@ -32,6 +32,7 @@ export {} // simple way to mark side-effects-only file as 'module' without any i ``` ...whereas the following file would be reported: + ```js (function x() { return 42 })() ``` @@ -48,9 +49,9 @@ a `module`. ## Further Reading -- [Unambiguous JavaScript Grammar] -- [`parserOptions.sourceType`] -- [node-eps#13](https://github.com/nodejs/node-eps/issues/13) + - [Unambiguous JavaScript Grammar] + - [`parserOptions.sourceType`] + - [node-eps#13](https://github.com/nodejs/node-eps/issues/13) [`parserOptions.sourceType`]: https://eslint.org/docs/user-guide/configuring#specifying-parser-options [Unambiguous JavaScript Grammar]: https://github.com/nodejs/node-eps/blob/HEAD/002-es-modules.md#32-determining-if-source-is-an-es-module diff --git a/memo-parser/.nycrc b/memo-parser/.nycrc new file mode 100644 index 0000000000..5d75e2157c --- /dev/null +++ b/memo-parser/.nycrc @@ -0,0 +1,19 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "lcov", "text", "html", "json"], + "require": [ + "babel-register" + ], + "sourceMap": true, + "instrument": false, + "exclude": [ + "coverage", + "test", + "tests", + "resolvers/*/test", + "scripts", + "memo-parser", + "lib" + ] +} diff --git a/memo-parser/README.md b/memo-parser/README.md index 8a2a3cb5cd..741e0ed4d0 100644 --- a/memo-parser/README.md +++ b/memo-parser/README.md @@ -1,13 +1,11 @@ # eslint-plugin-import/memo-parser - -## NOTE! +## NOTE This used to improve performance, but as of ESLint 5 and v2 of this plugin, it seems to just consume a bunch of memory and slightly increase lint times. **Not recommended for use at this time!** - This parser is just a memoizing wrapper around some actual parser. To configure, just add your _actual_ parser to the `parserOptions`, like so: diff --git a/memo-parser/index.js b/memo-parser/index.js index de558ffa3e..7868b7e953 100644 --- a/memo-parser/index.js +++ b/memo-parser/index.js @@ -17,7 +17,7 @@ const parserOptions = { }; exports.parse = function parse(content, options) { - options = Object.assign({}, options, parserOptions); + options = { ...options, ...parserOptions }; if (!options.filePath) { throw new Error('no file path provided!'); @@ -30,7 +30,7 @@ exports.parse = function parse(content, options) { const key = keyHash.digest('hex'); let ast = cache.get(key); - if (ast != null) return ast; + if (ast != null) { return ast; } const realParser = moduleRequire(options.parser); diff --git a/package.json b/package.json index c1044f1ea5..638942f97c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-import", - "version": "2.26.0", + "version": "2.29.1", "description": "Import with sanity.", "engines": { "node": ">=4" @@ -24,7 +24,7 @@ "copy-metafiles": "node --require babel-register ./scripts/copyMetafiles", "watch": "npm run tests-only -- -- --watch", "pretest": "linklocal", - "posttest": "eslint . && npm run update:eslint-docs -- --check", + "posttest": "eslint . && npm run update:eslint-docs -- --check && markdownlint \"**/*.md\"", "mocha": "cross-env BABEL_ENV=test nyc mocha", "tests-only": "npm run mocha tests/src", "test": "npm run tests-only", @@ -42,6 +42,7 @@ "keywords": [ "eslint", "eslintplugin", + "eslint-plugin", "es6", "jsnext", "modules", @@ -55,11 +56,10 @@ }, "homepage": "https://github.com/import-js/eslint-plugin-import", "devDependencies": { - "@angular-eslint/template-parser": "^13.2.1", + "@angular-eslint/template-parser": "^13.5.0", "@eslint/import-test-order-redirect-scoped": "file:./tests/files/order-redirect-scoped", "@test-scope/some-module": "file:./tests/files/symlinked-module", "@typescript-eslint/parser": "^2.23.0 || ^3.3.0 || ^4.29.3 || ^5.10.0", - "array.prototype.flatmap": "^1.3.0", "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-eslint": "=8.0.3 || ^8.2.6", @@ -69,11 +69,11 @@ "babel-preset-flow": "^6.23.0", "babel-register": "^6.26.0", "babylon": "^6.18.0", - "chai": "^4.3.6", + "chai": "^4.3.10", "cross-env": "^4.0.0", "escope": "^3.6.0", "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", - "eslint-doc-generator": "^1.0.0", + "eslint-doc-generator": "^1.6.1", "eslint-import-resolver-node": "file:./resolvers/node", "eslint-import-resolver-typescript": "^1.0.2 || ^1.1.1", "eslint-import-resolver-webpack": "file:./resolvers/webpack", @@ -83,17 +83,19 @@ "eslint-plugin-import": "2.x", "eslint-plugin-json": "^2.1.2", "fs-copy-file-sync": "^1.1.1", - "glob": "^7.2.0", + "glob": "^7.2.3", "in-publish": "^2.0.1", + "jackspeak": "=2.1.1", + "jsonc-parser": "=3.2.0", "linklocal": "^2.8.2", "lodash.isarray": "^4.0.0", + "markdownlint-cli": "~0.35", "mocha": "^3.5.3", "npm-which": "^3.0.1", "nyc": "^11.9.0", "redux": "^3.7.2", "rimraf": "^2.7.1", "safe-publish-latest": "^2.0.0", - "semver": "^6.3.0", "sinon": "^2.4.1", "typescript": "^2.8.1 || ~3.9.5 || ~4.5.2", "typescript-eslint-parser": "^15 || ^20 || ^22" @@ -102,18 +104,22 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" }, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "debug": "^2.6.9", + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.4", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.4", - "has": "^1.0.3", - "is-core-module": "^2.11.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.1", + "is-core-module": "^2.13.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.values": "^1.1.6", - "resolve": "^1.22.1", - "tsconfig-paths": "^3.14.1" + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.2", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" } } diff --git a/resolvers/README.md b/resolvers/README.md index b664721b83..e55ebb6104 100644 --- a/resolvers/README.md +++ b/resolvers/README.md @@ -2,14 +2,16 @@ Resolvers must export two names: -### `interfaceVersion => Number` +## `interfaceVersion => Number` This document currently describes version 2 of the resolver interface. As such, a resolver implementing this version should ```js export const interfaceVersion = 2 ``` + or + ```js exports.interfaceVersion = 2 ``` @@ -18,9 +20,10 @@ To the extent it is feasible, trailing versions of the resolvers will continue t Currently, version 1 is assumed if no `interfaceVersion` is available. (didn't think to define it until v2, heh. 😅) -### `resolve(source, file, config) => { found: Boolean, path: String? }` +## `resolve(source, file, config) => { found: Boolean, path: String? }` Given: + ```js // /some/path/to/module.js import ... from './imported-file' @@ -37,27 +40,29 @@ settings: node: { paths: [a, b, c] } ``` -#### Arguments +### Arguments The arguments provided will be: -##### `source` +#### `source` + the module identifier (`./imported-file`). -##### `file` +#### `file` + the absolute path to the file making the import (`/some/path/to/module.js`) -##### `config` +#### `config` an object provided via the `import/resolver` setting. `my-cool-resolver` will get `["some", "stuff"]` as its `config`, while `node` will get `{ "paths": ["a", "b", "c"] }` provided as `config`. -#### Return value +### Return value The first resolver to return `{found: true}` is considered the source of truth. The returned object has: -- `found`: `true` if the `source` module can be resolved relative to `file`, else `false` -- `path`: an absolute path `String` if the module can be located on the filesystem; else, `null`. + - `found`: `true` if the `source` module can be resolved relative to `file`, else `false` + - `path`: an absolute path `String` if the module can be located on the filesystem; else, `null`. An example of a `null` path is a Node core module, such as `fs` or `crypto`. These modules can always be resolved, but the path need not be provided as the plugin will not attempt to parse core modules at this time. @@ -68,13 +73,13 @@ If the resolver cannot resolve `source` relative to `file`, it should just retur Here is most of the [Node resolver] at the time of this writing. It is just a wrapper around substack/Browserify's synchronous [`resolve`]: ```js -var resolve = require('resolve'); +var resolve = require('resolve/sync'); var isCoreModule = require('is-core-module'); exports.resolve = function (source, file, config) { if (isCoreModule(source)) return { found: true, path: null }; try { - return { found: true, path: resolve.sync(source, opts(file, config)) }; + return { found: true, path: resolve(source, opts(file, config)) }; } catch (err) { return { found: false }; } diff --git a/resolvers/node/.nycrc b/resolvers/node/.nycrc new file mode 100644 index 0000000000..1084360870 --- /dev/null +++ b/resolvers/node/.nycrc @@ -0,0 +1,15 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "lcov", "text", "html", "json"], + "instrument": false, + "exclude": [ + "coverage", + "test", + "tests", + "resolvers/*/test", + "scripts", + "memo-parser", + "lib" + ] +} diff --git a/resolvers/node/CHANGELOG.md b/resolvers/node/CHANGELOG.md index 8812f12760..8e11359a92 100644 --- a/resolvers/node/CHANGELOG.md +++ b/resolvers/node/CHANGELOG.md @@ -5,56 +5,62 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## v0.3.9 - 2023-08-08 + - [fix] restore node 6 compatibility + +## v0.3.8 - 2023-08-05 + - [deps] update `is-core-module`, `resolve` + - [eslint] tighten up rules + +## v0.3.7 - 2023-01-11 +### Changed + - [Refactor] use `is-core-module` directly + ## v0.3.6 - 2021-08-15 ### Fixed -- when "module" does not exist, fall back to "main" ([#2186], thanks [@ljharb]) + - when "module" does not exist, fall back to "main" ([#2186], thanks [@ljharb]) ## v0.3.5 - 2021-08-08 ### Added -- use "module" in the same spot as "jsnext:main" ([#2166], thanks [@MustafaHaddara]) + - use "module" in the same spot as "jsnext:main" ([#2166], thanks [@MustafaHaddara]) ## v0.3.4 - 2020-06-16 ### Added -- add `.node` extension ([#1663]) + - add `.node` extension ([#1663]) ## v0.3.3 - 2020-01-10 ### Changed -- [meta] copy LICENSE file to all npm packages on prepublish ([#1595], thanks [@opichals]) + - [meta] copy LICENSE file to all npm packages on prepublish ([#1595], thanks [@opichals]) ## v0.3.2 - 2018-01-05 ### Added -- `.mjs` extension detected by default to support `experimental-modules` ([#939]) + - `.mjs` extension detected by default to support `experimental-modules` ([#939]) ### Deps -- update `debug`, `resolve` + - update `debug`, `resolve` ## v0.3.1 - 2017-06-23 ### Changed -- bumped `debug` dep to match other packages + - bumped `debug` dep to match other packages ## v0.3.0 - 2016-12-15 ### Changed -- bumped `resolve` to fix issues with Node builtins (thanks [@SkeLLLa] and [@ljharb]) + - bumped `resolve` to fix issues with Node builtins (thanks [@SkeLLLa] and [@ljharb]) ### Fixed -- use `files` in `package.json` to ship only `index.js` ([#531], thanks for noticing [@lukeapage]) + - use `files` in `package.json` to ship only `index.js` ([#531], thanks for noticing [@lukeapage]) ## v0.2.3 - 2016-08-20 ### Added -- debug logging (use `DEBUG=eslint-plugin-import:resolver:node eslint [...]`) + - debug logging (use `DEBUG=eslint-plugin-import:resolver:node eslint [...]`) ## v0.2.2 - 2016-07-14 ### Fixed -- Node resolver no longer declares the import plugin as a `peerDependency`. See [#437] - for a well-articulated and thoughtful expression of why this doesn't make sense. - Thanks [@jasonkarns] for the issue and the PR to fix it ([#438]). - - Also, apologies to the others who expressed this before, but I never understood - what the problem was.😅 + - Node resolver no longer declares the import plugin as a `peerDependency`. See [#437] for a well-articulated and thoughtful expression of why this doesn't make sense. Thanks [@jasonkarns] for the issue and the PR to fix it ([#438]). Also, apologies to the others who expressed this before, but I never understood what the problem was.😅 ## v0.2.1 ### Fixed -- find files with `.json` extensions (#333, thanks for noticing @jfmengels) + - find files with `.json` extensions (#333, thanks for noticing @jfmengels) [#2186]: https://github.com/import-js/eslint-plugin-import/issues/2186 [#2166]: https://github.com/import-js/eslint-plugin-import/pull/2166 @@ -66,8 +72,8 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [#437]: https://github.com/import-js/eslint-plugin-import/issues/437 [@jasonkarns]: https://github.com/jasonkarns -[@lukeapage]: https://github.com/lukeapage -[@SkeLLLa]: https://github.com/SkeLLLa [@ljharb]: https://github.com/ljharb -[@opichals]: https://github.com/opichals +[@lukeapage]: https://github.com/lukeapage [@MustafaHaddara]: https://github.com/MustafaHaddara +[@opichals]: https://github.com/opichals +[@SkeLLLa]: https://github.com/SkeLLLa diff --git a/resolvers/node/index.js b/resolvers/node/index.js index ac478ef029..9e0e753cc7 100644 --- a/resolvers/node/index.js +++ b/resolvers/node/index.js @@ -1,6 +1,6 @@ 'use strict'; -const resolve = require('resolve'); +const resolve = require('resolve/sync'); const isCoreModule = require('is-core-module'); const path = require('path'); @@ -8,34 +8,11 @@ const log = require('debug')('eslint-plugin-import:resolver:node'); exports.interfaceVersion = 2; -exports.resolve = function (source, file, config) { - log('Resolving:', source, 'from:', file); - let resolvedPath; - - if (isCoreModule(source)) { - log('resolved to core'); - return { found: true, path: null }; - } - - try { - const cachedFilter = function (pkg, dir) { return packageFilter(pkg, dir, config); }; - resolvedPath = resolve.sync(source, opts(file, config, cachedFilter)); - log('Resolved to:', resolvedPath); - return { found: true, path: resolvedPath }; - } catch (err) { - log('resolve threw error:', err); - return { found: false }; - } -}; - function opts(file, config, packageFilter) { - return Object.assign({ - // more closely matches Node (#333) + return Object.assign({ // more closely matches Node (#333) // plus 'mjs' for native modules! (#939) extensions: ['.mjs', '.js', '.json', '.node'], - }, - config, - { + }, config, { // path.resolve will handle paths relative to CWD basedir: path.dirname(path.resolve(file)), packageFilter, @@ -49,7 +26,7 @@ function packageFilter(pkg, dir, config) { const file = path.join(dir, 'dummy.js'); if (pkg.module) { try { - resolve.sync(String(pkg.module).replace(/^(?:\.\/)?/, './'), opts(file, config, identity)); + resolve(String(pkg.module).replace(/^(?:\.\/)?/, './'), opts(file, config, identity)); pkg.main = pkg.module; found = true; } catch (err) { @@ -58,7 +35,7 @@ function packageFilter(pkg, dir, config) { } if (!found && pkg['jsnext:main']) { try { - resolve.sync(String(pkg['jsnext:main']).replace(/^(?:\.\/)?/, './'), opts(file, config, identity)); + resolve(String(pkg['jsnext:main']).replace(/^(?:\.\/)?/, './'), opts(file, config, identity)); pkg.main = pkg['jsnext:main']; found = true; } catch (err) { @@ -67,3 +44,23 @@ function packageFilter(pkg, dir, config) { } return pkg; } + +exports.resolve = function (source, file, config) { + log('Resolving:', source, 'from:', file); + let resolvedPath; + + if (isCoreModule(source)) { + log('resolved to core'); + return { found: true, path: null }; + } + + try { + const cachedFilter = function (pkg, dir) { return packageFilter(pkg, dir, config); }; + resolvedPath = resolve(source, opts(file, config, cachedFilter)); + log('Resolved to:', resolvedPath); + return { found: true, path: resolvedPath }; + } catch (err) { + log('resolve threw error:', err); + return { found: false }; + } +}; diff --git a/resolvers/node/package.json b/resolvers/node/package.json index 771249ee58..bfaab40413 100644 --- a/resolvers/node/package.json +++ b/resolvers/node/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-node", - "version": "0.3.6", + "version": "0.3.9", "description": "Node default behavior import resolution plugin for eslint-plugin-import.", "main": "index.js", "files": [ @@ -30,8 +30,8 @@ "homepage": "https://github.com/import-js/eslint-plugin-import", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.11.0", - "resolve": "^1.22.1" + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" }, "devDependencies": { "chai": "^3.5.0", diff --git a/resolvers/node/test/packageMains.js b/resolvers/node/test/packageMains.js index caac6221ca..170b10e1a1 100644 --- a/resolvers/node/test/packageMains.js +++ b/resolvers/node/test/packageMains.js @@ -8,7 +8,6 @@ const resolver = require('../'); const file = path.join(__dirname, 'package-mains', 'dummy.js'); - describe('packageMains', function () { it('captures module', function () { expect(resolver.resolve('./module', file)).property('path') diff --git a/resolvers/node/test/paths.js b/resolvers/node/test/paths.js index 1c42b46167..e6ffdafcd9 100644 --- a/resolvers/node/test/paths.js +++ b/resolvers/node/test/paths.js @@ -11,7 +11,6 @@ describe('paths', function () { }); }); - describe('core', function () { it('returns found, but null path, for core Node modules', function () { const resolved = node.resolve('fs', './test/file.js'); @@ -20,7 +19,6 @@ describe('core', function () { }); }); - describe('default options', function () { it('finds .json files', function () { diff --git a/resolvers/webpack/.nycrc b/resolvers/webpack/.nycrc new file mode 100644 index 0000000000..1084360870 --- /dev/null +++ b/resolvers/webpack/.nycrc @@ -0,0 +1,15 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "lcov", "text", "html", "json"], + "instrument": false, + "exclude": [ + "coverage", + "test", + "tests", + "resolvers/*/test", + "scripts", + "memo-parser", + "lib" + ] +} diff --git a/resolvers/webpack/CHANGELOG.md b/resolvers/webpack/CHANGELOG.md index 1626bb2720..4fed046b46 100644 --- a/resolvers/webpack/CHANGELOG.md +++ b/resolvers/webpack/CHANGELOG.md @@ -5,6 +5,32 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +## 0.13.8 - 2023-10-22 + - [refactor] use `hasown` instead of `has` + - [deps] update `array.prototype.find`, `is-core-module`, `resolve` + + +## 0.13.7 - 2023-08-19 + - [fix] use the `dirname` of the `configPath` as `basedir` ([#2859]) + +## 0.13.6 - 2023-08-16 + - [refactor] revert back to `lodash/isEqual` + +## 0.13.5 - 2023-08-15 + - [refactor] replace `lodash/isEqual` usage with `deep-equal` + - [refactor] remove `lodash/get` usage + - [refactor] switch to a maintained `array.prototype.find` package + - [deps] update `resolve` + +## 0.13.4 - 2023-08-08 + - [fix] restore node 6 compatibility + +## 0.13.3 - 2023-08-05 + - [deps] update `is-core-module`, `resolve`, `semver` + - [eslint] tighten up rules + - [Tests] consolidate eslint config + - [Docs] HTTP => HTTPS ([#2287], thanks [@Schweinepriester]) + ## 0.13.2 - 2021-10-20 ### Changed @@ -13,35 +39,35 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## 0.13.1 - 2021-05-13 ### Added -- add support for webpack5 'externals function' ([#2023], thanks [@jet2jet]) + - add support for webpack5 'externals function' ([#2023], thanks [@jet2jet]) ### Changed -- Add warning about async Webpack configs ([#1962], thanks [@ogonkov]) -- Replace `node-libs-browser` with `is-core-module` ([#1967], thanks [@andersk]) -- [meta] add "engines" field to document existing requirements -- [Refactor] use `is-regex` instead of `instanceof RegExp` -- [Refactor] use `Array.isArray` instead of `instanceof Array` -- [deps] update `debug`, `interpret`, `is-core-module`, `lodash`, `resolve` + - Add warning about async Webpack configs ([#1962], thanks [@ogonkov]) + - Replace `node-libs-browser` with `is-core-module` ([#1967], thanks [@andersk]) + - [meta] add "engines" field to document existing requirements + - [Refactor] use `is-regex` instead of `instanceof RegExp` + - [Refactor] use `Array.isArray` instead of `instanceof Array` + - [deps] update `debug`, `interpret`, `is-core-module`, `lodash`, `resolve` ## 0.13.0 - 2020-09-27 ### Breaking -- [Breaking] Allow to resolve config path relative to working directory (#1276) + - [Breaking] Allow to resolve config path relative to working directory (#1276) ## 0.12.2 - 2020-06-16 ### Fixed -- [fix] provide config fallback ([#1705], thanks [@migueloller]) + - [fix] provide config fallback ([#1705], thanks [@migueloller]) ## 0.12.1 - 2020-01-10 ### Changed -- [meta] copy LICENSE file to all npm packages on prepublish ([#1595], thanks [@opichals]) + - [meta] copy LICENSE file to all npm packages on prepublish ([#1595], thanks [@opichals]) ## 0.12.0 - 2019-12-07 ### Added -- [New] enable passing cwd as an option to `eslint-import-resolver-webpack` ([#1503], thanks [@Aghassi]) + - [New] enable passing cwd as an option to `eslint-import-resolver-webpack` ([#1503], thanks [@Aghassi]) ## 0.11.1 - 2019-04-13 @@ -51,110 +77,108 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## 0.11.0 - 2018-01-22 ### Added -- support for `argv` parameter when config is a function. ([#1261], thanks [@keann]) + - support for `argv` parameter when config is a function. ([#1261], thanks [@keann]) ### Fixed -- crash when webpack config is an array of functions ([#1219]/[#1220] by [@idudinov]) + - crash when webpack config is an array of functions ([#1219]/[#1220] by [@idudinov]) ## 0.10.1 - 2018-06-24 ### Fixed -- log a useful error in a module bug arises ([#768]/[#767], thanks [@mattkrick]) + - log a useful error in a module bug arises ([#768]/[#767], thanks [@mattkrick]) ## 0.10.0 - 2018-05-17 ### Changed -- cache webpack resolve function, for performance ([#788]/[#1091]) + - cache webpack resolve function, for performance ([#788]/[#1091]) ## 0.9.0 - 2018-03-29 ### Breaking -- Fix with `pnpm` by bumping `resolve` ([#968]) + - Fix with `pnpm` by bumping `resolve` ([#968]) ## 0.8.4 - 2018-01-05 ### Changed -- allow newer version of node-libs-browser ([#969]) + - allow newer version of node-libs-browser ([#969]) ## 0.8.3 - 2017-06-23 ### Changed -- `debug` bumped to match others + - `debug` bumped to match others ## 0.8.2 - 2017-06-22 ### Changed -- `webpack` peer dep updated to >= 1.11 (works fine with webpack 3 AFAICT) + - `webpack` peer dep updated to >= 1.11 (works fine with webpack 3 AFAICT) ## 0.8.1 - 2017-01-19 ### Changed -- official support for Webpack 2.2.0 (RC), thanks [@graingert] + - official support for Webpack 2.2.0 (RC), thanks [@graingert] ## 0.8.0 - 2016-12-15 ### Changed -- bumped `resolve` to fix issues with Node builtins (thanks [@SkeLLLa] and [@ljharb]) -- allow `enhanced-resolve` to be version `>= 2` (thanks [@Kovensky]) + - bumped `resolve` to fix issues with Node builtins (thanks [@SkeLLLa] and [@ljharb]) + - allow `enhanced-resolve` to be version `>= 2` (thanks [@Kovensky]) ## 0.7.1 ### Fixed -- missing `has` dependency ([#681] + [#683], thanks [@benmvp] + [@ljharb]) + - missing `has` dependency ([#681] + [#683], thanks [@benmvp] + [@ljharb]) ## 0.7.0 ### Added -- Support for explicit Webpack config object in `.eslintrc.*`. ([#572], thanks [@jameslnewell]) -- Added `resolve.modules` to configs for webpack2 support ([#569], thanks [@toshafed]) + - Support for explicit Webpack config object in `.eslintrc.*`. ([#572], thanks [@jameslnewell]) + - Added `resolve.modules` to configs for webpack2 support ([#569], thanks [@toshafed]) ## 0.6.0 - 2016-09-13 ### Added -- support for config-as-function ([#533], thanks [@grahamb]) + - support for config-as-function ([#533], thanks [@grahamb]) ## 0.5.1 - 2016-08-11 ### Fixed -- don't throw and die if no webpack config is found + - don't throw and die if no webpack config is found ## 0.5.0 - 2016-08-11 ### Added -- support for Webpack 2 + `module` package.json key! ([#475], thanks [@taion]) + - support for Webpack 2 + `module` package.json key! ([#475], thanks [@taion]) ### Changed -- don't swallow errors, assume config exists ([#435], thanks [@Kovensky]) + - don't swallow errors, assume config exists ([#435], thanks [@Kovensky]) ## 0.4.0 - 2016-07-17 ### Added -- support for `webpack.ResolverPlugin` ([#377], thanks [@Rogeres]) + - support for `webpack.ResolverPlugin` ([#377], thanks [@Rogeres]) ### Fixed -- provide string `context` to `externals` functions ([#411] + [#413], thanks [@Satyam]) + - provide string `context` to `externals` functions ([#411] + [#413], thanks [@Satyam]) ## 0.3.2 - 2016-06-30 ### Added -- shared config ([config.js](./config.js)) with barebones settings needed to use this resolver. ([#283]) + - shared config ([config.js](./config.js)) with barebones settings needed to use this resolver. ([#283]) ### Fixed -- strip resource query ([#357], thanks [@daltones]) -- allow `externals` to be defined as a function ([#363], thanks [@kesne]) + - strip resource query ([#357], thanks [@daltones]) + - allow `externals` to be defined as a function ([#363], thanks [@kesne]) ## 0.3.1 - 2016-06-02 ### Added -- debug logging. run with `DEBUG=eslint-plugin-import:*` to see log output. + - debug logging. run with `DEBUG=eslint-plugin-import:*` to see log output. ## 0.3.0 - 2016-06-01 ### Changed -- use `enhanced-resolve` to support additional plugins instead of re-implementing - aliases, etc. + - use `enhanced-resolve` to support additional plugins instead of re-implementing aliases, etc. ## 0.2.5 - 2016-05-23 ### Added -- Added support for multiple webpack configs ([#181], thanks [@GreenGremlin]) + - Added support for multiple webpack configs ([#181], thanks [@GreenGremlin]) ## 0.2.4 - 2016-04-29 ### Changed -- automatically find webpack config with `interpret`-able extensions ([#287], thanks [@taion]) + - automatically find webpack config with `interpret`-able extensions ([#287], thanks [@taion]) ## 0.2.3 - 2016-04-28 ### Fixed -- `interpret` dependency was declared in the wrong `package.json`. - Thanks [@jonboiser] for sleuthing ([#286]) and fixing ([#289]). + - `interpret` dependency was declared in the wrong `package.json`. Thanks [@jonboiser] for sleuthing ([#286]) and fixing ([#289]). ## 0.2.2 - 2016-04-27 ### Added -- `interpret` configs (such as `.babel.js`). - Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). + - `interpret` configs (such as `.babel.js`). Thanks to [@gausie] for the initial PR ([#164], ages ago! 😅) and [@jquense] for tests ([#278]). +[#2287]: https://github.com/import-js/eslint-plugin-import/pull/2287 [#2023]: https://github.com/import-js/eslint-plugin-import/pull/2023 [#1967]: https://github.com/import-js/eslint-plugin-import/pull/1967 [#1962]: https://github.com/import-js/eslint-plugin-import/pull/1962 @@ -181,6 +205,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [#181]: https://github.com/import-js/eslint-plugin-import/pull/181 [#164]: https://github.com/import-js/eslint-plugin-import/pull/164 +[#2859]: https://github.com/import-js/eslint-plugin-import/issues/2859 [#2268]: https://github.com/import-js/eslint-plugin-import/issues/2268 [#1219]: https://github.com/import-js/eslint-plugin-import/issues/1219 [#788]: https://github.com/import-js/eslint-plugin-import/issues/788 @@ -192,29 +217,30 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange [#286]: https://github.com/import-js/eslint-plugin-import/issues/286 [#283]: https://github.com/import-js/eslint-plugin-import/issues/283 +[@Aghassi]: https://github.com/Aghassi +[@andersk]: https://github.com/andersk +[@benmvp]: https://github.com/benmvp +[@daltones]: https://github.com/daltones +[@echenley]: https://github.com/echenley [@gausie]: https://github.com/gausie -[@jquense]: https://github.com/jquense -[@taion]: https://github.com/taion +[@grahamb]: https://github.com/grahamb +[@graingert]: https://github.com/graingert [@GreenGremlin]: https://github.com/GreenGremlin -[@daltones]: https://github.com/daltones +[@idudinov]: https://github.com/idudinov +[@jameslnewell]: https://github.com/jameslnewell +[@jet2jet]: https://github.com/jet2jet +[@jquense]: https://github.com/jquense +[@keann]: https://github.com/keann [@kesne]: https://github.com/kesne -[@Satyam]: https://github.com/Satyam -[@Rogeres]: https://github.com/Rogeres [@Kovensky]: https://github.com/Kovensky -[@grahamb]: https://github.com/grahamb -[@jameslnewell]: https://github.com/jameslnewell -[@toshafed]: https://github.com/toshafed -[@benmvp]: https://github.com/benmvp [@ljharb]: https://github.com/ljharb -[@SkeLLLa]: https://github.com/SkeLLLa -[@graingert]: https://github.com/graingert [@mattkrick]: https://github.com/mattkrick -[@idudinov]: https://github.com/idudinov -[@keann]: https://github.com/keann -[@echenley]: https://github.com/echenley -[@Aghassi]: https://github.com/Aghassi [@migueloller]: https://github.com/migueloller -[@opichals]: https://github.com/opichals -[@andersk]: https://github.com/andersk [@ogonkov]: https://github.com/ogonkov -[@jet2jet]: https://github.com/jet2jet +[@opichals]: https://github.com/opichals +[@Rogeres]: https://github.com/Rogeres +[@Satyam]: https://github.com/Satyam +[@Schweinepriester]: https://github.com/Schweinepriester +[@SkeLLLa]: https://github.com/SkeLLLa +[@taion]: https://github.com/taion +[@toshafed]: https://github.com/toshafed diff --git a/resolvers/webpack/README.md b/resolvers/webpack/README.md index cdb9222fae..06513ba141 100644 --- a/resolvers/webpack/README.md +++ b/resolvers/webpack/README.md @@ -13,13 +13,13 @@ changes. To use with `eslint-plugin-import`, run: -``` +```sh npm i eslint-import-resolver-webpack -g ``` or if you manage ESLint as a dev dependency: -``` +```sh # inside your project's working tree npm install eslint-import-resolver-webpack --save-dev ``` diff --git a/resolvers/webpack/index.js b/resolvers/webpack/index.js index b569d53224..da16eda593 100644 --- a/resolvers/webpack/index.js +++ b/resolvers/webpack/index.js @@ -2,180 +2,253 @@ const findRoot = require('find-root'); const path = require('path'); -const get = require('lodash/get'); const isEqual = require('lodash/isEqual'); -const find = require('array-find'); +const find = require('array.prototype.find'); const interpret = require('interpret'); const fs = require('fs'); const isCore = require('is-core-module'); -const resolve = require('resolve'); +const resolve = require('resolve/sync'); const semver = require('semver'); -const has = require('has'); +const hasOwn = require('hasown'); const isRegex = require('is-regex'); const log = require('debug')('eslint-plugin-import:resolver:webpack'); exports.interfaceVersion = 2; -/** - * Find the full path to 'source', given 'file' as a full reference path. - * - * 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' - * @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) - */ -exports.resolve = function (source, file, settings) { - - // strip loaders - const finalBang = source.lastIndexOf('!'); - if (finalBang >= 0) { - source = source.slice(finalBang + 1); - } - - // strip resource query - const finalQuestionMark = source.lastIndexOf('?'); - if (finalQuestionMark >= 0) { - source = source.slice(0, finalQuestionMark); +function registerCompiler(moduleDescriptor) { + if (moduleDescriptor) { + if (typeof moduleDescriptor === 'string') { + require(moduleDescriptor); + } else if (!Array.isArray(moduleDescriptor)) { + moduleDescriptor.register(require(moduleDescriptor.module)); + } else { + for (let i = 0; i < moduleDescriptor.length; i++) { + try { + registerCompiler(moduleDescriptor[i]); + break; + } catch (e) { + log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor); + } + } + } } +} - let webpackConfig; - - const _configPath = get(settings, 'config'); - /** - * Attempt to set the current working directory. - * If none is passed, default to the `cwd` where the config is located. - */ - const cwd = get(settings, 'cwd'); - const configIndex = get(settings, 'config-index'); - const env = get(settings, 'env'); - const argv = get(settings, 'argv', {}); - let packageDir; - - let configPath = typeof _configPath === 'string' && _configPath.startsWith('.') - ? path.resolve(_configPath) - : _configPath; +function findConfigPath(configPath, packageDir) { + const extensions = Object.keys(interpret.extensions).sort(function (a, b) { + return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length; + }); + let extension; - log('Config path from settings:', configPath); + if (configPath) { + // extensions is not reused below, so safe to mutate it here. + extensions.reverse(); + extensions.forEach(function (maybeExtension) { + if (extension) { + return; + } - // see if we've got a config path, a config object, an array of config objects or a config function - if (!configPath || typeof configPath === 'string') { + if (configPath.substr(-maybeExtension.length) === maybeExtension) { + extension = maybeExtension; + } + }); // see if we've got an absolute path - if (!configPath || !path.isAbsolute(configPath)) { - // if not, find ancestral package.json and use its directory as base for the path - packageDir = findRoot(path.resolve(file)); - if (!packageDir) throw new Error('package not found above ' + file); + if (!path.isAbsolute(configPath)) { + configPath = path.join(packageDir, configPath); } + } else { + extensions.forEach(function (maybeExtension) { + if (extension) { + return; + } - configPath = findConfigPath(configPath, packageDir); - - log('Config path resolved to:', configPath); - if (configPath) { - try { - webpackConfig = require(configPath); - } catch (e) { - console.log('Error resolving webpackConfig', e); - throw e; + const maybePath = path.resolve( + path.join(packageDir, 'webpack.config' + maybeExtension) + ); + if (fs.existsSync(maybePath)) { + configPath = maybePath; + extension = maybeExtension; } - } else { - log('No config path found relative to', file, '; using {}'); - webpackConfig = {}; - } + }); + } - if (webpackConfig && webpackConfig.default) { - log('Using ES6 module "default" key instead of module.exports.'); - webpackConfig = webpackConfig.default; - } + registerCompiler(interpret.extensions[extension]); + return configPath; +} - } else { - webpackConfig = configPath; - configPath = null; +function findExternal(source, externals, context, resolveSync) { + if (!externals) { return false; } + + // string match + if (typeof externals === 'string') { return source === externals; } + + // array: recurse + if (Array.isArray(externals)) { + return externals.some(function (e) { return findExternal(source, e, context, resolveSync); }); } - if (typeof webpackConfig === 'function') { - webpackConfig = webpackConfig(env, argv); + if (isRegex(externals)) { + return externals.test(source); } - if (Array.isArray(webpackConfig)) { - webpackConfig = webpackConfig.map(cfg => { - if (typeof cfg === 'function') { - return cfg(env, argv); + if (typeof externals === 'function') { + let functionExternalFound = false; + const callback = function (err, value) { + if (err) { + functionExternalFound = false; + } else { + functionExternalFound = findExternal(source, value, context, resolveSync); } - - return cfg; - }); - - if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { - webpackConfig = webpackConfig[configIndex]; + }; + // - for prior webpack 5, 'externals function' uses 3 arguments + // - for webpack 5, the count of arguments is less than 3 + if (externals.length === 3) { + externals.call(null, context, source, callback); } else { - webpackConfig = find(webpackConfig, function findFirstWithResolve(config) { - return !!config.resolve; - }); + const ctx = { + context, + request: source, + contextInfo: { + issuer: '', + issuerLayer: null, + compiler: '', + }, + getResolve: () => (resolveContext, requestToResolve, cb) => { + if (cb) { + try { + cb(null, resolveSync(resolveContext, requestToResolve)); + } catch (e) { + cb(e); + } + } else { + log('getResolve without callback not supported'); + return Promise.reject(new Error('Not supported')); + } + }, + }; + const result = externals.call(null, ctx, callback); + // todo handling Promise object (using synchronous-promise package?) + if (result && typeof result.then === 'function') { + log('Asynchronous functions for externals not supported'); + } } + return functionExternalFound; } - if (typeof webpackConfig.then === 'function') { - webpackConfig = {}; - - console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.'); + // else, vanilla object + for (const key in externals) { + if (!hasOwn(externals, key)) { continue; } + if (source === key) { return true; } } + return false; +} - if (webpackConfig == null) { - webpackConfig = {}; +/** + * webpack 2 defaults: + * https://github.com/webpack/webpack/blob/v2.1.0-beta.20/lib/WebpackOptionsDefaulter.js#L72-L87 + * @type {Object} + */ +const webpack2DefaultResolveConfig = { + unsafeCache: true, // Probably a no-op, since how can we cache anything at all here? + modules: ['node_modules'], + extensions: ['.js', '.json'], + aliasFields: ['browser'], + mainFields: ['browser', 'module', 'main'], +}; - console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.'); - } +function createWebpack2ResolveSync(webpackRequire, resolveConfig) { + const EnhancedResolve = webpackRequire('enhanced-resolve'); - log('Using config: ', webpackConfig); + return EnhancedResolve.create.sync(Object.assign({}, webpack2DefaultResolveConfig, resolveConfig)); +} - const resolveSync = getResolveSync(configPath, webpackConfig, cwd); +/** + * webpack 1 defaults: https://webpack.github.io/docs/configuration.html#resolve-packagemains + * @type {Array} + */ +const webpack1DefaultMains = [ + 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main', +]; - // externals - if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) { - return { found: true, path: null }; +/* eslint-disable */ +// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365 +function makeRootPlugin(ModulesInRootPlugin, name, root) { + if (typeof root === 'string') { + return new ModulesInRootPlugin(name, root); + } else if (Array.isArray(root)) { + return function() { + root.forEach(function (root) { + this.apply(new ModulesInRootPlugin(name, root)); + }, this); + }; } + return function () {}; +} +/* eslint-enable */ - // otherwise, resolve "normally" +// adapted from tests & +// https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322 +function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) { + const Resolver = webpackRequire('enhanced-resolve/lib/Resolver'); + const SyncNodeJsInputFileSystem = webpackRequire('enhanced-resolve/lib/SyncNodeJsInputFileSystem'); - try { - return { found: true, path: resolveSync(path.dirname(file), source) }; - } catch (err) { - if (isCore(source)) { - return { found: true, path: null }; - } + const ModuleAliasPlugin = webpackRequire('enhanced-resolve/lib/ModuleAliasPlugin'); + const ModulesInDirectoriesPlugin = webpackRequire('enhanced-resolve/lib/ModulesInDirectoriesPlugin'); + const ModulesInRootPlugin = webpackRequire('enhanced-resolve/lib/ModulesInRootPlugin'); + const ModuleAsFilePlugin = webpackRequire('enhanced-resolve/lib/ModuleAsFilePlugin'); + const ModuleAsDirectoryPlugin = webpackRequire('enhanced-resolve/lib/ModuleAsDirectoryPlugin'); + const DirectoryDescriptionFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFilePlugin'); + const DirectoryDefaultFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDefaultFilePlugin'); + const FileAppendPlugin = webpackRequire('enhanced-resolve/lib/FileAppendPlugin'); + const ResultSymlinkPlugin = webpackRequire('enhanced-resolve/lib/ResultSymlinkPlugin'); + const DirectoryDescriptionFileFieldAliasPlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin'); - log('Error during module resolution:', err); - return { found: false }; - } -}; + const resolver = new Resolver(new SyncNodeJsInputFileSystem()); -const MAX_CACHE = 10; -const _cache = []; -function getResolveSync(configPath, webpackConfig, cwd) { - const cacheKey = { configPath, webpackConfig }; - let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); }); - if (!cached) { - cached = { - key: cacheKey, - value: createResolveSync(configPath, webpackConfig, cwd), - }; - // put in front and pop last item - if (_cache.unshift(cached) > MAX_CACHE) { - _cache.pop(); - } + resolver.apply( + resolveConfig.packageAlias + ? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolveConfig.packageAlias) + : function () {}, + new ModuleAliasPlugin(resolveConfig.alias || {}), + makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.root), + new ModulesInDirectoriesPlugin( + 'module', + resolveConfig.modulesDirectories || resolveConfig.modules || ['web_modules', 'node_modules'] + ), + makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.fallback), + new ModuleAsFilePlugin('module'), + new ModuleAsDirectoryPlugin('module'), + new DirectoryDescriptionFilePlugin( + 'package.json', + ['module', 'jsnext:main'].concat(resolveConfig.packageMains || webpack1DefaultMains) + ), + new DirectoryDefaultFilePlugin(['index']), + new FileAppendPlugin(resolveConfig.extensions || ['', '.webpack.js', '.web.js', '.js']), + new ResultSymlinkPlugin() + ); + + const resolvePlugins = []; + + // support webpack.ResolverPlugin + if (plugins) { + plugins.forEach(function (plugin) { + if ( + plugin.constructor + && plugin.constructor.name === 'ResolverPlugin' + && Array.isArray(plugin.plugins) + ) { + resolvePlugins.push.apply(resolvePlugins, plugin.plugins); + } + }); } - return cached.value; + + resolver.apply.apply(resolver, resolvePlugins); + + return function () { + return resolver.resolveSync.apply(resolver, arguments); + }; } function createResolveSync(configPath, webpackConfig, cwd) { @@ -184,17 +257,17 @@ function createResolveSync(configPath, webpackConfig, cwd) { if (typeof configPath === 'string') { // This can be changed via the settings passed in when defining the resolver - basedir = cwd || configPath; + basedir = cwd || path.dirname(configPath); log(`Attempting to load webpack path from ${basedir}`); } try { // Attempt to resolve webpack from the given `basedir` - const webpackFilename = resolve.sync('webpack', { basedir, preserveSymlinks: false }); + const webpackFilename = resolve('webpack', { basedir, preserveSymlinks: false }); const webpackResolveOpts = { basedir: path.dirname(webpackFilename), preserveSymlinks: false }; webpackRequire = function (id) { - return require(resolve.sync(id, webpackResolveOpts)); + return require(resolve(id, webpackResolveOpts)); }; } catch (e) { // Something has gone wrong (or we're in a test). Use our own bundled @@ -216,239 +289,163 @@ function createResolveSync(configPath, webpackConfig, cwd) { return createWebpack1ResolveSync(webpackRequire, resolveConfig, webpackConfig.plugins); } -/** - * webpack 2 defaults: - * https://github.com/webpack/webpack/blob/v2.1.0-beta.20/lib/WebpackOptionsDefaulter.js#L72-L87 - * @type {Object} - */ -const webpack2DefaultResolveConfig = { - unsafeCache: true, // Probably a no-op, since how can we cache anything at all here? - modules: ['node_modules'], - extensions: ['.js', '.json'], - aliasFields: ['browser'], - mainFields: ['browser', 'module', 'main'], -}; - -function createWebpack2ResolveSync(webpackRequire, resolveConfig) { - const EnhancedResolve = webpackRequire('enhanced-resolve'); - - return EnhancedResolve.create.sync(Object.assign({}, webpack2DefaultResolveConfig, resolveConfig)); +const MAX_CACHE = 10; +const _cache = []; +function getResolveSync(configPath, webpackConfig, cwd) { + const cacheKey = { configPath, webpackConfig }; + let cached = find(_cache, function (entry) { return isEqual(entry.key, cacheKey); }); + if (!cached) { + cached = { + key: cacheKey, + value: createResolveSync(configPath, webpackConfig, cwd), + }; + // put in front and pop last item + if (_cache.unshift(cached) > MAX_CACHE) { + _cache.pop(); + } + } + return cached.value; } /** - * webpack 1 defaults: https://webpack.github.io/docs/configuration.html#resolve-packagemains - * @type {Array} + * Find the full path to 'source', given 'file' as a full reference path. + * + * 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' + * @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) */ -const webpack1DefaultMains = [ - 'webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main', -]; - -// adapted from tests & -// https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L322 -function createWebpack1ResolveSync(webpackRequire, resolveConfig, plugins) { - const Resolver = webpackRequire('enhanced-resolve/lib/Resolver'); - const SyncNodeJsInputFileSystem = webpackRequire('enhanced-resolve/lib/SyncNodeJsInputFileSystem'); +exports.resolve = function (source, file, settings) { - const ModuleAliasPlugin = webpackRequire('enhanced-resolve/lib/ModuleAliasPlugin'); - const ModulesInDirectoriesPlugin = webpackRequire('enhanced-resolve/lib/ModulesInDirectoriesPlugin'); - const ModulesInRootPlugin = webpackRequire('enhanced-resolve/lib/ModulesInRootPlugin'); - const ModuleAsFilePlugin = webpackRequire('enhanced-resolve/lib/ModuleAsFilePlugin'); - const ModuleAsDirectoryPlugin = webpackRequire('enhanced-resolve/lib/ModuleAsDirectoryPlugin'); - const DirectoryDescriptionFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFilePlugin'); - const DirectoryDefaultFilePlugin = webpackRequire('enhanced-resolve/lib/DirectoryDefaultFilePlugin'); - const FileAppendPlugin = webpackRequire('enhanced-resolve/lib/FileAppendPlugin'); - const ResultSymlinkPlugin = webpackRequire('enhanced-resolve/lib/ResultSymlinkPlugin'); - const DirectoryDescriptionFileFieldAliasPlugin = webpackRequire('enhanced-resolve/lib/DirectoryDescriptionFileFieldAliasPlugin'); + // strip loaders + const finalBang = source.lastIndexOf('!'); + if (finalBang >= 0) { + source = source.slice(finalBang + 1); + } - const resolver = new Resolver(new SyncNodeJsInputFileSystem()); + // strip resource query + const finalQuestionMark = source.lastIndexOf('?'); + if (finalQuestionMark >= 0) { + source = source.slice(0, finalQuestionMark); + } - resolver.apply( - resolveConfig.packageAlias - ? new DirectoryDescriptionFileFieldAliasPlugin('package.json', resolveConfig.packageAlias) - : function () {}, - new ModuleAliasPlugin(resolveConfig.alias || {}), - makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.root), - new ModulesInDirectoriesPlugin( - 'module', - resolveConfig.modulesDirectories || resolveConfig.modules || ['web_modules', 'node_modules'], - ), - makeRootPlugin(ModulesInRootPlugin, 'module', resolveConfig.fallback), - new ModuleAsFilePlugin('module'), - new ModuleAsDirectoryPlugin('module'), - new DirectoryDescriptionFilePlugin( - 'package.json', - ['module', 'jsnext:main'].concat(resolveConfig.packageMains || webpack1DefaultMains), - ), - new DirectoryDefaultFilePlugin(['index']), - new FileAppendPlugin(resolveConfig.extensions || ['', '.webpack.js', '.web.js', '.js']), - new ResultSymlinkPlugin(), - ); + let webpackConfig; + const _configPath = settings && settings.config; + /** + * Attempt to set the current working directory. + * If none is passed, default to the `cwd` where the config is located. + */ + const cwd = settings && settings.cwd; + const configIndex = settings && settings['config-index']; + const env = settings && settings.env; + const argv = settings && typeof settings.argv !== 'undefined' ? settings.argv : {}; + let packageDir; - const resolvePlugins = []; + let configPath = typeof _configPath === 'string' && _configPath.startsWith('.') + ? path.resolve(_configPath) + : _configPath; - // support webpack.ResolverPlugin - if (plugins) { - plugins.forEach(function (plugin) { - if ( - plugin.constructor && - plugin.constructor.name === 'ResolverPlugin' && - Array.isArray(plugin.plugins) - ) { - resolvePlugins.push.apply(resolvePlugins, plugin.plugins); - } - }); - } + log('Config path from settings:', configPath); - resolver.apply.apply(resolver, resolvePlugins); + // see if we've got a config path, a config object, an array of config objects or a config function + if (!configPath || typeof configPath === 'string') { - return function () { - return resolver.resolveSync.apply(resolver, arguments); - }; -} + // see if we've got an absolute path + if (!configPath || !path.isAbsolute(configPath)) { + // if not, find ancestral package.json and use its directory as base for the path + packageDir = findRoot(path.resolve(file)); + if (!packageDir) { throw new Error('package not found above ' + file); } + } -/* eslint-disable */ -// from https://github.com/webpack/webpack/blob/v1.13.0/lib/WebpackOptionsApply.js#L365 -function makeRootPlugin(ModulesInRootPlugin, name, root) { - if (typeof root === 'string') { - return new ModulesInRootPlugin(name, root); - } else if (Array.isArray(root)) { - return function() { - root.forEach(function (root) { - this.apply(new ModulesInRootPlugin(name, root)); - }, this); - }; - } - return function () {}; -} -/* eslint-enable */ + configPath = findConfigPath(configPath, packageDir); -function findExternal(source, externals, context, resolveSync) { - if (!externals) return false; + log('Config path resolved to:', configPath); + if (configPath) { + try { + webpackConfig = require(configPath); + } catch (e) { + console.log('Error resolving webpackConfig', e); + throw e; + } + } else { + log('No config path found relative to', file, '; using {}'); + webpackConfig = {}; + } - // string match - if (typeof externals === 'string') return (source === externals); + if (webpackConfig && webpackConfig.default) { + log('Using ES6 module "default" key instead of module.exports.'); + webpackConfig = webpackConfig.default; + } - // array: recurse - if (Array.isArray(externals)) { - return externals.some(function (e) { return findExternal(source, e, context, resolveSync); }); + } else { + webpackConfig = configPath; + configPath = null; } - if (isRegex(externals)) { - return externals.test(source); + if (typeof webpackConfig === 'function') { + webpackConfig = webpackConfig(env, argv); } - if (typeof externals === 'function') { - let functionExternalFound = false; - const callback = function (err, value) { - if (err) { - functionExternalFound = false; - } else { - functionExternalFound = findExternal(source, value, context, resolveSync); + if (Array.isArray(webpackConfig)) { + webpackConfig = webpackConfig.map((cfg) => { + if (typeof cfg === 'function') { + return cfg(env, argv); } - }; - // - for prior webpack 5, 'externals function' uses 3 arguments - // - for webpack 5, the count of arguments is less than 3 - if (externals.length === 3) { - externals.call(null, context, source, callback); + + return cfg; + }); + + if (typeof configIndex !== 'undefined' && webpackConfig.length > configIndex) { + webpackConfig = webpackConfig[configIndex]; } else { - const ctx = { - context, - request: source, - contextInfo: { - issuer: '', - issuerLayer: null, - compiler: '', - }, - getResolve: () => (resolveContext, requestToResolve, cb) => { - if (cb) { - try { - cb(null, resolveSync(resolveContext, requestToResolve)); - } catch (e) { - cb(e); - } - } else { - log('getResolve without callback not supported'); - return Promise.reject(new Error('Not supported')); - } - }, - }; - const result = externals.call(null, ctx, callback); - // todo handling Promise object (using synchronous-promise package?) - if (result && typeof result.then === 'function') { - log('Asynchronous functions for externals not supported'); - } + webpackConfig = find(webpackConfig, function findFirstWithResolve(config) { + return !!config.resolve; + }); } - return functionExternalFound; } - // else, vanilla object - for (const key in externals) { - if (!has(externals, key)) continue; - if (source === key) return true; - } - return false; -} + if (typeof webpackConfig.then === 'function') { + webpackConfig = {}; -function findConfigPath(configPath, packageDir) { - const extensions = Object.keys(interpret.extensions).sort(function (a, b) { - return a === '.js' ? -1 : b === '.js' ? 1 : a.length - b.length; - }); - let extension; + console.warn('Webpack config returns a `Promise`; that signature is not supported at the moment. Using empty object instead.'); + } + if (webpackConfig == null) { + webpackConfig = {}; - if (configPath) { - // extensions is not reused below, so safe to mutate it here. - extensions.reverse(); - extensions.forEach(function (maybeExtension) { - if (extension) { - return; - } + console.warn('No webpack configuration with a "resolve" field found. Using empty object instead.'); + } - if (configPath.substr(-maybeExtension.length) === maybeExtension) { - extension = maybeExtension; - } - }); + log('Using config: ', webpackConfig); - // see if we've got an absolute path - if (!path.isAbsolute(configPath)) { - configPath = path.join(packageDir, configPath); - } - } else { - extensions.forEach(function (maybeExtension) { - if (extension) { - return; - } + const resolveSync = getResolveSync(configPath, webpackConfig, cwd); - const maybePath = path.resolve( - path.join(packageDir, 'webpack.config' + maybeExtension), - ); - if (fs.existsSync(maybePath)) { - configPath = maybePath; - extension = maybeExtension; - } - }); + // externals + if (findExternal(source, webpackConfig.externals, path.dirname(file), resolveSync)) { + return { found: true, path: null }; } - registerCompiler(interpret.extensions[extension]); - return configPath; -} + // otherwise, resolve "normally" -function registerCompiler(moduleDescriptor) { - if (moduleDescriptor) { - if (typeof moduleDescriptor === 'string') { - require(moduleDescriptor); - } else if (!Array.isArray(moduleDescriptor)) { - moduleDescriptor.register(require(moduleDescriptor.module)); - } else { - for (let i = 0; i < moduleDescriptor.length; i++) { - try { - registerCompiler(moduleDescriptor[i]); - break; - } catch (e) { - log('Failed to register compiler for moduleDescriptor[]:', i, moduleDescriptor); - } - } + try { + return { found: true, path: resolveSync(path.dirname(file), source) }; + } catch (err) { + if (isCore(source)) { + return { found: true, path: null }; } + + log('Error during module resolution:', err); + return { found: false }; } -} +}; diff --git a/resolvers/webpack/package.json b/resolvers/webpack/package.json index 42d5e7470e..3fa47d9362 100644 --- a/resolvers/webpack/package.json +++ b/resolvers/webpack/package.json @@ -1,6 +1,6 @@ { "name": "eslint-import-resolver-webpack", - "version": "0.13.2", + "version": "0.13.8", "description": "Resolve paths to dependencies, given a webpack.config.js. Plugin for eslint-plugin-import.", "main": "index.js", "scripts": { @@ -30,17 +30,17 @@ }, "homepage": "https://github.com/import-js/eslint-plugin-import/tree/HEAD/resolvers/webpack", "dependencies": { - "array-find": "^1.0.0", + "array.prototype.find": "^2.2.2", "debug": "^3.2.7", "enhanced-resolve": "^0.9.1", "find-root": "^1.1.0", - "has": "^1.0.3", + "hasown": "^2.0.0", "interpret": "^1.4.0", - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.1", "is-regex": "^1.1.4", "lodash": "^4.17.21", - "resolve": "^1.22.1", - "semver": "^5.7.1" + "resolve": "^2.0.0-next.5", + "semver": "^5.7.2" }, "peerDependencies": { "eslint-plugin-import": ">=1.4.0", diff --git a/resolvers/webpack/test/config-extensions/webpack.config.babel.js b/resolvers/webpack/test/config-extensions/webpack.config.babel.js index a63434f9bb..c8b3cd5780 100644 --- a/resolvers/webpack/test/config-extensions/webpack.config.babel.js +++ b/resolvers/webpack/test/config-extensions/webpack.config.babel.js @@ -3,7 +3,7 @@ import path from 'path'; export default { resolve: { alias: { - 'foo': path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), + foo: path.join(__dirname, 'some', 'goofy', 'path', 'foo.js'), }, modules: [ path.join(__dirname, 'src'), @@ -17,7 +17,7 @@ export default { }, externals: [ - { 'jquery': 'jQuery' }, + { jquery: 'jQuery' }, 'bootstrap', ], }; diff --git a/resolvers/webpack/test/extensions.js b/resolvers/webpack/test/extensions.js index c028f5c913..096df77281 100644 --- a/resolvers/webpack/test/extensions.js +++ b/resolvers/webpack/test/extensions.js @@ -6,7 +6,6 @@ const path = require('path'); const resolve = require('../index').resolve; - const file = path.join(__dirname, 'files', 'dummy.js'); const extensions = path.join(__dirname, 'custom-extensions', 'dummy.js'); diff --git a/resolvers/webpack/test/fallback.js b/resolvers/webpack/test/fallback.js index 87c15eecd7..b164209e14 100644 --- a/resolvers/webpack/test/fallback.js +++ b/resolvers/webpack/test/fallback.js @@ -6,7 +6,6 @@ const path = require('path'); const resolve = require('../index').resolve; - const file = path.join(__dirname, 'files', 'src', 'dummy.js'); describe('fallback', function () { diff --git a/resolvers/webpack/test/loaders.js b/resolvers/webpack/test/loaders.js index 6b5604592d..e250894a54 100644 --- a/resolvers/webpack/test/loaders.js +++ b/resolvers/webpack/test/loaders.js @@ -6,7 +6,6 @@ const path = require('path'); const resolve = require('../index').resolve; - const file = path.join(__dirname, 'files', 'dummy.js'); describe('inline loader syntax', function () { diff --git a/resolvers/webpack/test/packageMains.js b/resolvers/webpack/test/packageMains.js index fef3dde073..d3ddad9dab 100644 --- a/resolvers/webpack/test/packageMains.js +++ b/resolvers/webpack/test/packageMains.js @@ -8,7 +8,6 @@ const resolver = require('../'); const file = path.join(__dirname, 'package-mains', 'dummy.js'); - describe('packageMains', function () { it('captures module', function () { diff --git a/resolvers/webpack/test/root.js b/resolvers/webpack/test/root.js index 154dbeef95..194bb8fc88 100644 --- a/resolvers/webpack/test/root.js +++ b/resolvers/webpack/test/root.js @@ -6,7 +6,6 @@ const path = require('path'); const resolve = require('../index').resolve; - const file = path.join(__dirname, 'files', 'src', 'dummy.js'); const webpackDir = path.join(__dirname, 'different-package-location'); diff --git a/scripts/copyMetafiles.js b/scripts/copyMetafiles.js index d14964f1c7..01ff4f36f2 100644 --- a/scripts/copyMetafiles.js +++ b/scripts/copyMetafiles.js @@ -5,14 +5,13 @@ import resolverDirectories from './resolverDirectories'; const files = [ 'LICENSE', '.npmrc', - '.nycrc', ]; -const directories = [ +const directories = [].concat( 'memo-parser', - ...resolverDirectories, + resolverDirectories, 'utils', -]; +); for (const directory of directories) { for (const file of files) { diff --git a/scripts/testAll.js b/scripts/testAll.js index fc30b1ac7d..0e4a12c68a 100644 --- a/scripts/testAll.js +++ b/scripts/testAll.js @@ -10,11 +10,11 @@ const spawnOptions = { spawnSync( npmPath, ['test'], - Object.assign({ cwd: __dirname }, spawnOptions)); + { cwd: __dirname, ...spawnOptions }); for (const resolverDir of resolverDirectories) { spawnSync( npmPath, ['test'], - Object.assign({ cwd: resolverDir }, spawnOptions)); + { cwd: resolverDir, ...spawnOptions }); } diff --git a/src/ExportMap.js b/src/ExportMap.js index d95fdb7a75..9ee65a504f 100644 --- a/src/ExportMap.js +++ b/src/ExportMap.js @@ -24,7 +24,98 @@ let ts; const log = debug('eslint-plugin-import:ExportMap'); const exportCache = new Map(); -const tsConfigCache = new Map(); +const tsconfigCache = new Map(); + +/** + * parse docs from the first node that has leading comments + */ +function captureDoc(source, docStyleParsers, ...nodes) { + const metadata = {}; + + // 'some' short-circuits on first 'true' + nodes.some((n) => { + try { + + let leadingComments; + + // n.leadingComments is legacy `attachComments` behavior + if ('leadingComments' in n) { + leadingComments = n.leadingComments; + } else if (n.range) { + leadingComments = source.getCommentsBefore(n); + } + + if (!leadingComments || leadingComments.length === 0) { return false; } + + for (const name in docStyleParsers) { + const doc = docStyleParsers[name](leadingComments); + if (doc) { + metadata.doc = doc; + } + } + + return true; + } catch (err) { + return false; + } + }); + + return metadata; +} + +/** + * parse JSDoc from leading comments + * @param {object[]} comments + * @return {{ doc: object }} + */ +function captureJsDoc(comments) { + let doc; + + // capture XSDoc + comments.forEach((comment) => { + // skip non-block comments + if (comment.type !== 'Block') { return; } + try { + doc = doctrine.parse(comment.value, { unwrap: true }); + } catch (err) { + /* don't care, for now? maybe add to `errors?` */ + } + }); + + return doc; +} + +/** + * parse TomDoc section from comments + */ +function captureTomDoc(comments) { + // collect lines up to first paragraph break + const lines = []; + for (let i = 0; i < comments.length; i++) { + const comment = comments[i]; + if (comment.value.match(/^\s*$/)) { break; } + lines.push(comment.value.trim()); + } + + // return doctrine-like object + const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); + if (statusMatch) { + return { + description: statusMatch[2], + tags: [{ + title: statusMatch[1].toLowerCase(), + description: statusMatch[2], + }], + }; + } +} + +const availableDocStyleParsers = { + jsdoc: captureJsDoc, + tomdoc: captureTomDoc, +}; + +const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); export default class ExportMap { constructor(path) { @@ -53,10 +144,10 @@ export default class ExportMap { get size() { let size = this.namespace.size + this.reexports.size; - this.dependencies.forEach(dep => { + this.dependencies.forEach((dep) => { const d = dep(); // CJS / ignored dependencies won't exist (#717) - if (d == null) return; + if (d == null) { return; } size += d.size; }); return size; @@ -70,8 +161,8 @@ export default class ExportMap { * @return {Boolean} true if `name` is exported by this module. */ has(name) { - if (this.namespace.has(name)) return true; - if (this.reexports.has(name)) return true; + if (this.namespace.has(name)) { return true; } + if (this.reexports.has(name)) { return true; } // default exports must be explicitly re-exported (#328) if (name !== 'default') { @@ -79,9 +170,9 @@ export default class ExportMap { const innerMap = dep(); // todo: report as unresolved? - if (!innerMap) continue; + if (!innerMap) { continue; } - if (innerMap.has(name)) return true; + if (innerMap.has(name)) { return true; } } } @@ -94,14 +185,14 @@ export default class ExportMap { * @return {{ found: boolean, path: ExportMap[] }} */ hasDeep(name) { - if (this.namespace.has(name)) return { found: true, path: [this] }; + if (this.namespace.has(name)) { return { found: true, path: [this] }; } if (this.reexports.has(name)) { const reexports = this.reexports.get(name); const imported = reexports.getImport(); // if import is ignored, return explicit 'null' - if (imported == null) return { found: true, path: [this] }; + if (imported == null) { return { found: true, path: [this] }; } // safeguard against cycles, only if name matches if (imported.path === this.path && reexports.local === name) { @@ -114,17 +205,16 @@ export default class ExportMap { return deep; } - // default exports must be explicitly re-exported (#328) if (name !== 'default') { for (const dep of this.dependencies) { const innerMap = dep(); - if (innerMap == null) return { found: true, path: [this] }; + if (innerMap == null) { return { found: true, path: [this] }; } // todo: report as unresolved? - if (!innerMap) continue; + if (!innerMap) { continue; } // safeguard against cycles - if (innerMap.path === this.path) continue; + if (innerMap.path === this.path) { continue; } const innerValue = innerMap.hasDeep(name); if (innerValue.found) { @@ -138,17 +228,17 @@ export default class ExportMap { } get(name) { - if (this.namespace.has(name)) return this.namespace.get(name); + if (this.namespace.has(name)) { return this.namespace.get(name); } if (this.reexports.has(name)) { const reexports = this.reexports.get(name); const imported = reexports.getImport(); // if import is ignored, return explicit 'null' - if (imported == null) return null; + if (imported == null) { return null; } // safeguard against cycles, only if name matches - if (imported.path === this.path && reexports.local === name) return undefined; + if (imported.path === this.path && reexports.local === name) { return undefined; } return imported.get(reexports.local); } @@ -158,13 +248,13 @@ export default class ExportMap { for (const dep of this.dependencies) { const innerMap = dep(); // todo: report as unresolved? - if (!innerMap) continue; + if (!innerMap) { continue; } // safeguard against cycles - if (innerMap.path === this.path) continue; + if (innerMap.path === this.path) { continue; } const innerValue = innerMap.get(name); - if (innerValue !== undefined) return innerValue; + if (innerValue !== undefined) { return innerValue; } } } @@ -172,8 +262,7 @@ export default class ExportMap { } forEach(callback, thisArg) { - this.namespace.forEach((v, n) => - callback.call(thisArg, v, n, this)); + this.namespace.forEach((v, n) => { callback.call(thisArg, v, n, this); }); this.reexports.forEach((reexports, name) => { const reexported = reexports.getImport(); @@ -181,555 +270,467 @@ export default class ExportMap { callback.call(thisArg, reexported && reexported.get(reexports.local), name, this); }); - this.dependencies.forEach(dep => { + this.dependencies.forEach((dep) => { const d = dep(); // CJS / ignored dependencies won't exist (#717) - if (d == null) return; + if (d == null) { return; } - d.forEach((v, n) => - n !== 'default' && callback.call(thisArg, v, n, this)); + d.forEach((v, n) => { + if (n !== 'default') { + callback.call(thisArg, v, n, this); + } + }); }); } // todo: keys, values, entries? reportErrors(context, declaration) { + const msg = this.errors + .map((e) => `${e.message} (${e.lineNumber}:${e.column})`) + .join(', '); context.report({ node: declaration.source, - message: `Parse errors in imported module '${declaration.source.value}': ` + - `${this.errors - .map(e => `${e.message} (${e.lineNumber}:${e.column})`) - .join(', ')}`, + message: `Parse errors in imported module '${declaration.source.value}': ${msg}`, }); } -} -/** - * parse docs from the first node that has leading comments - */ -function captureDoc(source, docStyleParsers, ...nodes) { - const metadata = {}; + static get(source, context) { + const path = resolve(source, context); + if (path == null) { return null; } - // 'some' short-circuits on first 'true' - nodes.some(n => { - try { + return ExportMap.for(childContext(path, context)); + } - let leadingComments; + static for(context) { + const { path } = context; - // n.leadingComments is legacy `attachComments` behavior - if ('leadingComments' in n) { - leadingComments = n.leadingComments; - } else if (n.range) { - leadingComments = source.getCommentsBefore(n); - } + const cacheKey = context.cacheKey || hashObject(context).digest('hex'); + let exportMap = exportCache.get(cacheKey); - if (!leadingComments || leadingComments.length === 0) return false; + // return cached ignore + if (exportMap === null) { return null; } - for (const name in docStyleParsers) { - const doc = docStyleParsers[name](leadingComments); - if (doc) { - metadata.doc = doc; - } + const stats = fs.statSync(path); + if (exportMap != null) { + // date equality check + if (exportMap.mtime - stats.mtime === 0) { + return exportMap; } - - return true; - } catch (err) { - return false; + // future: check content equality? } - }); - return metadata; -} - -const availableDocStyleParsers = { - jsdoc: captureJsDoc, - tomdoc: captureTomDoc, -}; - -/** - * parse JSDoc from leading comments - * @param {object[]} comments - * @return {{ doc: object }} - */ -function captureJsDoc(comments) { - let doc; - - // capture XSDoc - comments.forEach(comment => { - // skip non-block comments - if (comment.type !== 'Block') return; - try { - doc = doctrine.parse(comment.value, { unwrap: true }); - } catch (err) { - /* don't care, for now? maybe add to `errors?` */ + // check valid extensions first + if (!hasValidExtension(path, context)) { + exportCache.set(cacheKey, null); + return null; } - }); - - return doc; -} - -/** - * parse TomDoc section from comments - */ -function captureTomDoc(comments) { - // collect lines up to first paragraph break - const lines = []; - for (let i = 0; i < comments.length; i++) { - const comment = comments[i]; - if (comment.value.match(/^\s*$/)) break; - lines.push(comment.value.trim()); - } - - // return doctrine-like object - const statusMatch = lines.join(' ').match(/^(Public|Internal|Deprecated):\s*(.+)/); - if (statusMatch) { - return { - description: statusMatch[2], - tags: [{ - title: statusMatch[1].toLowerCase(), - description: statusMatch[2], - }], - }; - } -} - -const supportedImportTypes = new Set(['ImportDefaultSpecifier', 'ImportNamespaceSpecifier']); - -ExportMap.get = function (source, context) { - const path = resolve(source, context); - if (path == null) return null; - - return ExportMap.for(childContext(path, context)); -}; - -ExportMap.for = function (context) { - const { path } = context; - - const cacheKey = hashObject(context).digest('hex'); - let exportMap = exportCache.get(cacheKey); - // return cached ignore - if (exportMap === null) return null; - - const stats = fs.statSync(path); - if (exportMap != null) { - // date equality check - if (exportMap.mtime - stats.mtime === 0) { - return exportMap; + // check for and cache ignore + if (isIgnored(path, context)) { + log('ignored path due to ignore settings:', path); + exportCache.set(cacheKey, null); + return null; } - // future: check content equality? - } - - // check valid extensions first - if (!hasValidExtension(path, context)) { - exportCache.set(cacheKey, null); - 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 unambiguous modules - if (!unambiguous.test(content)) { - log('ignored path due to unambiguous regex:', path); - exportCache.set(cacheKey, null); - return null; - } - - log('cache miss', cacheKey, 'for path', path); - exportMap = ExportMap.parse(path, content, context); - - // ambiguous modules return null - if (exportMap == null) { - log('ignored path due to ambiguous parse:', path); - exportCache.set(cacheKey, null); - return null; - } + const content = fs.readFileSync(path, { encoding: 'utf8' }); - exportMap.mtime = stats.mtime; + // check for and cache unambiguous modules + if (!unambiguous.test(content)) { + log('ignored path due to unambiguous regex:', path); + exportCache.set(cacheKey, null); + return null; + } - exportCache.set(cacheKey, exportMap); - return exportMap; -}; + log('cache miss', cacheKey, 'for path', path); + exportMap = ExportMap.parse(path, content, context); + // ambiguous modules return null + if (exportMap == null) { + log('ignored path due to ambiguous parse:', path); + exportCache.set(cacheKey, null); + return null; + } -ExportMap.parse = function (path, content, context) { - const m = new ExportMap(path); - const isEsModuleInteropTrue = isEsModuleInterop(); + exportMap.mtime = stats.mtime; - let ast; - let visitorKeys; - try { - const result = parse(path, content, context); - ast = result.ast; - visitorKeys = result.visitorKeys; - } catch (err) { - m.errors.push(err); - return m; // can't continue + exportCache.set(cacheKey, exportMap); + return exportMap; } - m.visitorKeys = visitorKeys; + static parse(path, content, context) { + const m = new ExportMap(path); + const isEsModuleInteropTrue = isEsModuleInterop(); - let hasDynamicImports = false; - - function processDynamicImport(source) { - hasDynamicImports = true; - if (source.type !== 'Literal') { - return null; - } - const p = remotePath(source.value); - if (p == null) { - return null; + let ast; + let visitorKeys; + try { + const result = parse(path, content, context); + ast = result.ast; + visitorKeys = result.visitorKeys; + } catch (err) { + m.errors.push(err); + return m; // can't continue } - const importedSpecifiers = new Set(); - importedSpecifiers.add('ImportNamespaceSpecifier'); - const getter = thunkFor(p, context); - m.imports.set(p, { - getter, - declarations: new Set([{ - source: { - // capturing actual node reference holds full AST in memory! - value: source.value, - loc: source.loc, - }, - importedSpecifiers, - dynamic: true, - }]), - }); - } - visit(ast, visitorKeys, { - ImportExpression(node) { - processDynamicImport(node.source); - }, - CallExpression(node) { - if (node.callee.type === 'Import') { - processDynamicImport(node.arguments[0]); - } - }, - }); + m.visitorKeys = visitorKeys; - const unambiguouslyESM = unambiguous.isModule(ast); - if (!unambiguouslyESM && !hasDynamicImports) return null; + let hasDynamicImports = false; - const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc']; - const docStyleParsers = {}; - docstyle.forEach(style => { - docStyleParsers[style] = availableDocStyleParsers[style]; - }); + function processDynamicImport(source) { + hasDynamicImports = true; + if (source.type !== 'Literal') { + return null; + } + const p = remotePath(source.value); + if (p == null) { + return null; + } + const importedSpecifiers = new Set(); + importedSpecifiers.add('ImportNamespaceSpecifier'); + const getter = thunkFor(p, context); + m.imports.set(p, { + getter, + declarations: new Set([{ + source: { + // capturing actual node reference holds full AST in memory! + value: source.value, + loc: source.loc, + }, + importedSpecifiers, + dynamic: true, + }]), + }); + } - // attempt to collect module doc - if (ast.comments) { - ast.comments.some(c => { - if (c.type !== 'Block') return false; - try { - const doc = doctrine.parse(c.value, { unwrap: true }); - if (doc.tags.some(t => t.title === 'module')) { - m.doc = doc; - return true; + visit(ast, visitorKeys, { + ImportExpression(node) { + processDynamicImport(node.source); + }, + CallExpression(node) { + if (node.callee.type === 'Import') { + processDynamicImport(node.arguments[0]); } - } catch (err) { /* ignore */ } - return false; + }, }); - } - const namespaces = new Map(); + const unambiguouslyESM = unambiguous.isModule(ast); + if (!unambiguouslyESM && !hasDynamicImports) { return null; } - function remotePath(value) { - return resolve.relative(value, path, context.settings); - } + const docstyle = context.settings && context.settings['import/docstyle'] || ['jsdoc']; + const docStyleParsers = {}; + docstyle.forEach((style) => { + docStyleParsers[style] = availableDocStyleParsers[style]; + }); - function resolveImport(value) { - const rp = remotePath(value); - if (rp == null) return null; - return ExportMap.for(childContext(rp, context)); - } + // attempt to collect module doc + if (ast.comments) { + ast.comments.some((c) => { + if (c.type !== 'Block') { return false; } + try { + const doc = doctrine.parse(c.value, { unwrap: true }); + if (doc.tags.some((t) => t.title === 'module')) { + m.doc = doc; + return true; + } + } catch (err) { /* ignore */ } + return false; + }); + } - function getNamespace(identifier) { - if (!namespaces.has(identifier.name)) return; + const namespaces = new Map(); - return function () { - return resolveImport(namespaces.get(identifier.name)); - }; - } + function remotePath(value) { + return resolve.relative(value, path, context.settings); + } - function addNamespace(object, identifier) { - const nsfn = getNamespace(identifier); - if (nsfn) { - Object.defineProperty(object, 'namespace', { get: nsfn }); + function resolveImport(value) { + const rp = remotePath(value); + if (rp == null) { return null; } + return ExportMap.for(childContext(rp, context)); } - return object; - } + function getNamespace(identifier) { + if (!namespaces.has(identifier.name)) { return; } - function processSpecifier(s, n, m) { - const nsource = n.source && n.source.value; - const exportMeta = {}; - let local; + return function () { + return resolveImport(namespaces.get(identifier.name)); + }; + } - switch (s.type) { - case 'ExportDefaultSpecifier': - if (!nsource) return; - local = 'default'; - break; - case 'ExportNamespaceSpecifier': - m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { - get() { return resolveImport(nsource); }, - })); - return; - case 'ExportAllDeclaration': - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); - return; - case 'ExportSpecifier': - if (!n.source) { - m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); - return; + function addNamespace(object, identifier) { + const nsfn = getNamespace(identifier); + if (nsfn) { + Object.defineProperty(object, 'namespace', { get: nsfn }); } - // else falls through - default: - local = s.local.name; - break; + + return object; } - // todo: JSDoc - m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); - } + function processSpecifier(s, n, m) { + const nsource = n.source && n.source.value; + const exportMeta = {}; + let local; - function captureDependencyWithSpecifiers(n) { - // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) - const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; - // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and - // shouldn't be considered to be just importing types - let specifiersOnlyImportingTypes = n.specifiers.length > 0; - const importedSpecifiers = new Set(); - n.specifiers.forEach(specifier => { - if (specifier.type === 'ImportSpecifier') { - importedSpecifiers.add(specifier.imported.name || specifier.imported.value); - } else if (supportedImportTypes.has(specifier.type)) { - importedSpecifiers.add(specifier.type); + switch (s.type) { + case 'ExportDefaultSpecifier': + if (!nsource) { return; } + local = 'default'; + break; + case 'ExportNamespaceSpecifier': + m.namespace.set(s.exported.name, Object.defineProperty(exportMeta, 'namespace', { + get() { return resolveImport(nsource); }, + })); + return; + case 'ExportAllDeclaration': + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.source.value)); + return; + case 'ExportSpecifier': + if (!n.source) { + m.namespace.set(s.exported.name || s.exported.value, addNamespace(exportMeta, s.local)); + return; + } + // else falls through + default: + local = s.local.name; + break; } - // import { type Foo } (Flow); import { typeof Foo } (Flow) - specifiersOnlyImportingTypes = specifiersOnlyImportingTypes - && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); - }); - captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); - } - - function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { - if (source == null) return null; - - const p = remotePath(source.value); - if (p == null) return null; + // todo: JSDoc + m.reexports.set(s.exported.name, { local, getImport: () => resolveImport(nsource) }); + } - const declarationMetadata = { - // capturing actual node reference holds full AST in memory! - source: { value: source.value, loc: source.loc }, - isOnlyImportingTypes, - importedSpecifiers, - }; + function captureDependencyWithSpecifiers(n) { + // import type { Foo } (TS and Flow); import typeof { Foo } (Flow) + const declarationIsType = n.importKind === 'type' || n.importKind === 'typeof'; + // import './foo' or import {} from './foo' (both 0 specifiers) is a side effect and + // shouldn't be considered to be just importing types + let specifiersOnlyImportingTypes = n.specifiers.length > 0; + const importedSpecifiers = new Set(); + n.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + importedSpecifiers.add(specifier.imported.name || specifier.imported.value); + } else if (supportedImportTypes.has(specifier.type)) { + importedSpecifiers.add(specifier.type); + } - const existing = m.imports.get(p); - if (existing != null) { - existing.declarations.add(declarationMetadata); - return existing.getter; + // import { type Foo } (Flow); import { typeof Foo } (Flow) + specifiersOnlyImportingTypes = specifiersOnlyImportingTypes + && (specifier.importKind === 'type' || specifier.importKind === 'typeof'); + }); + captureDependency(n, declarationIsType || specifiersOnlyImportingTypes, importedSpecifiers); } - const getter = thunkFor(p, context); - m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); - return getter; - } + function captureDependency({ source }, isOnlyImportingTypes, importedSpecifiers = new Set()) { + if (source == null) { return null; } - const source = makeSourceCode(content, ast); + const p = remotePath(source.value); + if (p == null) { return null; } - function readTsConfig(context) { - const tsConfigInfo = tsConfigLoader({ - cwd: - (context.parserOptions && context.parserOptions.tsconfigRootDir) || - process.cwd(), - getEnv: (key) => process.env[key], - }); - try { - if (tsConfigInfo.tsConfigPath !== undefined) { - // Projects not using TypeScript won't have `typescript` installed. - if (!ts) { ts = require('typescript'); } - - const configFile = ts.readConfigFile(tsConfigInfo.tsConfigPath, ts.sys.readFile); - return ts.parseJsonConfigFileContent( - configFile.config, - ts.sys, - dirname(tsConfigInfo.tsConfigPath), - ); - } - } catch (e) { - // Catch any errors - } + const declarationMetadata = { + // capturing actual node reference holds full AST in memory! + source: { value: source.value, loc: source.loc }, + isOnlyImportingTypes, + importedSpecifiers, + }; - return null; - } + const existing = m.imports.get(p); + if (existing != null) { + existing.declarations.add(declarationMetadata); + return existing.getter; + } - function isEsModuleInterop() { - const cacheKey = hashObject({ - tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, - }).digest('hex'); - let tsConfig = tsConfigCache.get(cacheKey); - if (typeof tsConfig === 'undefined') { - tsConfig = readTsConfig(context); - tsConfigCache.set(cacheKey, tsConfig); + const getter = thunkFor(p, context); + m.imports.set(p, { getter, declarations: new Set([declarationMetadata]) }); + return getter; } - return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; - } + const source = makeSourceCode(content, ast); - ast.body.forEach(function (n) { - if (n.type === 'ExportDefaultDeclaration') { - const exportMeta = captureDoc(source, docStyleParsers, n); - if (n.declaration.type === 'Identifier') { - addNamespace(exportMeta, n.declaration); + function readTsConfig(context) { + const tsconfigInfo = tsConfigLoader({ + cwd: context.parserOptions && context.parserOptions.tsconfigRootDir || process.cwd(), + getEnv: (key) => process.env[key], + }); + try { + if (tsconfigInfo.tsConfigPath !== undefined) { + // Projects not using TypeScript won't have `typescript` installed. + if (!ts) { ts = require('typescript'); } // eslint-disable-line import/no-extraneous-dependencies + + const configFile = ts.readConfigFile(tsconfigInfo.tsConfigPath, ts.sys.readFile); + return ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + dirname(tsconfigInfo.tsConfigPath), + ); + } + } catch (e) { + // Catch any errors } - m.namespace.set('default', exportMeta); - return; - } - if (n.type === 'ExportAllDeclaration') { - const getter = captureDependency(n, n.exportKind === 'type'); - if (getter) m.dependencies.add(getter); - if (n.exported) { - processSpecifier(n, n.exported, m); - } - return; + return null; } - // capture namespaces in case of later export - if (n.type === 'ImportDeclaration') { - captureDependencyWithSpecifiers(n); - - const ns = n.specifiers.find(s => s.type === 'ImportNamespaceSpecifier'); - if (ns) { - namespaces.set(ns.local.name, n.source.value); + function isEsModuleInterop() { + const cacheKey = hashObject({ + tsconfigRootDir: context.parserOptions && context.parserOptions.tsconfigRootDir, + }).digest('hex'); + let tsConfig = tsconfigCache.get(cacheKey); + if (typeof tsConfig === 'undefined') { + tsConfig = readTsConfig(context); + tsconfigCache.set(cacheKey, tsConfig); } - return; + + return tsConfig && tsConfig.options ? tsConfig.options.esModuleInterop : false; } - if (n.type === 'ExportNamedDeclaration') { - captureDependencyWithSpecifiers(n); - - // capture declaration - if (n.declaration != null) { - switch (n.declaration.type) { - case 'FunctionDeclaration': - case 'ClassDeclaration': - case 'TypeAlias': // flowtype with babel-eslint parser - case 'InterfaceDeclaration': - case 'DeclareFunction': - case 'TSDeclareFunction': - case 'TSEnumDeclaration': - case 'TSTypeAliasDeclaration': - case 'TSInterfaceDeclaration': - case 'TSAbstractClassDeclaration': - case 'TSModuleDeclaration': - m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); - break; - case 'VariableDeclaration': - n.declaration.declarations.forEach((d) => - recursivePatternCapture(d.id, - id => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)))); - break; + ast.body.forEach(function (n) { + if (n.type === 'ExportDefaultDeclaration') { + const exportMeta = captureDoc(source, docStyleParsers, n); + if (n.declaration.type === 'Identifier') { + addNamespace(exportMeta, n.declaration); } + m.namespace.set('default', exportMeta); + return; } - n.specifiers.forEach((s) => processSpecifier(s, n, m)); - } + if (n.type === 'ExportAllDeclaration') { + const getter = captureDependency(n, n.exportKind === 'type'); + if (getter) { m.dependencies.add(getter); } + if (n.exported) { + processSpecifier(n, n.exported, m); + } + return; + } - const exports = ['TSExportAssignment']; - if (isEsModuleInteropTrue) { - exports.push('TSNamespaceExportDeclaration'); - } + // capture namespaces in case of later export + if (n.type === 'ImportDeclaration') { + captureDependencyWithSpecifiers(n); - // This doesn't declare anything, but changes what's being exported. - if (includes(exports, n.type)) { - const exportedName = n.type === 'TSNamespaceExportDeclaration' - ? (n.id || n.name).name - : (n.expression && n.expression.name || (n.expression.id && n.expression.id.name) || null); - const declTypes = [ - 'VariableDeclaration', - 'ClassDeclaration', - 'TSDeclareFunction', - 'TSEnumDeclaration', - 'TSTypeAliasDeclaration', - 'TSInterfaceDeclaration', - 'TSAbstractClassDeclaration', - 'TSModuleDeclaration', - ]; - const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( - (id && id.name === exportedName) || (declarations && declarations.find((d) => d.id.name === exportedName)) - )); - if (exportedDecls.length === 0) { - // Export is not referencing any local declaration, must be re-exporting - m.namespace.set('default', captureDoc(source, docStyleParsers, n)); + const ns = n.specifiers.find((s) => s.type === 'ImportNamespaceSpecifier'); + if (ns) { + namespaces.set(ns.local.name, n.source.value); + } return; } - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export + + if (n.type === 'ExportNamedDeclaration') { + captureDependencyWithSpecifiers(n); + + // capture declaration + if (n.declaration != null) { + switch (n.declaration.type) { + case 'FunctionDeclaration': + case 'ClassDeclaration': + case 'TypeAlias': // flowtype with babel-eslint parser + case 'InterfaceDeclaration': + case 'DeclareFunction': + case 'TSDeclareFunction': + case 'TSEnumDeclaration': + case 'TSTypeAliasDeclaration': + case 'TSInterfaceDeclaration': + case 'TSAbstractClassDeclaration': + case 'TSModuleDeclaration': + m.namespace.set(n.declaration.id.name, captureDoc(source, docStyleParsers, n)); + break; + case 'VariableDeclaration': + n.declaration.declarations.forEach((d) => { + recursivePatternCapture( + d.id, + (id) => m.namespace.set(id.name, captureDoc(source, docStyleParsers, d, n)), + ); + }); + break; + default: + } + } + + n.specifiers.forEach((s) => processSpecifier(s, n, m)); + } + + const exports = ['TSExportAssignment']; + if (isEsModuleInteropTrue) { + exports.push('TSNamespaceExportDeclaration'); } - exportedDecls.forEach((decl) => { - if (decl.type === 'TSModuleDeclaration') { - if (decl.body && decl.body.type === 'TSModuleDeclaration') { - m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); - } else if (decl.body && decl.body.body) { - decl.body.body.forEach((moduleBlockNode) => { - // Export-assignment exports all members in the namespace, - // explicitly exported or not. - const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' ? - moduleBlockNode.declaration : - moduleBlockNode; - - if (!namespaceDecl) { - // TypeScript can check this for us; we needn't - } else if (namespaceDecl.type === 'VariableDeclaration') { - namespaceDecl.declarations.forEach((d) => - recursivePatternCapture(d.id, (id) => m.namespace.set( + + // This doesn't declare anything, but changes what's being exported. + if (includes(exports, n.type)) { + const exportedName = n.type === 'TSNamespaceExportDeclaration' + ? (n.id || n.name).name + : n.expression && n.expression.name || n.expression.id && n.expression.id.name || null; + const declTypes = [ + 'VariableDeclaration', + 'ClassDeclaration', + 'TSDeclareFunction', + 'TSEnumDeclaration', + 'TSTypeAliasDeclaration', + 'TSInterfaceDeclaration', + 'TSAbstractClassDeclaration', + 'TSModuleDeclaration', + ]; + const exportedDecls = ast.body.filter(({ type, id, declarations }) => includes(declTypes, type) && ( + id && id.name === exportedName || declarations && declarations.find((d) => d.id.name === exportedName) + )); + if (exportedDecls.length === 0) { + // Export is not referencing any local declaration, must be re-exporting + m.namespace.set('default', captureDoc(source, docStyleParsers, n)); + return; + } + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && !m.namespace.has('default') // and default isn't added already + ) { + m.namespace.set('default', {}); // add default export + } + exportedDecls.forEach((decl) => { + if (decl.type === 'TSModuleDeclaration') { + if (decl.body && decl.body.type === 'TSModuleDeclaration') { + m.namespace.set(decl.body.id.name, captureDoc(source, docStyleParsers, decl.body)); + } else if (decl.body && decl.body.body) { + decl.body.body.forEach((moduleBlockNode) => { + // Export-assignment exports all members in the namespace, + // explicitly exported or not. + const namespaceDecl = moduleBlockNode.type === 'ExportNamedDeclaration' + ? moduleBlockNode.declaration + : moduleBlockNode; + + if (!namespaceDecl) { + // TypeScript can check this for us; we needn't + } else if (namespaceDecl.type === 'VariableDeclaration') { + namespaceDecl.declarations.forEach((d) => recursivePatternCapture(d.id, (id) => m.namespace.set( id.name, captureDoc(source, docStyleParsers, decl, namespaceDecl, moduleBlockNode), )), - ); - } else { - m.namespace.set( - namespaceDecl.id.name, - captureDoc(source, docStyleParsers, moduleBlockNode)); - } - }); + ); + } else { + m.namespace.set( + namespaceDecl.id.name, + captureDoc(source, docStyleParsers, moduleBlockNode)); + } + }); + } + } else { + // Export as default + m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); } - } else { - // Export as default - m.namespace.set('default', captureDoc(source, docStyleParsers, decl)); - } - }); - } - }); + }); + } + }); - if ( - isEsModuleInteropTrue // esModuleInterop is on in tsconfig - && m.namespace.size > 0 // anything is exported - && !m.namespace.has('default') // and default isn't added already - ) { - m.namespace.set('default', {}); // add default export - } + if ( + isEsModuleInteropTrue // esModuleInterop is on in tsconfig + && m.namespace.size > 0 // anything is exported + && !m.namespace.has('default') // and default isn't added already + ) { + m.namespace.set('default', {}); // add default export + } - if (unambiguouslyESM) { - m.parseGoal = 'Module'; + if (unambiguouslyESM) { + m.parseGoal = 'Module'; + } + return m; } - return m; -}; +} /** * The creation of this closure is isolated from other scopes @@ -740,7 +741,6 @@ function thunkFor(p, context) { return () => ExportMap.for(childContext(p, context)); } - /** * Traverse a pattern/identifier node, calling 'callback' * for each leaf identifier. @@ -750,43 +750,61 @@ function thunkFor(p, context) { */ export function recursivePatternCapture(pattern, callback) { switch (pattern.type) { - case 'Identifier': // base case - callback(pattern); - break; - - case 'ObjectPattern': - pattern.properties.forEach(p => { - if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { - callback(p.argument); - return; - } - recursivePatternCapture(p.value, callback); - }); - break; + case 'Identifier': // base case + callback(pattern); + break; - case 'ArrayPattern': - pattern.elements.forEach((element) => { - if (element == null) return; - if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { - callback(element.argument); - return; - } - recursivePatternCapture(element, callback); - }); - break; + case 'ObjectPattern': + pattern.properties.forEach((p) => { + if (p.type === 'ExperimentalRestProperty' || p.type === 'RestElement') { + callback(p.argument); + return; + } + recursivePatternCapture(p.value, callback); + }); + break; - case 'AssignmentPattern': - callback(pattern.left); - break; + case 'ArrayPattern': + pattern.elements.forEach((element) => { + if (element == null) { return; } + if (element.type === 'ExperimentalRestProperty' || element.type === 'RestElement') { + callback(element.argument); + return; + } + recursivePatternCapture(element, callback); + }); + break; + + case 'AssignmentPattern': + callback(pattern.left); + break; + default: } } +let parserOptionsHash = ''; +let prevParserOptions = ''; +let settingsHash = ''; +let prevSettings = ''; /** * don't hold full context object in memory, just grab what we need. + * also calculate a cacheKey, where parts of the cacheKey hash are memoized */ function childContext(path, context) { const { settings, parserOptions, parserPath } = context; + + if (JSON.stringify(settings) !== prevSettings) { + settingsHash = hashObject({ settings }).digest('hex'); + prevSettings = JSON.stringify(settings); + } + + if (JSON.stringify(parserOptions) !== prevParserOptions) { + parserOptionsHash = hashObject({ parserOptions }).digest('hex'); + prevParserOptions = JSON.stringify(parserOptions); + } + return { + cacheKey: String(parserPath) + parserOptionsHash + settingsHash + String(path), settings, parserOptions, parserPath, @@ -794,7 +812,6 @@ function childContext(path, context) { }; } - /** * sometimes legacy support isn't _that_ hard... right? */ diff --git a/src/core/importType.js b/src/core/importType.js index ebdb306bc9..32e200f1de 100644 --- a/src/core/importType.js +++ b/src/core/importType.js @@ -4,6 +4,11 @@ import isCoreModule from 'is-core-module'; import resolve from 'eslint-module-utils/resolve'; import { getContextPackagePath } from './packagePath'; +const scopedRegExp = /^@[^/]+\/?[^/]+/; +export function isScoped(name) { + return name && scopedRegExp.test(name); +} + function baseModule(name) { if (isScoped(name)) { const [scope, pkg] = name.split('/'); @@ -14,7 +19,7 @@ function baseModule(name) { } function isInternalRegexMatch(name, settings) { - const internalScope = (settings && settings['import/internal-regex']); + const internalScope = settings && settings['import/internal-regex']; return internalScope && new RegExp(internalScope).test(name); } @@ -24,26 +29,12 @@ export function isAbsolute(name) { // path is defined only when a resolver resolves to a non-standard path export function isBuiltIn(name, settings, path) { - if (path || !name) return false; + if (path || !name) { return false; } const base = baseModule(name); - const extras = (settings && settings['import/core-modules']) || []; + const extras = settings && settings['import/core-modules'] || []; return isCoreModule(base) || extras.indexOf(base) > -1; } -export function isExternalModule(name, path, context) { - if (arguments.length < 3) { - throw new TypeError('isExternalModule: name, path, and context are all required'); - } - return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external'; -} - -export function isExternalModuleMain(name, path, context) { - if (arguments.length < 3) { - throw new TypeError('isExternalModule: name, path, and context are all required'); - } - return isModuleMain(name) && typeTest(name, context, path) === 'external'; -} - const moduleRegExp = /^\w/; function isModule(name) { return name && moduleRegExp.test(name); @@ -54,27 +45,16 @@ function isModuleMain(name) { return name && moduleMainRegExp.test(name); } -const scopedRegExp = /^@[^/]+\/?[^/]+/; -export function isScoped(name) { - return name && scopedRegExp.test(name); -} - -const scopedMainRegExp = /^@[^/]+\/?[^/]+$/; -export function isScopedMain(name) { - return name && scopedMainRegExp.test(name); -} - function isRelativeToParent(name) { - return /^\.\.$|^\.\.[\\/]/.test(name); + return (/^\.\.$|^\.\.[\\/]/).test(name); } - const indexFiles = ['.', './', './index', './index.js']; function isIndex(name) { return indexFiles.indexOf(name) !== -1; } function isRelativeToSibling(name) { - return /^\.[\\/]/.test(name); + return (/^\.[\\/]/).test(name); } function isExternalPath(path, context) { @@ -89,7 +69,7 @@ function isExternalPath(path, context) { return true; } - const folders = (settings && settings['import/external-module-folders']) || ['node_modules']; + const folders = settings && settings['import/external-module-folders'] || ['node_modules']; return folders.some((folder) => { const folderPath = nodeResolve(packagePath, folder); const relativePath = relative(folderPath, path); @@ -109,7 +89,7 @@ function isExternalLookingName(name) { return isModule(name) || isScoped(name); } -function typeTest(name, context, path ) { +function typeTest(name, context, path) { const { settings } = context; if (isInternalRegexMatch(name, settings)) { return 'internal'; } if (isAbsolute(name, settings, path)) { return 'absolute'; } @@ -123,6 +103,25 @@ function typeTest(name, context, path ) { return 'unknown'; } +export function isExternalModule(name, path, context) { + if (arguments.length < 3) { + throw new TypeError('isExternalModule: name, path, and context are all required'); + } + return (isModule(name) || isScoped(name)) && typeTest(name, context, path) === 'external'; +} + +export function isExternalModuleMain(name, path, context) { + if (arguments.length < 3) { + throw new TypeError('isExternalModule: name, path, and context are all required'); + } + return isModuleMain(name) && typeTest(name, context, path) === 'external'; +} + +const scopedMainRegExp = /^@[^/]+\/?[^/]+$/; +export function isScopedMain(name) { + return name && scopedMainRegExp.test(name); +} + export default function resolveImportType(name, context) { return typeTest(name, context, resolve(name, context)); } diff --git a/src/core/packagePath.js b/src/core/packagePath.js index 2b5a2d41ef..142f44aa4d 100644 --- a/src/core/packagePath.js +++ b/src/core/packagePath.js @@ -2,16 +2,15 @@ import { dirname } from 'path'; import pkgUp from 'eslint-module-utils/pkgUp'; import readPkgUp from 'eslint-module-utils/readPkgUp'; - -export function getContextPackagePath(context) { - return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); -} - export function getFilePackagePath(filePath) { const fp = pkgUp({ cwd: filePath }); return dirname(fp); } +export function getContextPackagePath(context) { + return getFilePackagePath(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); +} + export function getFilePackageName(filePath) { const { pkg, path } = readPkgUp({ cwd: filePath, normalize: false }); if (pkg) { diff --git a/src/core/staticRequire.js b/src/core/staticRequire.js index 502d39317d..88b5000c89 100644 --- a/src/core/staticRequire.js +++ b/src/core/staticRequire.js @@ -1,10 +1,10 @@ // todo: merge with module visitor export default function isStaticRequire(node) { - return node && - node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'require' && - node.arguments.length === 1 && - node.arguments[0].type === 'Literal' && - typeof node.arguments[0].value === 'string'; + return node + && node.callee + && node.callee.type === 'Identifier' + && node.callee.name === 'require' + && node.arguments.length === 1 + && node.arguments[0].type === 'Literal' + && typeof node.arguments[0].value === 'string'; } diff --git a/src/index.js b/src/index.js index 15f98d96f2..feafba9003 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,12 @@ export const rules = { 'no-unresolved': require('./rules/no-unresolved'), - 'named': require('./rules/named'), - 'default': require('./rules/default'), - 'namespace': require('./rules/namespace'), + named: require('./rules/named'), + default: require('./rules/default'), + namespace: require('./rules/namespace'), 'no-namespace': require('./rules/no-namespace'), - 'export': require('./rules/export'), + export: require('./rules/export'), 'no-mutable-exports': require('./rules/no-mutable-exports'), - 'extensions': require('./rules/extensions'), + extensions: require('./rules/extensions'), 'no-restricted-paths': require('./rules/no-restricted-paths'), 'no-internal-modules': require('./rules/no-internal-modules'), 'group-exports': require('./rules/group-exports'), @@ -25,19 +25,19 @@ export const rules = { 'no-commonjs': require('./rules/no-commonjs'), 'no-amd': require('./rules/no-amd'), 'no-duplicates': require('./rules/no-duplicates'), - 'first': require('./rules/first'), + first: require('./rules/first'), 'max-dependencies': require('./rules/max-dependencies'), 'no-extraneous-dependencies': require('./rules/no-extraneous-dependencies'), 'no-absolute-path': require('./rules/no-absolute-path'), 'no-nodejs-modules': require('./rules/no-nodejs-modules'), 'no-webpack-loader-syntax': require('./rules/no-webpack-loader-syntax'), - 'order': require('./rules/order'), + order: require('./rules/order'), 'newline-after-import': require('./rules/newline-after-import'), 'prefer-default-export': require('./rules/prefer-default-export'), 'no-default-export': require('./rules/no-default-export'), 'no-named-export': require('./rules/no-named-export'), 'no-dynamic-require': require('./rules/no-dynamic-require'), - 'unambiguous': require('./rules/unambiguous'), + unambiguous: require('./rules/unambiguous'), 'no-unassigned-import': require('./rules/no-unassigned-import'), 'no-useless-path-segments': require('./rules/no-useless-path-segments'), 'dynamic-import-chunkname': require('./rules/dynamic-import-chunkname'), @@ -55,17 +55,17 @@ export const rules = { }; export const configs = { - 'recommended': require('../config/recommended'), + recommended: require('../config/recommended'), - 'errors': require('../config/errors'), - 'warnings': require('../config/warnings'), + errors: require('../config/errors'), + warnings: require('../config/warnings'), // shhhh... work in progress "secret" rules 'stage-0': require('../config/stage-0'), // useful stuff for folks using various environments - 'react': require('../config/react'), + react: require('../config/react'), 'react-native': require('../config/react-native'), - 'electron': require('../config/electron'), - 'typescript': require('../config/typescript'), + electron: require('../config/electron'), + typescript: require('../config/typescript'), }; diff --git a/src/rules/consistent-type-specifier-style.js b/src/rules/consistent-type-specifier-style.js index 869eea91ff..9119976b19 100644 --- a/src/rules/consistent-type-specifier-style.js +++ b/src/rules/consistent-type-specifier-style.js @@ -7,9 +7,9 @@ function isComma(token) { function removeSpecifiers(fixes, fixer, sourceCode, specifiers) { for (const specifier of specifiers) { // remove the trailing comma - const comma = sourceCode.getTokenAfter(specifier, isComma); - if (comma) { - fixes.push(fixer.remove(comma)); + const token = sourceCode.getTokenAfter(specifier); + if (token && isComma(token)) { + fixes.push(fixer.remove(token)); } fixes.push(fixer.remove(specifier)); } @@ -26,7 +26,7 @@ function getImportText( return ''; } - const names = specifiers.map(s => { + const names = specifiers.map((s) => { if (s.imported.name === s.local.name) { return s.imported.name; } @@ -67,12 +67,14 @@ module.exports = { if ( // no specifiers (import type {} from '') have no specifiers to mark as inline - node.specifiers.length === 0 || - (node.specifiers.length === 1 && - // default imports are both "inline" and "top-level" - (node.specifiers[0].type === 'ImportDefaultSpecifier' || - // namespace imports are both "inline" and "top-level" - node.specifiers[0].type === 'ImportNamespaceSpecifier')) + node.specifiers.length === 0 + || node.specifiers.length === 1 + // default imports are both "inline" and "top-level" + && ( + node.specifiers[0].type === 'ImportDefaultSpecifier' + // namespace imports are both "inline" and "top-level" + || node.specifiers[0].type === 'ImportNamespaceSpecifier' + ) ) { return; } @@ -101,15 +103,17 @@ module.exports = { ImportDeclaration(node) { if ( // already top-level is valid - node.importKind === 'type' || - node.importKind === 'typeof' || + node.importKind === 'type' + || node.importKind === 'typeof' // no specifiers (import {} from '') cannot have inline - so is valid - node.specifiers.length === 0 || - (node.specifiers.length === 1 && - // default imports are both "inline" and "top-level" - (node.specifiers[0].type === 'ImportDefaultSpecifier' || - // namespace imports are both "inline" and "top-level" - node.specifiers[0].type === 'ImportNamespaceSpecifier')) + || node.specifiers.length === 0 + || node.specifiers.length === 1 + // default imports are both "inline" and "top-level" + && ( + node.specifiers[0].type === 'ImportDefaultSpecifier' + // namespace imports are both "inline" and "top-level" + || node.specifiers[0].type === 'ImportNamespaceSpecifier' + ) ) { return; } @@ -195,7 +199,7 @@ module.exports = { const comma = sourceCode.getTokenAfter(defaultSpecifier, isComma); const closingBrace = sourceCode.getTokenAfter( node.specifiers[node.specifiers.length - 1], - token => token.type === 'Punctuator' && token.value === '}', + (token) => token.type === 'Punctuator' && token.value === '}', ); fixes.push(fixer.removeRange([ comma.range[0], diff --git a/src/rules/default.js b/src/rules/default.js index 6ca918ef66..297a80c463 100644 --- a/src/rules/default.js +++ b/src/rules/default.js @@ -13,16 +13,14 @@ module.exports = { }, create(context) { - function checkDefault(specifierType, node) { - const defaultSpecifier = node.specifiers.find( - specifier => specifier.type === specifierType, + (specifier) => specifier.type === specifierType, ); - if (!defaultSpecifier) return; + if (!defaultSpecifier) { return; } const imports = Exports.get(node.source.value, context); - if (imports == null) return; + if (imports == null) { return; } if (imports.errors.length) { imports.reportErrors(context, node); @@ -35,8 +33,8 @@ module.exports = { } return { - 'ImportDeclaration': checkDefault.bind(null, 'ImportDefaultSpecifier'), - 'ExportNamedDeclaration': checkDefault.bind(null, 'ExportDefaultSpecifier'), + ImportDeclaration: checkDefault.bind(null, 'ImportDefaultSpecifier'), + ExportNamedDeclaration: checkDefault.bind(null, 'ExportDefaultSpecifier'), }; }, }; diff --git a/src/rules/dynamic-import-chunkname.js b/src/rules/dynamic-import-chunkname.js index 87a8523dad..a62e5c6c12 100644 --- a/src/rules/dynamic-import-chunkname.js +++ b/src/rules/dynamic-import-chunkname.js @@ -19,6 +19,9 @@ module.exports = { type: 'string', }, }, + allowEmpty: { + type: 'boolean', + }, webpackChunknameFormat: { type: 'string', }, @@ -28,7 +31,7 @@ module.exports = { create(context) { const config = context.options[0]; - const { importFunctions = [] } = config || {}; + const { importFunctions = [], allowEmpty = false } = config || {}; const { webpackChunknameFormat = '([0-9a-zA-Z-_/.]|\\[(request|index)\\])+' } = config || {}; const paddedCommentRegex = /^ (\S[\s\S]+\S) $/; @@ -42,7 +45,7 @@ module.exports = { ? sourceCode.getCommentsBefore(arg) // This method is available in ESLint >= 4. : sourceCode.getComments(arg).leading; // This method is deprecated in ESLint 7. - if (!leadingComments || leadingComments.length === 0) { + if ((!leadingComments || leadingComments.length === 0) && !allowEmpty) { context.report({ node, message: 'dynamic imports require a leading comment with the webpack chunkname', @@ -72,8 +75,7 @@ module.exports = { try { // just like webpack itself does vm.runInNewContext(`(function() {return {${comment.value}}})()`); - } - catch (error) { + } catch (error) { context.report({ node, message: `dynamic imports require a "webpack" comment with valid syntax`, @@ -95,7 +97,7 @@ module.exports = { } } - if (!isChunknamePresent) { + if (!isChunknamePresent && !allowEmpty) { context.report({ node, message: diff --git a/src/rules/export.js b/src/rules/export.js index da0df1ee75..c540f1e3c9 100644 --- a/src/rules/export.js +++ b/src/rules/export.js @@ -37,20 +37,21 @@ const tsTypePrefix = 'type:'; */ function isTypescriptFunctionOverloads(nodes) { const nodesArr = Array.from(nodes); - const types = new Set(nodesArr.map(node => node.parent.type)); - const idents = flatMap(nodesArr, (node) => ( - node.declaration && ( + const idents = flatMap( + nodesArr, + (node) => node.declaration && ( node.declaration.type === 'TSDeclareFunction' // eslint 6+ || node.declaration.type === 'TSEmptyBodyFunctionDeclaration' // eslint 4-5 ) ? node.declaration.id.name - : [] - )); + : [], + ); if (new Set(idents).size !== idents.length) { return true; } + const types = new Set(nodesArr.map((node) => node.parent.type)); if (!types.has('TSDeclareFunction')) { return false; } @@ -73,17 +74,17 @@ function isTypescriptFunctionOverloads(nodes) { * @returns {boolean} */ function isTypescriptNamespaceMerging(nodes) { - const types = new Set(Array.from(nodes, node => node.parent.type)); + const types = new Set(Array.from(nodes, (node) => node.parent.type)); const noNamespaceNodes = Array.from(nodes).filter((node) => node.parent.type !== 'TSModuleDeclaration'); return types.has('TSModuleDeclaration') && ( types.size === 1 // Merging with functions - || (types.size === 2 && (types.has('FunctionDeclaration') || types.has('TSDeclareFunction'))) - || (types.size === 3 && types.has('FunctionDeclaration') && types.has('TSDeclareFunction')) + || types.size === 2 && (types.has('FunctionDeclaration') || types.has('TSDeclareFunction')) + || types.size === 3 && types.has('FunctionDeclaration') && types.has('TSDeclareFunction') // Merging with classes or enums - || (types.size === 2 && (types.has('ClassDeclaration') || types.has('TSEnumDeclaration')) && noNamespaceNodes.length === 1) + || types.size === 2 && (types.has('ClassDeclaration') || types.has('TSEnumDeclaration')) && noNamespaceNodes.length === 1 ); } @@ -99,7 +100,7 @@ function isTypescriptNamespaceMerging(nodes) { * @returns {boolean} */ function shouldSkipTypescriptNamespace(node, nodes) { - const types = new Set(Array.from(nodes, node => node.parent.type)); + const types = new Set(Array.from(nodes, (node) => node.parent.type)); return !isTypescriptNamespaceMerging(nodes) && node.parent.type === 'TSModuleDeclaration' @@ -166,7 +167,7 @@ module.exports = { }, ExportNamedDeclaration(node) { - if (node.declaration == null) return; + if (node.declaration == null) { return; } const parent = getParent(node); // support for old TypeScript versions @@ -185,20 +186,19 @@ module.exports = { if (node.declaration.declarations != null) { for (const declaration of node.declaration.declarations) { - recursivePatternCapture(declaration.id, v => - addNamed(v.name, v, parent, isTypeVariableDecl)); + recursivePatternCapture(declaration.id, (v) => { addNamed(v.name, v, parent, isTypeVariableDecl); }); } } }, ExportAllDeclaration(node) { - if (node.source == null) return; // not sure if this is ever true + if (node.source == null) { return; } // not sure if this is ever true // `export * as X from 'path'` does not conflict - if (node.exported && node.exported.name) return; + if (node.exported && node.exported.name) { return; } const remoteExports = ExportMap.get(node.source.value, context); - if (remoteExports == null) return; + if (remoteExports == null) { return; } if (remoteExports.errors.length) { remoteExports.reportErrors(context, node); @@ -223,15 +223,15 @@ module.exports = { } }, - 'Program:exit': function () { + 'Program:exit'() { for (const [, named] of namespace) { for (const [name, nodes] of named) { - if (nodes.size <= 1) continue; + if (nodes.size <= 1) { continue; } - if (isTypescriptFunctionOverloads(nodes) || isTypescriptNamespaceMerging(nodes)) continue; + if (isTypescriptFunctionOverloads(nodes) || isTypescriptNamespaceMerging(nodes)) { continue; } for (const node of nodes) { - if (shouldSkipTypescriptNamespace(node, nodes)) continue; + if (shouldSkipTypescriptNamespace(node, nodes)) { continue; } if (name === 'default') { context.report(node, 'Multiple default exports.'); diff --git a/src/rules/exports-last.js b/src/rules/exports-last.js index ed77758d20..1e79f8339c 100644 --- a/src/rules/exports-last.js +++ b/src/rules/exports-last.js @@ -1,9 +1,11 @@ +import findLastIndex from 'array.prototype.findlastindex'; + import docsUrl from '../docsUrl'; function isNonExportStatement({ type }) { - return type !== 'ExportDefaultDeclaration' && - type !== 'ExportNamedDeclaration' && - type !== 'ExportAllDeclaration'; + return type !== 'ExportDefaultDeclaration' + && type !== 'ExportNamedDeclaration' + && type !== 'ExportAllDeclaration'; } module.exports = { @@ -20,15 +22,10 @@ module.exports = { create(context) { return { Program({ body }) { - const lastNonExportStatementIndex = body.reduce(function findLastIndex(acc, item, index) { - if (isNonExportStatement(item)) { - return index; - } - return acc; - }, -1); + const lastNonExportStatementIndex = findLastIndex(body, isNonExportStatement); if (lastNonExportStatementIndex !== -1) { - body.slice(0, lastNonExportStatementIndex).forEach(function checkNonExport(node) { + body.slice(0, lastNonExportStatementIndex).forEach((node) => { if (!isNonExportStatement(node)) { context.report({ node, diff --git a/src/rules/extensions.js b/src/rules/extensions.js index 7d026c787f..b1e5c6d9f1 100644 --- a/src/rules/extensions.js +++ b/src/rules/extensions.js @@ -5,7 +5,7 @@ import { isBuiltIn, isExternalModule, isScoped } from '../core/importType'; import moduleVisitor from 'eslint-module-utils/moduleVisitor'; import docsUrl from '../docsUrl'; -const enumValues = { enum: [ 'always', 'ignorePackages', 'never' ] }; +const enumValues = { enum: ['always', 'ignorePackages', 'never'] }; const patternProperties = { type: 'object', patternProperties: { '.*': enumValues }, @@ -13,8 +13,8 @@ const patternProperties = { const properties = { type: 'object', properties: { - 'pattern': patternProperties, - 'ignorePackages': { type: 'boolean' }, + pattern: patternProperties, + ignorePackages: { type: 'boolean' }, }, }; @@ -26,7 +26,7 @@ function buildProperties(context) { ignorePackages: false, }; - context.options.forEach(obj => { + context.options.forEach((obj) => { // If this is a string, set defaultConfig to its value if (typeof obj === 'string') { @@ -130,27 +130,28 @@ module.exports = { } function isExternalRootModule(file) { + if (file === '.' || file === '..') { return false; } const slashCount = file.split('/').length - 1; - if (slashCount === 0) return true; - if (isScoped(file) && slashCount <= 1) return true; + if (slashCount === 0) { return true; } + if (isScoped(file) && slashCount <= 1) { return true; } return false; } function checkFileExtension(source, node) { // bail if the declaration doesn't have a source, e.g. "export { foo };", or if it's only partially typed like in an editor - if (!source || !source.value) return; + if (!source || !source.value) { return; } const importPathWithQueryString = source.value; // don't enforce anything on builtins - if (isBuiltIn(importPathWithQueryString, 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; + if (isExternalRootModule(importPath)) { return; } const resolvedPath = resolve(importPath, context); @@ -167,7 +168,7 @@ module.exports = { if (!extension || !importPath.endsWith(`.${extension}`)) { // ignore type-only imports and exports - if (node.importKind === 'type' || node.exportKind === 'type') return; + if (node.importKind === 'type' || node.exportKind === 'type') { return; } const extensionRequired = isUseOfExtensionRequired(extension, isPackage); const extensionForbidden = isUseOfExtensionForbidden(extension); if (extensionRequired && !extensionForbidden) { diff --git a/src/rules/first.js b/src/rules/first.js index ebead6cf27..f8cc273a31 100644 --- a/src/rules/first.js +++ b/src/rules/first.js @@ -25,13 +25,13 @@ module.exports = { create(context) { function isPossibleDirective(node) { - return node.type === 'ExpressionStatement' && - node.expression.type === 'Literal' && - typeof node.expression.value === 'string'; + return node.type === 'ExpressionStatement' + && node.expression.type === 'Literal' + && typeof node.expression.value === 'string'; } return { - 'Program': function (n) { + Program(n) { const body = n.body; if (!body) { return; @@ -56,7 +56,7 @@ module.exports = { if (node.type === 'ImportDeclaration' || node.type === 'TSImportEqualsDeclaration') { if (absoluteFirst) { - if (/^\./.test(getImportValue(node))) { + if ((/^\./).test(getImportValue(node))) { anyRelative = true; } else if (anyRelative) { context.report({ @@ -67,7 +67,7 @@ module.exports = { } if (nonImportCount > 0) { for (const variable of context.getDeclaredVariables(node)) { - if (!shouldSort) break; + if (!shouldSort) { break; } const references = variable.references; if (references.length) { for (const reference of references) { @@ -90,7 +90,7 @@ module.exports = { nonImportCount++; } }); - if (!errorInfos.length) return; + if (!errorInfos.length) { return; } errorInfos.forEach(function (errorInfo, index) { const node = errorInfo.node; const infos = { @@ -112,26 +112,27 @@ module.exports = { const nodeSourceCode = String.prototype.slice.apply( originSourceCode, _errorInfo.range, ); - if (/\S/.test(nodeSourceCode[0])) { - return '\n' + nodeSourceCode; + if ((/\S/).test(nodeSourceCode[0])) { + return `\n${nodeSourceCode}`; } return nodeSourceCode; }).join(''); let insertFixer = null; let replaceSourceCode = ''; if (!lastLegalImp) { - insertSourceCode = - insertSourceCode.trim() + insertSourceCode.match(/^(\s+)/)[0]; + insertSourceCode = insertSourceCode.trim() + insertSourceCode.match(/^(\s+)/)[0]; } - insertFixer = lastLegalImp ? - fixer.insertTextAfter(lastLegalImp, insertSourceCode) : - fixer.insertTextBefore(body[0], insertSourceCode); + insertFixer = lastLegalImp + ? fixer.insertTextAfter(lastLegalImp, insertSourceCode) + : fixer.insertTextBefore(body[0], insertSourceCode); + const fixers = [insertFixer].concat(removeFixers); - fixers.forEach(function (computedFixer, i) { - replaceSourceCode += (originSourceCode.slice( + fixers.forEach((computedFixer, i) => { + replaceSourceCode += originSourceCode.slice( fixers[i - 1] ? fixers[i - 1].range[1] : 0, computedFixer.range[0], - ) + computedFixer.text); + ) + computedFixer.text; }); + return fixer.replaceTextRange(range, replaceSourceCode); }; } diff --git a/src/rules/group-exports.js b/src/rules/group-exports.js index 63af9d9141..7978130d34 100644 --- a/src/rules/group-exports.js +++ b/src/rules/group-exports.js @@ -98,7 +98,7 @@ function create(context) { 'Program:exit': function onExit() { // Report multiple `export` declarations (ES2015 modules) if (nodes.modules.set.size > 1) { - nodes.modules.set.forEach(node => { + nodes.modules.set.forEach((node) => { context.report({ node, message: errors[node.type], @@ -108,7 +108,7 @@ function create(context) { // Report multiple `aggregated exports` from the same module (ES2015 modules) flat(values(nodes.modules.sources) - .filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1)) + .filter((nodesWithSource) => Array.isArray(nodesWithSource) && nodesWithSource.length > 1)) .forEach((node) => { context.report({ node, @@ -118,7 +118,7 @@ function create(context) { // Report multiple `export type` declarations (FLOW ES2015 modules) if (nodes.types.set.size > 1) { - nodes.types.set.forEach(node => { + nodes.types.set.forEach((node) => { context.report({ node, message: errors[node.type], @@ -128,7 +128,7 @@ function create(context) { // Report multiple `aggregated type exports` from the same module (FLOW ES2015 modules) flat(values(nodes.types.sources) - .filter(nodesWithSource => Array.isArray(nodesWithSource) && nodesWithSource.length > 1)) + .filter((nodesWithSource) => Array.isArray(nodesWithSource) && nodesWithSource.length > 1)) .forEach((node) => { context.report({ node, @@ -138,7 +138,7 @@ function create(context) { // Report multiple `module.exports` assignments (CommonJS) if (nodes.commonjs.set.size > 1) { - nodes.commonjs.set.forEach(node => { + nodes.commonjs.set.forEach((node) => { context.report({ node, message: errors[node.type], diff --git a/src/rules/imports-first.js b/src/rules/imports-first.js index 07bb4633de..966367e99f 100644 --- a/src/rules/imports-first.js +++ b/src/rules/imports-first.js @@ -2,13 +2,14 @@ import docsUrl from '../docsUrl'; const first = require('./first'); -const newMeta = Object.assign({}, first.meta, { +const newMeta = { + ...first.meta, deprecated: true, docs: { category: 'Style guide', description: 'Replaced by `import/first`.', url: docsUrl('imports-first', '7b25c1cb95ee18acc1531002fd343e1e6031f9ed'), }, -}); +}; -module.exports = Object.assign({}, first, { meta: newMeta }); +module.exports = { ...first, meta: newMeta }; diff --git a/src/rules/max-dependencies.js b/src/rules/max-dependencies.js index 95f34176f5..488e906182 100644 --- a/src/rules/max-dependencies.js +++ b/src/rules/max-dependencies.js @@ -24,17 +24,17 @@ module.exports = { schema: [ { - 'type': 'object', - 'properties': { - 'max': { 'type': 'number' }, - 'ignoreTypeImports': { 'type': 'boolean' }, + type: 'object', + properties: { + max: { type: 'number' }, + ignoreTypeImports: { type: 'boolean' }, }, - 'additionalProperties': false, + additionalProperties: false, }, ], }, - create: context => { + create(context) { const { ignoreTypeImports = DEFAULT_IGNORE_TYPE_IMPORTS, } = context.options[0] || {}; @@ -42,15 +42,19 @@ module.exports = { const dependencies = new Set(); // keep track of dependencies let lastNode; // keep track of the last node to report on - return Object.assign({ - 'Program:exit': function () { + return { + 'Program:exit'() { countDependencies(dependencies, lastNode, context); }, - }, moduleVisitor((source, { importKind }) => { - if (importKind !== TYPE_IMPORT || !ignoreTypeImports) { - dependencies.add(source.value); - } - lastNode = source; - }, { commonjs: true })); + ...moduleVisitor( + (source, { importKind }) => { + if (importKind !== TYPE_IMPORT || !ignoreTypeImports) { + dependencies.add(source.value); + } + lastNode = source; + }, + { commonjs: true }, + ), + }; }, }; diff --git a/src/rules/named.js b/src/rules/named.js index 050f835056..e7fe4e4dce 100644 --- a/src/rules/named.js +++ b/src/rules/named.js @@ -67,12 +67,12 @@ module.exports = { if (!deepLookup.found) { if (deepLookup.path.length > 1) { const deepPath = deepLookup.path - .map(i => path.relative(path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()), i.path)) + .map((i) => path.relative(path.dirname(context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()), i.path)) .join(' -> '); context.report(im[key], `${name} not found via ${deepPath}`); } else { - context.report(im[key], name + ' not found in \'' + node.source.value + '\''); + context.report(im[key], `${name} not found in '${node.source.value}'`); } } }); @@ -121,12 +121,12 @@ module.exports = { if (!deepLookup.found) { if (deepLookup.path.length > 1) { const deepPath = deepLookup.path - .map(i => path.relative(path.dirname(context.getFilename()), i.path)) + .map((i) => path.relative(path.dirname(context.getFilename()), i.path)) .join(' -> '); context.report(im.key, `${im.key.name} not found via ${deepPath}`); } else { - context.report(im.key, im.key.name + ' not found in \'' + source.value + '\''); + context.report(im.key, `${im.key.name} not found in '${source.value}'`); } } }); diff --git a/src/rules/namespace.js b/src/rules/namespace.js index 3b6019da8d..77a3ea9077 100644 --- a/src/rules/namespace.js +++ b/src/rules/namespace.js @@ -4,12 +4,12 @@ import importDeclaration from '../importDeclaration'; import docsUrl from '../docsUrl'; function processBodyStatement(context, namespaces, declaration) { - if (declaration.type !== 'ImportDeclaration') return; + if (declaration.type !== 'ImportDeclaration') { return; } - if (declaration.specifiers.length === 0) return; + if (declaration.specifiers.length === 0) { return; } const imports = Exports.get(declaration.source.value, context); - if (imports == null) return null; + if (imports == null) { return null; } if (imports.errors.length > 0) { imports.reportErrors(context, declaration); @@ -18,25 +18,26 @@ function processBodyStatement(context, namespaces, declaration) { declaration.specifiers.forEach((specifier) => { switch (specifier.type) { - case 'ImportNamespaceSpecifier': - if (!imports.size) { - context.report( - specifier, - `No exported names found in module '${declaration.source.value}'.`, + case 'ImportNamespaceSpecifier': + if (!imports.size) { + context.report( + specifier, + `No exported names found in module '${declaration.source.value}'.`, + ); + } + namespaces.set(specifier.local.name, imports); + break; + case 'ImportDefaultSpecifier': + case 'ImportSpecifier': { + const meta = imports.get( + // default to 'default' for default https://i.imgur.com/nj6qAWy.jpg + specifier.imported ? specifier.imported.name || specifier.imported.value : 'default', ); + if (!meta || !meta.namespace) { break; } + namespaces.set(specifier.local.name, meta.namespace); + break; } - namespaces.set(specifier.local.name, imports); - break; - case 'ImportDefaultSpecifier': - case 'ImportSpecifier': { - const meta = imports.get( - // default to 'default' for default https://i.imgur.com/nj6qAWy.jpg - specifier.imported ? (specifier.imported.name || specifier.imported.value) : 'default', - ); - if (!meta || !meta.namespace) { break; } - namespaces.set(specifier.local.name, meta.namespace); - break; - } + default: } }); } @@ -66,7 +67,6 @@ module.exports = { }, create: function namespaceRule(context) { - // read options const { allowComputed = false, @@ -81,7 +81,7 @@ module.exports = { return { // pick up all imports at body entry time, to properly respect hoisting Program({ body }) { - body.forEach(x => processBodyStatement(context, namespaces, x)); + body.forEach((x) => { processBodyStatement(context, namespaces, x); }); }, // same as above, but does not add names to local map @@ -89,7 +89,7 @@ module.exports = { const declaration = importDeclaration(context); const imports = Exports.get(declaration.source.value, context); - if (imports == null) return null; + if (imports == null) { return null; } if (imports.errors.length) { imports.reportErrors(context, declaration); @@ -107,9 +107,9 @@ module.exports = { // todo: check for possible redefinition MemberExpression(dereference) { - if (dereference.object.type !== 'Identifier') return; - if (!namespaces.has(dereference.object.name)) return; - if (declaredScope(context, dereference.object.name) !== 'module') return; + if (dereference.object.type !== 'Identifier') { return; } + if (!namespaces.has(dereference.object.name)) { return; } + if (declaredScope(context, dereference.object.name) !== 'module') { return; } if (dereference.parent.type === 'AssignmentExpression' && dereference.parent.left === dereference) { context.report( @@ -142,7 +142,7 @@ module.exports = { } const exported = namespace.get(dereference.property.name); - if (exported == null) return; + if (exported == null) { return; } // stash and pop namepath.push(dereference.property.name); @@ -152,18 +152,18 @@ module.exports = { }, VariableDeclarator({ id, init }) { - if (init == null) return; - if (init.type !== 'Identifier') return; - if (!namespaces.has(init.name)) return; + if (init == null) { return; } + if (init.type !== 'Identifier') { return; } + if (!namespaces.has(init.name)) { return; } // check for redefinition in intermediate scopes - if (declaredScope(context, init.name) !== 'module') return; + if (declaredScope(context, init.name) !== 'module') { return; } // DFS traverse child namespaces function testKey(pattern, namespace, path = [init.name]) { - if (!(namespace instanceof Exports)) return; + if (!(namespace instanceof Exports)) { return; } - if (pattern.type !== 'ObjectPattern') return; + if (pattern.type !== 'ObjectPattern') { return; } for (const property of pattern.properties) { if ( @@ -204,7 +204,7 @@ module.exports = { }, JSXMemberExpression({ object, property }) { - if (!namespaces.has(object.name)) return; + if (!namespaces.has(object.name)) { return; } const namespace = namespaces.get(object.name); if (!namespace.has(property.name)) { context.report({ diff --git a/src/rules/newline-after-import.js b/src/rules/newline-after-import.js index 36678bfc4e..a33bb615b9 100644 --- a/src/rules/newline-after-import.js +++ b/src/rules/newline-after-import.js @@ -63,22 +63,28 @@ module.exports = { fixable: 'whitespace', schema: [ { - 'type': 'object', - 'properties': { - 'count': { - 'type': 'integer', - 'minimum': 1, + type: 'object', + properties: { + count: { + type: 'integer', + minimum: 1, }, - 'considerComments': { 'type': 'boolean' }, + exactCount: { type: 'boolean' }, + considerComments: { type: 'boolean' }, }, - 'additionalProperties': false, + additionalProperties: false, }, ], }, create(context) { let level = 0; const requireCalls = []; - const options = Object.assign({ count: 1, considerComments: false }, context.options[0]); + const options = { + count: 1, + exactCount: false, + considerComments: false, + ...context.options[0], + }; function checkForNewLine(node, nextNode, type) { if (isExportDefaultClass(nextNode) || isExportNameClass(nextNode)) { @@ -94,7 +100,10 @@ module.exports = { const lineDifference = getLineDifference(node, nextNode); const EXPECTED_LINE_DIFFERENCE = options.count + 1; - if (lineDifference < EXPECTED_LINE_DIFFERENCE) { + if ( + lineDifference < EXPECTED_LINE_DIFFERENCE + || options.exactCount && lineDifference !== EXPECTED_LINE_DIFFERENCE + ) { let column = node.loc.start.column; if (node.loc.start.line !== node.loc.end.line) { @@ -107,7 +116,7 @@ module.exports = { column, }, message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} after ${type} statement not followed by another ${type}.`, - fix: fixer => fixer.insertTextAfter( + fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? undefined : (fixer) => fixer.insertTextAfter( node, '\n'.repeat(EXPECTED_LINE_DIFFERENCE - lineDifference), ), @@ -132,7 +141,7 @@ module.exports = { column, }, message: `Expected ${options.count} empty line${options.count > 1 ? 's' : ''} after import statement not followed by another import.`, - fix: fixer => fixer.insertTextAfter( + fix: options.exactCount && EXPECTED_LINE_DIFFERENCE < lineDifference ? undefined : (fixer) => fixer.insertTextAfter( node, '\n'.repeat(EXPECTED_LINE_DIFFERENCE - lineDifference), ), @@ -149,16 +158,20 @@ module.exports = { function checkImport(node) { const { parent } = node; + + if (!parent || !parent.body) { + return; + } + const nodePosition = parent.body.indexOf(node); const nextNode = parent.body[nodePosition + 1]; const endLine = node.loc.end.line; let nextComment; if (typeof parent.comments !== 'undefined' && options.considerComments) { - nextComment = parent.comments.find(o => o.loc.start.line === endLine + 1); + nextComment = parent.comments.find((o) => o.loc.start.line >= endLine && o.loc.start.line <= endLine + options.count + 1); } - // skip "export import"s if (node.type === 'TSImportEqualsDeclaration' && node.isExport) { return; @@ -179,12 +192,12 @@ module.exports = { requireCalls.push(node); } }, - 'Program:exit': function () { + 'Program:exit'() { log('exit processing for', context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename()); const scopeBody = getScopeBody(context.getScope()); log('got scope:', scopeBody); - requireCalls.forEach(function (node, index) { + requireCalls.forEach((node, index) => { const nodePosition = findNodeIndexInScopeBody(scopeBody, node); log('node position in scope:', nodePosition); @@ -196,8 +209,12 @@ module.exports = { return; } - if (nextStatement && - (!nextRequireCall || !containsNodeOrEqual(nextStatement, nextRequireCall))) { + if ( + nextStatement && ( + !nextRequireCall + || !containsNodeOrEqual(nextStatement, nextRequireCall) + ) + ) { checkForNewLine(statementWithRequireCall, nextStatement, 'require'); } diff --git a/src/rules/no-absolute-path.js b/src/rules/no-absolute-path.js index 19dae6b6fb..04f67383f2 100644 --- a/src/rules/no-absolute-path.js +++ b/src/rules/no-absolute-path.js @@ -12,7 +12,7 @@ module.exports = { url: docsUrl('no-absolute-path'), }, fixable: 'code', - schema: [ makeOptionsSchema() ], + schema: [makeOptionsSchema()], }, create(context) { @@ -21,12 +21,12 @@ module.exports = { context.report({ node: source, message: 'Do not import modules using an absolute path', - fix: fixer => { + fix(fixer) { const resolvedContext = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); // node.js and web imports work with posix style paths ("/") let relativePath = path.posix.relative(path.dirname(resolvedContext), source.value); if (!relativePath.startsWith('.')) { - relativePath = './' + relativePath; + relativePath = `./${relativePath}`; } return fixer.replaceText(source, JSON.stringify(relativePath)); }, @@ -34,7 +34,7 @@ module.exports = { } } - const options = Object.assign({ esmodule: true, commonjs: true }, context.options[0]); + const options = { esmodule: true, commonjs: true, ...context.options[0] }; return moduleVisitor(reportIfAbsolute, options); }, }; diff --git a/src/rules/no-amd.js b/src/rules/no-amd.js index 90359cd5fd..5edfe3e698 100644 --- a/src/rules/no-amd.js +++ b/src/rules/no-amd.js @@ -22,18 +22,17 @@ module.exports = { create(context) { return { - 'CallExpression': function (node) { - if (context.getScope().type !== 'module') return; + CallExpression(node) { + if (context.getScope().type !== 'module') { return; } - if (node.callee.type !== 'Identifier') return; - if (node.callee.name !== 'require' && - node.callee.name !== 'define') return; + if (node.callee.type !== 'Identifier') { return; } + if (node.callee.name !== 'require' && node.callee.name !== 'define') { return; } // todo: capture define((require, module, exports) => {}) form? - if (node.arguments.length !== 2) return; + if (node.arguments.length !== 2) { return; } const modules = node.arguments[0]; - if (modules.type !== 'ArrayExpression') return; + if (modules.type !== 'ArrayExpression') { return; } // todo: check second arg type? (identifier or callback) diff --git a/src/rules/no-anonymous-default-export.js b/src/rules/no-anonymous-default-export.js index d9edcc2b36..4f6947e814 100644 --- a/src/rules/no-anonymous-default-export.js +++ b/src/rules/no-anonymous-default-export.js @@ -3,8 +3,11 @@ * @author Duncan Beevers */ +import hasOwn from 'hasown'; +import values from 'object.values'; +import fromEntries from 'object.fromentries'; + import docsUrl from '../docsUrl'; -import has from 'has'; const defs = { ArrayExpression: { @@ -57,23 +60,12 @@ const defs = { }, }; -const schemaProperties = Object.keys(defs) - .map((key) => defs[key]) - .reduce((acc, def) => { - acc[def.option] = { - description: def.description, - type: 'boolean', - }; - - return acc; - }, {}); +const schemaProperties = fromEntries(values(defs).map((def) => [def.option, { + description: def.description, + type: 'boolean', +}])); -const defaults = Object.keys(defs) - .map((key) => defs[key]) - .reduce((acc, def) => { - acc[def.option] = has(def, 'default') ? def.default : false; - return acc; - }, {}); +const defaults = fromEntries(values(defs).map((def) => [def.option, hasOwn(def, 'default') ? def.default : false])); module.exports = { meta: { @@ -88,16 +80,16 @@ module.exports = { { type: 'object', properties: schemaProperties, - 'additionalProperties': false, + additionalProperties: false, }, ], }, create(context) { - const options = Object.assign({}, defaults, context.options[0]); + const options = { ...defaults, ...context.options[0] }; return { - 'ExportDefaultDeclaration': (node) => { + ExportDefaultDeclaration(node) { const def = defs[node.declaration.type]; // Recognized node type and allowed by configuration, diff --git a/src/rules/no-commonjs.js b/src/rules/no-commonjs.js index 7a35fc8a08..dde509222b 100644 --- a/src/rules/no-commonjs.js +++ b/src/rules/no-commonjs.js @@ -16,9 +16,9 @@ function normalizeLegacyOptions(options) { } function allowPrimitive(node, options) { - if (!options.allowPrimitiveModules) return false; - if (node.parent.type !== 'AssignmentExpression') return false; - return (node.parent.right.type !== 'ObjectExpression'); + if (!options.allowPrimitiveModules) { return false; } + if (node.parent.type !== 'AssignmentExpression') { return false; } + return node.parent.right.type !== 'ObjectExpression'; } function allowRequire(node, options) { @@ -40,14 +40,16 @@ function isConditional(node) { || node.type === 'TryStatement' || node.type === 'LogicalExpression' || node.type === 'ConditionalExpression' - ) return true; - if (node.parent) return isConditional(node.parent); + ) { + return true; + } + if (node.parent) { return isConditional(node.parent); } return false; } function isLiteralString(node) { - return (node.type === 'Literal' && typeof node.value === 'string') || - (node.type === 'TemplateLiteral' && node.expressions.length === 0); + return node.type === 'Literal' && typeof node.value === 'string' + || node.type === 'TemplateLiteral' && node.expressions.length === 0; } //------------------------------------------------------------------------------ @@ -58,9 +60,9 @@ const schemaString = { enum: ['allow-primitive-modules'] }; const schemaObject = { type: 'object', properties: { - allowPrimitiveModules: { 'type': 'boolean' }, - allowRequire: { 'type': 'boolean' }, - allowConditionalRequire: { 'type': 'boolean' }, + allowPrimitiveModules: { type: 'boolean' }, + allowRequire: { type: 'boolean' }, + allowConditionalRequire: { type: 'boolean' }, }, additionalProperties: false, }; @@ -95,11 +97,11 @@ module.exports = { return { - 'MemberExpression': function (node) { + MemberExpression(node) { // module.exports if (node.object.name === 'module' && node.property.name === 'exports') { - if (allowPrimitive(node, options)) return; + if (allowPrimitive(node, options)) { return; } context.report({ node, message: EXPORT_MESSAGE }); } @@ -107,25 +109,25 @@ module.exports = { if (node.object.name === 'exports') { const isInScope = context.getScope() .variables - .some(variable => variable.name === 'exports'); - if (! isInScope) { + .some((variable) => variable.name === 'exports'); + if (!isInScope) { context.report({ node, message: EXPORT_MESSAGE }); } } }, - 'CallExpression': function (call) { - if (!validateScope(context.getScope())) return; + CallExpression(call) { + if (!validateScope(context.getScope())) { return; } - if (call.callee.type !== 'Identifier') return; - if (call.callee.name !== 'require') return; + if (call.callee.type !== 'Identifier') { return; } + if (call.callee.name !== 'require') { return; } - if (call.arguments.length !== 1) return; - if (!isLiteralString(call.arguments[0])) return; + if (call.arguments.length !== 1) { return; } + if (!isLiteralString(call.arguments[0])) { return; } - if (allowRequire(call, options)) return; + if (allowRequire(call, options)) { return; } - if (allowConditionalRequire(call, options) && isConditional(call.parent)) return; + if (allowConditionalRequire(call, options) && isConditional(call.parent)) { return; } // keeping it simple: all 1-string-arg `require` calls are reported context.report({ diff --git a/src/rules/no-cycle.js b/src/rules/no-cycle.js index e12a81cea6..11c2f44fc0 100644 --- a/src/rules/no-cycle.js +++ b/src/rules/no-cycle.js @@ -11,6 +11,10 @@ import docsUrl from '../docsUrl'; const traversed = new Set(); +function routeString(route) { + return route.map((s) => `${s.value}:${s.loc.start.line}`).join('=>'); +} + module.exports = { meta: { type: 'suggestion', @@ -48,7 +52,7 @@ module.exports = { create(context) { const myPath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); - if (myPath === '') return {}; // can't cycle-check a non-file + if (myPath === '') { return {}; } // can't cycle-check a non-file const options = context.options[0] || {}; const maxDepth = typeof options.maxDepth === 'number' ? options.maxDepth : Infinity; @@ -62,20 +66,23 @@ module.exports = { if (ignoreModule(sourceNode.value)) { return; // ignore external modules } - if (options.allowUnsafeDynamicCyclicDependency && ( - // Ignore `import()` - importer.type === 'ImportExpression' || - // `require()` calls are always checked (if possible) - (importer.type === 'CallExpression' && importer.callee.name !== 'require'))) { + if ( + options.allowUnsafeDynamicCyclicDependency && ( + // Ignore `import()` + importer.type === 'ImportExpression' + // `require()` calls are always checked (if possible) + || importer.type === 'CallExpression' && importer.callee.name !== 'require' + ) + ) { return; // cycle via dynamic import allowed by config } if ( importer.type === 'ImportDeclaration' && ( // import type { Foo } (TS and Flow) - importer.importKind === 'type' || + importer.importKind === 'type' // import { type Foo } (Flow) - importer.specifiers.every(({ importKind }) => importKind === 'type') + || importer.specifiers.every(({ importKind }) => importKind === 'type') ) ) { return; // ignore type imports @@ -91,25 +98,24 @@ module.exports = { return; // no-self-import territory } - const untraversed = [{ mget: () => imported, route:[] }]; + const untraversed = [{ mget: () => imported, route: [] }]; function detectCycle({ mget, route }) { const m = mget(); - if (m == null) return; - if (traversed.has(m.path)) return; + if (m == null) { return; } + if (traversed.has(m.path)) { return; } traversed.add(m.path); for (const [path, { getter, declarations }] of m.imports) { - if (traversed.has(path)) continue; - const toTraverse = [...declarations].filter(({ source, isOnlyImportingTypes }) => - !ignoreModule(source.value) && + if (traversed.has(path)) { continue; } + const toTraverse = [...declarations].filter(({ source, isOnlyImportingTypes }) => !ignoreModule(source.value) // Ignore only type imports - !isOnlyImportingTypes, + && !isOnlyImportingTypes, ); /* If cyclic dependency is allowed via dynamic import, skip checking if any module is imported dynamically */ - if (options.allowUnsafeDynamicCyclicDependency && toTraverse.some(d => d.dynamic)) return; + if (options.allowUnsafeDynamicCyclicDependency && toTraverse.some((d) => d.dynamic)) { return; } /* Only report as a cycle if there are any import declarations that are considered by @@ -121,7 +127,7 @@ module.exports = { b.ts: import type { Bar } from './a' */ - if (path === myPath && toTraverse.length > 0) return true; + if (path === myPath && toTraverse.length > 0) { return true; } if (route.length + 1 < maxDepth) { for (const { source } of toTraverse) { untraversed.push({ mget: getter, route: route.concat(source) }); @@ -133,9 +139,9 @@ module.exports = { while (untraversed.length > 0) { const next = untraversed.shift(); // bfs! if (detectCycle(next)) { - const message = (next.route.length > 0 + const message = next.route.length > 0 ? `Dependency cycle via ${routeString(next.route)}` - : 'Dependency cycle detected.'); + : 'Dependency cycle detected.'; context.report(importer, message); return; } @@ -143,13 +149,9 @@ module.exports = { } return Object.assign(moduleVisitor(checkSourceValue, context.options[0]), { - 'Program:exit': () => { + 'Program:exit'() { traversed.clear(); }, }); }, }; - -function routeString(route) { - return route.map(s => `${s.value}:${s.loc.start.line}`).join('=>'); -} diff --git a/src/rules/no-default-export.js b/src/rules/no-default-export.js index 5fc8c40e4c..dabbae543a 100644 --- a/src/rules/no-default-export.js +++ b/src/rules/no-default-export.js @@ -27,14 +27,16 @@ module.exports = { }, ExportNamedDeclaration(node) { - node.specifiers.filter(specifier => (specifier.exported.name || specifier.exported.value) === 'default').forEach(specifier => { - const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {}; - if (specifier.type === 'ExportDefaultSpecifier') { - context.report({ node, message: preferNamed, loc }); - } else if (specifier.type === 'ExportSpecifier') { - context.report({ node, message: noAliasDefault(specifier), loc }); - } - }); + node.specifiers + .filter((specifier) => (specifier.exported.name || specifier.exported.value) === 'default') + .forEach((specifier) => { + const { loc } = context.getSourceCode().getFirstTokens(node)[1] || {}; + if (specifier.type === 'ExportDefaultSpecifier') { + context.report({ node, message: preferNamed, loc }); + } else if (specifier.type === 'ExportSpecifier') { + context.report({ node, message: noAliasDefault(specifier), loc }); + } + }); }, }; }, diff --git a/src/rules/no-deprecated.js b/src/rules/no-deprecated.js index 7a35a8e673..06eeff8ea7 100644 --- a/src/rules/no-deprecated.js +++ b/src/rules/no-deprecated.js @@ -3,13 +3,13 @@ import Exports from '../ExportMap'; import docsUrl from '../docsUrl'; function message(deprecation) { - return 'Deprecated' + (deprecation.description ? ': ' + deprecation.description : '.'); + return `Deprecated${deprecation.description ? `: ${deprecation.description}` : '.'}`; } function getDeprecation(metadata) { - if (!metadata || !metadata.doc) return; + if (!metadata || !metadata.doc) { return; } - return metadata.doc.tags.find(t => t.title === 'deprecated'); + return metadata.doc.tags.find((t) => t.title === 'deprecated'); } module.exports = { @@ -28,13 +28,13 @@ module.exports = { const namespaces = new Map(); function checkSpecifiers(node) { - if (node.type !== 'ImportDeclaration') return; - if (node.source == null) return; // local export, ignore + if (node.type !== 'ImportDeclaration') { return; } + if (node.source == null) { return; } // local export, ignore const imports = Exports.get(node.source.value, context); - if (imports == null) return; + if (imports == null) { return; } - const moduleDeprecation = imports.doc && imports.doc.tags.find(t => t.title === 'deprecated'); + const moduleDeprecation = imports.doc && imports.doc.tags.find((t) => t.title === 'deprecated'); if (moduleDeprecation) { context.report({ node, message: message(moduleDeprecation) }); } @@ -48,35 +48,34 @@ module.exports = { let imported; let local; switch (im.type) { + case 'ImportNamespaceSpecifier': { + if (!imports.size) { return; } + namespaces.set(im.local.name, imports); + return; + } - case 'ImportNamespaceSpecifier':{ - if (!imports.size) return; - namespaces.set(im.local.name, imports); - return; - } - - case 'ImportDefaultSpecifier': - imported = 'default'; - local = im.local.name; - break; + case 'ImportDefaultSpecifier': + imported = 'default'; + local = im.local.name; + break; - case 'ImportSpecifier': - imported = im.imported.name; - local = im.local.name; - break; + case 'ImportSpecifier': + imported = im.imported.name; + local = im.local.name; + break; - default: return; // can't handle this one + default: return; // can't handle this one } // unknown thing can't be deprecated const exported = imports.get(imported); - if (exported == null) return; + if (exported == null) { return; } // capture import of deep namespace - if (exported.namespace) namespaces.set(local, exported.namespace); + if (exported.namespace) { namespaces.set(local, exported.namespace); } const deprecation = getDeprecation(imports.get(imported)); - if (!deprecation) return; + if (!deprecation) { return; } context.report({ node: im, message: message(deprecation) }); @@ -86,44 +85,42 @@ module.exports = { } return { - 'Program': ({ body }) => body.forEach(checkSpecifiers), + Program: ({ body }) => body.forEach(checkSpecifiers), - 'Identifier': function (node) { + Identifier(node) { if (node.parent.type === 'MemberExpression' && node.parent.property === node) { return; // handled by MemberExpression } // ignore specifier identifiers - if (node.parent.type.slice(0, 6) === 'Import') return; + if (node.parent.type.slice(0, 6) === 'Import') { return; } - if (!deprecated.has(node.name)) return; + if (!deprecated.has(node.name)) { return; } - if (declaredScope(context, node.name) !== 'module') return; + if (declaredScope(context, node.name) !== 'module') { return; } context.report({ node, message: message(deprecated.get(node.name)), }); }, - 'MemberExpression': function (dereference) { - if (dereference.object.type !== 'Identifier') return; - if (!namespaces.has(dereference.object.name)) return; + MemberExpression(dereference) { + if (dereference.object.type !== 'Identifier') { return; } + if (!namespaces.has(dereference.object.name)) { return; } - if (declaredScope(context, dereference.object.name) !== 'module') return; + if (declaredScope(context, dereference.object.name) !== 'module') { return; } // go deep let namespace = namespaces.get(dereference.object.name); const namepath = [dereference.object.name]; // while property is namespace and parent is member expression, keep validating - while (namespace instanceof Exports && - dereference.type === 'MemberExpression') { - + while (namespace instanceof Exports && dereference.type === 'MemberExpression') { // ignore computed parts for now - if (dereference.computed) return; + if (dereference.computed) { return; } const metadata = namespace.get(dereference.property.name); - if (!metadata) break; + if (!metadata) { break; } const deprecation = getDeprecation(metadata); if (deprecation) { diff --git a/src/rules/no-duplicates.js b/src/rules/no-duplicates.js index b896f442ae..033e854e03 100644 --- a/src/rules/no-duplicates.js +++ b/src/rules/no-duplicates.js @@ -1,30 +1,76 @@ import resolve from 'eslint-module-utils/resolve'; -import docsUrl from '../docsUrl'; import semver from 'semver'; -import typescriptPkg from 'typescript/package.json'; +import flatMap from 'array.prototype.flatmap'; -function checkImports(imported, context) { - for (const [module, nodes] of imported.entries()) { - if (nodes.length > 1) { - const message = `'${module}' imported multiple times.`; - const [first, ...rest] = nodes; - const sourceCode = context.getSourceCode(); - const fix = getFix(first, rest, sourceCode, context); +import docsUrl from '../docsUrl'; - context.report({ - node: first.source, - message, - fix, // Attach the autofix (if any) to the first import. - }); +let typescriptPkg; +try { + typescriptPkg = require('typescript/package.json'); // eslint-disable-line import/no-extraneous-dependencies +} catch (e) { /**/ } - for (const node of rest) { - context.report({ - node: node.source, - message, - }); - } - } - } +function isPunctuator(node, value) { + return node.type === 'Punctuator' && node.value === value; +} + +// Get the name of the default import of `node`, if any. +function getDefaultImportName(node) { + const defaultSpecifier = node.specifiers + .find((specifier) => specifier.type === 'ImportDefaultSpecifier'); + return defaultSpecifier != null ? defaultSpecifier.local.name : undefined; +} + +// Checks whether `node` has a namespace import. +function hasNamespace(node) { + const specifiers = node.specifiers + .filter((specifier) => specifier.type === 'ImportNamespaceSpecifier'); + return specifiers.length > 0; +} + +// Checks whether `node` has any non-default specifiers. +function hasSpecifiers(node) { + const specifiers = node.specifiers + .filter((specifier) => specifier.type === 'ImportSpecifier'); + return specifiers.length > 0; +} + +// Checks whether `node` has a comment (that ends) on the previous line or on +// the same line as `node` (starts). +function hasCommentBefore(node, sourceCode) { + return sourceCode.getCommentsBefore(node) + .some((comment) => comment.loc.end.line >= node.loc.start.line - 1); +} + +// Checks whether `node` has a comment (that starts) on the same line as `node` +// (ends). +function hasCommentAfter(node, sourceCode) { + return sourceCode.getCommentsAfter(node) + .some((comment) => comment.loc.start.line === node.loc.end.line); +} + +// Checks whether `node` has any comments _inside,_ except inside the `{...}` +// part (if any). +function hasCommentInsideNonSpecifiers(node, sourceCode) { + const tokens = sourceCode.getTokens(node); + const openBraceIndex = tokens.findIndex((token) => isPunctuator(token, '{')); + const closeBraceIndex = tokens.findIndex((token) => isPunctuator(token, '}')); + // Slice away the first token, since we're no looking for comments _before_ + // `node` (only inside). If there's a `{...}` part, look for comments before + // the `{`, but not before the `}` (hence the `+1`s). + const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0 + ? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1)) + : tokens.slice(1); + return someTokens.some((token) => sourceCode.getCommentsBefore(token).length > 0); +} + +// It's not obvious what the user wants to do with comments associated with +// duplicate imports, so skip imports with comments when autofixing. +function hasProblematicComments(node, sourceCode) { + return ( + hasCommentBefore(node, sourceCode) + || hasCommentAfter(node, sourceCode) + || hasCommentInsideNonSpecifiers(node, sourceCode) + ); } function getFix(first, rest, sourceCode, context) { @@ -47,7 +93,7 @@ function getFix(first, rest, sourceCode, context) { } const defaultImportNames = new Set( - [first, ...rest].map(getDefaultImportName).filter(Boolean), + flatMap([].concat(first, rest || []), (x) => getDefaultImportName(x) || []), ); // Bail if there are multiple different default import names – it's up to the @@ -58,16 +104,13 @@ function getFix(first, rest, sourceCode, context) { // Leave it to the user to handle comments. Also skip `import * as ns from // './foo'` imports, since they cannot be merged into another import. - const restWithoutComments = rest.filter(node => !( - hasProblematicComments(node, sourceCode) || - hasNamespace(node) - )); + const restWithoutComments = rest.filter((node) => !hasProblematicComments(node, sourceCode) && !hasNamespace(node)); const specifiers = restWithoutComments - .map(node => { + .map((node) => { const tokens = sourceCode.getTokens(node); - const openBrace = tokens.find(token => isPunctuator(token, '{')); - const closeBrace = tokens.find(token => isPunctuator(token, '}')); + const openBrace = tokens.find((token) => isPunctuator(token, '{')); + const closeBrace = tokens.find((token) => isPunctuator(token, '}')); if (openBrace == null || closeBrace == null) { return undefined; @@ -75,17 +118,15 @@ function getFix(first, rest, sourceCode, context) { return { importNode: node, - text: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]), - hasTrailingComma: isPunctuator(sourceCode.getTokenBefore(closeBrace), ','), + identifiers: sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]).split(','), // Split the text into separate identifiers (retaining any whitespace before or after) isEmpty: !hasSpecifiers(node), }; }) .filter(Boolean); - const unnecessaryImports = restWithoutComments.filter(node => - !hasSpecifiers(node) && - !hasNamespace(node) && - !specifiers.some(specifier => specifier.importNode === node), + const unnecessaryImports = restWithoutComments.filter((node) => !hasSpecifiers(node) + && !hasNamespace(node) + && !specifiers.some((specifier) => specifier.importNode === node), ); const shouldAddDefault = getDefaultImportName(first) == null && defaultImportNames.size === 1; @@ -96,37 +137,51 @@ function getFix(first, rest, sourceCode, context) { return undefined; } - return fixer => { + return (fixer) => { const tokens = sourceCode.getTokens(first); - const openBrace = tokens.find(token => isPunctuator(token, '{')); - const closeBrace = tokens.find(token => isPunctuator(token, '}')); + const openBrace = tokens.find((token) => isPunctuator(token, '{')); + const closeBrace = tokens.find((token) => isPunctuator(token, '}')); const firstToken = sourceCode.getFirstToken(first); const [defaultImportName] = defaultImportNames; - const firstHasTrailingComma = - closeBrace != null && - isPunctuator(sourceCode.getTokenBefore(closeBrace), ','); + const firstHasTrailingComma = closeBrace != null && isPunctuator(sourceCode.getTokenBefore(closeBrace), ','); const firstIsEmpty = !hasSpecifiers(first); + const firstExistingIdentifiers = firstIsEmpty + ? new Set() + : new Set(sourceCode.text.slice(openBrace.range[1], closeBrace.range[0]) + .split(',') + .map((x) => x.trim()), + ); const [specifiersText] = specifiers.reduce( - ([result, needsComma], specifier) => { + ([result, needsComma, existingIdentifiers], specifier) => { const isTypeSpecifier = specifier.importNode.importKind === 'type'; const preferInline = context.options[0] && context.options[0]['prefer-inline']; // a user might set prefer-inline but not have a supporting TypeScript version. Flow does not support inline types so this should fail in that case as well. - if (preferInline && !semver.satisfies(typescriptPkg.version, '>= 4.5')) { + if (preferInline && (!typescriptPkg || !semver.satisfies(typescriptPkg.version, '>= 4.5'))) { throw new Error('Your version of TypeScript does not support inline type imports.'); } - const insertText = `${preferInline && isTypeSpecifier ? 'type ' : ''}${specifier.text}`; + // Add *only* the new identifiers that don't already exist, and track any new identifiers so we don't add them again in the next loop + const [specifierText, updatedExistingIdentifiers] = specifier.identifiers.reduce(([text, set], cur) => { + const trimmed = cur.trim(); // Trim whitespace before/after to compare to our set of existing identifiers + const curWithType = trimmed.length > 0 && preferInline && isTypeSpecifier ? `type ${cur}` : cur; + if (existingIdentifiers.has(trimmed)) { + return [text, set]; + } + return [text.length > 0 ? `${text},${curWithType}` : curWithType, set.add(trimmed)]; + }, ['', existingIdentifiers]); + return [ - needsComma && !specifier.isEmpty - ? `${result},${insertText}` - : `${result}${insertText}`, + needsComma && !specifier.isEmpty && specifierText.length > 0 + ? `${result},${specifierText}` + : `${result}${specifierText}`, specifier.isEmpty ? needsComma : true, + updatedExistingIdentifiers, ]; }, - ['', !firstHasTrailingComma && !firstIsEmpty], + ['', !firstHasTrailingComma && !firstIsEmpty, firstExistingIdentifiers], ); const fixes = []; @@ -188,68 +243,28 @@ function getFix(first, rest, sourceCode, context) { }; } -function isPunctuator(node, value) { - return node.type === 'Punctuator' && node.value === value; -} - -// Get the name of the default import of `node`, if any. -function getDefaultImportName(node) { - const defaultSpecifier = node.specifiers - .find(specifier => specifier.type === 'ImportDefaultSpecifier'); - return defaultSpecifier != null ? defaultSpecifier.local.name : undefined; -} - -// Checks whether `node` has a namespace import. -function hasNamespace(node) { - const specifiers = node.specifiers - .filter(specifier => specifier.type === 'ImportNamespaceSpecifier'); - return specifiers.length > 0; -} - -// Checks whether `node` has any non-default specifiers. -function hasSpecifiers(node) { - const specifiers = node.specifiers - .filter(specifier => specifier.type === 'ImportSpecifier'); - return specifiers.length > 0; -} - -// It's not obvious what the user wants to do with comments associated with -// duplicate imports, so skip imports with comments when autofixing. -function hasProblematicComments(node, sourceCode) { - return ( - hasCommentBefore(node, sourceCode) || - hasCommentAfter(node, sourceCode) || - hasCommentInsideNonSpecifiers(node, sourceCode) - ); -} - -// Checks whether `node` has a comment (that ends) on the previous line or on -// the same line as `node` (starts). -function hasCommentBefore(node, sourceCode) { - return sourceCode.getCommentsBefore(node) - .some(comment => comment.loc.end.line >= node.loc.start.line - 1); -} +function checkImports(imported, context) { + for (const [module, nodes] of imported.entries()) { + if (nodes.length > 1) { + const message = `'${module}' imported multiple times.`; + const [first, ...rest] = nodes; + const sourceCode = context.getSourceCode(); + const fix = getFix(first, rest, sourceCode, context); -// Checks whether `node` has a comment (that starts) on the same line as `node` -// (ends). -function hasCommentAfter(node, sourceCode) { - return sourceCode.getCommentsAfter(node) - .some(comment => comment.loc.start.line === node.loc.end.line); -} + context.report({ + node: first.source, + message, + fix, // Attach the autofix (if any) to the first import. + }); -// Checks whether `node` has any comments _inside,_ except inside the `{...}` -// part (if any). -function hasCommentInsideNonSpecifiers(node, sourceCode) { - const tokens = sourceCode.getTokens(node); - const openBraceIndex = tokens.findIndex(token => isPunctuator(token, '{')); - const closeBraceIndex = tokens.findIndex(token => isPunctuator(token, '}')); - // Slice away the first token, since we're no looking for comments _before_ - // `node` (only inside). If there's a `{...}` part, look for comments before - // the `{`, but not before the `}` (hence the `+1`s). - const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0 - ? tokens.slice(1, openBraceIndex + 1).concat(tokens.slice(closeBraceIndex + 1)) - : tokens.slice(1); - return someTokens.some(token => sourceCode.getCommentsBefore(token).length > 0); + for (const node of rest) { + context.report({ + node: node.source, + message, + }); + } + } + } } module.exports = { @@ -279,16 +294,16 @@ module.exports = { create(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 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; + return `${defaultResolver(parts[1])}?${parts[2]}`; + } : defaultResolver; const moduleMaps = new Map(); @@ -302,10 +317,11 @@ module.exports = { }); } const map = moduleMaps.get(n.parent); - if (n.importKind === 'type') { + const preferInline = context.options[0] && context.options[0]['prefer-inline']; + if (!preferInline && n.importKind === 'type') { return n.specifiers.length > 0 && n.specifiers[0].type === 'ImportDefaultSpecifier' ? map.defaultTypesImported : map.namedTypesImported; } - if (n.specifiers.some((spec) => spec.importKind === 'type')) { + if (!preferInline && n.specifiers.some((spec) => spec.importKind === 'type')) { return map.namedTypesImported; } @@ -325,7 +341,7 @@ module.exports = { } }, - 'Program:exit': function () { + 'Program:exit'() { for (const map of moduleMaps.values()) { checkImports(map.imported, context); checkImports(map.nsImported, context); diff --git a/src/rules/no-dynamic-require.js b/src/rules/no-dynamic-require.js index f334adec67..f8b369a70f 100644 --- a/src/rules/no-dynamic-require.js +++ b/src/rules/no-dynamic-require.js @@ -1,22 +1,22 @@ import docsUrl from '../docsUrl'; function isRequire(node) { - return node && - node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'require' && - node.arguments.length >= 1; + return node + && node.callee + && node.callee.type === 'Identifier' + && node.callee.name === 'require' + && node.arguments.length >= 1; } function isDynamicImport(node) { - return node && - node.callee && - node.callee.type === 'Import'; + return node + && node.callee + && node.callee.type === 'Import'; } function isStaticValue(arg) { - return arg.type === 'Literal' || - (arg.type === 'TemplateLiteral' && arg.expressions.length === 0); + return arg.type === 'Literal' + || arg.type === 'TemplateLiteral' && arg.expressions.length === 0; } const dynamicImportErrorMessage = 'Calls to import() should use string literals'; diff --git a/src/rules/no-empty-named-blocks.js b/src/rules/no-empty-named-blocks.js index 114736f168..3ec1501b8f 100644 --- a/src/rules/no-empty-named-blocks.js +++ b/src/rules/no-empty-named-blocks.js @@ -8,7 +8,7 @@ function getEmptyBlockRange(tokens, index) { const end = nextToken.range[1]; // Remove block tokens and the previous comma - if (prevToken.value === ','|| prevToken.value === 'type' || prevToken.value === 'typeof') { + if (prevToken.value === ',' || prevToken.value === 'type' || prevToken.value === 'typeof') { start = prevToken.range[0]; } @@ -29,63 +29,75 @@ module.exports = { }, create(context) { + const importsWithoutNameds = []; + return { - Program(node) { - node.tokens.forEach((token, idx) => { - const nextToken = node.tokens[idx + 1]; + ImportDeclaration(node) { + if (!node.specifiers.some((x) => x.type === 'ImportSpecifier')) { + importsWithoutNameds.push(node); + } + }, + + 'Program:exit'(program) { + const importsTokens = importsWithoutNameds.map((node) => [node, program.tokens.filter((x) => x.range[0] >= node.range[0] && x.range[1] <= node.range[1])]); - if (nextToken && token.value === '{' && nextToken.value === '}') { - const hasOtherIdentifiers = node.tokens.some((token) => ( - token.type === 'Identifier' - && token.value !== 'from' - && token.value !== 'type' - && token.value !== 'typeof' - )); + importsTokens.forEach(([node, tokens]) => { + tokens.forEach((token) => { + const idx = program.tokens.indexOf(token); + const nextToken = program.tokens[idx + 1]; - // If it has no other identifiers it's the only thing in the import, so we can either remove the import - // completely or transform it in a side-effects only import - if (!hasOtherIdentifiers) { - context.report({ - node, - message: 'Unexpected empty named import block', - suggest: [ - { - desc: 'Remove unused import', - fix(fixer) { - // Remove the whole import - return fixer.remove(node); + if (nextToken && token.value === '{' && nextToken.value === '}') { + const hasOtherIdentifiers = tokens.some((token) => token.type === 'Identifier' + && token.value !== 'from' + && token.value !== 'type' + && token.value !== 'typeof', + ); + + // If it has no other identifiers it's the only thing in the import, so we can either remove the import + // completely or transform it in a side-effects only import + if (!hasOtherIdentifiers) { + context.report({ + node, + message: 'Unexpected empty named import block', + suggest: [ + { + desc: 'Remove unused import', + fix(fixer) { + // Remove the whole import + return fixer.remove(node); + }, }, - }, - { - desc: 'Remove empty import block', - fix(fixer) { - // Remove the empty block and the 'from' token, leaving the import only for its side - // effects, e.g. `import 'mod'` - const sourceCode = context.getSourceCode(); - const fromToken = node.tokens.find(t => t.value === 'from'); - const importToken = node.tokens.find(t => t.value === 'import'); - const hasSpaceAfterFrom = sourceCode.isSpaceBetween(fromToken, sourceCode.getTokenAfter(fromToken)); - const hasSpaceAfterImport = sourceCode.isSpaceBetween(importToken, sourceCode.getTokenAfter(fromToken)); + { + desc: 'Remove empty import block', + fix(fixer) { + // Remove the empty block and the 'from' token, leaving the import only for its side + // effects, e.g. `import 'mod'` + const sourceCode = context.getSourceCode(); + const fromToken = program.tokens.find((t) => t.value === 'from'); + const importToken = program.tokens.find((t) => t.value === 'import'); + const hasSpaceAfterFrom = sourceCode.isSpaceBetween(fromToken, sourceCode.getTokenAfter(fromToken)); + const hasSpaceAfterImport = sourceCode.isSpaceBetween(importToken, sourceCode.getTokenAfter(fromToken)); - const [start] = getEmptyBlockRange(node.tokens, idx); - const [, end] = fromToken.range; - const range = [start, hasSpaceAfterFrom ? end + 1 : end]; + const [start] = getEmptyBlockRange(program.tokens, idx); + const [, end] = fromToken.range; + const range = [start, hasSpaceAfterFrom ? end + 1 : end]; - return fixer.replaceTextRange(range, hasSpaceAfterImport ? '' : ' '); + return fixer.replaceTextRange(range, hasSpaceAfterImport ? '' : ' '); + }, }, + ], + }); + } else { + context.report({ + node, + message: 'Unexpected empty named import block', + fix(fixer) { + return fixer.removeRange(getEmptyBlockRange(program.tokens, idx)); }, - ], - }); - } else { - context.report({ - node, - message: 'Unexpected empty named import block', - fix(fixer) { - return fixer.removeRange(getEmptyBlockRange(node.tokens, idx)); - }, - }); + }); + } } - } + }); }); }, }; diff --git a/src/rules/no-extraneous-dependencies.js b/src/rules/no-extraneous-dependencies.js index d6437c2fd1..df97987901 100644 --- a/src/rules/no-extraneous-dependencies.js +++ b/src/rules/no-extraneous-dependencies.js @@ -64,18 +64,18 @@ function getDependencies(context, packageDir) { if (!Array.isArray(packageDir)) { paths = [path.resolve(packageDir)]; } else { - paths = packageDir.map(dir => path.resolve(dir)); + paths = packageDir.map((dir) => path.resolve(dir)); } } if (paths.length > 0) { // use rule config to find package.json - paths.forEach(dir => { + paths.forEach((dir) => { const packageJsonPath = path.join(dir, 'package.json'); const _packageContent = getPackageDepFields(packageJsonPath, true); - Object.keys(packageContent).forEach(depsKey => - Object.assign(packageContent[depsKey], _packageContent[depsKey]), - ); + Object.keys(packageContent).forEach((depsKey) => { + Object.assign(packageContent[depsKey], _packageContent[depsKey]); + }); }); } else { const packageJsonPath = pkgUp({ @@ -110,7 +110,7 @@ function getDependencies(context, packageDir) { } if (e.name === 'JSONError' || e instanceof SyntaxError) { context.report({ - message: 'The package.json file could not be parsed: ' + e.message, + message: `The package.json file could not be parsed: ${e.message}`, loc: { line: 0, column: 0 }, }); } @@ -120,8 +120,7 @@ function getDependencies(context, packageDir) { } function missingErrorMessage(packageName) { - return `'${packageName}' should be listed in the project's dependencies. ` + - `Run 'npm i -S ${packageName}' to add it`; + return `'${packageName}' should be listed in the project's dependencies. Run 'npm i -S ${packageName}' to add it`; } function devDepErrorMessage(packageName) { @@ -129,8 +128,7 @@ function devDepErrorMessage(packageName) { } function optDepErrorMessage(packageName) { - return `'${packageName}' should be listed in the project's dependencies, ` + - `not optionalDependencies.`; + return `'${packageName}' should be listed in the project's dependencies, not optionalDependencies.`; } function getModuleOriginalName(name) { @@ -162,23 +160,26 @@ function checkDependencyDeclaration(deps, packageName, declarationStatus) { } }); - return packageHierarchy.reduce((result, ancestorName) => { - return { - isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined, - isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined, - isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined, - isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined, - isInBundledDeps: + return packageHierarchy.reduce((result, ancestorName) => ({ + isInDeps: result.isInDeps || deps.dependencies[ancestorName] !== undefined, + isInDevDeps: result.isInDevDeps || deps.devDependencies[ancestorName] !== undefined, + isInOptDeps: result.isInOptDeps || deps.optionalDependencies[ancestorName] !== undefined, + isInPeerDeps: result.isInPeerDeps || deps.peerDependencies[ancestorName] !== undefined, + isInBundledDeps: result.isInBundledDeps || deps.bundledDependencies.indexOf(ancestorName) !== -1, - }; - }, newDeclarationStatus); + }), newDeclarationStatus); } function reportIfMissing(context, deps, depsOptions, node, name) { // Do not report when importing types unless option is enabled if ( - !depsOptions.verifyTypeImports && - (node.importKind === 'type' || node.importKind === 'typeof') + !depsOptions.verifyTypeImports + && ( + node.importKind === 'type' + || node.importKind === 'typeof' + || node.exportKind === 'type' + || Array.isArray(node.specifiers) && node.specifiers.length && node.specifiers.every((specifier) => specifier.importKind === 'type' || specifier.importKind === 'typeof') + ) ) { return; } @@ -199,11 +200,11 @@ function reportIfMissing(context, deps, depsOptions, node, name) { let declarationStatus = checkDependencyDeclaration(deps, importPackageName); if ( - declarationStatus.isInDeps || - (depsOptions.allowDevDeps && declarationStatus.isInDevDeps) || - (depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) || - (depsOptions.allowOptDeps && declarationStatus.isInOptDeps) || - (depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps) + declarationStatus.isInDeps + || depsOptions.allowDevDeps && declarationStatus.isInDevDeps + || depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps + || depsOptions.allowOptDeps && declarationStatus.isInOptDeps + || depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps ) { return; } @@ -215,11 +216,11 @@ function reportIfMissing(context, deps, depsOptions, node, name) { declarationStatus = checkDependencyDeclaration(deps, realPackageName, declarationStatus); if ( - declarationStatus.isInDeps || - (depsOptions.allowDevDeps && declarationStatus.isInDevDeps) || - (depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps) || - (depsOptions.allowOptDeps && declarationStatus.isInOptDeps) || - (depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps) + declarationStatus.isInDeps + || depsOptions.allowDevDeps && declarationStatus.isInDevDeps + || depsOptions.allowPeerDeps && declarationStatus.isInPeerDeps + || depsOptions.allowOptDeps && declarationStatus.isInOptDeps + || depsOptions.allowBundledDeps && declarationStatus.isInBundledDeps ) { return; } @@ -244,10 +245,9 @@ function testConfig(config, filename) { return config; } // Array of globs. - return config.some(c => ( - minimatch(filename, c) || - minimatch(filename, path.join(process.cwd(), c)) - )); + return config.some((c) => minimatch(filename, c) + || minimatch(filename, path.join(process.cwd(), c)), + ); } module.exports = { @@ -261,17 +261,17 @@ module.exports = { schema: [ { - 'type': 'object', - 'properties': { - 'devDependencies': { 'type': ['boolean', 'array'] }, - 'optionalDependencies': { 'type': ['boolean', 'array'] }, - 'peerDependencies': { 'type': ['boolean', 'array'] }, - 'bundledDependencies': { 'type': ['boolean', 'array'] }, - 'packageDir': { 'type': ['string', 'array'] }, - 'includeInternal': { 'type': ['boolean'] }, - 'includeTypes': { 'type': ['boolean'] }, + type: 'object', + properties: { + devDependencies: { type: ['boolean', 'array'] }, + optionalDependencies: { type: ['boolean', 'array'] }, + peerDependencies: { type: ['boolean', 'array'] }, + bundledDependencies: { type: ['boolean', 'array'] }, + packageDir: { type: ['string', 'array'] }, + includeInternal: { type: ['boolean'] }, + includeTypes: { type: ['boolean'] }, }, - 'additionalProperties': false, + additionalProperties: false, }, ], }, @@ -295,7 +295,7 @@ module.exports = { }, { commonjs: true }); }, - 'Program:exit': () => { + 'Program:exit'() { depFieldCache.clear(); }, }; diff --git a/src/rules/no-import-module-exports.js b/src/rules/no-import-module-exports.js index d40bae88ce..bc4605c39d 100644 --- a/src/rules/no-import-module-exports.js +++ b/src/rules/no-import-module-exports.js @@ -16,7 +16,12 @@ function getEntryPoint(context) { function findScope(context, identifier) { const { scopeManager } = context.getSourceCode(); - return scopeManager && scopeManager.scopes.slice().reverse().find((scope) => scope.variables.some(variable => variable.identifiers.some((node) => node.name === identifier))); + return scopeManager && scopeManager.scopes.slice().reverse().find((scope) => scope.variables.some((variable) => variable.identifiers.some((node) => node.name === identifier))); +} + +function findDefinition(objectScope, identifier) { + const variable = objectScope.variables.find((variable) => variable.name === identifier); + return variable.defs.find((def) => def.name.name === identifier); } module.exports = { @@ -30,11 +35,11 @@ module.exports = { fixable: 'code', schema: [ { - 'type': 'object', - 'properties': { - 'exceptions': { 'type': 'array' }, + type: 'object', + properties: { + exceptions: { type: 'array' }, }, - 'additionalProperties': false, + additionalProperties: false, }, ], }, @@ -50,15 +55,16 @@ module.exports = { const isIdentifier = node.object.type === 'Identifier'; const hasKeywords = (/^(module|exports)$/).test(node.object.name); const objectScope = hasKeywords && findScope(context, node.object.name); + const variableDefinition = objectScope && findDefinition(objectScope, node.object.name); + const isImportBinding = variableDefinition && variableDefinition.type === 'ImportBinding'; const hasCJSExportReference = hasKeywords && (!objectScope || objectScope.type === 'module'); - const isException = !!options.exceptions && options.exceptions.some(glob => minimatch(fileName, glob)); + const isException = !!options.exceptions && options.exceptions.some((glob) => minimatch(fileName, glob)); - if (isIdentifier && hasCJSExportReference && !isEntryPoint && !isException) { - importDeclarations.forEach(importDeclaration => { + if (isIdentifier && hasCJSExportReference && !isEntryPoint && !isException && !isImportBinding) { + importDeclarations.forEach((importDeclaration) => { context.report({ node: importDeclaration, - message: `Cannot use import declarations in modules that export using ` + - `CommonJS (module.exports = 'foo' or exports.bar = 'hi')`, + message: `Cannot use import declarations in modules that export using CommonJS (module.exports = 'foo' or exports.bar = 'hi')`, }); }); alreadyReported = true; diff --git a/src/rules/no-internal-modules.js b/src/rules/no-internal-modules.js index 2416c1ce3f..5ed4565471 100644 --- a/src/rules/no-internal-modules.js +++ b/src/rules/no-internal-modules.js @@ -48,8 +48,8 @@ module.exports = { create: function noReachingInside(context) { const options = context.options[0] || {}; - const allowRegexps = (options.allow || []).map(p => minimatch.makeRe(p)); - const forbidRegexps = (options.forbid || []).map(p => minimatch.makeRe(p)); + const allowRegexps = (options.allow || []).map((p) => minimatch.makeRe(p)); + const forbidRegexps = (options.forbid || []).map((p) => minimatch.makeRe(p)); // minimatch patterns are expected to use / path separators, like import // statements, so normalize paths to use the same @@ -58,44 +58,42 @@ module.exports = { } function toSteps(somePath) { - return normalizeSep(somePath) + return normalizeSep(somePath) .split('/') + .filter((step) => step && step !== '.') .reduce((acc, step) => { - if (!step || step === '.') { - return acc; - } else if (step === '..') { + if (step === '..') { return acc.slice(0, -1); - } else { - return acc.concat(step); } + return acc.concat(step); }, []); } // test if reaching to this destination is allowed function reachingAllowed(importPath) { - return allowRegexps.some(re => re.test(importPath)); + return allowRegexps.some((re) => re.test(importPath)); } // test if reaching to this destination is forbidden function reachingForbidden(importPath) { - return forbidRegexps.some(re => re.test(importPath)); + return forbidRegexps.some((re) => re.test(importPath)); } function isAllowViolation(importPath) { const steps = toSteps(importPath); - const nonScopeSteps = steps.filter(step => step.indexOf('@') !== 0); - if (nonScopeSteps.length <= 1) return false; + const nonScopeSteps = steps.filter((step) => step.indexOf('@') !== 0); + if (nonScopeSteps.length <= 1) { return false; } // before trying to resolve, see if the raw import (with relative // segments resolved) matches an allowed pattern const justSteps = steps.join('/'); - if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) return false; + if (reachingAllowed(justSteps) || reachingAllowed(`/${justSteps}`)) { return false; } // if the import statement doesn't match directly, try to match the // resolved path if the import is resolvable const resolved = resolve(importPath, context); - if (!resolved || reachingAllowed(normalizeSep(resolved))) return false; + if (!resolved || reachingAllowed(normalizeSep(resolved))) { return false; } // this import was not allowed by the allowed paths, and reaches // so it is a violation @@ -109,12 +107,12 @@ module.exports = { // segments resolved) matches a forbidden pattern const justSteps = steps.join('/'); - if (reachingForbidden(justSteps) || reachingForbidden(`/${justSteps}`)) return true; + if (reachingForbidden(justSteps) || reachingForbidden(`/${justSteps}`)) { return true; } // if the import statement doesn't match directly, try to match the // resolved path if the import is resolvable const resolved = resolve(importPath, context); - if (resolved && reachingForbidden(normalizeSep(resolved))) return true; + if (resolved && reachingForbidden(normalizeSep(resolved))) { return true; } // this import was not forbidden by the forbidden paths so it is not a violation return false; @@ -125,8 +123,9 @@ module.exports = { function checkImportForReaching(importPath, node) { const potentialViolationTypes = ['parent', 'index', 'sibling', 'external', 'internal']; - if (potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 && - isReachViolation(importPath) + if ( + potentialViolationTypes.indexOf(importType(importPath, context)) !== -1 + && isReachViolation(importPath) ) { context.report({ node, @@ -135,8 +134,11 @@ module.exports = { } } - return moduleVisitor((source) => { - checkImportForReaching(source.value, source); - }, { commonjs: true }); + return moduleVisitor( + (source) => { + checkImportForReaching(source.value, source); + }, + { commonjs: true }, + ); }, }; diff --git a/src/rules/no-mutable-exports.js b/src/rules/no-mutable-exports.js index 75a321b62a..433d64e167 100644 --- a/src/rules/no-mutable-exports.js +++ b/src/rules/no-mutable-exports.js @@ -52,8 +52,8 @@ module.exports = { } return { - 'ExportDefaultDeclaration': handleExportDefault, - 'ExportNamedDeclaration': handleExportNamed, + ExportDefaultDeclaration: handleExportDefault, + ExportNamedDeclaration: handleExportNamed, }; }, }; diff --git a/src/rules/no-named-as-default-member.js b/src/rules/no-named-as-default-member.js index 0fb0927249..e00a4cbc87 100644 --- a/src/rules/no-named-as-default-member.js +++ b/src/rules/no-named-as-default-member.js @@ -24,81 +24,68 @@ module.exports = { }, create(context) { - const fileImports = new Map(); const allPropertyLookups = new Map(); - function handleImportDefault(node) { - const declaration = importDeclaration(context); - const exportMap = Exports.get(declaration.source.value, context); - if (exportMap == null) return; - - if (exportMap.errors.length) { - exportMap.reportErrors(context, declaration); - return; - } - - fileImports.set(node.local.name, { - exportMap, - sourcePath: declaration.source.value, - }); - } - function storePropertyLookup(objectName, propName, node) { const lookups = allPropertyLookups.get(objectName) || []; lookups.push({ node, propName }); allPropertyLookups.set(objectName, lookups); } - function handlePropLookup(node) { - const objectName = node.object.name; - const propName = node.property.name; - storePropertyLookup(objectName, propName, node); - } + return { + ImportDefaultSpecifier(node) { + const declaration = importDeclaration(context); + const exportMap = Exports.get(declaration.source.value, context); + if (exportMap == null) { return; } - function handleDestructuringAssignment(node) { - const isDestructure = ( - node.id.type === 'ObjectPattern' && - node.init != null && - node.init.type === 'Identifier' - ); - if (!isDestructure) return; + if (exportMap.errors.length) { + exportMap.reportErrors(context, declaration); + return; + } - const objectName = node.init.name; - for (const { key } of node.id.properties) { - if (key == null) continue; // true for rest properties - storePropertyLookup(objectName, key.name, key); - } - } + fileImports.set(node.local.name, { + exportMap, + sourcePath: declaration.source.value, + }); + }, - function handleProgramExit() { - allPropertyLookups.forEach((lookups, objectName) => { - const fileImport = fileImports.get(objectName); - if (fileImport == null) return; + MemberExpression(node) { + const objectName = node.object.name; + const propName = node.property.name; + storePropertyLookup(objectName, propName, node); + }, - for (const { propName, node } of lookups) { - // the default import can have a "default" property - if (propName === 'default') continue; - if (!fileImport.exportMap.namespace.has(propName)) continue; + VariableDeclarator(node) { + const isDestructure = node.id.type === 'ObjectPattern' + && node.init != null + && node.init.type === 'Identifier'; + if (!isDestructure) { return; } - context.report({ - node, - message: ( - `Caution: \`${objectName}\` also has a named export ` + - `\`${propName}\`. Check if you meant to write ` + - `\`import {${propName}} from '${fileImport.sourcePath}'\` ` + - 'instead.' - ), - }); + const objectName = node.init.name; + for (const { key } of node.id.properties) { + if (key == null) { continue; } // true for rest properties + storePropertyLookup(objectName, key.name, key); } - }); - } + }, - return { - 'ImportDefaultSpecifier': handleImportDefault, - 'MemberExpression': handlePropLookup, - 'VariableDeclarator': handleDestructuringAssignment, - 'Program:exit': handleProgramExit, + 'Program:exit'() { + allPropertyLookups.forEach((lookups, objectName) => { + const fileImport = fileImports.get(objectName); + if (fileImport == null) { return; } + + for (const { propName, node } of lookups) { + // the default import can have a "default" property + if (propName === 'default') { continue; } + if (!fileImport.exportMap.namespace.has(propName)) { continue; } + + context.report({ + node, + message: `Caution: \`${objectName}\` also has a named export \`${propName}\`. Check if you meant to write \`import {${propName}} from '${fileImport.sourcePath}'\` instead.`, + }); + } + }); + }, }; }, }; diff --git a/src/rules/no-named-as-default.js b/src/rules/no-named-as-default.js index c3a35ff64a..40b1e175b2 100644 --- a/src/rules/no-named-as-default.js +++ b/src/rules/no-named-as-default.js @@ -16,30 +16,30 @@ module.exports = { create(context) { function checkDefault(nameKey, defaultSpecifier) { // #566: default is a valid specifier - if (defaultSpecifier[nameKey].name === 'default') return; + if (defaultSpecifier[nameKey].name === 'default') { return; } const declaration = importDeclaration(context); const imports = Exports.get(declaration.source.value, context); - if (imports == null) return; + if (imports == null) { return; } if (imports.errors.length) { imports.reportErrors(context, declaration); return; } - if (imports.has('default') && - imports.has(defaultSpecifier[nameKey].name)) { + if (imports.has('default') && imports.has(defaultSpecifier[nameKey].name)) { - context.report(defaultSpecifier, - 'Using exported name \'' + defaultSpecifier[nameKey].name + - '\' as identifier for default export.'); + context.report( + defaultSpecifier, + `Using exported name '${defaultSpecifier[nameKey].name}' as identifier for default export.`, + ); } } return { - 'ImportDefaultSpecifier': checkDefault.bind(null, 'local'), - 'ExportDefaultSpecifier': checkDefault.bind(null, 'exported'), + ImportDefaultSpecifier: checkDefault.bind(null, 'local'), + ExportDefaultSpecifier: checkDefault.bind(null, 'exported'), }; }, }; diff --git a/src/rules/no-named-default.js b/src/rules/no-named-default.js index 8745ce3890..1ed0e31df5 100644 --- a/src/rules/no-named-default.js +++ b/src/rules/no-named-default.js @@ -13,7 +13,7 @@ module.exports = { create(context) { return { - 'ImportDeclaration': function (node) { + ImportDeclaration(node) { node.specifiers.forEach(function (im) { if (im.importKind === 'type' || im.importKind === 'typeof') { return; diff --git a/src/rules/no-named-export.js b/src/rules/no-named-export.js index b0722f3596..efaf9dc4c8 100644 --- a/src/rules/no-named-export.js +++ b/src/rules/no-named-export.js @@ -29,7 +29,7 @@ module.exports = { return context.report({ node, message }); } - const someNamed = node.specifiers.some(specifier => (specifier.exported.name || specifier.exported.value) !== 'default'); + const someNamed = node.specifiers.some((specifier) => (specifier.exported.name || specifier.exported.value) !== 'default'); if (someNamed) { context.report({ node, message }); } diff --git a/src/rules/no-namespace.js b/src/rules/no-namespace.js index 4382007a59..3c6617a41c 100644 --- a/src/rules/no-namespace.js +++ b/src/rules/no-namespace.js @@ -6,10 +6,74 @@ import minimatch from 'minimatch'; import docsUrl from '../docsUrl'; -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ +/** + * @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.concat(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; +} + +/** + * @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') + ); + }); +} module.exports = { meta: { @@ -40,20 +104,20 @@ module.exports = { return { ImportNamespaceSpecifier(node) { - if (ignoreGlobs && ignoreGlobs.find(glob => minimatch(node.parent.source.value, glob, { matchBase: true }))) { + if (ignoreGlobs && ignoreGlobs.find((glob) => minimatch(node.parent.source.value, glob, { matchBase: true }))) { return; } 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 namespaceIdentifiers = namespaceReferences.map((reference) => reference.identifier); const canFix = namespaceIdentifiers.length > 0 && !usesNamespaceAsObject(namespaceIdentifiers); context.report({ node, message: `Unexpected namespace import.`, - fix: canFix && (fixer => { + fix: canFix && ((fixer) => { const scopeManager = context.getSourceCode().scopeManager; const fixes = []; @@ -82,11 +146,10 @@ module.exports = { ); // Replace the ImportNamespaceSpecifier with a list of ImportSpecifiers - const namedImportSpecifiers = importNames.map((importName) => ( - importName === importLocalNames[importName] - ? importName - : `${importName} as ${importLocalNames[importName]}` - )); + 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 @@ -105,74 +168,3 @@ module.exports = { }; }, }; - -/** - * @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-nodejs-modules.js b/src/rules/no-nodejs-modules.js index a87bff796f..82594bb603 100644 --- a/src/rules/no-nodejs-modules.js +++ b/src/rules/no-nodejs-modules.js @@ -4,7 +4,7 @@ import docsUrl from '../docsUrl'; function reportIfMissing(context, node, allowed, name) { if (allowed.indexOf(name) === -1 && importType(name, context) === 'builtin') { - context.report(node, 'Do not import Node.js builtin module "' + name + '"'); + context.report(node, `Do not import Node.js builtin module "${name}"`); } } diff --git a/src/rules/no-relative-packages.js b/src/rules/no-relative-packages.js index 6b0a627670..1d215519fd 100644 --- a/src/rules/no-relative-packages.js +++ b/src/rules/no-relative-packages.js @@ -47,7 +47,7 @@ function checkImportForRelativePackage(context, importPath, node) { context.report({ node, message: `Relative import from another package is not allowed. Use \`${properImport}\` instead of \`${importPath}\``, - fix: fixer => fixer.replaceText(node, JSON.stringify(toPosixPath(properImport))) + fix: (fixer) => fixer.replaceText(node, JSON.stringify(toPosixPath(properImport))) , }); } diff --git a/src/rules/no-relative-parent-imports.js b/src/rules/no-relative-parent-imports.js index fd8dcb302f..decd2ef7d2 100644 --- a/src/rules/no-relative-parent-imports.js +++ b/src/rules/no-relative-parent-imports.js @@ -18,7 +18,7 @@ module.exports = { create: function noRelativePackages(context) { const myPath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); - if (myPath === '') return {}; // can't check a non-file + if (myPath === '') { return {}; } // can't check a non-file function checkSourceValue(sourceNode) { const depPath = sourceNode.value; @@ -38,10 +38,7 @@ module.exports = { if (importType(relDepPath, context) === 'parent') { context.report({ node: sourceNode, - message: 'Relative imports from parent directories are not allowed. ' + - `Please either pass what you're importing through at runtime ` + - `(dependency injection), move \`${basename(myPath)}\` to same ` + - `directory as \`${depPath}\` or consider making \`${depPath}\` a package.`, + message: `Relative imports from parent directories are not allowed. Please either pass what you're importing through at runtime (dependency injection), move \`${basename(myPath)}\` to same directory as \`${depPath}\` or consider making \`${depPath}\` a package.`, }); } } diff --git a/src/rules/no-restricted-paths.js b/src/rules/no-restricted-paths.js index 2293119592..75952dd058 100644 --- a/src/rules/no-restricted-paths.js +++ b/src/rules/no-restricted-paths.js @@ -12,6 +12,15 @@ const containsPath = (filepath, target) => { return relative === '' || !relative.startsWith('..'); }; +function isMatchingTargetPath(filename, targetPath) { + if (isGlob(targetPath)) { + const mm = new Minimatch(targetPath); + return mm.match(filename); + } + + return containsPath(filename, targetPath); +} + module.exports = { meta: { type: 'problem', @@ -77,20 +86,11 @@ module.exports = { const restrictedPaths = options.zones || []; const basePath = options.basePath || process.cwd(); const currentFilename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); - const matchingZones = restrictedPaths.filter((zone) => { - return [].concat(zone.target) - .map(target => path.resolve(basePath, target)) - .some(targetPath => isMatchingTargetPath(currentFilename, targetPath)); - }); - - function isMatchingTargetPath(filename, targetPath) { - if (isGlob(targetPath)) { - const mm = new Minimatch(targetPath); - return mm.match(filename); - } - - return containsPath(filename, targetPath); - } + const matchingZones = restrictedPaths.filter( + (zone) => [].concat(zone.target) + .map((target) => path.resolve(basePath, target)) + .some((targetPath) => isMatchingTargetPath(currentFilename, targetPath)), + ); function isValidExceptionPath(absoluteFromPath, absoluteExceptionPath) { const relativeExceptionPath = path.relative(absoluteFromPath, absoluteExceptionPath); @@ -180,7 +180,7 @@ module.exports = { } function reportInvalidExceptions(validators, node) { - validators.forEach(validator => validator.reportInvalidException(node)); + validators.forEach((validator) => validator.reportInvalidException(node)); } function reportImportsInRestrictedZone(validators, node, importPath, customMessage) { @@ -203,7 +203,7 @@ module.exports = { const isGlobPattern = areGlobPatterns.every((isGlob) => isGlob); - return allZoneFrom.map(singleZoneFrom => { + return allZoneFrom.map((singleZoneFrom) => { const absoluteFrom = path.resolve(basePath, singleZoneFrom); if (isGlobPattern) { @@ -227,14 +227,13 @@ module.exports = { validators[index] = makePathValidators(zone.from, zone.except); } - const applicableValidatorsForImportPath = validators[index].filter(validator => validator.isPathRestricted(absoluteImportPath)); + const applicableValidatorsForImportPath = validators[index].filter((validator) => validator.isPathRestricted(absoluteImportPath)); - const validatorsWithInvalidExceptions = applicableValidatorsForImportPath.filter(validator => !validator.hasValidExceptions); + const validatorsWithInvalidExceptions = applicableValidatorsForImportPath.filter((validator) => !validator.hasValidExceptions); reportInvalidExceptions(validatorsWithInvalidExceptions, node); const applicableValidatorsForImportPathExcludingExceptions = applicableValidatorsForImportPath - .filter(validator => validator.hasValidExceptions) - .filter(validator => !validator.isPathException(absoluteImportPath)); + .filter((validator) => validator.hasValidExceptions && !validator.isPathException(absoluteImportPath)); reportImportsInRestrictedZone(applicableValidatorsForImportPathExcludingExceptions, node, importPath, zone.message); }); } diff --git a/src/rules/no-unassigned-import.js b/src/rules/no-unassigned-import.js index b790141927..0af9f2e9f3 100644 --- a/src/rules/no-unassigned-import.js +++ b/src/rules/no-unassigned-import.js @@ -24,16 +24,15 @@ function testIsAllow(globs, filename, source) { filePath = path.resolve(path.dirname(filename), source); // get source absolute path } - return globs.find(glob => ( - minimatch(filePath, glob) || - minimatch(filePath, path.join(process.cwd(), glob)) - )) !== undefined; + return globs.find((glob) => minimatch(filePath, glob) + || minimatch(filePath, path.join(process.cwd(), glob)), + ) !== undefined; } function create(context) { const options = context.options[0] || {}; const filename = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); - const isAllow = source => testIsAllow(options.allow, filename, source); + const isAllow = (source) => testIsAllow(options.allow, filename, source); return { ImportDeclaration(node) { @@ -42,9 +41,11 @@ function create(context) { } }, ExpressionStatement(node) { - if (node.expression.type === 'CallExpression' && - isStaticRequire(node.expression) && - !isAllow(node.expression.arguments[0].value)) { + if ( + node.expression.type === 'CallExpression' + && isStaticRequire(node.expression) + && !isAllow(node.expression.arguments[0].value) + ) { report(context, node.expression); } }, @@ -62,19 +63,19 @@ module.exports = { }, schema: [ { - 'type': 'object', - 'properties': { - 'devDependencies': { 'type': ['boolean', 'array'] }, - 'optionalDependencies': { 'type': ['boolean', 'array'] }, - 'peerDependencies': { 'type': ['boolean', 'array'] }, - 'allow': { - 'type': 'array', - 'items': { - 'type': 'string', + type: 'object', + properties: { + devDependencies: { type: ['boolean', 'array'] }, + optionalDependencies: { type: ['boolean', 'array'] }, + peerDependencies: { type: ['boolean', 'array'] }, + allow: { + type: 'array', + items: { + type: 'string', }, }, }, - 'additionalProperties': false, + additionalProperties: false, }, ], }, diff --git a/src/rules/no-unused-modules.js b/src/rules/no-unused-modules.js index bd8c524abb..ec3425dacd 100644 --- a/src/rules/no-unused-modules.js +++ b/src/rules/no-unused-modules.js @@ -4,15 +4,17 @@ * @author René Fermann */ -import Exports, { recursivePatternCapture } from '../ExportMap'; import { getFileExtensions } from 'eslint-module-utils/ignore'; import resolve from 'eslint-module-utils/resolve'; import visit from 'eslint-module-utils/visit'; -import docsUrl from '../docsUrl'; import { dirname, join } from 'path'; import readPkgUp from 'eslint-module-utils/readPkgUp'; import values from 'object.values'; import includes from 'array-includes'; +import flatMap from 'array.prototype.flatmap'; + +import Exports, { recursivePatternCapture } from '../ExportMap'; +import docsUrl from '../docsUrl'; let FileEnumerator; let listFilesToProcess; @@ -40,11 +42,7 @@ try { const { listFilesToProcess: originalListFilesToProcess } = require('eslint/lib/util/glob-util'); listFilesToProcess = function (src, extensions) { - const patterns = src.reduce((carry, pattern) => { - return carry.concat(extensions.map((extension) => { - return /\*\*|\*\./.test(pattern) ? pattern : `${pattern}/**/*${extension}`; - })); - }, src.slice()); + const patterns = src.concat(flatMap(src, (pattern) => extensions.map((extension) => (/\*\*|\*\./).test(pattern) ? pattern : `${pattern}/**/*${extension}`))); return originalListFilesToProcess(patterns); }; @@ -76,6 +74,7 @@ const FUNCTION_DECLARATION = 'FunctionDeclaration'; const CLASS_DECLARATION = 'ClassDeclaration'; const IDENTIFIER = 'Identifier'; const OBJECT_PATTERN = 'ObjectPattern'; +const ARRAY_PATTERN = 'ArrayPattern'; const TS_INTERFACE_DECLARATION = 'TSInterfaceDeclaration'; const TS_TYPE_ALIAS_DECLARATION = 'TSTypeAliasDeclaration'; const TS_ENUM_DECLARATION = 'TSEnumDeclaration'; @@ -84,11 +83,11 @@ const DEFAULT = 'default'; function forEachDeclarationIdentifier(declaration, cb) { if (declaration) { if ( - declaration.type === FUNCTION_DECLARATION || - declaration.type === CLASS_DECLARATION || - declaration.type === TS_INTERFACE_DECLARATION || - declaration.type === TS_TYPE_ALIAS_DECLARATION || - declaration.type === TS_ENUM_DECLARATION + declaration.type === FUNCTION_DECLARATION + || declaration.type === CLASS_DECLARATION + || declaration.type === TS_INTERFACE_DECLARATION + || declaration.type === TS_TYPE_ALIAS_DECLARATION + || declaration.type === TS_ENUM_DECLARATION ) { cb(declaration.id.name); } else if (declaration.type === VARIABLE_DECLARATION) { @@ -99,6 +98,10 @@ function forEachDeclarationIdentifier(declaration, cb) { cb(pattern.name); } }); + } else if (id.type === ARRAY_PATTERN) { + id.elements.forEach(({ name }) => { + cb(name); + }); } else { cb(id.name); } @@ -160,9 +163,7 @@ const visitorKeyMap = new Map(); const ignoredFiles = new Set(); const filesOutsideSrc = new Set(); -const isNodeModule = path => { - return /\/(node_modules)\//.test(path); -}; +const isNodeModule = (path) => (/\/(node_modules)\//).test(path); /** * read all files matching the patterns in src and ignoreExports @@ -172,18 +173,17 @@ const isNodeModule = path => { const resolveFiles = (src, ignoreExports, context) => { const extensions = Array.from(getFileExtensions(context.settings)); - const srcFiles = new Set(); const srcFileList = listFilesToProcess(src, extensions); // prepare list of ignored files - const ignoredFilesList = listFilesToProcess(ignoreExports, extensions); + const ignoredFilesList = listFilesToProcess(ignoreExports, extensions); ignoredFilesList.forEach(({ filename }) => ignoredFiles.add(filename)); // prepare list of source files, don't consider files from node_modules - srcFileList.filter(({ filename }) => !isNodeModule(filename)).forEach(({ filename }) => { - srcFiles.add(filename); - }); - return srcFiles; + + return new Set( + flatMap(srcFileList, ({ filename }) => isNodeModule(filename) ? [] : filename), + ); }; /** @@ -191,7 +191,7 @@ const resolveFiles = (src, ignoreExports, context) => { */ const prepareImportsAndExports = (srcFiles, context) => { const exportAll = new Map(); - srcFiles.forEach(file => { + srcFiles.forEach((file) => { const exports = new Map(); const imports = new Map(); const currentExports = Exports.get(file, context); @@ -207,7 +207,7 @@ const prepareImportsAndExports = (srcFiles, context) => { visitorKeyMap.set(file, visitorKeys); // dependencies === export * from const currentExportAll = new Set(); - dependencies.forEach(getDependency => { + dependencies.forEach((getDependency) => { const dependency = getDependency(); if (dependency === null) { return; @@ -247,9 +247,11 @@ const prepareImportsAndExports = (srcFiles, context) => { return; } const localImport = imports.get(key) || new Set(); - value.declarations.forEach(({ importedSpecifiers }) => - importedSpecifiers.forEach(specifier => localImport.add(specifier)), - ); + value.declarations.forEach(({ importedSpecifiers }) => { + importedSpecifiers.forEach((specifier) => { + localImport.add(specifier); + }); + }); imports.set(key, localImport); }); importList.set(file, imports); @@ -271,7 +273,7 @@ const prepareImportsAndExports = (srcFiles, context) => { exportList.set(file, exports); }); exportAll.forEach((value, key) => { - value.forEach(val => { + value.forEach((val) => { const currentExports = exportList.get(val); if (currentExports) { const currentExport = currentExports.get(EXPORT_ALL_DECLARATION); @@ -290,7 +292,7 @@ const determineUsage = () => { listValue.forEach((value, key) => { const exports = exportList.get(key); if (typeof exports !== 'undefined') { - value.forEach(currentImport => { + value.forEach((currentImport) => { let specifier; if (currentImport === IMPORT_NAMESPACE_SPECIFIER) { specifier = IMPORT_NAMESPACE_SPECIFIER; @@ -313,7 +315,7 @@ const determineUsage = () => { }); }; -const getSrc = src => { +const getSrc = (src) => { if (src) { return src; } @@ -347,33 +349,29 @@ const doPreparation = (src, ignoreExports, context) => { lastPrepareKey = prepareKey; }; -const newNamespaceImportExists = specifiers => - specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER); +const newNamespaceImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_NAMESPACE_SPECIFIER); -const newDefaultImportExists = specifiers => - specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER); +const newDefaultImportExists = (specifiers) => specifiers.some(({ type }) => type === IMPORT_DEFAULT_SPECIFIER); -const fileIsInPkg = file => { +const fileIsInPkg = (file) => { const { path, pkg } = readPkgUp({ cwd: file }); const basePath = dirname(path); - const checkPkgFieldString = pkgField => { + const checkPkgFieldString = (pkgField) => { if (join(basePath, pkgField) === file) { return true; } }; - const checkPkgFieldObject = pkgField => { - const pkgFieldFiles = values(pkgField) - .filter((value) => typeof value !== 'boolean') - .map(value => join(basePath, value)); + const checkPkgFieldObject = (pkgField) => { + const pkgFieldFiles = flatMap(values(pkgField), (value) => typeof value === 'boolean' ? [] : join(basePath, value)); if (includes(pkgFieldFiles, file)) { return true; } }; - const checkPkgField = pkgField => { + const checkPkgField = (pkgField) => { if (typeof pkgField === 'string') { return checkPkgFieldString(pkgField); } @@ -421,17 +419,16 @@ module.exports = { src: { description: 'files/paths to be analyzed (only for unused exports)', type: 'array', - minItems: 1, + uniqueItems: true, items: { type: 'string', minLength: 1, }, }, ignoreExports: { - description: - 'files/paths for which unused exports will not be reported (e.g module entry points)', + description: 'files/paths for which unused exports will not be reported (e.g module entry points)', type: 'array', - minItems: 1, + uniqueItems: true, items: { type: 'string', minLength: 1, @@ -446,41 +443,27 @@ module.exports = { type: 'boolean', }, }, - not: { - properties: { - unusedExports: { enum: [false] }, - missingExports: { enum: [false] }, - }, - }, - anyOf:[{ - not: { + anyOf: [ + { properties: { unusedExports: { enum: [true] }, + src: { + minItems: 1, + }, }, + required: ['unusedExports'], }, - required: ['missingExports'], - }, { - not: { + { properties: { missingExports: { enum: [true] }, }, + required: ['missingExports'], }, - required: ['unusedExports'], - }, { - properties: { - unusedExports: { enum: [true] }, - }, - required: ['unusedExports'], - }, { - properties: { - missingExports: { enum: [true] }, - }, - required: ['missingExports'], - }], + ], }], }, - create: context => { + create(context) { const { src, ignoreExports = [], @@ -494,7 +477,7 @@ module.exports = { const file = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename(); - const checkExportPresence = node => { + const checkExportPresence = (node) => { if (!missingExports) { return; } @@ -546,6 +529,10 @@ module.exports = { exports = exportList.get(file); + if (!exports) { + console.error(`file \`${file}\` has no exports. Please update to the latest, and if it still happens, report this on https://github.com/import-js/eslint-plugin-import/issues/2866!`); + } + // special case: export * from const exportAll = exports.get(EXPORT_ALL_DECLARATION); if (typeof exportAll !== 'undefined' && exportedValue !== IMPORT_DEFAULT_SPECIFIER) { @@ -589,7 +576,7 @@ module.exports = { * * update lists of existing exports during runtime */ - const updateExportUsage = node => { + const updateExportUsage = (node) => { if (ignoredFiles.has(file)) { return; } @@ -611,7 +598,7 @@ module.exports = { } if (type === EXPORT_NAMED_DECLARATION) { if (specifiers.length > 0) { - specifiers.forEach(specifier => { + specifiers.forEach((specifier) => { if (specifier.exported) { newExportIdentifiers.add(specifier.exported.name || specifier.exported.value); } @@ -631,7 +618,7 @@ module.exports = { }); // new export identifiers added: add to map of new exports - newExportIdentifiers.forEach(key => { + newExportIdentifiers.forEach((key) => { if (!exports.has(key)) { newExports.set(key, { whereUsed: new Set() }); } @@ -655,7 +642,7 @@ module.exports = { * * update lists of existing imports during runtime */ - const updateImportUsage = node => { + const updateImportUsage = (node) => { if (!unusedExports) { return; } @@ -686,9 +673,11 @@ module.exports = { if (value.has(IMPORT_DEFAULT_SPECIFIER)) { oldDefaultImports.add(key); } - value.forEach(val => { - if (val !== IMPORT_NAMESPACE_SPECIFIER && - val !== IMPORT_DEFAULT_SPECIFIER) { + value.forEach((val) => { + if ( + val !== IMPORT_NAMESPACE_SPECIFIER + && val !== IMPORT_DEFAULT_SPECIFIER + ) { oldImports.set(val, key); } }); @@ -716,14 +705,14 @@ module.exports = { }, }); - node.body.forEach(astNode => { + node.body.forEach((astNode) => { let resolvedPath; // support for export { value } from 'module' if (astNode.type === EXPORT_NAMED_DECLARATION) { if (astNode.source) { resolvedPath = resolve(astNode.source.raw.replace(/('|")/g, ''), context); - astNode.specifiers.forEach(specifier => { + astNode.specifiers.forEach((specifier) => { const name = specifier.local.name || specifier.local.value; if (name === DEFAULT) { newDefaultImports.add(resolvedPath); @@ -757,17 +746,15 @@ module.exports = { newDefaultImports.add(resolvedPath); } - astNode.specifiers.forEach(specifier => { - if (specifier.type === IMPORT_DEFAULT_SPECIFIER || - specifier.type === IMPORT_NAMESPACE_SPECIFIER) { - return; - } - newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath); - }); + astNode.specifiers + .filter((specifier) => specifier.type !== IMPORT_DEFAULT_SPECIFIER && specifier.type !== IMPORT_NAMESPACE_SPECIFIER) + .forEach((specifier) => { + newImports.set(specifier.imported.name || specifier.imported.value, resolvedPath); + }); } }); - newExportAll.forEach(value => { + newExportAll.forEach((value) => { if (!oldExportAll.has(value)) { let imports = oldImportPaths.get(value); if (typeof imports === 'undefined') { @@ -795,7 +782,7 @@ module.exports = { } }); - oldExportAll.forEach(value => { + oldExportAll.forEach((value) => { if (!newExportAll.has(value)) { const imports = oldImportPaths.get(value); imports.delete(EXPORT_ALL_DECLARATION); @@ -810,7 +797,7 @@ module.exports = { } }); - newDefaultImports.forEach(value => { + newDefaultImports.forEach((value) => { if (!oldDefaultImports.has(value)) { let imports = oldImportPaths.get(value); if (typeof imports === 'undefined') { @@ -838,7 +825,7 @@ module.exports = { } }); - oldDefaultImports.forEach(value => { + oldDefaultImports.forEach((value) => { if (!newDefaultImports.has(value)) { const imports = oldImportPaths.get(value); imports.delete(IMPORT_DEFAULT_SPECIFIER); @@ -853,7 +840,7 @@ module.exports = { } }); - newNamespaceImports.forEach(value => { + newNamespaceImports.forEach((value) => { if (!oldNamespaceImports.has(value)) { let imports = oldImportPaths.get(value); if (typeof imports === 'undefined') { @@ -881,7 +868,7 @@ module.exports = { } }); - oldNamespaceImports.forEach(value => { + oldNamespaceImports.forEach((value) => { if (!newNamespaceImports.has(value)) { const imports = oldImportPaths.get(value); imports.delete(IMPORT_NAMESPACE_SPECIFIER); @@ -941,17 +928,17 @@ module.exports = { }; return { - 'Program:exit': node => { + 'Program:exit'(node) { updateExportUsage(node); updateImportUsage(node); checkExportPresence(node); }, - 'ExportDefaultDeclaration': node => { + ExportDefaultDeclaration(node) { checkUsage(node, IMPORT_DEFAULT_SPECIFIER); }, - 'ExportNamedDeclaration': node => { - node.specifiers.forEach(specifier => { - checkUsage(node, specifier.exported.name || specifier.exported.value); + ExportNamedDeclaration(node) { + node.specifiers.forEach((specifier) => { + checkUsage(specifier, specifier.exported.name || specifier.exported.value); }); forEachDeclarationIdentifier(node.declaration, (name) => { checkUsage(node, name); diff --git a/src/rules/no-useless-path-segments.js b/src/rules/no-useless-path-segments.js index a328be2465..390a7546d3 100644 --- a/src/rules/no-useless-path-segments.js +++ b/src/rules/no-useless-path-segments.js @@ -25,7 +25,7 @@ import docsUrl from '../docsUrl'; function toRelativePath(relativePath) { const stripped = relativePath.replace(/\/$/g, ''); // Remove trailing / - return /^((\.\.)|(\.))($|\/)/.test(stripped) ? stripped : `./${stripped}`; + return (/^((\.\.)|(\.))($|\/)/).test(stripped) ? stripped : `./${stripped}`; } function normalize(fn) { @@ -33,7 +33,7 @@ function normalize(fn) { } function countRelativeParents(pathSegments) { - return pathSegments.reduce((sum, pathSegment) => pathSegment === '..' ? sum + 1 : sum, 0); + return pathSegments.filter((x) => x === '..').length; } module.exports = { @@ -71,7 +71,7 @@ module.exports = { node: source, // Note: Using messageIds is not possible due to the support for ESLint 2 and 3 message: `Useless path segments for "${importPath}", should be "${proposedPath}"`, - fix: fixer => proposedPath && fixer.replaceText(source, JSON.stringify(proposedPath)), + fix: (fixer) => proposedPath && fixer.replaceText(source, JSON.stringify(proposedPath)), }); } diff --git a/src/rules/no-webpack-loader-syntax.js b/src/rules/no-webpack-loader-syntax.js index faedeb4373..6ca7d603d6 100644 --- a/src/rules/no-webpack-loader-syntax.js +++ b/src/rules/no-webpack-loader-syntax.js @@ -3,9 +3,7 @@ import docsUrl from '../docsUrl'; function reportIfNonStandard(context, node, name) { if (name && name.indexOf('!') !== -1) { - context.report(node, `Unexpected '!' in '${name}'. ` + - 'Do not use import syntax to configure webpack loaders.', - ); + context.report(node, `Unexpected '!' in '${name}'. Do not use import syntax to configure webpack loaders.`); } } diff --git a/src/rules/order.js b/src/rules/order.js index dc9da64f22..7071513bf3 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -2,6 +2,7 @@ import minimatch from 'minimatch'; import includes from 'array-includes'; +import groupBy from 'object.groupby'; import importType from '../core/importType'; import isStaticRequire from '../core/staticRequire'; @@ -13,7 +14,7 @@ const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index']; function reverse(array) { return array.map(function (v) { - return Object.assign({}, v, { rank: -v.rank }); + return { ...v, rank: -v.rank }; }).reverse(); } @@ -91,6 +92,12 @@ function findRootNode(node) { return parent; } +function commentOnSameLineAs(node) { + return (token) => (token.type === 'Block' || token.type === 'Line') + && token.loc.start.line === token.loc.end.line + && token.loc.end.line === node.loc.end.line; +} + function findEndOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node)); const endOfTokens = tokensToEndOfLine.length > 0 @@ -110,12 +117,6 @@ function findEndOfLineWithComments(sourceCode, node) { return result; } -function commentOnSameLineAs(node) { - return token => (token.type === 'Block' || token.type === 'Line') && - token.loc.start.line === token.loc.end.line && - token.loc.end.line === node.loc.end.line; -} - function findStartOfLineWithComments(sourceCode, node) { const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node)); const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0]; @@ -130,13 +131,13 @@ function findStartOfLineWithComments(sourceCode, node) { } function isRequireExpression(expr) { - return expr != null && - expr.type === 'CallExpression' && - expr.callee != null && - expr.callee.name === 'require' && - expr.arguments != null && - expr.arguments.length === 1 && - expr.arguments[0].type === 'Literal'; + return expr != null + && expr.type === 'CallExpression' + && expr.callee != null + && expr.callee.name === 'require' + && expr.arguments != null + && expr.arguments.length === 1 + && expr.arguments[0].type === 'Literal'; } function isSupportedRequireModule(node) { @@ -147,16 +148,16 @@ function isSupportedRequireModule(node) { return false; } const decl = node.declarations[0]; - const isPlainRequire = decl.id && - (decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') && - isRequireExpression(decl.init); - const isRequireWithMemberExpression = decl.id && - (decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') && - decl.init != null && - decl.init.type === 'CallExpression' && - decl.init.callee != null && - decl.init.callee.type === 'MemberExpression' && - isRequireExpression(decl.init.callee.object); + const isPlainRequire = decl.id + && (decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') + && isRequireExpression(decl.init); + const isRequireWithMemberExpression = decl.id + && (decl.id.type === 'Identifier' || decl.id.type === 'ObjectPattern') + && decl.init != null + && decl.init.type === 'CallExpression' + && decl.init.callee != null + && decl.init.callee.type === 'MemberExpression' + && isRequireExpression(decl.init.callee.object); return isPlainRequire || isRequireWithMemberExpression; } @@ -211,7 +212,7 @@ function fixOutOfOrder(context, firstNode, secondNode, order) { let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd); if (newCode[newCode.length - 1] !== '\n') { - newCode = newCode + '\n'; + newCode = `${newCode}\n`; } const firstImport = `${makeImportDescription(firstNode)} of \`${firstNode.displayName}\``; @@ -222,21 +223,19 @@ function fixOutOfOrder(context, firstNode, secondNode, order) { context.report({ node: secondNode.node, message, - fix: canFix && (fixer => - fixer.replaceTextRange( - [firstRootStart, secondRootEnd], - newCode + sourceCode.text.substring(firstRootStart, secondRootStart), - )), + fix: canFix && ((fixer) => fixer.replaceTextRange( + [firstRootStart, secondRootEnd], + newCode + sourceCode.text.substring(firstRootStart, secondRootStart), + )), }); } else if (order === 'after') { context.report({ node: secondNode.node, message, - fix: canFix && (fixer => - fixer.replaceTextRange( - [secondRootStart, firstRootEnd], - sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode, - )), + fix: canFix && ((fixer) => fixer.replaceTextRange( + [secondRootStart, firstRootEnd], + sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode, + )), }); } } @@ -255,6 +254,7 @@ function makeOutOfOrderReport(context, imported) { if (!outOfOrder.length) { return; } + // There are things to report. Try to minimize the number of reported errors. const reversedImported = reverse(imported); const reversedOrder = findOutOfOrder(reversedImported); @@ -285,8 +285,8 @@ const getNormalizedValue = (node, toLowerCase) => { function getSorter(alphabetizeOptions) { const multiplier = alphabetizeOptions.order === 'asc' ? 1 : -1; const orderImportKind = alphabetizeOptions.orderImportKind; - const multiplierImportKind = orderImportKind !== 'ignore' && - (alphabetizeOptions.orderImportKind === 'asc' ? 1 : -1); + const multiplierImportKind = orderImportKind !== 'ignore' + && (alphabetizeOptions.orderImportKind === 'asc' ? 1 : -1); return function importsSorter(nodeA, nodeB) { const importA = getNormalizedValue(nodeA, alphabetizeOptions.caseInsensitive); @@ -303,7 +303,7 @@ function getSorter(alphabetizeOptions) { for (let i = 0; i < Math.min(a, b); i++) { result = compareString(A[i], B[i]); - if (result) break; + if (result) { break; } } if (!result && a !== b) { @@ -326,18 +326,15 @@ function getSorter(alphabetizeOptions) { } 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); - return acc; - }, {}); - - const groupRanks = Object.keys(groupedByRanks); + const groupedByRanks = groupBy(imported, (item) => item.rank); const sorterFn = getSorter(alphabetizeOptions); + // sort group keys so that they can be iterated on in order + const groupRanks = Object.keys(groupedByRanks).sort(function (a, b) { + return a - b; + }); + // sort imports locally within their group groupRanks.forEach(function (groupRank) { groupedByRanks[groupRank].sort(sorterFn); @@ -345,7 +342,7 @@ function mutateRanksToAlphabetize(imported, alphabetizeOptions) { // assign globally unique rank to each import let newRank = 0; - const alphabetizedRanks = groupRanks.sort().reduce(function (acc, groupRank) { + const alphabetizedRanks = groupRanks.reduce(function (acc, groupRank) { groupedByRanks[groupRank].forEach(function (importedItem) { acc[`${importedItem.value}|${importedItem.node.importKind}`] = parseInt(groupRank, 10) + newRank; newRank += 1; @@ -365,7 +362,7 @@ 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); + return ranks[group] + position / maxPosition; } } } @@ -396,7 +393,7 @@ function computeRank(context, ranks, importEntry, excludedImportTypes) { function registerNode(context, importEntry, ranks, imported, excludedImportTypes) { const rank = computeRank(context, ranks, importEntry, excludedImportTypes); if (rank !== -1) { - imported.push(Object.assign({}, importEntry, { rank })); + imported.push({ ...importEntry, rank }); } } @@ -405,15 +402,15 @@ function getRequireBlock(node) { // Handle cases like `const baz = require('foo').bar.baz` // and `const foo = require('foo')()` while ( - (n.parent.type === 'MemberExpression' && n.parent.object === n) || - (n.parent.type === 'CallExpression' && n.parent.callee === n) + n.parent.type === 'MemberExpression' && n.parent.object === n + || n.parent.type === 'CallExpression' && n.parent.callee === n ) { n = n.parent; } if ( - n.parent.type === 'VariableDeclarator' && - n.parent.parent.type === 'VariableDeclaration' && - n.parent.parent.parent.type === 'Program' + n.parent.type === 'VariableDeclarator' + && n.parent.parent.type === 'VariableDeclaration' + && n.parent.parent.parent.type === 'Program' ) { return n.parent.parent.parent; } @@ -426,16 +423,12 @@ const types = ['builtin', 'external', 'internal', 'unknown', 'parent', 'sibling' // Will throw an error if it contains a type that does not exist, or has a duplicate function convertGroupsToRanks(groups) { const rankObject = groups.reduce(function (res, group, index) { - if (typeof group === 'string') { - group = [group]; - } - group.forEach(function (groupItem) { + [].concat(group).forEach(function (groupItem) { if (types.indexOf(groupItem) === -1) { - throw new Error('Incorrect configuration of the rule: Unknown type `' + - JSON.stringify(groupItem) + '`'); + throw new Error(`Incorrect configuration of the rule: Unknown type \`${JSON.stringify(groupItem)}\``); } if (res[groupItem] !== undefined) { - throw new Error('Incorrect configuration of the rule: `' + groupItem + '` is duplicated'); + throw new Error(`Incorrect configuration of the rule: \`${groupItem}\` is duplicated`); } res[groupItem] = index * 2; }); @@ -443,7 +436,7 @@ function convertGroupsToRanks(groups) { }, {}); const omittedTypes = types.filter(function (type) { - return rankObject[type] === undefined; + return typeof rankObject[type] === 'undefined'; }); const ranks = omittedTypes.reduce(function (res, type) { @@ -473,7 +466,7 @@ function convertPathGroupsForRanks(pathGroups) { before[group].push(index); } - return Object.assign({}, pathGroup, { position }); + return { ...pathGroup, position }; }); let maxPosition = 1; @@ -517,7 +510,7 @@ function removeNewLineAfterImport(context, currentImport, previousImport) { findEndOfLineWithComments(sourceCode, prevRoot), findStartOfLineWithComments(sourceCode, currRoot), ]; - if (/^\s*$/.test(sourceCode.text.substring(rangeToRemove[0], rangeToRemove[1]))) { + if ((/^\s*$/).test(sourceCode.text.substring(rangeToRemove[0], rangeToRemove[1]))) { return (fixer) => fixer.removeRange(rangeToRemove); } return undefined; @@ -532,9 +525,7 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, di return linesBetweenImports.filter((line) => !line.trim().length).length; }; - const getIsStartOfDistinctGroup = (currentImport, previousImport) => { - return currentImport.rank - 1 >= previousImport.rank; - }; + const getIsStartOfDistinctGroup = (currentImport, previousImport) => currentImport.rank - 1 >= previousImport.rank; let previousImport = imported[0]; imported.slice(1).forEach(function (currentImport) { @@ -544,7 +535,7 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, di if (newlinesBetweenImports === 'always' || newlinesBetweenImports === 'always-and-inside-groups') { if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) { - if (distinctGroup || (!distinctGroup && isStartOfDistinctGroup)) { + if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) { context.report({ node: previousImport.node, message: 'There should be at least one empty line between import groups', @@ -553,7 +544,7 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, di } } else if (emptyLinesBetween > 0 && newlinesBetweenImports !== 'always-and-inside-groups') { - if ((distinctGroup && currentImport.rank === previousImport.rank) || (!distinctGroup && !isStartOfDistinctGroup)) { + if (distinctGroup && currentImport.rank === previousImport.rank || !distinctGroup && !isStartOfDistinctGroup) { context.report({ node: previousImport.node, message: 'There should be no empty line within import group', @@ -672,7 +663,7 @@ 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', 'object']); + const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']); const alphabetize = getAlphabetizeConfig(options); const distinctGroup = options.distinctGroup == null ? defaultDistinctGroup : !!options.distinctGroup; let ranks; diff --git a/src/rules/prefer-default-export.js b/src/rules/prefer-default-export.js index 32ef5004fa..581f02502e 100644 --- a/src/rules/prefer-default-export.js +++ b/src/rules/prefer-default-export.js @@ -15,7 +15,7 @@ module.exports = { }, schema: [{ type: 'object', - properties:{ + properties: { target: { type: 'string', enum: ['single', 'any'], @@ -51,11 +51,11 @@ module.exports = { } return { - 'ExportDefaultSpecifier': function () { + ExportDefaultSpecifier() { hasDefaultExport = true; }, - 'ExportSpecifier': function (node) { + ExportSpecifier(node) { if ((node.exported.name || node.exported.value) === 'default') { hasDefaultExport = true; } else { @@ -64,17 +64,17 @@ module.exports = { } }, - 'ExportNamedDeclaration': function (node) { + ExportNamedDeclaration(node) { // if there are specifiers, node.declaration should be null - if (!node.declaration) return; + if (!node.declaration) { return; } const { type } = node.declaration; if ( - type === 'TSTypeAliasDeclaration' || - type === 'TypeAlias' || - type === 'TSInterfaceDeclaration' || - type === 'InterfaceDeclaration' + type === 'TSTypeAliasDeclaration' + || type === 'TypeAlias' + || type === 'TSInterfaceDeclaration' + || type === 'InterfaceDeclaration' ) { specifierExportCount++; hasTypeExport = true; @@ -93,15 +93,15 @@ module.exports = { namedExportNode = node; }, - 'ExportDefaultDeclaration': function () { + ExportDefaultDeclaration() { hasDefaultExport = true; }, - 'ExportAllDeclaration': function () { + ExportAllDeclaration() { hasStarExport = true; }, - 'Program:exit': function () { + 'Program:exit'() { if (hasDefaultExport || hasStarExport || hasTypeExport) { return; } diff --git a/tests/files/foo-bar-resolver-no-version.js b/tests/files/foo-bar-resolver-no-version.js index f00198562e..2a2d451850 100644 --- a/tests/files/foo-bar-resolver-no-version.js +++ b/tests/files/foo-bar-resolver-no-version.js @@ -1,12 +1,12 @@ -var path = require('path') - -exports.resolveImport = function (modulePath, sourceFile, config) { - var sourceFileName = path.basename(sourceFile) - if (sourceFileName === 'foo.js') { - return path.join(__dirname, 'bar.jsx') - } - if (sourceFileName === 'exception.js') { - throw new Error('foo-bar-resolver-v1 resolveImport test exception') - } - return undefined; -} +var path = require('path') + +exports.resolveImport = function (modulePath, sourceFile, config) { + var sourceFileName = path.basename(sourceFile) + if (sourceFileName === 'foo.js') { + return path.join(__dirname, 'bar.jsx') + } + if (sourceFileName === 'exception.js') { + throw new Error('foo-bar-resolver-v1 resolveImport test exception') + } + return undefined; +} diff --git a/tests/files/foo-bar-resolver-v1.js b/tests/files/foo-bar-resolver-v1.js index af9da1b7a6..7ba97cb55f 100644 --- a/tests/files/foo-bar-resolver-v1.js +++ b/tests/files/foo-bar-resolver-v1.js @@ -1,14 +1,14 @@ -var path = require('path') - -exports.resolveImport = function (modulePath, sourceFile, config) { - var sourceFileName = path.basename(sourceFile) - if (sourceFileName === 'foo.js') { - return path.join(__dirname, 'bar.jsx'); - } - if (sourceFileName === 'exception.js') { - throw new Error('foo-bar-resolver-v1 resolveImport test exception'); - } - return undefined; -}; - -exports.interfaceVersion = 1; +var path = require('path') + +exports.resolveImport = function (modulePath, sourceFile, config) { + var sourceFileName = path.basename(sourceFile) + if (sourceFileName === 'foo.js') { + return path.join(__dirname, 'bar.jsx'); + } + if (sourceFileName === 'exception.js') { + throw new Error('foo-bar-resolver-v1 resolveImport test exception'); + } + return undefined; +}; + +exports.interfaceVersion = 1; diff --git a/tests/files/foo-bar-resolver-v2.js b/tests/files/foo-bar-resolver-v2.js index 7f8bcc0f86..13135e3925 100644 --- a/tests/files/foo-bar-resolver-v2.js +++ b/tests/files/foo-bar-resolver-v2.js @@ -1,14 +1,14 @@ -var path = require('path') - -exports.resolve = function (modulePath, sourceFile, config) { - var sourceFileName = path.basename(sourceFile) - if (sourceFileName === 'foo.js') { - return { found: true, path: path.join(__dirname, 'bar.jsx') } - } - if (sourceFileName === 'exception.js') { - throw new Error('foo-bar-resolver-v2 resolve test exception') - } - return { found: false }; -}; - -exports.interfaceVersion = 2; +var path = require('path') + +exports.resolve = function (modulePath, sourceFile, config) { + var sourceFileName = path.basename(sourceFile) + if (sourceFileName === 'foo.js') { + return { found: true, path: path.join(__dirname, 'bar.jsx') } + } + if (sourceFileName === 'exception.js') { + throw new Error('foo-bar-resolver-v2 resolve test exception') + } + return { found: false }; +}; + +exports.interfaceVersion = 2; diff --git a/tests/files/internal-modules/test.js b/tests/files/internal-modules/test.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000000..abc02b839c --- /dev/null +++ b/tests/index.js @@ -0,0 +1 @@ +export * from './files'; diff --git a/tests/src/cli.js b/tests/src/cli.js index e6afd8e441..8a73454878 100644 --- a/tests/src/cli.js +++ b/tests/src/cli.js @@ -21,7 +21,7 @@ describe('CLI regression tests', function () { rulePaths: ['./src/rules'], overrideConfig: { rules: { - 'named': 2, + named: 2, }, }, plugins: { 'eslint-plugin-import': importPlugin }, @@ -32,7 +32,7 @@ describe('CLI regression tests', function () { configFile: './tests/files/issue210.config.js', rulePaths: ['./src/rules'], rules: { - 'named': 2, + named: 2, }, }); cli.addPlugin('eslint-plugin-import', importPlugin); @@ -78,7 +78,7 @@ describe('CLI regression tests', function () { it('throws an error on invalid JSON', () => { const invalidJSON = './tests/files/just-json-files/invalid.json'; if (eslint) { - return eslint.lintFiles([invalidJSON]).then(results => { + return eslint.lintFiles([invalidJSON]).then((results) => { expect(results).to.eql( [ { @@ -97,16 +97,16 @@ describe('CLI regression tests', function () { }, ], errorCount: 1, - ...(semver.satisfies(eslintPkg.version, '>= 7.32 || ^8.0.0') && { + ...semver.satisfies(eslintPkg.version, '>= 7.32 || ^8.0.0') && { fatalErrorCount: 0, - }), + }, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, source: results[0].source, // NewLine-characters might differ depending on git-settings - ...(semver.satisfies(eslintPkg.version, '>= 8.8') && { + ...semver.satisfies(eslintPkg.version, '>= 8.8') && { suppressedMessages: [], - }), + }, usedDeprecatedRules: results[0].usedDeprecatedRules, // we don't care about this one }, ], diff --git a/tests/src/core/getExports.js b/tests/src/core/getExports.js index 86c1915968..1dd6e88014 100644 --- a/tests/src/core/getExports.js +++ b/tests/src/core/getExports.js @@ -58,11 +58,10 @@ describe('ExportMap', function () { const firstAccess = ExportMap.get('./named-exports', fakeContext); expect(firstAccess).to.exist; - const differentSettings = Object.assign( - {}, - fakeContext, - { parserPath: 'espree' }, - ); + const differentSettings = { + ...fakeContext, + parserPath: 'espree', + }; expect(ExportMap.get('./named-exports', differentSettings)) .to.exist.and @@ -320,17 +319,19 @@ describe('ExportMap', function () { }); it(`'has' circular reference`, function () { expect(ExportMap.get('./narcissist', fakeContext)) - .to.exist.and.satisfy(m => m.has('soGreat')); + .to.exist.and.satisfy((m) => m.has('soGreat')); }); it(`can 'get' circular reference`, function () { expect(ExportMap.get('./narcissist', fakeContext)) - .to.exist.and.satisfy(m => m.get('soGreat') != null); + .to.exist.and.satisfy((m) => m.get('soGreat') != null); }); }); context('issue #478: never parse non-whitelist extensions', function () { - const context = Object.assign({}, fakeContext, - { settings: { 'import/extensions': ['.js'] } }); + const context = { + ...fakeContext, + settings: { 'import/extensions': ['.js'] }, + }; let imports; before('load imports', function () { @@ -359,11 +360,13 @@ describe('ExportMap', function () { configs.forEach(([description, parserConfig]) => { describe(description, function () { - const context = Object.assign({}, fakeContext, - { settings: { + const context = { + ...fakeContext, + settings: { 'import/extensions': ['.js'], 'import/parsers': parserConfig, - } }); + }, + }; let imports; before('load imports', function () { @@ -404,30 +407,24 @@ describe('ExportMap', function () { }); it('should cache tsconfig until tsconfigRootDir parser option changes', function () { - const customContext = Object.assign( - {}, - context, - { - parserOptions: { - tsconfigRootDir: null, - }, + const customContext = { + ...context, + parserOptions: { + tsconfigRootDir: null, }, - ); + }; expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(0); ExportMap.parse('./baz.ts', 'export const baz = 5', customContext); expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1); ExportMap.parse('./baz.ts', 'export const baz = 5', customContext); expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(1); - const differentContext = Object.assign( - {}, - context, - { - parserOptions: { - tsconfigRootDir: process.cwd(), - }, + const differentContext = { + ...context, + parserOptions: { + tsconfigRootDir: process.cwd(), }, - ); + }; ExportMap.parse('./baz.ts', 'export const baz = 5', differentContext); expect(tsConfigLoader.tsConfigLoader.callCount).to.equal(2); @@ -460,7 +457,7 @@ describe('ExportMap', function () { for (const [testFile, expectedRegexResult] of testFiles) { it(`works for ${testFile} (${expectedRegexResult})`, function () { - const content = fs.readFileSync('./tests/files/' + testFile, 'utf8'); + const content = fs.readFileSync(`./tests/files/${testFile}`, 'utf8'); expect(testUnambiguous(content)).to.equal(expectedRegexResult); }); } diff --git a/tests/src/core/hash.js b/tests/src/core/hash.js index e75783fb06..785b8abc34 100644 --- a/tests/src/core/hash.js +++ b/tests/src/core/hash.js @@ -7,7 +7,7 @@ const createHash = require('crypto').createHash; function expectHash(actualHash, expectedString) { const expectedHash = createHash('sha256'); expectedHash.update(expectedString); - expect(actualHash.digest('hex'), 'to be a hex digest of sha256 hash of string <' + expectedString + '>').to.equal(expectedHash.digest('hex')); + expect(actualHash.digest('hex'), `to be a hex digest of sha256 hash of string <${expectedString}>`).to.equal(expectedHash.digest('hex')); } describe('hash', function () { @@ -29,7 +29,7 @@ describe('hash', function () { }); it('handles Array instances', function () { - expectHash(hashify([ 'a string' ]), '["a string",]'); + expectHash(hashify(['a string']), '["a string",]'); }); it('handles empty Array instances', function () { @@ -45,13 +45,13 @@ describe('hash', function () { }); it('handles nested Object and Array instances', function () { - expectHash(hashify({ foo: 123.456, 'a key': 'a value', obj: { arr: [ { def: 'ghi' } ] } }), '{"a key":"a value","foo":123.456,"obj":{"arr":[{"def":"ghi",},],},}'); + expectHash(hashify({ foo: 123.456, 'a key': 'a value', obj: { arr: [{ def: 'ghi' }] } }), '{"a key":"a value","foo":123.456,"obj":{"arr":[{"def":"ghi",},],},}'); }); }); describe('hashArray', function () { it('handles Array instances', function () { - expectHash(hashArray([ 'a string' ]), '["a string",]'); + expectHash(hashArray(['a string']), '["a string",]'); }); it('handles empty Array instances', function () { @@ -69,7 +69,7 @@ describe('hash', function () { }); it('handles nested Object and Array instances', function () { - expectHash(hashObject({ foo: 123.456, 'a key': 'a value', obj: { arr: [ { def: 'ghi' } ] } }), '{"a key":"a value","foo":123.456,"obj":{"arr":[{"def":"ghi",},],},}'); + expectHash(hashObject({ foo: 123.456, 'a key': 'a value', obj: { arr: [{ def: 'ghi' }] } }), '{"a key":"a value","foo":123.456,"obj":{"arr":[{"def":"ghi",},],},}'); }); }); diff --git a/tests/src/core/ignore.js b/tests/src/core/ignore.js index 2b2126c8b5..3212781363 100644 --- a/tests/src/core/ignore.js +++ b/tests/src/core/ignore.js @@ -19,7 +19,7 @@ describe('ignore', function () { }); it('ignores paths with invalid extensions when configured with import/extensions', function () { - const testContext = utils.testContext({ 'import/extensions': [ '.js', '.jsx', '.ts' ] }); + const testContext = utils.testContext({ 'import/extensions': ['.js', '.jsx', '.ts'] }); expect(isIgnored('../files/foo.js', testContext)).to.equal(false); @@ -45,7 +45,7 @@ describe('ignore', function () { }); it('can be configured with import/extensions', function () { - const testContext = utils.testContext({ 'import/extensions': [ '.foo', '.bar' ] }); + const testContext = utils.testContext({ 'import/extensions': ['.foo', '.bar'] }); expect(hasValidExtension('../files/foo.foo', testContext)).to.equal(true); diff --git a/tests/src/core/importType.js b/tests/src/core/importType.js index 937f193033..c4dca866e2 100644 --- a/tests/src/core/importType.js +++ b/tests/src/core/importType.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as path from 'path'; +import isCoreModule from 'is-core-module'; import importType, { isExternalModule, isScoped, isAbsolute } from 'core/importType'; @@ -16,8 +17,12 @@ describe('importType(name)', function () { }); it("should return 'builtin' for node.js modules", function () { - expect(importType('fs', context)).to.equal('builtin'); - expect(importType('path', context)).to.equal('builtin'); + ['fs', 'fs/promises', 'path'].filter((x) => isCoreModule(x)).forEach((x) => { + expect(importType(x, context)).to.equal('builtin'); + if (isCoreModule(`node:${x}`)) { + expect(importType(`node:${x}`, context)).to.equal('builtin'); + } + }); }); it("should return 'external' for non-builtin modules without a relative path", function () { diff --git a/tests/src/core/parse.js b/tests/src/core/parse.js index 407070aa2f..275b93982b 100644 --- a/tests/src/core/parse.js +++ b/tests/src/core/parse.js @@ -13,9 +13,19 @@ describe('parse(content, { settings, ecmaFeatures })', function () { const eslintParserPath = require.resolve('./eslintParser'); let content; - before((done) => - fs.readFile(path, { encoding: 'utf8' }, - (err, f) => { if (err) { done(err); } else { content = f; done(); }})); + before((done) => { + fs.readFile( + path, + { encoding: 'utf8' }, + (err, f) => { + if (err) { + done(err); + } else { + content = f; done(); + } + }, + ); + }); it('doesn\'t support JSX by default', function () { expect(() => parse(path, content, { parserPath: 'espree' })).to.throw(Error); @@ -66,7 +76,66 @@ describe('parse(content, { settings, ecmaFeatures })', function () { const parseSpy = sinon.spy(); const parserOptions = { ecmaFeatures: { jsx: true } }; parseStubParser.parse = parseSpy; - expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: [ '.js' ] } }, parserPath: null, parserOptions })).not.to.throw(Error); + expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: ['.js'] } }, parserPath: null, parserOptions })).not.to.throw(Error); expect(parseSpy.callCount, 'custom parser to be called once').to.equal(1); }); + + it('throws on invalid languageOptions', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: null })).to.throw(Error); + }); + + it('throws on non-object languageOptions.parser', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: 'espree' } })).to.throw(Error); + }); + + it('throws on null languageOptions.parser', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: null } })).to.throw(Error); + }); + + it('throws on empty languageOptions.parser', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: {} } })).to.throw(Error); + }); + + it('throws on non-function languageOptions.parser.parse', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parse: 'espree' } } })).to.throw(Error); + }); + + it('throws on non-function languageOptions.parser.parse', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parseForESLint: 'espree' } } })).to.throw(Error); + }); + + it('requires only one of the parse methods', function () { + expect(parse.bind(null, path, content, { settings: {}, parserPath: null, languageOptions: { parser: { parseForESLint: () => ({ ast: {} }) } } })).not.to.throw(Error); + }); + + it('uses parse from languageOptions.parser', function () { + const parseSpy = sinon.spy(); + expect(parse.bind(null, path, content, { settings: {}, languageOptions: { parser: { parse: parseSpy } } })).not.to.throw(Error); + expect(parseSpy.callCount, 'passed parser to be called once').to.equal(1); + }); + + it('uses parseForESLint from languageOptions.parser', function () { + const parseSpy = sinon.spy(() => ({ ast: {} })); + expect(parse.bind(null, path, content, { settings: {}, languageOptions: { parser: { parseForESLint: parseSpy } } })).not.to.throw(Error); + expect(parseSpy.callCount, 'passed parser to be called once').to.equal(1); + }); + + it('prefers parsers specified in the settings over languageOptions.parser', () => { + const parseSpy = sinon.spy(); + parseStubParser.parse = parseSpy; + expect(parse.bind(null, path, content, { settings: { 'import/parsers': { [parseStubParserPath]: ['.js'] } }, parserPath: null, languageOptions: { parser: { parse() {} } } })).not.to.throw(Error); + expect(parseSpy.callCount, 'custom parser to be called once').to.equal(1); + }); + + it('ignores parser options from language options set to null', () => { + const parseSpy = sinon.spy(); + parseStubParser.parse = parseSpy; + expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: null }, parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } })).not.to.throw(Error); + }); + + it('prefers languageOptions.parserOptions over parserOptions', () => { + const parseSpy = sinon.spy(); + parseStubParser.parse = parseSpy; + expect(parse.bind(null, path, content, { settings: {}, parserPath: 'espree', languageOptions: { parserOptions: { sourceType: 'module', ecmaVersion: 2015, ecmaFeatures: { jsx: true } } }, parserOptions: { sourceType: 'script' } })).not.to.throw(Error); + }); }); diff --git a/tests/src/core/resolve.js b/tests/src/core/resolve.js index 05a6aaeb68..0db9b05f49 100644 --- a/tests/src/core/resolve.js +++ b/tests/src/core/resolve.js @@ -21,32 +21,38 @@ describe('resolve', function () { it('resolves via a custom resolver with interface version 1', function () { const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-v1' }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); - expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('not-found.js'); } }), + expect(resolve( + '../files/not-found', + { ...testContext, getFilename() { return utils.getFilename('not-found.js'); } }, )).to.equal(undefined); }); it('resolves via a custom resolver with interface version 1 assumed if not specified', function () { const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-no-version' }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); - expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('not-found.js'); } }), + expect(resolve( + '../files/not-found', + { ...testContext, getFilename() { return utils.getFilename('not-found.js'); } }, )).to.equal(undefined); }); @@ -57,54 +63,61 @@ describe('resolve', function () { testContextReports.push(reportInfo); }; - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); testContextReports.length = 0; - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); expect(testContextReports[0]).to.be.an('object'); 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() { return utils.getFilename('not-found.js'); } }), + expect(resolve( + '../files/not-found', + { ...testContext, getFilename() { return utils.getFilename('not-found.js'); } }, )).to.equal(undefined); expect(testContextReports.length).to.equal(0); }); it('respects import/resolver as array of strings', function () { - const testContext = utils.testContext({ 'import/resolver': [ './foo-bar-resolver-v2', './foo-bar-resolver-v1' ] }); + const testContext = utils.testContext({ 'import/resolver': ['./foo-bar-resolver-v2', './foo-bar-resolver-v1'] }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); }); it('respects import/resolver as object', function () { const testContext = utils.testContext({ 'import/resolver': { './foo-bar-resolver-v2': {} } }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); }); it('respects import/resolver as array of objects', function () { - const testContext = utils.testContext({ 'import/resolver': [ { './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} } ] }); + const testContext = utils.testContext({ 'import/resolver': [{ './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} }] }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { 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': {} } }); + const testContext = utils.testContext({ 'import/resolver': { foo: {} } }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); }); @@ -116,8 +129,9 @@ describe('resolve', function () { }; testContextReports.length = 0; - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { 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'); @@ -132,8 +146,9 @@ describe('resolve', function () { testContextReports.push(reportInfo); }; testContextReports.length = 0; - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename() { 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`); @@ -141,10 +156,11 @@ describe('resolve', function () { }); it('respects import/resolve extensions', function () { - const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] } }); + const testContext = utils.testContext({ 'import/resolve': { extensions: ['.jsx'] } }); - expect(resolve( './jsx/MyCoolComponent' - , testContext, + expect(resolve( + './jsx/MyCoolComponent', + testContext, )).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx')); }); @@ -155,8 +171,9 @@ describe('resolve', function () { testContextReports.push(reportInfo); }; - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); expect(testContextReports[0]).to.be.an('object'); expect(replaceErrorStackForTest(testContextReports[0].message)).to.equal('Resolve error: SyntaxError: TEST SYNTAX ERROR\n'); @@ -172,32 +189,38 @@ describe('resolve', function () { it('resolves via a custom resolver with interface version 1', function () { const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-v1' }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); - expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('not-found.js'); } }), + expect(resolve( + '../files/not-found', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('not-found.js'); } }, )).to.equal(undefined); }); it('resolves via a custom resolver with interface version 1 assumed if not specified', function () { const testContext = utils.testContext({ 'import/resolver': './foo-bar-resolver-no-version' }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); - expect(resolve( '../files/not-found' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('not-found.js'); } }), + expect(resolve( + '../files/not-found', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('not-found.js'); } }, )).to.equal(undefined); }); @@ -208,54 +231,61 @@ describe('resolve', function () { testContextReports.push(reportInfo); }; - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); testContextReports.length = 0; - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); expect(testContextReports[0]).to.be.an('object'); 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: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('not-found.js'); } }), + expect(resolve( + '../files/not-found', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('not-found.js'); } }, )).to.equal(undefined); expect(testContextReports.length).to.equal(0); }); it('respects import/resolver as array of strings', function () { - const testContext = utils.testContext({ 'import/resolver': [ './foo-bar-resolver-v2', './foo-bar-resolver-v1' ] }); + const testContext = utils.testContext({ 'import/resolver': ['./foo-bar-resolver-v2', './foo-bar-resolver-v1'] }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); }); it('respects import/resolver as object', function () { const testContext = utils.testContext({ 'import/resolver': { './foo-bar-resolver-v2': {} } }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); }); it('respects import/resolver as array of objects', function () { - const testContext = utils.testContext({ 'import/resolver': [ { './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} } ] }); + const testContext = utils.testContext({ 'import/resolver': [{ './foo-bar-resolver-v2': {} }, { './foo-bar-resolver-v1': {} }] }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { 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': {} } }); + const testContext = utils.testContext({ 'import/resolver': { foo: {} } }); - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }, )).to.equal(utils.testFilePath('./bar.jsx')); }); @@ -267,8 +297,9 @@ describe('resolve', function () { }; testContextReports.length = 0; - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { 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'); @@ -283,8 +314,9 @@ describe('resolve', function () { testContextReports.push(reportInfo); }; testContextReports.length = 0; - expect(resolve( '../files/foo' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('foo.js'); } }), + expect(resolve( + '../files/foo', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { 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`); @@ -292,10 +324,11 @@ describe('resolve', function () { }); it('respects import/resolve extensions', function () { - const testContext = utils.testContext({ 'import/resolve': { 'extensions': ['.jsx'] } }); + const testContext = utils.testContext({ 'import/resolve': { extensions: ['.jsx'] } }); - expect(resolve( './jsx/MyCoolComponent' - , testContext, + expect(resolve( + './jsx/MyCoolComponent', + testContext, )).to.equal(utils.testFilePath('./jsx/MyCoolComponent.jsx')); }); @@ -306,8 +339,9 @@ describe('resolve', function () { testContextReports.push(reportInfo); }; - expect(resolve( '../files/exception' - , Object.assign({}, testContext, { getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }), + expect(resolve( + '../files/exception', + { ...testContext, getFilename: unexpectedCallToGetFilename, getPhysicalFilename() { return utils.getFilename('exception.js'); } }, )).to.equal(undefined); expect(testContextReports[0]).to.be.an('object'); expect(replaceErrorStackForTest(testContextReports[0].message)).to.equal('Resolve error: SyntaxError: TEST SYNTAX ERROR\n'); @@ -315,11 +349,11 @@ describe('resolve', function () { }); }); - const caseDescribe = (!CASE_SENSITIVE_FS ? describe : describe.skip); + const caseDescribe = !CASE_SENSITIVE_FS ? describe : describe.skip; caseDescribe('case sensitivity', function () { let file; const testContext = utils.testContext({ - 'import/resolve': { 'extensions': ['.jsx'] }, + 'import/resolve': { extensions: ['.jsx'] }, 'import/cache': { lifetime: 0 }, }); const testSettings = testContext.settings; @@ -333,31 +367,30 @@ describe('resolve', function () { }); it('detects case does not match FS', function () { expect(fileExistsWithCaseSync(file, testSettings)) - .to.be.false; + .to.equal(false); }); it('detecting case does not include parent folder path (issue #720)', function () { const f = path.join(process.cwd().toUpperCase(), './tests/files/jsx/MyUnCoolComponent.jsx'); expect(fileExistsWithCaseSync(f, testSettings)) - .to.be.true; + .to.equal(true); }); it('detecting case should include parent folder path', function () { const f = path.join(process.cwd().toUpperCase(), './tests/files/jsx/MyUnCoolComponent.jsx'); expect(fileExistsWithCaseSync(f, testSettings, true)) - .to.be.false; + .to.equal(false); }); }); describe('rename cache correctness', function () { const context = utils.testContext({ - 'import/cache': { 'lifetime': 1 }, + 'import/cache': { lifetime: 1 }, }); - const infiniteContexts = [ '∞', 'Infinity' ].map(inf => [inf, + const infiniteContexts = ['∞', 'Infinity'].map((inf) => [inf, utils.testContext({ - 'import/cache': { 'lifetime': inf }, + 'import/cache': { lifetime: inf }, })]); - const pairs = [ ['./CaseyKasem.js', './CASEYKASEM2.js'], ]; @@ -372,7 +405,7 @@ describe('resolve', function () { // settings are part of cache key before('warm up infinite entries', function () { - infiniteContexts.forEach(([,c]) => { + infiniteContexts.forEach(([, c]) => { expect(resolve(original, c)).to.exist; }); }); @@ -384,10 +417,9 @@ describe('resolve', function () { done); }); - before('verify rename', (done) => - fs.exists( - utils.testFilePath(changed), - exists => done(exists ? null : new Error('new file does not exist')))); + before('verify rename', (done) => fs.exists( + utils.testFilePath(changed), + (exists) => done(exists ? null : new Error('new file does not exist')))); it('gets cached values within cache lifetime', function () { // get cached values initially @@ -426,7 +458,8 @@ describe('resolve', function () { fs.rename( utils.testFilePath(changed), utils.testFilePath(original), - done); + done, + ); }); }); }); diff --git a/tests/src/package.js b/tests/src/package.js index f759819758..c56bd1333d 100644 --- a/tests/src/package.js +++ b/tests/src/package.js @@ -27,8 +27,7 @@ describe('package', function () { expect(err).not.to.exist; files.filter(isJSFile).forEach(function (f) { - expect(module.rules).to.have - .property(path.basename(f, '.js')); + expect(module.rules).to.have.property(path.basename(f, '.js')); }); done(); @@ -38,14 +37,21 @@ describe('package', function () { it('exports all configs', function (done) { fs.readdir(path.join(process.cwd(), 'config'), function (err, files) { if (err) { done(err); return; } - files.filter(isJSFile).forEach(file => { - if (file[0] === '.') return; + files.filter(isJSFile).forEach((file) => { + if (file[0] === '.') { return; } expect(module.configs).to.have.property(path.basename(file, '.js')); }); done(); }); }); + function getRulePath(ruleName) { + // 'require' does not work with dynamic paths because of the compilation step by babel + // (which resolves paths according to the root folder configuration) + // the usage of require.resolve on a static path gets around this + return path.resolve(require.resolve('rules/no-unresolved'), '..', ruleName); + } + it('has configs only for rules that exist', function () { for (const configFile in module.configs) { const preamble = 'import/'; @@ -55,18 +61,11 @@ describe('package', function () { .not.to.throw(Error); } } - - function getRulePath(ruleName) { - // 'require' does not work with dynamic paths because of the compilation step by babel - // (which resolves paths according to the root folder configuration) - // the usage of require.resolve on a static path gets around this - return path.resolve(require.resolve('rules/no-unresolved'), '..', ruleName); - } }); it('marks deprecated rules in their metadata', function () { expect(module.rules['imports-first'].meta.deprecated).to.be.true; - expect(module.rules['first'].meta.deprecated).not.to.be.true; + expect(module.rules.first.meta.deprecated).not.to.be.true; }); }); diff --git a/tests/src/rules/consistent-type-specifier-style.js b/tests/src/rules/consistent-type-specifier-style.js index 31a4c09ffe..7799238c32 100644 --- a/tests/src/rules/consistent-type-specifier-style.js +++ b/tests/src/rules/consistent-type-specifier-style.js @@ -168,6 +168,33 @@ const COMMON_TESTS = { type: 'ImportSpecifier', }], }, + // https://github.com/import-js/eslint-plugin-import/issues/2753 + { + code: `\ +import { Component, type ComponentProps } from "package-1"; +import { + Component1, + Component2, + Component3, + Component4, + Component5, +} from "package-2";`, + output: `\ +import { Component } from "package-1"; +import type {ComponentProps} from "package-1"; +import { + Component1, + Component2, + Component3, + Component4, + Component5, +} from "package-2";`, + options: ['prefer-top-level'], + errors: [{ + message: 'Prefer using a top-level type-only import instead of inline type specifiers.', + type: 'ImportSpecifier', + }], + }, // // prefer-inline @@ -367,14 +394,14 @@ context('TypeScript', () => { }, }); ruleTester.run('consistent-type-specifier-style', rule, { - valid: [ - ...COMMON_TESTS.valid, - ...TS_ONLY.valid, - ], - invalid: [ - ...COMMON_TESTS.invalid, - ...TS_ONLY.invalid, - ], + valid: [].concat( + COMMON_TESTS.valid, + TS_ONLY.valid, + ), + invalid: [].concat( + COMMON_TESTS.invalid, + TS_ONLY.invalid, + ), }); }); @@ -391,13 +418,13 @@ context('Babel/Flow', () => { }, }); ruleTester.run('consistent-type-specifier-style', rule, { - valid: [ - ...COMMON_TESTS.valid, - ...FLOW_ONLY.valid, - ], - invalid: [ - ...COMMON_TESTS.invalid, - ...FLOW_ONLY.invalid, - ], + valid: [].concat( + COMMON_TESTS.valid, + FLOW_ONLY.valid, + ), + invalid: [].concat( + COMMON_TESTS.invalid, + FLOW_ONLY.invalid, + ), }); }); diff --git a/tests/src/rules/dynamic-import-chunkname.js b/tests/src/rules/dynamic-import-chunkname.js index 46a1b97afe..c710507b26 100644 --- a/tests/src/rules/dynamic-import-chunkname.js +++ b/tests/src/rules/dynamic-import-chunkname.js @@ -12,6 +12,10 @@ const pickyCommentOptions = [{ importFunctions: ['dynamicImport'], webpackChunknameFormat: pickyCommentFormat, }]; +const allowEmptyOptions = [{ + importFunctions: ['dynamicImport'], + allowEmpty: true, +}]; const multipleImportFunctionOptions = [{ importFunctions: ['dynamicImport', 'definitelyNotStaticImport'], }]; @@ -83,6 +87,19 @@ ruleTester.run('dynamic-import-chunkname', rule, { )`, options, }, + { + code: `import('test')`, + options: allowEmptyOptions, + parser, + }, + { + code: `import( + /* webpackMode: "lazy" */ + 'test' + )`, + options: allowEmptyOptions, + parser, + }, { code: `import( /* webpackChunkName: "someModule" */ @@ -969,12 +986,25 @@ ruleTester.run('dynamic-import-chunkname', rule, { context('TypeScript', () => { getTSParsers().forEach((typescriptParser) => { - const nodeType = typescriptParser === parsers.TS_OLD || (typescriptParser === parsers.TS_NEW && semver.satisfies(require('@typescript-eslint/parser/package.json').version, '^2')) + const nodeType = typescriptParser === parsers.TS_OLD || typescriptParser === parsers.TS_NEW && semver.satisfies(require('@typescript-eslint/parser/package.json').version, '^2') ? 'CallExpression' : 'ImportExpression'; ruleTester.run('dynamic-import-chunkname', rule, { valid: [ + { + code: `import('test')`, + options: allowEmptyOptions, + parser: typescriptParser, + }, + { + code: `import( + /* webpackMode: "lazy" */ + 'test' + )`, + options: allowEmptyOptions, + parser: typescriptParser, + }, { code: `import( /* webpackChunkName: "someModule" */ diff --git a/tests/src/rules/export.js b/tests/src/rules/export.js index 95093bf4a8..a7f2bec122 100644 --- a/tests/src/rules/export.js +++ b/tests/src/rules/export.js @@ -137,7 +137,6 @@ ruleTester.run('export', rule, { // errors: ['Parsing error: Duplicate export \'bar\''], // }), - // #328: "export * from" does not export a default test({ code: 'export * from "./default-export"', @@ -158,7 +157,6 @@ ruleTester.run('export', rule, { ), }); - context('TypeScript', function () { getTSParsers().forEach((parser) => { const parserConfig = { diff --git a/tests/src/rules/exports-last.js b/tests/src/rules/exports-last.js index 9f01f27f42..d7122e9a00 100644 --- a/tests/src/rules/exports-last.js +++ b/tests/src/rules/exports-last.js @@ -5,7 +5,7 @@ import rule from 'rules/exports-last'; const ruleTester = new RuleTester(); -const error = type => ({ +const error = (type) => ({ message: 'Export statements should appear at the end of the file', type, }); diff --git a/tests/src/rules/extensions.js b/tests/src/rules/extensions.js index 45b4498fe9..14d84eaa62 100644 --- a/tests/src/rules/extensions.js +++ b/tests/src/rules/extensions.js @@ -11,18 +11,18 @@ ruleTester.run('extensions', rule, { test({ code: 'import dot from "./file.with.dot"' }), test({ code: 'import a from "a/index.js"', - options: [ 'always' ], + options: ['always'], }), test({ code: 'import dot from "./file.with.dot.js"', - options: [ 'always' ], + options: ['always'], }), test({ code: [ 'import a from "a"', 'import packageConfig from "./package.json"', ].join('\n'), - options: [ { json: 'always', js: 'never' } ], + options: [{ json: 'always', js: 'never' }], }), test({ code: [ @@ -30,8 +30,8 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), - options: [ 'never' ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + options: ['never'], + settings: { 'import/resolve': { extensions: ['.js', '.jsx', '.json'] } }, }), test({ @@ -40,8 +40,8 @@ ruleTester.run('extensions', rule, { 'import barjson from "./bar.json"', 'import barhbs from "./bar.hbs"', ].join('\n'), - options: [ 'always', { js: 'never', jsx: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json', '.hbs' ] } }, + options: ['always', { js: 'never', jsx: 'never' }], + settings: { 'import/resolve': { extensions: ['.js', '.jsx', '.json', '.hbs'] } }, }), test({ @@ -49,16 +49,16 @@ ruleTester.run('extensions', rule, { 'import bar from "./bar.js"', 'import pack from "./package"', ].join('\n'), - options: [ 'never', { js: 'always', json: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.json' ] } }, + options: ['never', { js: 'always', json: 'never' }], + settings: { 'import/resolve': { extensions: ['.js', '.json'] } }, }), // unresolved (#271/#295) test({ code: 'import path from "path"' }), - test({ code: 'import path from "path"', options: [ 'never' ] }), - test({ code: 'import path from "path"', options: [ 'always' ] }), - test({ code: 'import thing from "./fake-file.js"', options: [ 'always' ] }), - test({ code: 'import thing from "non-package"', options: [ 'never' ] }), + test({ code: 'import path from "path"', options: ['never'] }), + test({ code: 'import path from "path"', options: ['always'] }), + test({ code: 'import thing from "./fake-file.js"', options: ['always'] }), + test({ code: 'import thing from "non-package"', options: ['never'] }), test({ code: ` @@ -67,7 +67,7 @@ ruleTester.run('extensions', rule, { import Component from './Component.jsx' import express from 'express' `, - options: [ 'ignorePackages' ], + options: ['ignorePackages'], }), test({ @@ -77,7 +77,7 @@ ruleTester.run('extensions', rule, { import Component from './Component.jsx' import express from 'express' `, - options: [ 'always', { ignorePackages: true } ], + options: ['always', { ignorePackages: true }], }), test({ @@ -87,17 +87,17 @@ ruleTester.run('extensions', rule, { import Component from './Component' import express from 'express' `, - options: [ 'never', { ignorePackages: true } ], + options: ['never', { ignorePackages: true }], }), test({ code: 'import exceljs from "exceljs"', - options: [ 'always', { js: 'never', jsx: 'never' } ], + options: ['always', { js: 'never', jsx: 'never' }], filename: testFilePath('./internal-modules/plugins/plugin.js'), settings: { 'import/resolver': { - 'node': { 'extensions': [ '.js', '.jsx', '.json' ] }, - 'webpack': { 'config': 'webpack.empty.config.js' }, + node: { extensions: ['.js', '.jsx', '.json'] }, + webpack: { config: 'webpack.empty.config.js' }, }, }, }), @@ -108,14 +108,14 @@ ruleTester.run('extensions', rule, { 'export { foo } from "./foo.js"', 'let bar; export { bar }', ].join('\n'), - options: [ 'always' ], + options: ['always'], }), test({ code: [ 'export { foo } from "./foo"', 'let bar; export { bar }', ].join('\n'), - options: [ 'never' ], + options: ['never'], }), // Root packages should be ignored and they are names not files @@ -125,17 +125,17 @@ ruleTester.run('extensions', rule, { 'import lib2 from "pgk/package"', 'import lib3 from "@name/pkg.js"', ].join('\n'), - options: [ 'never' ], + options: ['never'], }), // Query strings. test({ code: 'import bare from "./foo?a=True.ext"', - options: [ 'never' ], + options: ['never'], }), test({ code: 'import bare from "./foo.js?a=True"', - options: [ 'always' ], + options: ['always'], }), test({ @@ -144,22 +144,22 @@ ruleTester.run('extensions', rule, { 'import lib2 from "pgk/package.js"', 'import lib3 from "@name/pkg"', ].join('\n'), - options: [ 'always' ], + options: ['always'], }), ], invalid: [ test({ code: 'import a from "a/index.js"', - errors: [ { + errors: [{ message: 'Unexpected use of file extension "js" for "a/index.js"', line: 1, column: 15, - } ], + }], }), test({ code: 'import dot from "./file.with.dot"', - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension "js" for "./file.with.dot"', @@ -173,8 +173,8 @@ ruleTester.run('extensions', rule, { 'import a from "a/index.js"', 'import packageConfig from "./package"', ].join('\n'), - options: [ { json: 'always', js: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.json' ] } }, + options: [{ json: 'always', js: 'never' }], + settings: { 'import/resolve': { extensions: ['.js', '.json'] } }, errors: [ { message: 'Unexpected use of file extension "js" for "a/index.js"', @@ -194,8 +194,8 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), - options: [ 'never' ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + options: ['never'], + settings: { 'import/resolve': { extensions: ['.js', '.jsx', '.json'] } }, errors: [ { message: 'Unexpected use of file extension "js" for "./bar.js"', @@ -210,8 +210,8 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), - options: [ { json: 'always', js: 'never', jsx: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + options: [{ json: 'always', js: 'never', jsx: 'never' }], + settings: { 'import/resolve': { extensions: ['.js', '.jsx', '.json'] } }, errors: [ { message: 'Unexpected use of file extension "js" for "./bar.js"', @@ -226,8 +226,8 @@ ruleTester.run('extensions', rule, { 'import component from "./bar.jsx"', 'import data from "./bar.json"', ].join('\n'), - options: [ { json: 'always', js: 'never', jsx: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.jsx', '.json', '.js' ] } }, + options: [{ json: 'always', js: 'never', jsx: 'never' }], + settings: { 'import/resolve': { extensions: ['.jsx', '.json', '.js'] } }, errors: [ { message: 'Unexpected use of file extension "jsx" for "./bar.jsx"', @@ -246,7 +246,7 @@ ruleTester.run('extensions', rule, { }, ], options: ['never', { js: 'always', jsx: 'always' }], - settings: { 'import/resolve': { 'extensions': ['.coffee', '.js'] } }, + settings: { 'import/resolve': { extensions: ['.coffee', '.js'] } }, }), test({ @@ -255,8 +255,8 @@ ruleTester.run('extensions', rule, { 'import barjson from "./bar.json"', 'import barnone from "./bar"', ].join('\n'), - options: [ 'always', { json: 'always', js: 'never', jsx: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + options: ['always', { json: 'always', js: 'never', jsx: 'never' }], + settings: { 'import/resolve': { extensions: ['.js', '.jsx', '.json'] } }, errors: [ { message: 'Unexpected use of file extension "js" for "./bar.js"', @@ -266,14 +266,34 @@ ruleTester.run('extensions', rule, { ], }), + test({ + code: [ + 'import barjs from "."', + 'import barjs2 from ".."', + ].join('\n'), + options: ['always'], + errors: [ + { + message: 'Missing file extension "js" for "."', + line: 1, + column: 19, + }, + { + message: 'Missing file extension "js" for ".."', + line: 2, + column: 20, + }, + ], + }), + test({ code: [ 'import barjs from "./bar.js"', 'import barjson from "./bar.json"', 'import barnone from "./bar"', ].join('\n'), - options: [ 'never', { json: 'always', js: 'never', jsx: 'never' } ], - settings: { 'import/resolve': { 'extensions': [ '.js', '.jsx', '.json' ] } }, + options: ['never', { json: 'always', js: 'never', jsx: 'never' }], + settings: { 'import/resolve': { extensions: ['.js', '.jsx', '.json'] } }, errors: [ { message: 'Unexpected use of file extension "js" for "./bar.js"', @@ -286,7 +306,7 @@ ruleTester.run('extensions', rule, { // unresolved (#271/#295) test({ code: 'import thing from "./fake-file.js"', - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "./fake-file.js"', @@ -297,7 +317,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'import thing from "non-package/test"', - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "non-package/test"', @@ -309,7 +329,7 @@ ruleTester.run('extensions', rule, { test({ code: 'import thing from "@name/pkg/test"', - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "@name/pkg/test"', @@ -321,7 +341,7 @@ ruleTester.run('extensions', rule, { test({ code: 'import thing from "@name/pkg/test.js"', - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "@name/pkg/test.js"', @@ -331,7 +351,6 @@ ruleTester.run('extensions', rule, { ], }), - test({ code: ` import foo from './foo.js' @@ -342,7 +361,7 @@ ruleTester.run('extensions', rule, { import chart from '@/configs/chart' import express from 'express' `, - options: [ 'always', { ignorePackages: true } ], + options: ['always', { ignorePackages: true }], errors: [ { message: 'Missing file extension for "./Component"', @@ -367,7 +386,7 @@ ruleTester.run('extensions', rule, { import chart from '@/configs/chart' import express from 'express' `, - options: [ 'ignorePackages' ], + options: ['ignorePackages'], errors: [ { message: 'Missing file extension for "./Component"', @@ -400,7 +419,7 @@ ruleTester.run('extensions', rule, { column: 31, }, ], - options: [ 'never', { ignorePackages: true } ], + options: ['never', { ignorePackages: true }], }), test({ @@ -416,7 +435,7 @@ ruleTester.run('extensions', rule, { column: 31, }, ], - options: [ 'always', { pattern: { jsx: 'never' } } ], + options: ['always', { pattern: { jsx: 'never' } }], }), // export (#964) @@ -425,7 +444,7 @@ ruleTester.run('extensions', rule, { 'export { foo } from "./foo"', 'let bar; export { bar }', ].join('\n'), - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "./foo"', @@ -439,7 +458,7 @@ ruleTester.run('extensions', rule, { 'export { foo } from "./foo.js"', 'let bar; export { bar }', ].join('\n'), - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "./foo.js"', @@ -452,7 +471,7 @@ ruleTester.run('extensions', rule, { // Query strings. test({ code: 'import withExtension from "./foo.js?a=True"', - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "./foo.js?a=True"', @@ -463,7 +482,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'import withoutExtension from "./foo?a=True.ext"', - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "./foo?a=True.ext"', @@ -478,7 +497,7 @@ ruleTester.run('extensions', rule, { 'const { foo } = require("./foo")', 'export { foo }', ].join('\n'), - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "./foo"', @@ -492,7 +511,7 @@ ruleTester.run('extensions', rule, { 'const { foo } = require("./foo.js")', 'export { foo }', ].join('\n'), - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "./foo.js"', @@ -505,7 +524,7 @@ ruleTester.run('extensions', rule, { // export { } from test({ code: 'export { foo } from "./foo"', - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "./foo"', @@ -533,7 +552,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'export { foo } from "./foo.js"', - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "./foo.js"', @@ -546,7 +565,7 @@ ruleTester.run('extensions', rule, { // export * from test({ code: 'export * from "./foo"', - options: [ 'always' ], + options: ['always'], errors: [ { message: 'Missing file extension for "./foo"', @@ -557,7 +576,7 @@ ruleTester.run('extensions', rule, { }), test({ code: 'export * from "./foo.js"', - options: [ 'never' ], + options: ['never'], errors: [ { message: 'Unexpected use of file extension "js" for "./foo.js"', @@ -595,6 +614,35 @@ ruleTester.run('extensions', rule, { }, ], }), + + // TODO: properly ignore packages resolved via relative imports + test({ + code: [ + 'import * as test from "."', + ].join('\n'), + filename: testFilePath('./internal-modules/test.js'), + options: ['ignorePackages'], + errors: [ + { + message: 'Missing file extension for "."', + line: 1, + }, + ], + }), + // TODO: properly ignore packages resolved via relative imports + test({ + code: [ + 'import * as test from ".."', + ].join('\n'), + filename: testFilePath('./internal-modules/plugins/plugin.js'), + options: ['ignorePackages'], + errors: [ + { + message: 'Missing file extension for ".."', + line: 1, + }, + ], + }), ], }); diff --git a/tests/src/rules/first.js b/tests/src/rules/first.js index 8892ff3d62..f34f227b2d 100644 --- a/tests/src/rules/first.js +++ b/tests/src/rules/first.js @@ -81,7 +81,7 @@ ruleTester.run('first', rule, { code: "if (true) { console.log(1) }import a from 'b'", errors: 1, output: "import a from 'b'\nif (true) { console.log(1) }", - }), + }), ], }); diff --git a/tests/src/rules/named.js b/tests/src/rules/named.js index 227e242ef8..227bffc80d 100644 --- a/tests/src/rules/named.js +++ b/tests/src/rules/named.js @@ -4,12 +4,11 @@ import path from 'path'; import { CASE_SENSITIVE_FS } from 'eslint-module-utils/resolve'; - const ruleTester = new RuleTester(); const rule = require('rules/named'); function error(name, module, type = 'Identifier') { - return { message: name + ' not found in \'' + module + '\'', type }; + return { message: `${name} not found in '${module}'`, type }; } ruleTester.run('named', rule, { @@ -30,11 +29,10 @@ ruleTester.run('named', rule, { test({ code: 'import {RuleTester} from "./re-export-node_modules"' }), test({ code: 'import { jsxFoo } from "./jsx/AnotherComponent"', - settings: { 'import/resolve': { 'extensions': ['.js', '.jsx'] } } }), + settings: { 'import/resolve': { extensions: ['.js', '.jsx'] } } }), // validate that eslint-disable-line silences this properly - test({ code: 'import {a, b, d} from "./common"; ' + - '// eslint-disable-line named' }), + test({ code: 'import {a, b, d} from "./common"; // eslint-disable-line named' }), test({ code: 'import { foo, bar } from "./re-export-names"' }), @@ -192,7 +190,7 @@ ruleTester.run('named', rule, { }, })), - testVersion('>=7.8.0', () =>({ code: 'const { something } = require("./dynamic-import-in-commonjs")', + testVersion('>=7.8.0', () => ({ code: 'const { something } = require("./dynamic-import-in-commonjs")', parserOptions: { ecmaVersion: 2021 }, options: [{ commonjs: true }], })), @@ -210,7 +208,7 @@ ruleTester.run('named', rule, { invalid: [].concat( test({ code: 'import { somethingElse } from "./test-module"', - errors: [ error('somethingElse', './test-module') ] }), + errors: [error('somethingElse', './test-module')] }), test({ code: 'import { baz } from "./bar"', errors: [error('baz', './bar')] }), @@ -323,7 +321,6 @@ ruleTester.run('named', rule, { errors: ["bap not found in './re-export-default'"], }), - // #328: * exports do not include default test({ code: 'import { default as barDefault } from "./re-export"', @@ -333,7 +330,7 @@ ruleTester.run('named', rule, { // es2022: Arbitrary module namespace identifier names testVersion('>= 8.7', () => ({ code: 'import { "somethingElse" as somethingElse } from "./test-module"', - errors: [ error('somethingElse', './test-module', 'Literal') ], + errors: [error('somethingElse', './test-module', 'Literal')], parserOptions: { ecmaVersion: 2022 }, })), testVersion('>= 8.7', () => ({ @@ -381,7 +378,6 @@ ruleTester.run('named (export *)', rule, { ], }); - context('TypeScript', function () { getTSParsers().forEach((parser) => { const settings = { @@ -459,7 +455,7 @@ context('TypeScript', function () { parser, settings, }), - (source === 'typescript-declare' + source === 'typescript-declare' ? testVersion('> 5', () => ({ code: `import { getFoo } from "./${source}"`, parser, @@ -470,7 +466,7 @@ context('TypeScript', function () { parser, settings, }) - ), + , test({ code: `import { MyEnum } from "./${source}"`, parser, diff --git a/tests/src/rules/namespace.js b/tests/src/rules/namespace.js index 163ff163ea..1475ae9b7d 100644 --- a/tests/src/rules/namespace.js +++ b/tests/src/rules/namespace.js @@ -5,7 +5,6 @@ import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester({ env: { es6: true } }); const rule = require('rules/namespace'); - function error(name, namespace) { return { message: `'${name}' not found in imported namespace '${namespace}'.` }; } @@ -14,13 +13,10 @@ const valid = [ test({ code: 'import "./malformed.js"' }), test({ code: "import * as foo from './empty-folder';" }), - test({ code: 'import * as names from "./named-exports"; ' + - 'console.log((names.b).c); ' }), + test({ code: 'import * as names from "./named-exports"; console.log((names.b).c); ' }), - test({ code: 'import * as names from "./named-exports"; ' + - 'console.log(names.a);' }), - test({ code: 'import * as names from "./re-export-names"; ' + - 'console.log(names.foo);' }), + test({ code: 'import * as names from "./named-exports"; console.log(names.a);' }), + test({ code: 'import * as names from "./re-export-names"; console.log(names.foo);' }), test({ code: "import * as elements from './jsx';", parserOptions: { @@ -61,26 +57,23 @@ const valid = [ test({ code: "import * as foo from './common';" }), // destructuring namespaces - test({ code: 'import * as names from "./named-exports";' + - 'const { a } = names' }), - test({ code: 'import * as names from "./named-exports";' + - 'const { d: c } = names' }), - test({ code: 'import * as names from "./named-exports";' + - 'const { c } = foo\n' + - ' , { length } = "names"\n' + - ' , alt = names' }), + test({ code: 'import * as names from "./named-exports"; const { a } = names' }), + test({ code: 'import * as names from "./named-exports"; const { d: c } = names' }), + test({ + code: ` + import * as names from "./named-exports"; + const { c } = foo, + { length } = "names", + alt = names; + `, + }), // deep destructuring only cares about top level - test({ code: 'import * as names from "./named-exports";' + - 'const { ExportedClass: { length } } = names' }), + test({ code: 'import * as names from "./named-exports"; const { ExportedClass: { length } } = names' }), // detect scope redefinition - test({ code: 'import * as names from "./named-exports";' + - 'function b(names) { const { c } = names }' }), - test({ code: 'import * as names from "./named-exports";' + - 'function b() { let names = null; const { c } = names }' }), - test({ code: 'import * as names from "./named-exports";' + - 'const x = function names() { const { c } = names }' }), - + test({ code: 'import * as names from "./named-exports"; function b(names) { const { c } = names }' }), + test({ code: 'import * as names from "./named-exports"; function b() { let names = null; const { c } = names }' }), + test({ code: 'import * as names from "./named-exports"; const x = function names() { const { c } = names }' }), ///////// // es7 // @@ -101,8 +94,7 @@ const valid = [ // respect hoisting test({ code: - 'function x() { console.log((names.b).c); } ' + - 'import * as names from "./named-exports"; ', + 'function x() { console.log((names.b).c); } import * as names from "./named-exports"; ', }), // names.default is valid export @@ -241,13 +233,11 @@ const valid = [ ]; const invalid = [].concat( - test({ code: "import * as names from './named-exports'; " + - ' console.log(names.c);', - errors: [error('c', 'names')] }), + test({ code: "import * as names from './named-exports'; console.log(names.c)", + errors: [error('c', 'names')] }), - test({ code: "import * as names from './named-exports';" + - " console.log(names['a']);", - errors: ["Unable to validate computed reference to imported namespace 'names'."] }), + test({ code: "import * as names from './named-exports'; console.log(names['a']);", + errors: ["Unable to validate computed reference to imported namespace 'names'."] }), // assignment warning (from no-reassign) test({ code: 'import * as foo from \'./bar\'; foo.foo = \'y\';', @@ -269,8 +259,7 @@ const invalid = [].concat( errors: [{ type: 'Property', message: "'c' not found in imported namespace 'names'." }], }), test({ - code: 'import * as names from "./named-exports";' + - 'const { c: { d } } = names', + code: 'import * as names from "./named-exports"; const { c: { d } } = names', errors: [{ type: 'Property', message: "'c' not found in imported namespace 'names'." }], }), @@ -295,20 +284,16 @@ const invalid = [].concat( test({ code: "import b from './deep/default'; console.log(b.e)", - errors: [ "'e' not found in imported namespace 'b'." ], + errors: ["'e' not found in imported namespace 'b'."], }), // respect hoisting test({ - code: - 'console.log(names.c);' + - "import * as names from './named-exports'; ", + code: `console.log(names.c); import * as names from './named-exports';`, errors: [error('c', 'names')], }), test({ - code: - 'function x() { console.log(names.c) } ' + - "import * as names from './named-exports'; ", + code: `function x() { console.log(names.c) } import * as names from './named-exports';`, errors: [error('c', 'names')], }), @@ -332,20 +317,20 @@ const invalid = [].concat( // es2022: Arbitrary module namespace identifier names testVersion('>= 8.7', () => ({ code: `import { "b" as b } from "./deep/a"; console.log(b.e)`, - errors: [ "'e' not found in imported namespace 'b'." ], + errors: ["'e' not found in imported namespace 'b'."], parserOptions: { ecmaVersion: 2022 }, })), testVersion('>= 8.7', () => ({ code: `import { "b" as b } from "./deep/a"; console.log(b.c.e)`, - errors: [ "'e' not found in deeply imported namespace 'b.c'." ], + errors: ["'e' not found in deeply imported namespace 'b.c'."], parserOptions: { ecmaVersion: 2022 }, })), -) +); /////////////////////// // deep dereferences // ////////////////////// -;[['deep', require.resolve('espree')], ['deep-es7', parsers.BABEL_OLD]].forEach(function ([folder, parser]) { // close over params +[['deep', require.resolve('espree')], ['deep-es7', parsers.BABEL_OLD]].forEach(function ([folder, parser]) { // close over params valid.push( test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.c.d.e)` }), test({ parser, code: `import { b } from "./${folder}/a"; console.log(b.c.d.e)` }), @@ -360,32 +345,32 @@ const invalid = [].concat( test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.e)`, - errors: [ "'e' not found in deeply imported namespace 'a.b'." ], + errors: ["'e' not found in deeply imported namespace 'a.b'."], }), test({ parser, code: `import { b } from "./${folder}/a"; console.log(b.e)`, - errors: [ "'e' not found in imported namespace 'b'." ], + errors: ["'e' not found in imported namespace 'b'."], }), test({ parser, code: `import * as a from "./${folder}/a"; console.log(a.b.c.e)`, - errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], + errors: ["'e' not found in deeply imported namespace 'a.b.c'."], }), test({ parser, code: `import { b } from "./${folder}/a"; console.log(b.c.e)`, - errors: [ "'e' not found in deeply imported namespace 'b.c'." ], + errors: ["'e' not found in deeply imported namespace 'b.c'."], }), test({ parser, code: `import * as a from "./${folder}/a"; var {b:{ e }} = a`, - errors: [ "'e' not found in deeply imported namespace 'a.b'." ], + errors: ["'e' not found in deeply imported namespace 'a.b'."], }), test({ parser, code: `import * as a from "./${folder}/a"; var {b:{c:{ e }}} = a`, - errors: [ "'e' not found in deeply imported namespace 'a.b.c'." ], + errors: ["'e' not found in deeply imported namespace 'a.b.c'."], })); }); diff --git a/tests/src/rules/newline-after-import.js b/tests/src/rules/newline-after-import.js index bf91064f85..6a8fb83e40 100644 --- a/tests/src/rules/newline-after-import.js +++ b/tests/src/rules/newline-after-import.js @@ -6,9 +6,7 @@ import { version as tsEslintVersion } from 'typescript-eslint-parser/package.jso import { getTSParsers, parsers, testVersion } from '../utils'; const IMPORT_ERROR_MESSAGE = 'Expected 1 empty line after import statement not followed by another import.'; -const IMPORT_ERROR_MESSAGE_MULTIPLE = (count) => { - return `Expected ${count} empty lines after import statement not followed by another import.`; -}; +const IMPORT_ERROR_MESSAGE_MULTIPLE = (count) => `Expected ${count} empty lines after import statement not followed by another import.`; const REQUIRE_ERROR_MESSAGE = 'Expected 1 empty line after require statement not followed by another require.'; const ruleTester = new RuleTester(); @@ -22,21 +20,21 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { code: ` const x = () => require('baz') , y = () => require('bar')`, - parserOptions: { ecmaVersion: 6 } , + parserOptions: { ecmaVersion: 6 }, }, { code: ` const x = () => require('baz') , y = () => require('bar') - + // some comment here `, - parserOptions: { ecmaVersion: 6 } , + parserOptions: { ecmaVersion: 6 }, options: [{ considerComments: true }], }, { code: `const x = () => require('baz') && require('bar')`, - parserOptions: { ecmaVersion: 6 } , + parserOptions: { ecmaVersion: 6 }, }, { code: ` @@ -45,8 +43,18 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { // Some random single line comment var bar = 42; `, - parserOptions: { ecmaVersion: 6 } , - options: [{ 'considerComments': true }], + parserOptions: { ecmaVersion: 6 }, + options: [{ considerComments: true }], + }, + { + code: ` + const x = () => require('baz') && require('bar') + + // Some random single line comment + var bar = 42; + `, + parserOptions: { ecmaVersion: 6 }, + options: [{ considerComments: true, count: 1, exactCount: true }], }, { code: ` @@ -57,7 +65,7 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { **/ var bar = 42; `, - parserOptions: { ecmaVersion: 6 } , + parserOptions: { ecmaVersion: 6 }, }, `function x() { require('baz'); }`, `a(require('b'), require('c'), require('d'));`, @@ -122,12 +130,52 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { code: `import foo from 'foo';\n\n\nvar bar = 'bar';`, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - options: [{ 'count': 2 }], + options: [{ count: 2 }], + }, + { + code: `import foo from 'foo';\n\n\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 2, exactCount: true }], + }, + { + code: `import foo from 'foo';\n\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 1, exactCount: true }], + }, + { + code: `import foo from 'foo';\n\n// Some random comment\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 2, exactCount: true }], + }, + { + code: `import foo from 'foo';\n// Some random comment\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 1, exactCount: true }], + }, + { + code: `import foo from 'foo';\n\n\n// Some random comment\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 2, exactCount: true, considerComments: true }], + }, + { + code: `import foo from 'foo';\n\n// Some random comment\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 1, exactCount: true, considerComments: true }], + }, + { + code: `/**\n * A leading comment\n */\nimport foo from 'foo';\n\n// Some random comment\nexport {foo};`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 2, exactCount: true }], + }, + { + code: `import foo from 'foo';\n\n\nvar bar = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 1 }], }, { code: `import foo from 'foo';\n\n\n\n\nvar bar = 'bar';`, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - options: [{ 'count': 4 }], + options: [{ count: 4 }], }, { code: `var foo = require('foo-module');\n\nvar foo = 'bar';`, @@ -136,12 +184,27 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { code: `var foo = require('foo-module');\n\n\nvar foo = 'bar';`, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - options: [{ 'count': 2 }], + options: [{ count: 2 }], + }, + { + code: `var foo = require('foo-module');\n\n\n\n\nvar foo = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 4 }], }, { code: `var foo = require('foo-module');\n\n\n\n\nvar foo = 'bar';`, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - options: [{ 'count': 4 }], + options: [{ count: 4, exactCount: true }], + }, + { + code: `var foo = require('foo-module');\n\n// Some random comment\n\n\nvar foo = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 4, exactCount: true }], + }, + { + code: `var foo = require('foo-module');\n\n\n\n// Some random comment\nvar foo = 'bar';`, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ count: 4, exactCount: true, considerComments: true }], }, { code: `require('foo-module');\n\nvar foo = 'bar';`, @@ -202,12 +265,12 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parser: parsers.BABEL_OLD, }, { - code : `// issue 1004\nimport foo from 'foo';\n\n@SomeDecorator(foo)\nexport default class Test {}`, + code: `// issue 1004\nimport foo from 'foo';\n\n@SomeDecorator(foo)\nexport default class Test {}`, parserOptions: { sourceType: 'module' }, parser: parsers.BABEL_OLD, }, { - code : `// issue 1004\nconst foo = require('foo');\n\n@SomeDecorator(foo)\nexport default class Test {}`, + code: `// issue 1004\nconst foo = require('foo');\n\n@SomeDecorator(foo)\nexport default class Test {}`, parserOptions: { sourceType: 'module' }, parser: parsers.BABEL_OLD, }, @@ -275,6 +338,16 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parser, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, + { + code: ` + import { ns } from 'namespace'; + import Bar = ns.baz.foo.Bar; + + export import Foo = ns.baz.bar.Foo; + `, + parser, + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, )), { code: ` @@ -296,30 +369,30 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { **/ var bar = 42; `, - parserOptions: { ecmaVersion: 2015, sourceType: 'module' } , + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { code: ` import path from 'path';import foo from 'foo'; - + /** * some multiline comment here * another line of comment **/ var bar = 42; `, - parserOptions: { ecmaVersion: 2015, sourceType: 'module' } , - options: [{ 'considerComments': true }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ considerComments: true }], }, { code: ` import path from 'path'; import foo from 'foo'; - + // Some random single line comment var bar = 42; `, - parserOptions: { ecmaVersion: 2015, sourceType: 'module' } , + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, ), @@ -338,13 +411,13 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { // some comment var foo = 'bar'; `, - errors: [ { + errors: [{ line: 3, column: 1, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, - options: [{ 'considerComments': true }], + options: [{ considerComments: true }], }, { code: ` @@ -365,13 +438,13 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { **/ var bar = 42; `, - errors: [ { + errors: [{ line: 3, column: 9, message: IMPORT_ERROR_MESSAGE, - } ], - parserOptions: { ecmaVersion: 2015, sourceType: 'module' } , - options: [{ 'considerComments': true }], + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ considerComments: true }], }, { code: ` @@ -386,54 +459,54 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { // Some random single line comment var bar = 42; `, - errors: [ { + errors: [{ line: 3, column: 9, message: IMPORT_ERROR_MESSAGE, - } ], - parserOptions: { ecmaVersion: 2015, sourceType: 'module' } , - options: [{ 'considerComments': true, 'count': 1 }], + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ considerComments: true, count: 1 }], }, { code: `import foo from 'foo';\nexport default function() {};`, output: `import foo from 'foo';\n\nexport default function() {};`, - errors: [ { + errors: [{ line: 1, column: 1, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { code: `import foo from 'foo';\n\nexport default function() {};`, output: `import foo from 'foo';\n\n\nexport default function() {};`, - options: [{ 'count': 2 }], - errors: [ { + options: [{ count: 2 }], + errors: [{ line: 1, column: 1, message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { code: `var foo = require('foo-module');\nvar something = 123;`, output: `var foo = require('foo-module');\n\nvar something = 123;`, - errors: [ { + errors: [{ line: 1, column: 1, message: REQUIRE_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { code: `import foo from 'foo';\nexport default function() {};`, output: `import foo from 'foo';\n\nexport default function() {};`, - options: [{ 'count': 1 }], - errors: [ { + options: [{ count: 1 }], + errors: [{ line: 1, column: 1, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { @@ -487,20 +560,20 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { code: `var path = require('path');\nvar foo = require('foo');\nvar bar = 42;`, output: `var path = require('path');\nvar foo = require('foo');\n\nvar bar = 42;`, - errors: [ { + errors: [{ line: 2, column: 1, message: REQUIRE_ERROR_MESSAGE, - } ], + }], }, { code: `var assign = Object.assign || require('object-assign');\nvar foo = require('foo');\nvar bar = 42;`, output: `var assign = Object.assign || require('object-assign');\nvar foo = require('foo');\n\nvar bar = 42;`, - errors: [ { + errors: [{ line: 2, column: 1, message: REQUIRE_ERROR_MESSAGE, - } ], + }], }, { code: `require('a');\nfoo(require('b'), require('c'), require('d'));\nrequire('d');\nvar foo = 'bar';`, @@ -527,64 +600,64 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { { code: `import path from 'path';\nimport foo from 'foo';\nvar bar = 42;`, output: `import path from 'path';\nimport foo from 'foo';\n\nvar bar = 42;`, - errors: [ { + errors: [{ line: 2, column: 1, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { code: `import path from 'path';import foo from 'foo';var bar = 42;`, output: `import path from 'path';import foo from 'foo';\n\nvar bar = 42;`, - errors: [ { + errors: [{ line: 1, column: 25, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, { code: `import foo from 'foo';\n@SomeDecorator(foo)\nclass Foo {}`, output: `import foo from 'foo';\n\n@SomeDecorator(foo)\nclass Foo {}`, - errors: [ { + errors: [{ line: 1, column: 1, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, parser: parsers.BABEL_OLD, }, { code: `var foo = require('foo');\n@SomeDecorator(foo)\nclass Foo {}`, output: `var foo = require('foo');\n\n@SomeDecorator(foo)\nclass Foo {}`, - errors: [ { + errors: [{ line: 1, column: 1, message: REQUIRE_ERROR_MESSAGE, - } ], + }], parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, parser: parsers.BABEL_OLD, }, { code: `// issue 10042\nimport foo from 'foo';\n@SomeDecorator(foo)\nexport default class Test {}`, output: `// issue 10042\nimport foo from 'foo';\n\n@SomeDecorator(foo)\nexport default class Test {}`, - errors: [ { + errors: [{ line: 2, column: 1, message: IMPORT_ERROR_MESSAGE, - } ], + }], parserOptions: { sourceType: 'module' }, parser: parsers.BABEL_OLD, }, { code: `// issue 1004\nconst foo = require('foo');\n@SomeDecorator(foo)\nexport default class Test {}`, output: `// issue 1004\nconst foo = require('foo');\n\n@SomeDecorator(foo)\nexport default class Test {}`, - errors: [ { + errors: [{ line: 2, column: 1, message: REQUIRE_ERROR_MESSAGE, - } ], + }], parserOptions: { sourceType: 'module' }, parser: parsers.BABEL_OLD, }, @@ -612,5 +685,181 @@ ruleTester.run('newline-after-import', require('rules/newline-after-import'), { parserOptions: { sourceType: 'module' }, parser: parsers.BABEL_OLD, })) || [], + { + code: `import foo from 'foo';\n\nexport default function() {};`, + output: `import foo from 'foo';\n\n\nexport default function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n\n\n\nexport default function() {};`, + output: `import foo from 'foo';\n\n\n\nexport default function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n\n\n\n\nexport default function() {};`, + output: `import foo from 'foo';\n\n\n\n\nexport default function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n// some random comment\nexport default function() {};`, + output: `import foo from 'foo';\n\n// some random comment\nexport default function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n// some random comment\n\n\nexport default function() {};`, + output: `import foo from 'foo';\n// some random comment\n\n\nexport default function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n// some random comment\n\n\n\nexport default function() {};`, + output: `import foo from 'foo';\n// some random comment\n\n\n\nexport default function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n// some random comment\nexport default function() {};`, + output: `import foo from 'foo';\n\n\n// some random comment\nexport default function() {};`, + options: [{ count: 2, exactCount: true, considerComments: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n\n// some random comment\nexport default function() {};`, + output: `import foo from 'foo';\n\n\n// some random comment\nexport default function() {};`, + options: [{ count: 2, exactCount: true, considerComments: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `import foo from 'foo';\n\n\n\n// some random comment\nexport default function() {};`, + output: `import foo from 'foo';\n\n\n\n// some random comment\nexport default function() {};`, + options: [{ count: 2, exactCount: true, considerComments: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE_MULTIPLE(2), + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: ` + import foo from 'foo'; + + + // Some random single line comment + var bar = 42; + `, + output: ` + import foo from 'foo'; + + + // Some random single line comment + var bar = 42; + `, + errors: [{ + line: 2, + column: 9, + message: IMPORT_ERROR_MESSAGE, + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + options: [{ considerComments: true, count: 1, exactCount: true }], + }, + { + code: `import foo from 'foo';export default function() {};`, + output: `import foo from 'foo';\n\nexport default function() {};`, + options: [{ count: 1, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE, + }], + parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, + }, + { + code: `const foo = require('foo');\n\n\n\nconst bar = function() {};`, + output: `const foo = require('foo');\n\n\n\nconst bar = function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: 'Expected 2 empty lines after require statement not followed by another require.', + }], + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: `const foo = require('foo');\n\n\n\n// some random comment\nconst bar = function() {};`, + output: `const foo = require('foo');\n\n\n\n// some random comment\nconst bar = function() {};`, + options: [{ count: 2, exactCount: true }], + errors: [{ + line: 1, + column: 1, + message: 'Expected 2 empty lines after require statement not followed by another require.', + }], + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: `import foo from 'foo';// some random comment\nexport default function() {};`, + output: `import foo from 'foo';\n\n// some random comment\nexport default function() {};`, + options: [{ count: 1, exactCount: true, considerComments: true }], + errors: [{ + line: 1, + column: 1, + message: IMPORT_ERROR_MESSAGE, + }], + parserOptions: { ecmaVersion: 2015, considerComments: true, sourceType: 'module' }, + }, + { + code: `const foo = require('foo');\n\n\n// some random comment\nconst bar = function() {};`, + options: [{ count: 2, exactCount: true, considerComments: true }], + errors: [{ + line: 1, + column: 1, + message: 'Expected 2 empty lines after require statement not followed by another require.', + }], + parserOptions: { ecmaVersion: 2015 }, + }, ), }); diff --git a/tests/src/rules/no-amd.js b/tests/src/rules/no-amd.js index 91e29234c8..5317aa8fde 100644 --- a/tests/src/rules/no-amd.js +++ b/tests/src/rules/no-amd.js @@ -28,10 +28,10 @@ ruleTester.run('no-amd', require('rules/no-amd'), { ], invalid: semver.satisfies(eslintPkg.version, '< 4.0.0') ? [] : [ - { code: 'define([], function() {})', errors: [ { message: 'Expected imports instead of AMD define().' }] }, - { code: 'define(["a"], function(a) { console.log(a); })', errors: [ { message: 'Expected imports instead of AMD define().' }] }, + { code: 'define([], function() {})', errors: [{ message: 'Expected imports instead of AMD define().' }] }, + { code: 'define(["a"], function(a) { console.log(a); })', errors: [{ message: 'Expected imports instead of AMD define().' }] }, - { code: 'require([], function() {})', errors: [ { message: 'Expected imports instead of AMD require().' }] }, - { code: 'require(["a"], function(a) { console.log(a); })', errors: [ { message: 'Expected imports instead of AMD require().' }] }, + { code: 'require([], function() {})', errors: [{ message: 'Expected imports instead of AMD require().' }] }, + { code: 'require(["a"], function(a) { console.log(a); })', errors: [{ message: 'Expected imports instead of AMD require().' }] }, ], }); diff --git a/tests/src/rules/no-commonjs.js b/tests/src/rules/no-commonjs.js index a2e3464ca7..b7c0aa803f 100644 --- a/tests/src/rules/no-commonjs.js +++ b/tests/src/rules/no-commonjs.js @@ -19,12 +19,12 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { { code: 'export default "x"', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, { code: 'export function house() {}', parserOptions: { ecmaVersion: 2015, sourceType: 'module' } }, { - code: - 'function someFunc() {\n'+ - ' const exports = someComputation();\n'+ - '\n'+ - ' expect(exports.someProp).toEqual({ a: \'value\' });\n'+ - '}', + code: ` + function someFunc() { + const exports = someComputation(); + expect(exports.someProp).toEqual({ a: 'value' }); + } + `, parserOptions: { ecmaVersion: 2015, sourceType: 'module' }, }, @@ -68,48 +68,48 @@ ruleTester.run('no-commonjs', require('rules/no-commonjs'), { invalid: [ // imports - ...(semver.satisfies(eslintPkg.version, '< 4.0.0') ? [] : [ - { code: 'var x = require("x")', output: 'var x = require("x")', errors: [ { message: IMPORT_MESSAGE }] }, - { code: 'x = require("x")', output: 'x = require("x")', errors: [ { message: IMPORT_MESSAGE }] }, - { code: 'require("x")', output: 'require("x")', errors: [ { message: IMPORT_MESSAGE }] }, + ...semver.satisfies(eslintPkg.version, '< 4.0.0') ? [] : [ + { code: 'var x = require("x")', output: 'var x = require("x")', errors: [{ message: IMPORT_MESSAGE }] }, + { code: 'x = require("x")', output: 'x = require("x")', errors: [{ message: IMPORT_MESSAGE }] }, + { code: 'require("x")', output: 'require("x")', errors: [{ message: IMPORT_MESSAGE }] }, { code: 'require(`x`)', parserOptions: { ecmaVersion: 2015 }, output: 'require(`x`)', - errors: [ { message: IMPORT_MESSAGE }], + errors: [{ message: IMPORT_MESSAGE }], }, { code: 'if (typeof window !== "undefined") require("x")', options: [{ allowConditionalRequire: false }], output: 'if (typeof window !== "undefined") require("x")', - errors: [ { message: IMPORT_MESSAGE }], + errors: [{ message: IMPORT_MESSAGE }], }, { code: 'if (typeof window !== "undefined") { require("x") }', options: [{ allowConditionalRequire: false }], output: 'if (typeof window !== "undefined") { require("x") }', - errors: [ { message: IMPORT_MESSAGE }], + errors: [{ message: IMPORT_MESSAGE }], }, { code: 'try { require("x") } catch (error) {}', options: [{ allowConditionalRequire: false }], output: 'try { require("x") } catch (error) {}', - errors: [ { message: IMPORT_MESSAGE }], + errors: [{ message: IMPORT_MESSAGE }], }, - ]), + ], // exports - { code: 'exports.face = "palm"', output: 'exports.face = "palm"', errors: [ { message: EXPORT_MESSAGE }] }, - { code: 'module.exports.face = "palm"', output: 'module.exports.face = "palm"', errors: [ { message: EXPORT_MESSAGE }] }, - { code: 'module.exports = face', output: 'module.exports = face', errors: [ { message: EXPORT_MESSAGE }] }, - { code: 'exports = module.exports = {}', output: 'exports = module.exports = {}', errors: [ { message: EXPORT_MESSAGE }] }, - { code: 'var x = module.exports = {}', output: 'var x = module.exports = {}', errors: [ { message: EXPORT_MESSAGE }] }, + { code: 'exports.face = "palm"', output: 'exports.face = "palm"', errors: [{ message: EXPORT_MESSAGE }] }, + { code: 'module.exports.face = "palm"', output: 'module.exports.face = "palm"', errors: [{ message: EXPORT_MESSAGE }] }, + { code: 'module.exports = face', output: 'module.exports = face', errors: [{ message: EXPORT_MESSAGE }] }, + { code: 'exports = module.exports = {}', output: 'exports = module.exports = {}', errors: [{ message: EXPORT_MESSAGE }] }, + { code: 'var x = module.exports = {}', output: 'var x = module.exports = {}', errors: [{ message: EXPORT_MESSAGE }] }, { code: 'module.exports = {}', options: ['allow-primitive-modules'], output: 'module.exports = {}', - errors: [ { message: EXPORT_MESSAGE }], + errors: [{ message: EXPORT_MESSAGE }], }, { code: 'var x = module.exports', options: ['allow-primitive-modules'], output: 'var x = module.exports', - errors: [ { message: EXPORT_MESSAGE }], + errors: [{ message: EXPORT_MESSAGE }], }, ], }); diff --git a/tests/src/rules/no-cycle.js b/tests/src/rules/no-cycle.js index 155f257b71..d2adbf61f9 100644 --- a/tests/src/rules/no-cycle.js +++ b/tests/src/rules/no-cycle.js @@ -6,9 +6,9 @@ import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester(); const rule = require('rules/no-cycle'); -const error = message => ({ message }); +const error = (message) => ({ message }); -const test = def => _test(Object.assign(def, { +const test = (def) => _test(Object.assign(def, { filename: testFilePath('./cycles/depth-zero.js'), })); const testVersion = (specifier, t) => _testVersion(specifier, () => Object.assign(t(), { diff --git a/tests/src/rules/no-deprecated.js b/tests/src/rules/no-deprecated.js index 290946735f..318ea7c368 100644 --- a/tests/src/rules/no-deprecated.js +++ b/tests/src/rules/no-deprecated.js @@ -38,7 +38,6 @@ ruleTester.run('no-deprecated', rule, { code: "import { deepDep } from './deep-deprecated'; function x(deepDep) { console.log(deepDep.MY_TERRIBLE_ACTION) }", }), - ...SYNTAX_CASES, ], invalid: [ @@ -210,18 +209,20 @@ describe('TypeScript', function () { ruleTester.run(parser, rule, { valid: [ - test(Object.assign({ + test({ code: 'import * as hasDeprecated from \'./ts-deprecated.ts\'', - }, parserConfig)), + ...parserConfig, + }), ], invalid: [ - test(Object.assign({ + test({ code: 'import { foo } from \'./ts-deprecated.ts\'; console.log(foo())', errors: [ { type: 'ImportSpecifier', message: 'Deprecated: don\'t use this!' }, { type: 'Identifier', message: 'Deprecated: don\'t use this!' }, - ] }, - parserConfig)), + ], + ...parserConfig, + }), ], }); }); diff --git a/tests/src/rules/no-duplicates.js b/tests/src/rules/no-duplicates.js index f8a27a743b..f83221105a 100644 --- a/tests/src/rules/no-duplicates.js +++ b/tests/src/rules/no-duplicates.js @@ -5,13 +5,14 @@ import jsxConfig from '../../../config/react'; import { RuleTester } from 'eslint'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; +import flatMap from 'array.prototype.flatmap'; const ruleTester = new RuleTester(); const rule = require('rules/no-duplicates'); // autofix only possible with eslint 4+ const test = semver.satisfies(eslintPkg.version, '< 4') - ? t => testUtil(Object.assign({}, t, { output: t.code })) + ? (t) => testUtil({ ...t, output: t.code }) : testUtil; ruleTester.run('no-duplicates', rule, { @@ -21,8 +22,7 @@ ruleTester.run('no-duplicates', rule, { test({ code: "import { x } from './foo'; import { y } from './bar'" }), // #86: every unresolved module should not show up as 'null' and duplicate - test({ code: 'import foo from "234artaf";' + - 'import { shoop } from "234q25ad"' }), + test({ code: 'import foo from "234artaf"; import { shoop } from "234q25ad"' }), // #225: ignore duplicate if is a flow type import test({ @@ -33,12 +33,12 @@ ruleTester.run('no-duplicates', rule, { // #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 }], + options: [{ considerQueryString: true }], settings: { 'import/resolver': 'webpack' }, }), test({ code: "import x from './foo'; import y from './bar';", - options: [{ 'considerQueryString': true }], + options: [{ considerQueryString: true }], settings: { 'import/resolver': 'webpack' }, }), @@ -67,10 +67,11 @@ ruleTester.run('no-duplicates', rule, { test({ code: "import { x } from './bar'; import { y } from 'bar';", output: "import { x , y } from './bar'; ", - settings: { 'import/resolve': { - paths: [path.join( process.cwd() - , 'tests', 'files', - )] } }, + settings: { + 'import/resolve': { + paths: [path.join(process.cwd(), 'tests', 'files')], + }, + }, errors: 2, // path ends up hardcoded }), @@ -89,7 +90,7 @@ ruleTester.run('no-duplicates', rule, { // #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 }], + options: [{ considerQueryString: true }], settings: { 'import/resolver': 'webpack' }, errors: 2, // path ends up hardcoded }), @@ -130,6 +131,36 @@ ruleTester.run('no-duplicates', rule, { errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], }), + // These test cases use duplicate import identifiers, which causes a fatal parsing error using ESPREE (default) and TS_OLD. + ...flatMap([parsers.BABEL_OLD, parsers.TS_NEW], (parser) => { + if (!parser) { return []; } // TS_NEW is not always available + return [ + // #2347: duplicate identifiers should be removed + test({ + code: "import {a} from './foo'; import { a } from './foo'", + output: "import {a} from './foo'; ", + errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], + parser, + }), + + // #2347: duplicate identifiers should be removed + test({ + code: "import {a,b} from './foo'; import { b, c } from './foo'; import {b,c,d} from './foo'", + output: "import {a,b, c ,d} from './foo'; ", + errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], + parser, + }), + + // #2347: duplicate identifiers should be removed, but not if they are adjacent to comments + test({ + code: "import {a} from './foo'; import { a/*,b*/ } from './foo'", + output: "import {a, a/*,b*/ } from './foo'; ", + errors: ['\'./foo\' imported multiple times.', '\'./foo\' imported multiple times.'], + parser, + }), + ]; + }), + test({ code: "import {x} from './foo'; import {} from './foo'; import {/*c*/} from './foo'; import {y} from './foo'", output: "import {x/*c*/,y} from './foo'; ", @@ -451,6 +482,68 @@ import {x,y} from './foo' errors: ["'../constants' imported multiple times.", "'../constants' imported multiple times."], ...jsxConfig, }), + + test({ + code: ` + import {A1,} from 'foo'; + import {B1,} from 'foo'; + import {C1,} from 'foo'; + + import { + A2, + } from 'bar'; + import { + B2, + } from 'bar'; + import { + C2, + } from 'bar'; + + `, + output: ` + import {A1,B1,C1} from 'foo'; + ${''} + import { + A2, + ${''} + B2, + C2} from 'bar'; + ${''} + `, + errors: [ + { + message: "'foo' imported multiple times.", + line: 2, + column: 27, + }, + { + message: "'foo' imported multiple times.", + line: 3, + column: 27, + }, + { + message: "'foo' imported multiple times.", + line: 4, + column: 27, + }, + { + message: "'bar' imported multiple times.", + line: 8, + column: 16, + }, + { + message: "'bar' imported multiple times.", + line: 11, + column: 16, + }, + { + message: "'bar' imported multiple times.", + line: 14, + column: 16, + }, + ], + ...jsxConfig, + }), ], }); @@ -680,6 +773,25 @@ context('TypeScript', function () { }, ], }), + // #2834 Detect duplicates across type and regular imports + test({ + code: "import {AValue} from './foo'; import type {AType} from './foo'", + ...parserConfig, + options: [{ 'prefer-inline': true }], + output: `import {AValue,type AType} from './foo'; `, + errors: [ + { + line: 1, + column: 22, + message: "'./foo' imported multiple times.", + }, + { + line: 1, + column: 56, + message: "'./foo' imported multiple times.", + }, + ], + }), ]); ruleTester.run('no-duplicates', rule, { diff --git a/tests/src/rules/no-dynamic-require.js b/tests/src/rules/no-dynamic-require.js index 0b141ccd76..e316470ec8 100644 --- a/tests/src/rules/no-dynamic-require.js +++ b/tests/src/rules/no-dynamic-require.js @@ -30,10 +30,9 @@ ruleTester.run('no-dynamic-require', rule, { //dynamic import ...flatMap([parsers.ESPREE, parsers.BABEL_OLD], (parser) => { - const _test = - parser === parsers.ESPREE - ? (testObj) => testVersion('>= 6.2.0', () => testObj) - : (testObj) => test(testObj); + const _test = parser === parsers.ESPREE + ? (testObj) => testVersion('>= 6.2.0', () => testObj) + : (testObj) => test(testObj); return [].concat( _test({ code: 'import("foo")', @@ -143,10 +142,9 @@ ruleTester.run('no-dynamic-require', rule, { // dynamic import ...flatMap([parsers.ESPREE, parsers.BABEL_OLD], (parser) => { - const _test = - parser === parsers.ESPREE - ? (testObj) => testVersion('>= 6.2.0', () => testObj) - : (testObj) => test(testObj); + const _test = parser === parsers.ESPREE + ? (testObj) => testVersion('>= 6.2.0', () => testObj) + : (testObj) => test(testObj); return [].concat( _test({ code: 'import("../" + name)', diff --git a/tests/src/rules/no-empty-named-blocks.js b/tests/src/rules/no-empty-named-blocks.js index ee21db3478..f65e5a2045 100644 --- a/tests/src/rules/no-empty-named-blocks.js +++ b/tests/src/rules/no-empty-named-blocks.js @@ -5,9 +5,8 @@ import { RuleTester } from 'eslint'; const ruleTester = new RuleTester(); const rule = require('rules/no-empty-named-blocks'); - function generateSuggestionsTestCases(cases, parser) { - return cases.map(code => test({ + return cases.map((code) => test({ code, parser, errors: [{ @@ -42,9 +41,27 @@ ruleTester.run('no-empty-named-blocks', rule, { ] : [], // Flow - test({ code: `import typeof Default from 'mod';`, parser: parsers.BABEL_OLD }), - test({ code: `import typeof { Named } from 'mod';`, parser: parsers.BABEL_OLD }), - test({ code: `import typeof Default, { Named } from 'mod';`, parser: parsers.BABEL_OLD }), + test({ code: `import typeof Default from 'mod'; // babel old`, parser: parsers.BABEL_OLD }), + test({ code: `import typeof { Named } from 'mod'; // babel old`, parser: parsers.BABEL_OLD }), + test({ code: `import typeof Default, { Named } from 'mod'; // babel old`, parser: parsers.BABEL_OLD }), + test({ + code: ` + module.exports = { + rules: { + 'keyword-spacing': ['error', {overrides: {}}], + } + }; + `, + }), + test({ + code: ` + import { DESCRIPTORS, NODE } from '../helpers/constants'; + // ... + import { timeLimitedPromise } from '../helpers/helpers'; + // ... + import { DESCRIPTORS2 } from '../helpers/constants'; + `, + }), ), invalid: [].concat( test({ diff --git a/tests/src/rules/no-extraneous-dependencies.js b/tests/src/rules/no-extraneous-dependencies.js index c1018a9149..cb0398ada2 100644 --- a/tests/src/rules/no-extraneous-dependencies.js +++ b/tests/src/rules/no-extraneous-dependencies.js @@ -57,7 +57,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { // 'project' type test({ code: 'import "importType"', - settings: { 'import/resolver': { node: { paths: [ path.join(__dirname, '../../files') ] } } }, + settings: { 'import/resolver': { node: { paths: [path.join(__dirname, '../../files')] } } }, }), test({ code: 'import chai from "chai"', @@ -308,7 +308,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { code: 'import foo from "foo"', options: [{ packageDir: packageDirWithSyntaxError }], errors: [{ - message: 'The package.json file could not be parsed: ' + packageFileWithSyntaxErrorMessage, + message: `The package.json file could not be parsed: ${packageFileWithSyntaxErrorMessage}`, }], }), test({ @@ -396,7 +396,7 @@ ruleTester.run('no-extraneous-dependencies', rule, { test({ code: 'import "not-a-dependency"', settings: { - 'import/resolver': { node: { paths: [ path.join(__dirname, '../../files') ] } }, + 'import/resolver': { node: { paths: [path.join(__dirname, '../../files')] } }, 'import/internal-regex': '^not-a-dependency.*', }, options: [{ includeInternal: true }], @@ -422,31 +422,41 @@ describe('TypeScript', () => { ruleTester.run('no-extraneous-dependencies', rule, { valid: [ - test(Object.assign({ + test({ code: 'import type T from "a";', options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false }], - }, parserConfig)), + ...parserConfig, + }), + + test({ + code: 'import type { T } from "a"; export type { T };', + options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false }], + ...parserConfig, + }), + + test({ + code: 'export type { T } from "a";', + options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false }], + ...parserConfig, + }), ], invalid: [ - test(Object.assign({ + test({ code: 'import T from "a";', options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false }], - errors: [{ - message: "'a' should be listed in the project's dependencies, not devDependencies.", - }], - }, parserConfig)), + errors: [{ message: "'a' should be listed in the project's dependencies, not devDependencies." }], + ...parserConfig, + }), - test(Object.assign({ - code: 'import type T from "a";', - options: [{ + test({ code: 'import type T from "a";', + options: [{ packageDir: packageDirWithTypescriptDevDependencies, devDependencies: false, includeTypes: true, }], - errors: [{ - message: "'a' should be listed in the project's dependencies, not devDependencies.", - }], - }, parserConfig)), + errors: [{ message: "'a' should be listed in the project's dependencies, not devDependencies." }], + ...parserConfig, + }), ], }); }); @@ -464,6 +474,16 @@ typescriptRuleTester.run('no-extraneous-dependencies typescript type imports', r filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), parser: parsers.BABEL_OLD, }), + test({ + code: 'import { type MyType } from "not-a-dependency";', + filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), + parser: parsers.BABEL_OLD, + }), + test({ + code: 'import { type MyType, type OtherType } from "not-a-dependency";', + filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), + parser: parsers.BABEL_OLD, + }), ], invalid: [ test({ @@ -475,5 +495,30 @@ typescriptRuleTester.run('no-extraneous-dependencies typescript type imports', r message: `'not-a-dependency' should be listed in the project's dependencies. Run 'npm i -S not-a-dependency' to add it`, }], }), + test({ + code: `import type { Foo } from 'not-a-dependency';`, + options: [{ includeTypes: true }], + filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), + parser: parsers.BABEL_OLD, + errors: [{ + message: `'not-a-dependency' should be listed in the project's dependencies. Run 'npm i -S not-a-dependency' to add it`, + }], + }), + test({ + code: 'import Foo, { type MyType } from "not-a-dependency";', + filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), + parser: parsers.BABEL_OLD, + errors: [{ + message: `'not-a-dependency' should be listed in the project's dependencies. Run 'npm i -S not-a-dependency' to add it`, + }], + }), + test({ + code: 'import { type MyType, Foo } from "not-a-dependency";', + filename: testFilePath('./no-unused-modules/typescript/file-ts-a.ts'), + parser: parsers.BABEL_OLD, + errors: [{ + 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-import-module-exports.js b/tests/src/rules/no-import-module-exports.js index a40eb7e276..c2bf7ed132 100644 --- a/tests/src/rules/no-import-module-exports.js +++ b/tests/src/rules/no-import-module-exports.js @@ -1,7 +1,7 @@ import path from 'path'; import { RuleTester } from 'eslint'; -import { test, testVersion } from '../utils'; +import { eslintVersionSatisfies, test, testVersion } from '../utils'; const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6, sourceType: 'module' }, @@ -9,8 +9,7 @@ const ruleTester = new RuleTester({ const rule = require('rules/no-import-module-exports'); const error = { - message: `Cannot use import declarations in modules that export using CommonJS ` + - `(module.exports = 'foo' or exports.bar = 'hi')`, + message: `Cannot use import declarations in modules that export using CommonJS (module.exports = 'foo' or exports.bar = 'hi')`, type: 'ImportDeclaration', }; @@ -40,6 +39,12 @@ ruleTester.run('no-import-module-exports', rule, { exports.foo = bar `, }), + eslintVersionSatisfies('>= 4') ? test({ + code: ` + import { module } from 'qunit' + module.skip('A test', function () {}) + `, + }) : [], test({ code: ` import foo from 'path'; diff --git a/tests/src/rules/no-internal-modules.js b/tests/src/rules/no-internal-modules.js index 4a733d142a..c1c3015453 100644 --- a/tests/src/rules/no-internal-modules.js +++ b/tests/src/rules/no-internal-modules.js @@ -41,51 +41,51 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'import b from "../../api/service"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - allow: [ '**/api/*' ], - } ], + options: [{ + allow: ['**/api/*'], + }], }), test({ code: 'import "jquery/dist/jquery"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - allow: [ 'jquery/dist/*' ], - } ], + options: [{ + allow: ['jquery/dist/*'], + }], }), test({ code: 'import "./app/index.js";\nimport "./app/index"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - allow: [ '**/index{.js,}' ], - } ], + options: [{ + allow: ['**/index{.js,}'], + }], }), test({ code: 'import a from "./plugin2/thing"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - forbid: [ '**/api/*' ], - } ], + options: [{ + forbid: ['**/api/*'], + }], }), test({ code: 'const a = require("./plugin2/thing")', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - forbid: [ '**/api/*' ], - } ], + options: [{ + forbid: ['**/api/*'], + }], }), test({ code: 'import b from "app/a"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ 'app/**/**' ], - } ], + options: [{ + forbid: ['app/**/**'], + }], }), test({ code: 'import b from "@org/package"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '@org/package/*' ], - } ], + options: [{ + forbid: ['@org/package/*'], + }], }), // exports test({ @@ -103,23 +103,23 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'export {b} from "../../api/service"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - allow: [ '**/api/*' ], - } ], + options: [{ + allow: ['**/api/*'], + }], }), test({ code: 'export * from "jquery/dist/jquery"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - allow: [ 'jquery/dist/*' ], - } ], + options: [{ + allow: ['jquery/dist/*'], + }], }), test({ code: 'export * from "./app/index.js";\nexport * from "./app/index"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - allow: [ '**/index{.js,}' ], - } ], + options: [{ + allow: ['**/index{.js,}'], + }], }), test({ code: ` @@ -145,30 +145,30 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'export * from "./plugin2/thing"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - forbid: [ '**/api/*' ], - } ], + options: [{ + forbid: ['**/api/*'], + }], }), test({ code: 'export * from "app/a"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ 'app/**/**' ], - } ], + options: [{ + forbid: ['app/**/**'], + }], }), test({ code: 'export { b } from "@org/package"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '@org/package/*' ], - } ], + options: [{ + forbid: ['@org/package/*'], + }], }), test({ code: 'export * from "./app/index.js";\nexport * from "./app/index"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '**/index.ts' ], - } ], + options: [{ + forbid: ['**/index.ts'], + }], }), ], @@ -177,39 +177,39 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'import "./plugin2/index.js";\nimport "./plugin2/app/index"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - allow: [ '*/index.js' ], - } ], - errors: [ { + options: [{ + allow: ['*/index.js'], + }], + errors: [{ message: 'Reaching to "./plugin2/app/index" is not allowed.', line: 2, column: 8, - } ], + }], }), test({ code: 'import "./app/index.js"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - errors: [ { + errors: [{ message: 'Reaching to "./app/index.js" is not allowed.', line: 1, column: 8, - } ], + }], }), test({ code: 'import b from "./plugin2/internal"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - errors: [ { + errors: [{ message: 'Reaching to "./plugin2/internal" is not allowed.', line: 1, column: 15, - } ], + }], }), test({ code: 'import a from "../api/service/index"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - allow: [ '**/internal-modules/*' ], - } ], + options: [{ + allow: ['**/internal-modules/*'], + }], errors: [ { message: 'Reaching to "../api/service/index" is not allowed.', @@ -243,58 +243,58 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'import "./app/index.js"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '*/app/*' ], - } ], - errors: [ { + options: [{ + forbid: ['*/app/*'], + }], + errors: [{ message: 'Reaching to "./app/index.js" is not allowed.', line: 1, column: 8, - } ], + }], }), test({ code: 'import b from "@org/package"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '@org/**' ], - } ], - errors: [ { + options: [{ + forbid: ['@org/**'], + }], + errors: [{ message: 'Reaching to "@org/package" is not allowed.', line: 1, column: 15, - } ], + }], }), test({ code: 'import b from "app/a/b"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ 'app/**/**' ], - } ], - errors: [ { + options: [{ + forbid: ['app/**/**'], + }], + errors: [{ message: 'Reaching to "app/a/b" is not allowed.', line: 1, column: 15, - } ], + }], }), test({ code: 'import get from "lodash.get"', filename: testFilePath('./internal-modules/plugins/plugin2/index.js'), - options: [ { - forbid: [ 'lodash.*' ], - } ], - errors: [ { + options: [{ + forbid: ['lodash.*'], + }], + errors: [{ message: 'Reaching to "lodash.get" is not allowed.', line: 1, column: 17, - } ], + }], }), test({ code: 'import "./app/index.js";\nimport "./app/index"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '**/index{.js,}' ], - } ], - errors: [ { + options: [{ + forbid: ['**/index{.js,}'], + }], + errors: [{ message: 'Reaching to "./app/index.js" is not allowed.', line: 1, column: 8, @@ -302,18 +302,18 @@ ruleTester.run('no-internal-modules', rule, { message: 'Reaching to "./app/index" is not allowed.', line: 2, column: 8, - } ], + }], }), test({ code: 'import "@/api/service";', - options: [ { - forbid: [ '**/api/*' ], - } ], - errors: [ { + options: [{ + forbid: ['**/api/*'], + }], + errors: [{ message: 'Reaching to "@/api/service" is not allowed.', line: 1, column: 8, - } ], + }], settings: { 'import/resolver': { webpack: { @@ -332,39 +332,39 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'export * from "./plugin2/index.js";\nexport * from "./plugin2/app/index"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - allow: [ '*/index.js' ], - } ], - errors: [ { + options: [{ + allow: ['*/index.js'], + }], + errors: [{ message: 'Reaching to "./plugin2/app/index" is not allowed.', line: 2, column: 15, - } ], + }], }), test({ code: 'export * from "./app/index.js"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - errors: [ { + errors: [{ message: 'Reaching to "./app/index.js" is not allowed.', line: 1, column: 15, - } ], + }], }), test({ code: 'export {b} from "./plugin2/internal"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - errors: [ { + errors: [{ message: 'Reaching to "./plugin2/internal" is not allowed.', line: 1, column: 17, - } ], + }], }), test({ code: 'export {a} from "../api/service/index"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - allow: [ '**/internal-modules/*' ], - } ], + options: [{ + allow: ['**/internal-modules/*'], + }], errors: [ { message: 'Reaching to "../api/service/index" is not allowed.', @@ -398,9 +398,9 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'export * from "./plugin2/thing"', filename: testFilePath('./internal-modules/plugins/plugin.js'), - options: [ { - forbid: [ '**/plugin2/*' ], - } ], + options: [{ + forbid: ['**/plugin2/*'], + }], errors: [ { message: 'Reaching to "./plugin2/thing" is not allowed.', @@ -412,9 +412,9 @@ ruleTester.run('no-internal-modules', rule, { test({ code: 'export * from "app/a"', filename: testFilePath('./internal-modules/plugins/plugin2/internal.js'), - options: [ { - forbid: [ '**' ], - } ], + options: [{ + forbid: ['**'], + }], errors: [ { message: 'Reaching to "app/a" is not allowed.', diff --git a/tests/src/rules/no-named-as-default-member.js b/tests/src/rules/no-named-as-default-member.js index 53cba230ba..1773176f4f 100644 --- a/tests/src/rules/no-named-as-default-member.js +++ b/tests/src/rules/no-named-as-default-member.js @@ -25,40 +25,28 @@ ruleTester.run('no-named-as-default-member', rule, { test({ code: 'import bar from "./bar"; const foo = bar.foo;', errors: [{ - message: ( - 'Caution: `bar` also has a named export `foo`. ' + - 'Check if you meant to write `import {foo} from \'./bar\'` instead.' - ), + message: 'Caution: `bar` also has a named export `foo`. Check if you meant to write `import {foo} from \'./bar\'` instead.', type: 'MemberExpression', }], }), test({ code: 'import bar from "./bar"; bar.foo();', errors: [{ - message: ( - 'Caution: `bar` also has a named export `foo`. ' + - 'Check if you meant to write `import {foo} from \'./bar\'` instead.' - ), + message: 'Caution: `bar` also has a named export `foo`. Check if you meant to write `import {foo} from \'./bar\'` instead.', type: 'MemberExpression', }], }), test({ code: 'import bar from "./bar"; const {foo} = bar;', errors: [{ - message: ( - 'Caution: `bar` also has a named export `foo`. ' + - 'Check if you meant to write `import {foo} from \'./bar\'` instead.' - ), + message: 'Caution: `bar` also has a named export `foo`. Check if you meant to write `import {foo} from \'./bar\'` instead.', type: 'Identifier', }], }), test({ code: 'import bar from "./bar"; const {foo: foo2, baz} = bar;', errors: [{ - message: ( - 'Caution: `bar` also has a named export `foo`. ' + - 'Check if you meant to write `import {foo} from \'./bar\'` instead.' - ), + message: 'Caution: `bar` also has a named export `foo`. Check if you meant to write `import {foo} from \'./bar\'` instead.', type: 'Identifier', }], }), @@ -66,10 +54,7 @@ ruleTester.run('no-named-as-default-member', rule, { testVersion('>= 8.7', () => ({ code: 'import bar from "./export-default-string-and-named"; const foo = bar.foo;', errors: [{ - message: ( - 'Caution: `bar` also has a named export `foo`. ' + - 'Check if you meant to write `import {foo} from \'./export-default-string-and-named\'` instead.' - ), + message: 'Caution: `bar` also has a named export `foo`. Check if you meant to write `import {foo} from \'./export-default-string-and-named\'` instead.', type: 'MemberExpression', }], parserOptions: { ecmaVersion: 2022 }, diff --git a/tests/src/rules/no-named-as-default.js b/tests/src/rules/no-named-as-default.js index 04ec28e615..c6646a4f0d 100644 --- a/tests/src/rules/no-named-as-default.js +++ b/tests/src/rules/no-named-as-default.js @@ -33,28 +33,28 @@ ruleTester.run('no-named-as-default', rule, { invalid: [].concat( test({ code: 'import foo from "./bar";', - errors: [ { + errors: [{ message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ImportDefaultSpecifier' } ] }), + type: 'ImportDefaultSpecifier' }] }), test({ code: 'import foo, { foo as bar } from "./bar";', - errors: [ { + errors: [{ message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ImportDefaultSpecifier' } ] }), + type: 'ImportDefaultSpecifier' }] }), // es7 test({ code: 'export foo from "./bar";', parser: parsers.BABEL_OLD, - errors: [ { + errors: [{ message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ExportDefaultSpecifier' } ] }), + type: 'ExportDefaultSpecifier' }] }), test({ code: 'export foo, { foo as bar } from "./bar";', parser: parsers.BABEL_OLD, - errors: [ { + errors: [{ message: 'Using exported name \'foo\' as identifier for default export.', - type: 'ExportDefaultSpecifier' } ] }), + type: 'ExportDefaultSpecifier' }] }), test({ code: 'import foo from "./malformed.js"', diff --git a/tests/src/rules/no-namespace.js b/tests/src/rules/no-namespace.js index d75928c1d8..03a23e3dd7 100644 --- a/tests/src/rules/no-namespace.js +++ b/tests/src/rules/no-namespace.js @@ -20,7 +20,7 @@ const FIX_TESTS = semver.satisfies(eslintPkg.version, '>5.0.0') ? [ florp(bar); florp(baz); `.trim(), - errors: [ { + errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, @@ -43,7 +43,7 @@ const FIX_TESTS = semver.satisfies(eslintPkg.version, '>5.0.0') ? [ florp(foo_bar); florp(foo_baz_1); `.trim(), - errors: [ { + errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, @@ -64,7 +64,7 @@ const FIX_TESTS = semver.satisfies(eslintPkg.version, '>5.0.0') ? [ florp(foo_arg); } `.trim(), - errors: [ { + errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, @@ -85,29 +85,29 @@ ruleTester.run('no-namespace', require('rules/no-namespace'), { test({ code: 'import * as foo from \'foo\';', output: 'import * as foo from \'foo\';', - errors: [ { + errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, - } ], + }], }), test({ code: 'import defaultExport, * as foo from \'foo\';', output: 'import defaultExport, * as foo from \'foo\';', - errors: [ { + errors: [{ line: 1, column: 23, message: ERROR_MESSAGE, - } ], + }], }), test({ code: 'import * as foo from \'./foo\';', output: 'import * as foo from \'./foo\';', - errors: [ { + errors: [{ line: 1, column: 8, message: ERROR_MESSAGE, - } ], + }], }), ...FIX_TESTS, ], diff --git a/tests/src/rules/no-nodejs-modules.js b/tests/src/rules/no-nodejs-modules.js index 9be605709a..b25eb0ce85 100644 --- a/tests/src/rules/no-nodejs-modules.js +++ b/tests/src/rules/no-nodejs-modules.js @@ -6,7 +6,7 @@ const isCore = require('is-core-module'); const ruleTester = new RuleTester(); const rule = require('rules/no-nodejs-modules'); -const error = message => ({ +const error = (message) => ({ message, }); @@ -69,7 +69,7 @@ ruleTester.run('no-nodejs-modules', rule, { allow: ['node:events'], }], }), - ]: [], + ] : [], isCore('node:path') ? [ test({ code: 'import path from "node:path"', diff --git a/tests/src/rules/no-relative-packages.js b/tests/src/rules/no-relative-packages.js index 2d27bcc91e..6104aeb9ca 100644 --- a/tests/src/rules/no-relative-packages.js +++ b/tests/src/rules/no-relative-packages.js @@ -42,41 +42,41 @@ ruleTester.run('no-relative-packages', rule, { test({ code: 'import foo from "./package-named"', filename: testFilePath('./bar.js'), - errors: [ { + errors: [{ message: 'Relative import from another package is not allowed. Use `package-named` instead of `./package-named`', line: 1, column: 17, - } ], + }], output: 'import foo from "package-named"', }), test({ code: 'import foo from "../package-named"', filename: testFilePath('./package/index.js'), - errors: [ { + errors: [{ message: 'Relative import from another package is not allowed. Use `package-named` instead of `../package-named`', line: 1, column: 17, - } ], + }], output: 'import foo from "package-named"', }), test({ code: 'import foo from "../package-scoped"', filename: testFilePath('./package/index.js'), - errors: [ { + errors: [{ message: `Relative import from another package is not allowed. Use \`${normalize('@scope/package-named')}\` instead of \`../package-scoped\``, line: 1, column: 17, - } ], + }], output: `import foo from "@scope/package-named"`, }), test({ code: 'import bar from "../bar"', filename: testFilePath('./package-named/index.js'), - errors: [ { + errors: [{ message: `Relative import from another package is not allowed. Use \`${normalize('eslint-plugin-import/tests/files/bar')}\` instead of \`../bar\``, line: 1, column: 17, - } ], + }], output: `import bar from "eslint-plugin-import/tests/files/bar"`, }), ], diff --git a/tests/src/rules/no-relative-parent-imports.js b/tests/src/rules/no-relative-parent-imports.js index 3050498026..1af9b8cf8d 100644 --- a/tests/src/rules/no-relative-parent-imports.js +++ b/tests/src/rules/no-relative-parent-imports.js @@ -2,7 +2,7 @@ import { RuleTester } from 'eslint'; import rule from 'rules/no-relative-parent-imports'; import { parsers, test as _test, testFilePath } from '../utils'; -const test = def => _test(Object.assign(def, { +const test = (def) => _test(Object.assign(def, { filename: testFilePath('./internal-modules/plugins/plugin2/index.js'), parser: parsers.BABEL_OLD, })); @@ -55,32 +55,32 @@ ruleTester.run('no-relative-parent-imports', rule, { invalid: [ test({ code: 'import foo from "../plugin.js"', - errors: [ { + errors: [{ message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../plugin.js` or consider making `../plugin.js` a package.', line: 1, column: 17, - } ], + }], }), test({ code: 'require("../plugin.js")', options: [{ commonjs: true }], - errors: [ { + errors: [{ message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../plugin.js` or consider making `../plugin.js` a package.', line: 1, column: 9, - } ], + }], }), test({ code: 'import("../plugin.js")', - errors: [ { + errors: [{ message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../plugin.js` or consider making `../plugin.js` a package.', line: 1, column: 8, - } ], + }], }), test({ code: 'import foo from "./../plugin.js"', - errors: [ { + errors: [{ message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `./../plugin.js` or consider making `./../plugin.js` a package.', line: 1, column: 17, @@ -88,7 +88,7 @@ ruleTester.run('no-relative-parent-imports', rule, { }), test({ code: 'import foo from "../../api/service"', - errors: [ { + errors: [{ message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../../api/service` or consider making `../../api/service` a package.', line: 1, column: 17, @@ -96,7 +96,7 @@ ruleTester.run('no-relative-parent-imports', rule, { }), test({ code: 'import("../../api/service")', - errors: [ { + errors: [{ message: 'Relative imports from parent directories are not allowed. Please either pass what you\'re importing through at runtime (dependency injection), move `index.js` to same directory as `../../api/service` or consider making `../../api/service` a package.', line: 1, column: 8, diff --git a/tests/src/rules/no-restricted-paths.js b/tests/src/rules/no-restricted-paths.js index 81182189f2..a83a804a0a 100644 --- a/tests/src/rules/no-restricted-paths.js +++ b/tests/src/rules/no-restricted-paths.js @@ -713,7 +713,7 @@ ruleTester.run('no-restricted-paths', rule, { }); context('Typescript', function () { - getTSParsers().forEach(parser => { + getTSParsers().forEach((parser) => { const settings = { 'import/parsers': { [parser]: ['.ts'] }, 'import/resolver': { 'eslint-import-resolver-typescript': true }, @@ -933,8 +933,7 @@ context('Typescript', function () { }], }], errors: [{ - message: 'Restricted path exceptions must be descendants of the configured ' + - '`from` path for that zone.', + message: 'Restricted path exceptions must be descendants of the configured `from` path for that zone.', line: 1, column: 20, }], diff --git a/tests/src/rules/no-unassigned-import.js b/tests/src/rules/no-unassigned-import.js index 8724b80d30..f96808cbcc 100644 --- a/tests/src/rules/no-unassigned-import.js +++ b/tests/src/rules/no-unassigned-import.js @@ -29,48 +29,48 @@ ruleTester.run('no-unassigned-import', rule, { test({ code: 'require("lodash")()' }), test({ code: 'import "app.css"', - options: [{ 'allow': ['**/*.css'] }], + options: [{ allow: ['**/*.css'] }], }), test({ code: 'import "app.css";', - options: [{ 'allow': ['*.css'] }], + options: [{ allow: ['*.css'] }], }), test({ code: 'import "./app.css"', - options: [{ 'allow': ['**/*.css'] }], + options: [{ allow: ['**/*.css'] }], }), test({ code: 'import "foo/bar"', - options: [{ 'allow': ['foo/**'] }], + options: [{ allow: ['foo/**'] }], }), test({ code: 'import "foo/bar"', - options: [{ 'allow': ['foo/bar'] }], + options: [{ allow: ['foo/bar'] }], }), test({ code: 'import "../dir/app.css"', - options: [{ 'allow': ['**/*.css'] }], + options: [{ allow: ['**/*.css'] }], }), test({ code: 'import "../dir/app.js"', - options: [{ 'allow': ['**/dir/**'] }], + options: [{ allow: ['**/dir/**'] }], }), test({ code: 'require("./app.css")', - options: [{ 'allow': ['**/*.css'] }], + options: [{ allow: ['**/*.css'] }], }), test({ code: 'import "babel-register"', - options: [{ 'allow': ['babel-register'] }], + options: [{ allow: ['babel-register'] }], }), test({ code: 'import "./styles/app.css"', - options: [{ 'allow': ['src/styles/**'] }], + options: [{ allow: ['src/styles/**'] }], filename: path.join(process.cwd(), 'src/app.js'), }), test({ code: 'import "../scripts/register.js"', - options: [{ 'allow': ['src/styles/**', '**/scripts/*.js'] }], + options: [{ allow: ['src/styles/**', '**/scripts/*.js'] }], filename: path.join(process.cwd(), 'src/app.js'), }), ], @@ -85,22 +85,22 @@ ruleTester.run('no-unassigned-import', rule, { }), test({ code: 'import "./app.css"', - options: [{ 'allow': ['**/*.js'] }], + options: [{ allow: ['**/*.js'] }], errors: [error], }), test({ code: 'import "./app.css"', - options: [{ 'allow': ['**/dir/**'] }], + options: [{ allow: ['**/dir/**'] }], errors: [error], }), test({ code: 'require("./app.css")', - options: [{ 'allow': ['**/*.js'] }], + options: [{ allow: ['**/*.js'] }], errors: [error], }), test({ code: 'import "./styles/app.css"', - options: [{ 'allow': ['styles/*.css'] }], + options: [{ allow: ['styles/*.css'] }], filename: path.join(process.cwd(), 'src/app.js'), errors: [error], }), diff --git a/tests/src/rules/no-unresolved.js b/tests/src/rules/no-unresolved.js index 024e8965ae..04a53d887b 100644 --- a/tests/src/rules/no-unresolved.js +++ b/tests/src/rules/no-unresolved.js @@ -17,7 +17,7 @@ function runResolverTests(resolver) { ...specs, settings: { ...specs.settings, - 'import/resolver': resolver, + 'import/resolver': resolver, 'import/cache': { lifetime: 0 }, }, }); @@ -161,7 +161,7 @@ function runResolverTests(resolver) { message: "Unable to resolve path to module './empty-folder'.", type: 'Literal', }, - ], + ], }), // sanity check that this module is _not_ found without proper settings @@ -185,7 +185,7 @@ function runResolverTests(resolver) { parser: parsers.BABEL_OLD, }), - rest({ + rest({ code: 'export { foo } from "./does-not-exist"', errors: ["Unable to resolve path to module './does-not-exist'."], }), @@ -359,7 +359,7 @@ ruleTester.run('no-unresolved (import/resolve legacy)', rule, { code: "import { DEEP } from 'in-alternate-root';", settings: { 'import/resolve': { - 'paths': [ + paths: [ path.join(process.cwd(), 'tests', 'files', 'alternate-root'), ], }, @@ -373,21 +373,21 @@ ruleTester.run('no-unresolved (import/resolve legacy)', rule, { paths: [ path.join('tests', 'files', 'src-root'), path.join('tests', 'files', 'alternate-root'), - ], + ], }, }, }), test({ code: 'import * as foo from "jsx-module/foo"', - settings: { 'import/resolve': { 'extensions': ['.jsx'] } }, + settings: { 'import/resolve': { extensions: ['.jsx'] } }, }), ], invalid: [ test({ code: 'import * as foo from "jsx-module/foo"', - errors: [ "Unable to resolve path to module 'jsx-module/foo'." ], + errors: ["Unable to resolve path to module 'jsx-module/foo'."], }), ], }); @@ -417,7 +417,6 @@ ruleTester.run('no-unresolved (webpack-specific)', rule, { ], }); - ruleTester.run('no-unresolved ignore list', rule, { valid: [ test({ @@ -440,7 +439,7 @@ ruleTester.run('no-unresolved ignore list', rule, { }), ], - invalid:[ + invalid: [ test({ code: 'import "./test.gif"', options: [{ ignore: ['.png$'] }], @@ -458,7 +457,7 @@ ruleTester.run('no-unresolved ignore list', rule, { ruleTester.run('no-unresolved unknown resolver', rule, { valid: [], - invalid:[ + invalid: [ // logs resolver load error test({ @@ -490,7 +489,7 @@ ruleTester.run('no-unresolved electron', rule, { settings: { 'import/core-modules': ['electron'] }, }), ], - invalid:[ + invalid: [ test({ code: 'import "electron"', errors: [`Unable to resolve path to module 'electron'.`], @@ -500,7 +499,7 @@ ruleTester.run('no-unresolved electron', rule, { ruleTester.run('no-unresolved syntax verification', rule, { valid: SYNTAX_CASES, - invalid:[], + invalid: [], }); // https://github.com/import-js/eslint-plugin-import/issues/2024 @@ -522,7 +521,7 @@ ruleTester.run('import() with built-in parser', rule, { context('TypeScript', () => { // Type-only imports were added in TypeScript ESTree 2.23.0 - getTSParsers().filter(x => x !== parsers.TS_OLD).forEach((parser) => { + getTSParsers().filter((x) => x !== parsers.TS_OLD).forEach((parser) => { ruleTester.run(`${parser}: no-unresolved ignore type-only`, rule, { valid: [ test({ @@ -537,12 +536,12 @@ context('TypeScript', () => { invalid: [ test({ code: 'import { JSONSchema7Type } from "@types/json-schema";', - errors: [ "Unable to resolve path to module '@types/json-schema'." ], + errors: ["Unable to resolve path to module '@types/json-schema'."], parser, }), test({ code: 'export { JSONSchema7Type } from "@types/json-schema";', - errors: [ "Unable to resolve path to module '@types/json-schema'." ], + errors: ["Unable to resolve path to module '@types/json-schema'."], parser, }), ], diff --git a/tests/src/rules/no-unused-modules.js b/tests/src/rules/no-unused-modules.js index de169c65da..b09d5d759c 100644 --- a/tests/src/rules/no-unused-modules.js +++ b/tests/src/rules/no-unused-modules.js @@ -7,6 +7,11 @@ import fs from 'fs'; import eslintPkg from 'eslint/package.json'; import semver from 'semver'; +let FlatRuleTester; +try { + ({ FlatRuleTester } = require('eslint/use-at-your-own-risk')); +} catch (e) { /**/ } + // TODO: figure out why these tests fail in eslint 4 and 5 const isESLint4TODO = semver.satisfies(eslintPkg.version, '^4 || ^5'); @@ -15,7 +20,7 @@ const typescriptRuleTester = new RuleTester(typescriptConfig); const jsxRuleTester = new RuleTester(jsxConfig); const rule = require('rules/no-unused-modules'); -const error = message => ({ message }); +const error = (message) => ({ message }); const missingExportsOptions = [{ missingExports: true, @@ -104,7 +109,6 @@ ruleTester.run('no-unused-modules', rule, { ], }); - // tests for exports ruleTester.run('no-unused-modules', rule, { valid: [ @@ -156,28 +160,51 @@ ruleTester.run('no-unused-modules', rule, { filename: testFilePath('./no-unused-modules/file-o.js'), parser: parsers.BABEL_OLD, }), + test({ + options: unusedExportsOptions, + code: ` + export const [o0, o2] = createLoadingAndErrorSelectors( + AUTH_USER + ); + `, + filename: testFilePath('./no-unused-modules/file-o.js'), + }), ], invalid: [ test({ options: unusedExportsOptions, - code: `import eslint from 'eslint' - import fileA from './file-a' - import { b } from './file-b' - import { c1, c2 } from './file-c' - import { d } from './file-d' - import { e } from './file-e' - import { e2 } from './file-e' - import { h2 } from './file-h' - import * as l from './file-l' - export * from './file-n' - export { default, o0, o3 } from './file-o' - export { p } from './file-p' - import s from './file-s'`, + code: ` + import eslint from 'eslint' + import fileA from './file-a' + import { b } from './file-b' + import { c1, c2 } from './file-c' + import { d } from './file-d' + import { e } from './file-e' + import { e2 } from './file-e' + import { h2 } from './file-h' + import * as l from './file-l' + export * from './file-n' + export { default, o0, o3 } from './file-o' + export { p } from './file-p' + import s from './file-s' + `, filename: testFilePath('./no-unused-modules/file-0.js'), errors: [ - error(`exported declaration 'default' not used within other modules`), - error(`exported declaration 'o0' not used within other modules`), - error(`exported declaration 'o3' not used within other modules`), + { + message: `exported declaration 'default' not used within other modules`, + line: 12, + column: 18, + }, + { + message: `exported declaration 'o0' not used within other modules`, + line: 12, + column: 27, + }, + { + message: `exported declaration 'o3' not used within other modules`, + line: 12, + column: 31, + }, error(`exported declaration 'p' not used within other modules`), ], }), @@ -242,7 +269,6 @@ ruleTester.run('no-unused-modules', rule, { ], }); - describe('dynamic imports', function () { if (semver.satisfies(eslintPkg.version, '< 6')) { beforeEach(function () { @@ -706,7 +732,7 @@ describe('renameDefault', () => { describe('test behavior for new file', () => { before(() => { - fs.writeFileSync(testFilePath('./no-unused-modules/file-added-0.js'), '', { encoding: 'utf8' }); + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-0.js'), '', { encoding: 'utf8', flag: 'w' }); }); // add import in newly created file @@ -812,10 +838,9 @@ describe('test behavior for new file', () => { ], }); - describe('test behavior for new file', () => { before(() => { - fs.writeFileSync(testFilePath('./no-unused-modules/file-added-1.js'), '', { encoding: 'utf8' }); + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-1.js'), '', { encoding: 'utf8', flag: 'w' }); }); ruleTester.run('no-unused-modules', rule, { valid: [ @@ -850,7 +875,7 @@ describe('test behavior for new file', () => { describe('test behavior for new file', () => { before(() => { - fs.writeFileSync(testFilePath('./no-unused-modules/file-added-2.js'), '', { encoding: 'utf8' }); + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-2.js'), '', { encoding: 'utf8', flag: 'w' }); }); ruleTester.run('no-unused-modules', rule, { valid: [ @@ -876,7 +901,7 @@ describe('test behavior for new file', () => { describe('test behavior for new file', () => { before(() => { - fs.writeFileSync(testFilePath('./no-unused-modules/file-added-3.js'), '', { encoding: 'utf8' }); + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-3.js'), '', { encoding: 'utf8', flag: 'w' }); }); ruleTester.run('no-unused-modules', rule, { valid: [ @@ -927,7 +952,7 @@ describe('test behavior for destructured exports', () => { describe('test behavior for new file', () => { before(() => { - fs.writeFileSync(testFilePath('./no-unused-modules/file-added-4.js.js'), '', { encoding: 'utf8' }); + fs.writeFileSync(testFilePath('./no-unused-modules/file-added-4.js.js'), '', { encoding: 'utf8', flag: 'w' }); }); ruleTester.run('no-unused-modules', rule, { valid: [ @@ -1371,3 +1396,22 @@ describe('parser ignores prefixes like BOM and hashbang', () => { invalid: [], }); }); + +(FlatRuleTester ? describe : describe.skip)('supports flat eslint', () => { + it('passes', () => { + const flatRuleTester = new FlatRuleTester(); + flatRuleTester.run('no-unused-modules', rule, { + valid: [{ + options: unusedExportsOptions, + code: 'import { o2 } from "./file-o"; export default () => 12', + filename: testFilePath('./no-unused-modules/file-a.js'), + }], + invalid: [{ + options: unusedExportsOptions, + code: 'export default () => 13', + filename: testFilePath('./no-unused-modules/file-f.js'), + errors: [error(`exported declaration 'default' not used within other modules`)], + }], + }); + }); +}); diff --git a/tests/src/rules/no-useless-path-segments.js b/tests/src/rules/no-useless-path-segments.js index f960953503..d6d0395dea 100644 --- a/tests/src/rules/no-useless-path-segments.js +++ b/tests/src/rules/no-useless-path-segments.js @@ -42,49 +42,49 @@ function runResolverTests(resolver) { code: 'require("./../files/malformed.js")', output: 'require("../files/malformed.js")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], + errors: ['Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], }), test({ code: 'require("./../files/malformed")', output: 'require("../files/malformed")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "./../files/malformed", should be "../files/malformed"'], + errors: ['Useless path segments for "./../files/malformed", should be "../files/malformed"'], }), test({ code: 'require("../files/malformed.js")', output: 'require("./malformed.js")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "../files/malformed.js", should be "./malformed.js"'], + errors: ['Useless path segments for "../files/malformed.js", should be "./malformed.js"'], }), test({ code: 'require("../files/malformed")', output: 'require("./malformed")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "../files/malformed", should be "./malformed"'], + errors: ['Useless path segments for "../files/malformed", should be "./malformed"'], }), test({ code: 'require("./test-module/")', output: 'require("./test-module")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "./test-module/", should be "./test-module"'], + errors: ['Useless path segments for "./test-module/", should be "./test-module"'], }), test({ code: 'require("./")', output: 'require(".")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "./", should be "."'], + errors: ['Useless path segments for "./", should be "."'], }), test({ code: 'require("../")', output: 'require("..")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "../", should be ".."'], + errors: ['Useless path segments for "../", should be ".."'], }), test({ code: 'require("./deep//a")', output: 'require("./deep/a")', options: [{ commonjs: true }], - errors: [ 'Useless path segments for "./deep//a", should be "./deep/a"'], + errors: ['Useless path segments for "./deep//a", should be "./deep/a"'], }), // CommonJS modules + noUselessIndex @@ -141,42 +141,42 @@ function runResolverTests(resolver) { test({ code: 'import "./../files/malformed.js"', output: 'import "../files/malformed.js"', - errors: [ 'Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], + errors: ['Useless path segments for "./../files/malformed.js", should be "../files/malformed.js"'], }), test({ code: 'import "./../files/malformed"', output: 'import "../files/malformed"', - errors: [ 'Useless path segments for "./../files/malformed", should be "../files/malformed"'], + errors: ['Useless path segments for "./../files/malformed", should be "../files/malformed"'], }), test({ code: 'import "../files/malformed.js"', output: 'import "./malformed.js"', - errors: [ 'Useless path segments for "../files/malformed.js", should be "./malformed.js"'], + errors: ['Useless path segments for "../files/malformed.js", should be "./malformed.js"'], }), test({ code: 'import "../files/malformed"', output: 'import "./malformed"', - errors: [ 'Useless path segments for "../files/malformed", should be "./malformed"'], + errors: ['Useless path segments for "../files/malformed", should be "./malformed"'], }), test({ code: 'import "./test-module/"', output: 'import "./test-module"', - errors: [ 'Useless path segments for "./test-module/", should be "./test-module"'], + errors: ['Useless path segments for "./test-module/", should be "./test-module"'], }), test({ code: 'import "./"', output: 'import "."', - errors: [ 'Useless path segments for "./", should be "."'], + errors: ['Useless path segments for "./", should be "."'], }), test({ code: 'import "../"', output: 'import ".."', - errors: [ 'Useless path segments for "../", should be ".."'], + errors: ['Useless path segments for "../", should be ".."'], }), test({ code: 'import "./deep//a"', output: 'import "./deep/a"', - errors: [ 'Useless path segments for "./deep//a", should be "./deep/a"'], + errors: ['Useless path segments for "./deep//a", should be "./deep/a"'], }), // ES modules + noUselessIndex @@ -231,19 +231,19 @@ function runResolverTests(resolver) { test({ code: 'import("./")', output: 'import(".")', - errors: [ 'Useless path segments for "./", should be "."'], + errors: ['Useless path segments for "./", should be "."'], parser: parsers.BABEL_OLD, }), test({ code: 'import("../")', output: 'import("..")', - errors: [ 'Useless path segments for "../", should be ".."'], + errors: ['Useless path segments for "../", should be ".."'], parser: parsers.BABEL_OLD, }), test({ code: 'import("./deep//a")', output: 'import("./deep/a")', - errors: [ 'Useless path segments for "./deep//a", should be "./deep/a"'], + errors: ['Useless path segments for "./deep//a", should be "./deep/a"'], parser: parsers.BABEL_OLD, }), ], diff --git a/tests/src/rules/no-webpack-loader-syntax.js b/tests/src/rules/no-webpack-loader-syntax.js index 2b841e18a3..05ad242f50 100644 --- a/tests/src/rules/no-webpack-loader-syntax.js +++ b/tests/src/rules/no-webpack-loader-syntax.js @@ -88,12 +88,8 @@ context('TypeScript', function () { if (!(parser === parsers.TS_NEW && semver.satisfies(require('@typescript-eslint/parser/package.json').version, '>= 5'))) { ruleTester.run('no-webpack-loader-syntax', rule, { valid: [ - test(Object.assign({ - code: 'import { foo } from\nalert()', - }, parserConfig)), - test(Object.assign({ - code: 'import foo from\nalert()', - }, parserConfig)), + test({ code: 'import { foo } from\nalert()', ...parserConfig }), + test({ code: 'import foo from\nalert()', ...parserConfig }), ], invalid: [], }); diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index 07511ee4de..a6a8735a6f 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -5,9 +5,9 @@ import eslintPkg from 'eslint/package.json'; import semver from 'semver'; import flatMap from 'array.prototype.flatmap'; import { resolve } from 'path'; +import isCoreModule from 'is-core-module'; import { default as babelPresetFlow } from 'babel-preset-flow'; - const ruleTester = new RuleTester(); const flowRuleTester = new RuleTester({ parser: resolve(__dirname, '../../../node_modules/babel-eslint'), @@ -22,7 +22,7 @@ const flowRuleTester = new RuleTester({ const rule = require('rules/order'); function withoutAutofixOutput(test) { - return Object.assign({}, test, { output: test.code }); + return { ...test, output: test.code }; } ruleTester.run('order', rule, { @@ -193,7 +193,7 @@ ruleTester.run('order', rule, { var index = require('./'); `, }), - ...flatMap(getTSParsers(), parser => [ + ...flatMap(getTSParsers(), (parser) => [ // Export equals expressions should be on top alongside with ordinary import-statements. test({ code: ` @@ -236,7 +236,7 @@ ruleTester.run('order', rule, { import fs from 'fs'; import { add } from './helper';`, options: [{ - groups: [ 'unknown', 'builtin', 'external', 'parent', 'sibling', 'index' ], + groups: ['unknown', 'builtin', 'external', 'parent', 'sibling', 'index'], }], }), // Using unknown import types (e.g. using a resolver alias via babel) @@ -829,7 +829,7 @@ ruleTester.run('order', rule, { pathGroupsExcludedImportTypes: [], }], }), - ...flatMap(getTSParsers, parser => [ + ...flatMap(getTSParsers, (parser) => [ // Order of the `import ... = require(...)` syntax test({ code: ` @@ -950,18 +950,18 @@ ruleTester.run('order', rule, { options: [ { 'newlines-between': 'always', - 'distinctGroup': true, - 'pathGroupsExcludedImportTypes': [], - 'pathGroups': [ + distinctGroup: true, + pathGroupsExcludedImportTypes: [], + pathGroups: [ { - 'pattern': 'a', - 'group': 'external', - 'position': 'before', + pattern: 'a', + group: 'external', + position: 'before', }, { - 'pattern': 'b', - 'group': 'external', - 'position': 'after', + pattern: 'b', + group: 'external', + position: 'after', }, ], }, @@ -977,18 +977,18 @@ ruleTester.run('order', rule, { options: [ { 'newlines-between': 'always', - 'distinctGroup': false, - 'pathGroupsExcludedImportTypes': [], - 'pathGroups': [ + distinctGroup: false, + pathGroupsExcludedImportTypes: [], + pathGroups: [ { - 'pattern': 'a', - 'group': 'external', - 'position': 'before', + pattern: 'a', + group: 'external', + position: 'before', }, { - 'pattern': 'b', - 'group': 'external', - 'position': 'after', + pattern: 'b', + group: 'external', + position: 'after', }, ], }, @@ -1005,17 +1005,17 @@ ruleTester.run('order', rule, { options: [ { 'newlines-between': 'always', - 'distinctGroup': false, - 'pathGroupsExcludedImportTypes': [], - 'pathGroups': [ + distinctGroup: false, + pathGroupsExcludedImportTypes: [], + pathGroups: [ { - 'pattern': 'a', - 'group': 'external', + pattern: 'a', + group: 'external', }, { - 'pattern': 'b', - 'group': 'internal', - 'position': 'before', + pattern: 'b', + group: 'internal', + position: 'before', }, ], }, @@ -1041,53 +1041,53 @@ ruleTester.run('order', rule, { `, options: [ { - 'alphabetize': { - 'caseInsensitive': false, - 'order': 'asc', + alphabetize: { + caseInsensitive: false, + order: 'asc', }, 'newlines-between': 'always', - 'groups': [ + groups: [ ['builtin', 'external', 'internal', 'unknown', 'object', 'type'], 'parent', ['sibling', 'index'], ], - 'distinctGroup': false, - 'pathGroupsExcludedImportTypes': [], - 'pathGroups': [ + distinctGroup: false, + pathGroupsExcludedImportTypes: [], + pathGroups: [ { - 'pattern': './', - 'group': 'sibling', - 'position': 'before', + pattern: './', + group: 'sibling', + position: 'before', }, { - 'pattern': '.', - 'group': 'sibling', - 'position': 'before', + pattern: '.', + group: 'sibling', + position: 'before', }, { - 'pattern': '..', - 'group': 'parent', - 'position': 'before', + pattern: '..', + group: 'parent', + position: 'before', }, { - 'pattern': '../', - 'group': 'parent', - 'position': 'before', + pattern: '../', + group: 'parent', + position: 'before', }, { - 'pattern': '[a-z]*', - 'group': 'external', - 'position': 'before', + pattern: '[a-z]*', + group: 'external', + position: 'before', }, { - 'pattern': '../[a-z]*', - 'group': 'parent', - 'position': 'before', + pattern: '../[a-z]*', + group: 'parent', + position: 'before', }, { - 'pattern': './[a-z]*', - 'group': 'sibling', - 'position': 'before', + pattern: './[a-z]*', + group: 'sibling', + position: 'before', }, ], }, @@ -1096,16 +1096,15 @@ ruleTester.run('order', rule, { // orderImportKind option that is not used test({ code: ` - import B from './B'; - import b from './b'; - `, + import B from './B'; + import b from './b'; + `, options: [ { - 'alphabetize': { order: 'asc', orderImportKind: 'asc', 'caseInsensitive': true }, + alphabetize: { order: 'asc', orderImportKind: 'asc', caseInsensitive: true }, }, ], }), - ], invalid: [ // builtin before external module (require) @@ -1180,12 +1179,8 @@ ruleTester.run('order', rule, { }), // fix order with windows end of lines test({ - code: - `/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n` + - `/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n`, - output: - `/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n` + - `/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n`, + code: `/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n` + `/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n`, + output: `/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n` + `/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n`, errors: [{ message: '`fs` import should occur before import of `async`', }], @@ -1559,7 +1554,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur after import of `../foo/bar`', }], }), - ...flatMap(getTSParsers(), parser => [ + ...flatMap(getTSParsers(), (parser) => [ // Order of the `import ... = require(...)` syntax test({ code: ` @@ -2280,6 +2275,8 @@ ruleTester.run('order', rule, { }, ], }), + + // pathGroups overflowing to previous/next groups test({ code: ` import path from 'path'; @@ -2349,6 +2346,47 @@ ruleTester.run('order', rule, { errors: Array.from({ length: 11 }, () => 'There should be at least one empty line between import groups'), }), + // rankings that overflow to double-digit ranks + test({ + code: ` + import external from 'external'; + import a from '@namespace/a'; + import b from '@namespace/b'; + import { parent } from '../../parent'; + import local from './local'; + import './side-effect';`, + output: ` + import external from 'external'; + + import a from '@namespace/a'; + import b from '@namespace/b'; + + import { parent } from '../../parent'; + + import local from './local'; + import './side-effect';`, + options: [ + { + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + groups: ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], + 'newlines-between': 'always', + pathGroups: [ + { pattern: '@namespace', group: 'external', position: 'after' }, + { pattern: '@namespace/**', group: 'external', position: 'after' }, + ], + pathGroupsExcludedImportTypes: ['@namespace'], + }, + ], + errors: [ + 'There should be at least one empty line between import groups', + 'There should be at least one empty line between import groups', + 'There should be at least one empty line between import groups', + ], + }), + // reorder fix cannot cross non import or require test(withoutAutofixOutput({ code: ` @@ -2634,8 +2672,8 @@ ruleTester.run('order', rule, { options: [ { 'newlines-between': 'always', - 'distinctGroup': false, - 'pathGroupsExcludedImportTypes': [], + distinctGroup: false, + pathGroupsExcludedImportTypes: [], }, ], errors: [{ @@ -2656,18 +2694,18 @@ ruleTester.run('order', rule, { options: [ { 'newlines-between': 'always', - 'distinctGroup': false, - 'pathGroupsExcludedImportTypes': [], - 'pathGroups': [ + distinctGroup: false, + pathGroupsExcludedImportTypes: [], + pathGroups: [ { - 'pattern': 'a', - 'group': 'external', - 'position': 'before', + pattern: 'a', + group: 'external', + position: 'before', }, { - 'pattern': 'c', - 'group': 'external', - 'position': 'after', + pattern: 'c', + group: 'external', + position: 'after', }, ], }, @@ -2698,10 +2736,9 @@ ruleTester.run('order', rule, { }], }), ], - ].filter((t) => !!t), + ].filter(Boolean), }); - context('TypeScript', function () { getNonDefaultParsers() // Type-only imports were added in TypeScript ESTree 2.23.0 @@ -2716,7 +2753,7 @@ context('TypeScript', function () { }; ruleTester.run('order', rule, { - valid: [ + valid: [].concat( // #1667: typescript type import support // Option alphabetize: {order: 'asc'} @@ -2925,8 +2962,32 @@ context('TypeScript', function () { }, ], }), - ], - invalid: [ + isCoreModule('node:child_process') && isCoreModule('node:fs/promises') ? [ + test({ + code: ` + import express from 'express'; + import log4js from 'log4js'; + import chpro from 'node:child_process'; + // import fsp from 'node:fs/promises'; + `, + options: [{ + groups: [ + [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + ], + }], + }), + ] : [], + ), + invalid: [].concat( // Option alphabetize: {order: 'asc'} test({ code: ` @@ -3138,11 +3199,10 @@ context('TypeScript', function () { import type { ParsedPath } from 'path'; } `, - errors: [{ - message: '`fs` type import should occur before type import of `path`', - },{ - message: '`fs` type import should occur before type import of `path`', - }], + errors: [ + { message: '`fs` type import should occur before type import of `path`' }, + { message: '`fs` type import should occur before type import of `path`' }, + ], ...parserConfig, options: [ { @@ -3150,7 +3210,40 @@ context('TypeScript', function () { }, ], }), - ], + + isCoreModule('node:child_process') && isCoreModule('node:fs/promises') ? [ + test({ + code: ` + import express from 'express'; + import log4js from 'log4js'; + import chpro from 'node:child_process'; + // import fsp from 'node:fs/promises'; + `, + output: ` + import chpro from 'node:child_process'; + import express from 'express'; + import log4js from 'log4js'; + // import fsp from 'node:fs/promises'; + `, + options: [{ + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'type', + ], + }], + errors: [ + { message: '`node:child_process` import should occur before import of `express`' }, + // { message: '`node:fs/promises` import should occur before import of `express`' }, + ], + }), + ] : [], + ), }); }); }); @@ -3232,5 +3325,84 @@ flowRuleTester.run('order', rule, { message: '`./local/sub` typeof import should occur before import of `./local/sub`', }], }), + test({ + code: ` + import { cfg } from 'path/path/path/src/Cfg'; + import { l10n } from 'path/src/l10n'; + import { helpers } from 'path/path/path/helpers'; + import { tip } from 'path/path/tip'; + + import { controller } from '../../../../path/path/path/controller'; + import { component } from '../../../../path/path/path/component'; + `, + output: semver.satisfies(eslintPkg.version, '< 3') ? ` + import { cfg } from 'path/path/path/src/Cfg'; + import { tip } from 'path/path/tip'; + import { l10n } from 'path/src/l10n'; + import { helpers } from 'path/path/path/helpers'; + + import { component } from '../../../../path/path/path/component'; + import { controller } from '../../../../path/path/path/controller'; + ` : ` + import { helpers } from 'path/path/path/helpers'; + import { cfg } from 'path/path/path/src/Cfg'; + import { l10n } from 'path/src/l10n'; + import { tip } from 'path/path/tip'; + + import { component } from '../../../../path/path/path/component'; + import { controller } from '../../../../path/path/path/controller'; + `, + options: [ + { + groups: [ + ['builtin', 'external'], + 'internal', + ['sibling', 'parent'], + 'object', + 'type', + ], + pathGroups: [ + { + pattern: 'react', + group: 'builtin', + position: 'before', + patternOptions: { + matchBase: true, + }, + }, + { + pattern: '*.+(css|svg)', + group: 'type', + position: 'after', + patternOptions: { + matchBase: true, + }, + }, + ], + pathGroupsExcludedImportTypes: ['react'], + alphabetize: { + order: 'asc', + }, + 'newlines-between': 'always', + }, + ], + errors: [ + { + message: '`path/path/path/helpers` import should occur before import of `path/path/path/src/Cfg`', + line: 4, + column: 9, + }, + { + message: '`path/path/tip` import should occur before import of `path/src/l10n`', + line: 5, + column: 9, + }, + { + message: '`../../../../path/path/path/component` import should occur before import of `../../../../path/path/path/controller`', + line: 8, + column: 9, + }, + ], + }), ], }); diff --git a/tests/src/rules/prefer-default-export.js b/tests/src/rules/prefer-default-export.js index ae7c16a40e..a7310445b5 100644 --- a/tests/src/rules/prefer-default-export.js +++ b/tests/src/rules/prefer-default-export.js @@ -362,15 +362,15 @@ context('TypeScript', function () { ...parserConfig, }), semver.satisfies(tsEslintVersion, '>= 22') ? test({ - code: 'export type foo = string /* ' + parser.replace(process.cwd(), '$$PWD') + '*/', + code: `export type foo = string /* ${parser.replace(process.cwd(), '$$PWD')}*/`, ...parserConfig, }) : [], semver.satisfies(tsEslintVersion, '> 20') ? test({ - code: 'export interface foo { bar: string; } /* ' + parser.replace(process.cwd(), '$$PWD') + '*/', + code: `export interface foo { bar: string; } /* ${parser.replace(process.cwd(), '$$PWD')}*/`, ...parserConfig, }) : [], test({ - code: 'export interface foo { bar: string; }; export function goo() {} /* ' + parser.replace(process.cwd(), '$$PWD') + '*/', + code: `export interface foo { bar: string; }; export function goo() {} /* ${parser.replace(process.cwd(), '$$PWD')}*/`, ...parserConfig, }), ), diff --git a/tests/src/utils.js b/tests/src/utils.js index b82883a6f4..24d5504a71 100644 --- a/tests/src/utils.js +++ b/tests/src/utils.js @@ -42,22 +42,23 @@ export function eslintVersionSatisfies(specifier) { return semver.satisfies(eslintPkg.version, specifier); } -export function testVersion(specifier, t) { - return eslintVersionSatisfies(specifier) ? test(t()) : []; -} - export function test(t) { if (arguments.length !== 1) { throw new SyntaxError('`test` requires exactly one object argument'); } - return Object.assign({ + return { filename: FILENAME, - }, t, { - parserOptions: Object.assign({ + ...t, + parserOptions: { sourceType: 'module', ecmaVersion: 9, - }, t.parserOptions), - }); + ...t.parserOptions, + }, + }; +} + +export function testVersion(specifier, t) { + return eslintVersionSatisfies(specifier) ? test(t()) : []; } export function testContext(settings) { @@ -133,5 +134,4 @@ export const SYNTAX_CASES = [ test({ code: 'import { foo } from "./ignore.invalid.extension"', }), - ]; diff --git a/utils/.attw.json b/utils/.attw.json new file mode 100644 index 0000000000..45dd01e12f --- /dev/null +++ b/utils/.attw.json @@ -0,0 +1,5 @@ +{ + "ignoreRules": [ + "cjs-only-exports-default" + ] +} diff --git a/utils/.npmignore b/utils/.npmignore new file mode 100644 index 0000000000..366f3ebb6e --- /dev/null +++ b/utils/.npmignore @@ -0,0 +1 @@ +.attw.json diff --git a/utils/.nycrc b/utils/.nycrc new file mode 100644 index 0000000000..1084360870 --- /dev/null +++ b/utils/.nycrc @@ -0,0 +1,15 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "lcov", "text", "html", "json"], + "instrument": false, + "exclude": [ + "coverage", + "test", + "tests", + "resolvers/*/test", + "scripts", + "memo-parser", + "lib" + ] +} diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index 72fa611a14..3e2f5a8997 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -5,6 +5,25 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## Unreleased +### Changed +- [types] use shared config (thanks [@ljharb]) + +## v2.8.1 - 2024-02-26 + +### Fixed +- `parse`: also delete `parserOptions.EXPERIMENTAL_useProjectService` ([#2963], thanks [@JoshuaKGoldberg]) + +### Changed +- add types (thanks [@ljharb]) + +## v2.8.0 - 2023-04-14 + +### New +- `parse`: support flat config ([#2714], thanks [@DMartens]) + +### Fixed +- Improve performance of `fullResolve` for large projects ([#2755], thanks [@leipert]) + ## v2.7.4 - 2022-08-11 ### Fixed @@ -16,7 +35,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange ## v2.7.3 - 2022-01-26 ### Fixed -- [Fix] `parse`: restore compatibility by making the return value `ast` again ([#2350], thanks [@ljharb]) +- `parse`: restore compatibility by making the return value `ast` again ([#2350], thanks [@ljharb]) ## v2.7.2 - 2022-01-01 @@ -123,6 +142,9 @@ Yanked due to critical issue with cache key resulting from #839. ### Fixed - `unambiguous.test()` regex is now properly in multiline mode +[#2963]: https://github.com/import-js/eslint-plugin-import/pull/2963 +[#2755]: https://github.com/import-js/eslint-plugin-import/pull/2755 +[#2714]: https://github.com/import-js/eslint-plugin-import/pull/2714 [#2523]: https://github.com/import-js/eslint-plugin-import/pull/2523 [#2431]: https://github.com/import-js/eslint-plugin-import/pull/2431 [#2350]: https://github.com/import-js/eslint-plugin-import/issues/2350 @@ -155,11 +177,14 @@ Yanked due to critical issue with cache key resulting from #839. [@bradzacher]: https://github.com/bradzacher [@brettz9]: https://github.com/brettz9 [@christophercurrie]: https://github.com/christophercurrie +[@DMartens]: https://github.com/DMartens [@hulkish]: https://github.com/hulkish [@Hypnosphi]: https://github.com/Hypnosphi [@iamnapo]: https://github.com/iamnapo +[@JoshuaKGoldberg]: https://github.com/JoshuaKGoldberg [@JounQin]: https://github.com/JounQin [@kaiyoma]: https://github.com/kaiyoma +[@leipert]: https://github.com/leipert [@manuth]: https://github.com/manuth [@maxkomarychev]: https://github.com/maxkomarychev [@mgwalker]: https://github.com/mgwalker diff --git a/utils/ModuleCache.d.ts b/utils/ModuleCache.d.ts new file mode 100644 index 0000000000..72a72a0699 --- /dev/null +++ b/utils/ModuleCache.d.ts @@ -0,0 +1,22 @@ +import type { ESLintSettings } from "./types"; + +export type CacheKey = unknown; +export type CacheObject = { + result: unknown; + lastSeen: ReturnType; +}; + +declare class ModuleCache { + map: Map; + + constructor(map?: Map); + + get(cacheKey: CacheKey, settings: ESLintSettings): T | undefined; + + set(cacheKey: CacheKey, result: T): T; + + static getSettings(settings: ESLintSettings): { lifetime: number } & Omit; +} +export default ModuleCache; + +export type { ModuleCache } diff --git a/utils/ModuleCache.js b/utils/ModuleCache.js index a06616de9b..24c76849dd 100644 --- a/utils/ModuleCache.js +++ b/utils/ModuleCache.js @@ -1,47 +1,52 @@ 'use strict'; + exports.__esModule = true; const log = require('debug')('eslint-module-utils:ModuleCache'); +/** @type {import('./ModuleCache').ModuleCache} */ class ModuleCache { + /** @param {typeof import('./ModuleCache').ModuleCache.prototype.map} map */ constructor(map) { - this.map = map || new Map(); + this.map = map || /** @type {{typeof import('./ModuleCache').ModuleCache.prototype.map} */ new Map(); } - /** - * returns value for returning inline - * @param {[type]} cacheKey [description] - * @param {[type]} result [description] - */ + /** @type {typeof import('./ModuleCache').ModuleCache.prototype.set} */ set(cacheKey, result) { this.map.set(cacheKey, { result, lastSeen: process.hrtime() }); log('setting entry for', cacheKey); return result; } + /** @type {typeof import('./ModuleCache').ModuleCache.prototype.get} */ get(cacheKey, settings) { if (this.map.has(cacheKey)) { const f = this.map.get(cacheKey); // check freshness - if (process.hrtime(f.lastSeen)[0] < settings.lifetime) return f.result; - } else log('cache miss for', cacheKey); + // @ts-expect-error TS can't narrow properly from `has` and `get` + if (process.hrtime(f.lastSeen)[0] < settings.lifetime) { return f.result; } + } else { + log('cache miss for', cacheKey); + } // cache miss return undefined; } -} + /** @type {typeof import('./ModuleCache').ModuleCache.getSettings} */ + static getSettings(settings) { + /** @type {ReturnType} */ + const cacheSettings = Object.assign({ + lifetime: 30, // seconds + }, settings['import/cache']); -ModuleCache.getSettings = function (settings) { - const cacheSettings = Object.assign({ - lifetime: 30, // seconds - }, settings['import/cache']); + // parse infinity + // @ts-expect-error the lack of type overlap is because we're abusing `cacheSettings` as a temporary object + if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { + cacheSettings.lifetime = Infinity; + } - // parse infinity - if (cacheSettings.lifetime === '∞' || cacheSettings.lifetime === 'Infinity') { - cacheSettings.lifetime = Infinity; + return cacheSettings; } - - return cacheSettings; -}; +} exports.default = ModuleCache; diff --git a/utils/declaredScope.d.ts b/utils/declaredScope.d.ts new file mode 100644 index 0000000000..e37200d870 --- /dev/null +++ b/utils/declaredScope.d.ts @@ -0,0 +1,8 @@ +import { Rule, Scope } from 'eslint'; + +declare function declaredScope( + context: Rule.RuleContext, + name: string +): Scope.Scope['type'] | undefined; + +export default declaredScope; diff --git a/utils/declaredScope.js b/utils/declaredScope.js index ded2131e49..0f0a3d9458 100644 --- a/utils/declaredScope.js +++ b/utils/declaredScope.js @@ -1,9 +1,11 @@ 'use strict'; + exports.__esModule = true; +/** @type {import('./declaredScope').default} */ exports.default = function declaredScope(context, name) { const references = context.getScope().references; - const reference = references.find(x => x.identifier.name === name); - if (!reference) return undefined; + const reference = references.find((x) => x.identifier.name === name); + if (!reference || !reference.resolved) { return undefined; } return reference.resolved.scope.type; }; diff --git a/utils/hash.d.ts b/utils/hash.d.ts new file mode 100644 index 0000000000..5e4cf471bd --- /dev/null +++ b/utils/hash.d.ts @@ -0,0 +1,14 @@ +import type { Hash } from 'crypto'; + +declare function hashArray(value: Array, hash?: Hash): Hash; + +declare function hashObject(value: T, hash?: Hash): Hash; + +declare function hashify( + value: Array | object | unknown, + hash?: Hash, +): Hash; + +export default hashify; + +export { hashArray, hashObject }; diff --git a/utils/hash.js b/utils/hash.js index fcf00de38c..b3ce618b54 100644 --- a/utils/hash.js +++ b/utils/hash.js @@ -2,15 +2,18 @@ * utilities for hashing config objects. * basically iteratively updates hash with a JSON-like format */ + 'use strict'; + exports.__esModule = true; const createHash = require('crypto').createHash; const stringify = JSON.stringify; +/** @type {import('./hash').default} */ function hashify(value, hash) { - if (!hash) hash = createHash('sha256'); + if (!hash) { hash = createHash('sha256'); } if (Array.isArray(value)) { hashArray(value, hash); @@ -24,8 +27,9 @@ function hashify(value, hash) { } exports.default = hashify; +/** @type {import('./hash').hashArray} */ function hashArray(array, hash) { - if (!hash) hash = createHash('sha256'); + if (!hash) { hash = createHash('sha256'); } hash.update('['); for (let i = 0; i < array.length; i++) { @@ -39,13 +43,15 @@ function hashArray(array, hash) { hashify.array = hashArray; exports.hashArray = hashArray; -function hashObject(object, hash) { - if (!hash) hash = createHash('sha256'); +/** @type {import('./hash').hashObject} */ +function hashObject(object, optionalHash) { + const hash = optionalHash || createHash('sha256'); hash.update('{'); - Object.keys(object).sort().forEach(key => { + Object.keys(object).sort().forEach((key) => { hash.update(stringify(key)); hash.update(':'); + // @ts-expect-error the key is guaranteed to exist on the object here hashify(object[key], hash); hash.update(','); }); @@ -56,4 +62,3 @@ function hashObject(object, hash) { hashify.object = hashObject; exports.hashObject = hashObject; - diff --git a/utils/ignore.d.ts b/utils/ignore.d.ts new file mode 100644 index 0000000000..53953b33e9 --- /dev/null +++ b/utils/ignore.d.ts @@ -0,0 +1,12 @@ +import { Rule } from 'eslint'; +import type { ESLintSettings, Extension } from './types'; + +declare function ignore(path: string, context: Rule.RuleContext): boolean; + +declare function getFileExtensions(settings: ESLintSettings): Set; + +declare function hasValidExtension(path: string, context: Rule.RuleContext): path is `${string}${Extension}`; + +export default ignore; + +export { getFileExtensions, hasValidExtension } diff --git a/utils/ignore.js b/utils/ignore.js index 32bbbc6249..56f2ef7239 100644 --- a/utils/ignore.js +++ b/utils/ignore.js @@ -1,4 +1,5 @@ 'use strict'; + exports.__esModule = true; const extname = require('path').extname; @@ -6,20 +7,14 @@ const extname = require('path').extname; const log = require('debug')('eslint-plugin-import:utils:ignore'); // one-shot memoized -let cachedSet; let lastSettings; -function validExtensions(context) { - if (cachedSet && context.settings === lastSettings) { - return cachedSet; - } - - lastSettings = context.settings; - cachedSet = makeValidExtensionSet(context.settings); - return cachedSet; -} +/** @type {Set} */ let cachedSet; +/** @type {import('./types').ESLintSettings} */ let lastSettings; +/** @type {import('./ignore').getFileExtensions} */ function makeValidExtensionSet(settings) { // start with explicit JS-parsed extensions - const exts = new Set(settings['import/extensions'] || [ '.js' ]); + /** @type {Set} */ + const exts = new Set(settings['import/extensions'] || ['.js']); // all alternate parser extensions are also valid if ('import/parsers' in settings) { @@ -28,7 +23,7 @@ function makeValidExtensionSet(settings) { if (!Array.isArray(parserSettings)) { throw new TypeError('"settings" for ' + parser + ' must be an array'); } - parserSettings.forEach(ext => exts.add(ext)); + parserSettings.forEach((ext) => exts.add(ext)); } } @@ -36,11 +31,30 @@ function makeValidExtensionSet(settings) { } exports.getFileExtensions = makeValidExtensionSet; +/** @type {(context: import('eslint').Rule.RuleContext) => Set} */ +function validExtensions(context) { + if (cachedSet && context.settings === lastSettings) { + return cachedSet; + } + + lastSettings = context.settings; + cachedSet = makeValidExtensionSet(context.settings); + return cachedSet; +} + +/** @type {import('./ignore').hasValidExtension} */ +function hasValidExtension(path, context) { + // eslint-disable-next-line no-extra-parens + return validExtensions(context).has(/** @type {import('./types').Extension} */ (extname(path))); +} +exports.hasValidExtension = hasValidExtension; + +/** @type {import('./ignore').default} */ exports.default = function ignore(path, context) { // check extension whitelist first (cheap) - if (!hasValidExtension(path, context)) return true; + if (!hasValidExtension(path, context)) { return true; } - if (!('import/ignore' in context.settings)) return false; + if (!('import/ignore' in context.settings)) { return false; } const ignoreStrings = context.settings['import/ignore']; for (let i = 0; i < ignoreStrings.length; i++) { @@ -53,8 +67,3 @@ exports.default = function ignore(path, context) { return false; }; - -function hasValidExtension(path, context) { - return validExtensions(context).has(extname(path)); -} -exports.hasValidExtension = hasValidExtension; diff --git a/utils/module-require.d.ts b/utils/module-require.d.ts new file mode 100644 index 0000000000..91df90d616 --- /dev/null +++ b/utils/module-require.d.ts @@ -0,0 +1,3 @@ +declare function moduleRequire(p: string): T; + +export default moduleRequire; diff --git a/utils/module-require.js b/utils/module-require.js index c03671ce5a..14006c5dc6 100644 --- a/utils/module-require.js +++ b/utils/module-require.js @@ -1,27 +1,33 @@ 'use strict'; + exports.__esModule = true; const Module = require('module'); const path = require('path'); // borrowed from babel-eslint +/** @type {(filename: string) => Module} */ function createModule(filename) { const mod = new Module(filename); mod.filename = filename; + // @ts-expect-error _nodeModulesPaths are undocumented mod.paths = Module._nodeModulePaths(path.dirname(filename)); return mod; } +/** @type {import('./module-require').default} */ exports.default = function moduleRequire(p) { try { // attempt to get espree relative to eslint const eslintPath = require.resolve('eslint'); const eslintModule = createModule(eslintPath); + // @ts-expect-error _resolveFilename is undocumented return require(Module._resolveFilename(p, eslintModule)); } catch (err) { /* ignore */ } try { // try relative to entry point + // @ts-expect-error TODO: figure out what this is return require.main.require(p); } catch (err) { /* ignore */ } diff --git a/utils/moduleVisitor.d.ts b/utils/moduleVisitor.d.ts new file mode 100644 index 0000000000..6f30186d71 --- /dev/null +++ b/utils/moduleVisitor.d.ts @@ -0,0 +1,26 @@ +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; + +type Visitor = (source: Node, importer: unknown) => any; + +type Options = { + amd?: boolean; + commonjs?: boolean; + esmodule?: boolean; + ignore?: string[]; +}; + +declare function moduleVisitor( + visitor: Visitor, + options?: Options, +): object; + +export default moduleVisitor; + +export type Schema = NonNullable; + +declare function makeOptionsSchema(additionalProperties?: Partial): Schema + +declare const optionsSchema: Schema; + +export { makeOptionsSchema, optionsSchema }; diff --git a/utils/moduleVisitor.js b/utils/moduleVisitor.js index 4d93a0199b..acdee6774f 100644 --- a/utils/moduleVisitor.js +++ b/utils/moduleVisitor.js @@ -1,109 +1,124 @@ 'use strict'; + exports.__esModule = true; +/** @typedef {import('estree').Node} Node */ +/** @typedef {{ arguments: import('estree').CallExpression['arguments'], callee: Node }} Call */ +/** @typedef {import('estree').ImportDeclaration | import('estree').ExportNamedDeclaration | import('estree').ExportAllDeclaration} Declaration */ + /** * Returns an object of node visitors that will call * 'visitor' with every discovered module path. * - * todo: correct function prototype for visitor - * @param {Function(String)} visitor [description] - * @param {[type]} options [description] - * @return {object} + * @type {(import('./moduleVisitor').default)} */ exports.default = function visitModules(visitor, options) { + const ignore = options && options.ignore; + const amd = !!(options && options.amd); + const commonjs = !!(options && options.commonjs); // if esmodule is not explicitly disabled, it is assumed to be enabled - options = Object.assign({ esmodule: true }, options); + const esmodule = !!Object.assign({ esmodule: true }, options).esmodule; - let ignoreRegExps = []; - if (options.ignore != null) { - ignoreRegExps = options.ignore.map(p => new RegExp(p)); - } + const ignoreRegExps = ignore == null ? [] : ignore.map((p) => new RegExp(p)); + /** @type {(source: undefined | null | import('estree').Literal, importer: Parameters[1]) => void} */ function checkSourceValue(source, importer) { - if (source == null) return; //? + if (source == null) { return; } //? // handle ignore - if (ignoreRegExps.some(re => re.test(source.value))) return; + if (ignoreRegExps.some((re) => re.test(String(source.value)))) { return; } // fire visitor visitor(source, importer); } // for import-y declarations + /** @type {(node: Declaration) => void} */ function checkSource(node) { checkSourceValue(node.source, node); } // for esmodule dynamic `import()` calls + /** @type {(node: import('estree').ImportExpression | import('estree').CallExpression) => void} */ function checkImportCall(node) { + /** @type {import('estree').Expression | import('estree').Literal | import('estree').CallExpression['arguments'][0]} */ let modulePath; // refs https://github.com/estree/estree/blob/HEAD/es2020.md#importexpression if (node.type === 'ImportExpression') { modulePath = node.source; } else if (node.type === 'CallExpression') { - if (node.callee.type !== 'Import') return; - if (node.arguments.length !== 1) return; + // @ts-expect-error this structure is from an older version of eslint + if (node.callee.type !== 'Import') { return; } + if (node.arguments.length !== 1) { return; } modulePath = node.arguments[0]; + } else { + throw new TypeError('this should be unreachable'); } - if (modulePath.type !== 'Literal') return; - if (typeof modulePath.value !== 'string') return; + if (modulePath.type !== 'Literal') { return; } + if (typeof modulePath.value !== 'string') { return; } checkSourceValue(modulePath, node); } // for CommonJS `require` calls // adapted from @mctep: https://git.io/v4rAu + /** @type {(call: Call) => void} */ function checkCommon(call) { - if (call.callee.type !== 'Identifier') return; - if (call.callee.name !== 'require') return; - if (call.arguments.length !== 1) return; + if (call.callee.type !== 'Identifier') { return; } + if (call.callee.name !== 'require') { return; } + if (call.arguments.length !== 1) { return; } const modulePath = call.arguments[0]; - if (modulePath.type !== 'Literal') return; - if (typeof modulePath.value !== 'string') return; + if (modulePath.type !== 'Literal') { return; } + if (typeof modulePath.value !== 'string') { return; } checkSourceValue(modulePath, call); } + /** @type {(call: Call) => void} */ function checkAMD(call) { - if (call.callee.type !== 'Identifier') return; - if (call.callee.name !== 'require' && - call.callee.name !== 'define') return; - if (call.arguments.length !== 2) return; + if (call.callee.type !== 'Identifier') { return; } + if (call.callee.name !== 'require' && call.callee.name !== 'define') { return; } + if (call.arguments.length !== 2) { return; } const modules = call.arguments[0]; - if (modules.type !== 'ArrayExpression') return; + if (modules.type !== 'ArrayExpression') { return; } for (const element of modules.elements) { - if (element.type !== 'Literal') continue; - if (typeof element.value !== 'string') continue; + if (!element) { continue; } + if (element.type !== 'Literal') { continue; } + if (typeof element.value !== 'string') { continue; } - if (element.value === 'require' || - element.value === 'exports') continue; // magic modules: https://github.com/requirejs/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#magic-modules + if ( + element.value === 'require' + || element.value === 'exports' + ) { + continue; // magic modules: https://github.com/requirejs/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#magic-modules + } checkSourceValue(element, element); } } const visitors = {}; - if (options.esmodule) { + if (esmodule) { Object.assign(visitors, { - 'ImportDeclaration': checkSource, - 'ExportNamedDeclaration': checkSource, - 'ExportAllDeclaration': checkSource, - 'CallExpression': checkImportCall, - 'ImportExpression': checkImportCall, + ImportDeclaration: checkSource, + ExportNamedDeclaration: checkSource, + ExportAllDeclaration: checkSource, + CallExpression: checkImportCall, + ImportExpression: checkImportCall, }); } - if (options.commonjs || options.amd) { - const currentCallExpression = visitors['CallExpression']; - visitors['CallExpression'] = function (call) { - if (currentCallExpression) currentCallExpression(call); - if (options.commonjs) checkCommon(call); - if (options.amd) checkAMD(call); + if (commonjs || amd) { + const currentCallExpression = visitors.CallExpression; + visitors.CallExpression = /** @type {(call: Call) => void} */ function (call) { + if (currentCallExpression) { currentCallExpression(call); } + if (commonjs) { checkCommon(call); } + if (amd) { checkAMD(call); } }; } @@ -111,28 +126,30 @@ exports.default = function visitModules(visitor, options) { }; /** - * make an options schema for the module visitor, optionally - * adding extra fields. + * make an options schema for the module visitor, optionally adding extra fields. + * @type {import('./moduleVisitor').makeOptionsSchema} */ function makeOptionsSchema(additionalProperties) { + /** @type {import('./moduleVisitor').Schema} */ const base = { - 'type': 'object', - 'properties': { - 'commonjs': { 'type': 'boolean' }, - 'amd': { 'type': 'boolean' }, - 'esmodule': { 'type': 'boolean' }, - 'ignore': { - 'type': 'array', - 'minItems': 1, - 'items': { 'type': 'string' }, - 'uniqueItems': true, + type: 'object', + properties: { + commonjs: { type: 'boolean' }, + amd: { type: 'boolean' }, + esmodule: { type: 'boolean' }, + ignore: { + type: 'array', + minItems: 1, + items: { type: 'string' }, + uniqueItems: true, }, }, - 'additionalProperties': false, + additionalProperties: false, }; if (additionalProperties) { for (const key in additionalProperties) { + // @ts-expect-error TS always has trouble with arbitrary object assignment/mutation base.properties[key] = additionalProperties[key]; } } @@ -142,8 +159,6 @@ function makeOptionsSchema(additionalProperties) { exports.makeOptionsSchema = makeOptionsSchema; /** - * json schema object for options parameter. can be used to build - * rule options schema object. - * @type {Object} + * json schema object for options parameter. can be used to build rule options schema object. */ exports.optionsSchema = makeOptionsSchema(); diff --git a/utils/package.json b/utils/package.json index 0c0678a5ec..df4871790b 100644 --- a/utils/package.json +++ b/utils/package.json @@ -1,12 +1,13 @@ { "name": "eslint-module-utils", - "version": "2.7.4", + "version": "2.8.1", "description": "Core utilities to support eslint-plugin-import and other module-related plugins.", "engines": { "node": ">=4" }, "scripts": { "prepublishOnly": "cp ../{LICENSE,.npmrc} ./", + "tsc": "tsc -p .", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -28,9 +29,21 @@ "dependencies": { "debug": "^3.2.7" }, + "devDependencies": { + "@ljharb/tsconfig": "^0.2.0", + "@types/debug": "^4.1.12", + "@types/eslint": "^8.56.3", + "@types/node": "^20.11.20", + "typescript": "next" + }, "peerDependenciesMeta": { "eslint": { "optional": true } + }, + "publishConfig": { + "ignore": [ + ".attw.json" + ] } } diff --git a/utils/parse.d.ts b/utils/parse.d.ts new file mode 100644 index 0000000000..f92ab3edc6 --- /dev/null +++ b/utils/parse.d.ts @@ -0,0 +1,11 @@ +import { AST, Rule } from 'eslint'; + + + +declare function parse( + path: string, + content: string, + context: Rule.RuleContext +): AST.Program | null | undefined; + +export default parse; diff --git a/utils/parse.js b/utils/parse.js index ac728ec5b2..94aca1d0f8 100644 --- a/utils/parse.js +++ b/utils/parse.js @@ -1,12 +1,17 @@ 'use strict'; + exports.__esModule = true; +/** @typedef {`.${string}`} Extension */ +/** @typedef {NonNullable & { 'import/extensions'?: Extension[], 'import/parsers'?: { [k: string]: Extension[] }, 'import/cache'?: { lifetime: number | '∞' | 'Infinity' } }} ESLintSettings */ + const moduleRequire = require('./module-require').default; const extname = require('path').extname; const fs = require('fs'); const log = require('debug')('eslint-plugin-import:parse'); +/** @type {(parserPath: NonNullable) => unknown} */ function getBabelEslintVisitorKeys(parserPath) { if (parserPath.endsWith('index.js')) { const hypotheticalLocation = parserPath.replace('index.js', 'visitor-keys.js'); @@ -18,15 +23,16 @@ function getBabelEslintVisitorKeys(parserPath) { return null; } +/** @type {(parserPath: import('eslint').Rule.RuleContext['parserPath'], parserInstance: { VisitorKeys: unknown }, parsedResult?: { visitorKeys?: unknown }) => unknown} */ function keysFromParser(parserPath, parserInstance, parsedResult) { // Exposed by @typescript-eslint/parser and @babel/eslint-parser if (parsedResult && parsedResult.visitorKeys) { return parsedResult.visitorKeys; } - if (/.*espree.*/.test(parserPath)) { + if (typeof parserPath === 'string' && (/.*espree.*/).test(parserPath)) { return parserInstance.VisitorKeys; } - if (/.*babel-eslint.*/.test(parserPath)) { + if (typeof parserPath === 'string' && (/.*babel-eslint.*/).test(parserPath)) { return getBabelEslintVisitorKeys(parserPath); } return null; @@ -34,30 +40,77 @@ function keysFromParser(parserPath, parserInstance, parsedResult) { // this exists to smooth over the unintentional breaking change in v2.7. // TODO, semver-major: avoid mutating `ast` and return a plain object instead. +/** @type {(ast: T, visitorKeys: unknown) => T} */ function makeParseReturn(ast, visitorKeys) { if (ast) { + // @ts-expect-error see TODO ast.visitorKeys = visitorKeys; + // @ts-expect-error see TODO ast.ast = ast; } return ast; } +/** @type {(text: string) => string} */ function stripUnicodeBOM(text) { return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text; } +/** @type {(text: string) => string} */ function transformHashbang(text) { return text.replace(/^#!([^\r\n]+)/u, (_, captured) => `//${captured}`); } -exports.default = function parse(path, content, context) { - - if (context == null) throw new Error('need context to parse properly'); +/** @type {(path: string, context: import('eslint').Rule.RuleContext & { settings?: ESLintSettings }) => import('eslint').Rule.RuleContext['parserPath']} */ +function getParserPath(path, context) { + const parsers = context.settings['import/parsers']; + if (parsers != null) { + // eslint-disable-next-line no-extra-parens + const extension = /** @type {Extension} */ (extname(path)); + for (const parserPath in parsers) { + if (parsers[parserPath].indexOf(extension) > -1) { + // use this alternate parser + log('using alt parser:', parserPath); + return parserPath; + } + } + } + // default to use ESLint parser + return context.parserPath; +} - let parserOptions = context.parserOptions; +/** @type {(path: string, context: import('eslint').Rule.RuleContext) => string | null | (import('eslint').Linter.ParserModule)} */ +function getParser(path, context) { const parserPath = getParserPath(path, context); + if (parserPath) { + return parserPath; + } + if ( + !!context.languageOptions + && !!context.languageOptions.parser + && typeof context.languageOptions.parser !== 'string' + && ( + // @ts-expect-error TODO: figure out a better type + typeof context.languageOptions.parser.parse === 'function' + // @ts-expect-error TODO: figure out a better type + || typeof context.languageOptions.parser.parseForESLint === 'function' + ) + ) { + return context.languageOptions.parser; + } - if (!parserPath) throw new Error('parserPath is required!'); + return null; +} + +/** @type {import('./parse').default} */ +exports.default = function parse(path, content, context) { + if (context == null) { throw new Error('need context to parse properly'); } + + // ESLint in "flat" mode only sets context.languageOptions.parserOptions + let parserOptions = context.languageOptions && context.languageOptions.parserOptions || context.parserOptions; + const parserOrPath = getParser(path, context); + + if (!parserOrPath) { throw new Error('parserPath or languageOptions.parser is required!'); } // hack: espree blows up with frozen options parserOptions = Object.assign({}, parserOptions); @@ -80,11 +133,12 @@ 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/import-js/eslint-plugin-import/issues/1408#issuecomment-509298962 + delete parserOptions.EXPERIMENTAL_useProjectService; delete parserOptions.project; delete parserOptions.projects; // require the parser relative to the main module (i.e., ESLint) - const parser = moduleRequire(parserPath); + const parser = typeof parserOrPath === 'string' ? moduleRequire(parserOrPath) : parserOrPath; // replicate bom strip and hashbang transform of ESLint // https://github.com/eslint/eslint/blob/b93af98b3c417225a027cabc964c38e779adb945/lib/linter/linter.js#L779 @@ -95,39 +149,26 @@ exports.default = function parse(path, content, context) { try { const parserRaw = parser.parseForESLint(content, parserOptions); ast = parserRaw.ast; - return makeParseReturn(ast, keysFromParser(parserPath, parser, parserRaw)); + // @ts-expect-error TODO: FIXME + return makeParseReturn(ast, keysFromParser(parserOrPath, parser, parserRaw)); } catch (e) { console.warn(); console.warn('Error while parsing ' + parserOptions.filePath); + // @ts-expect-error e is almost certainly an Error here console.warn('Line ' + e.lineNumber + ', column ' + e.column + ': ' + e.message); } if (!ast || typeof ast !== 'object') { console.warn( - '`parseForESLint` from parser `' + - parserPath + - '` is invalid and will just be ignored' + // Can only be invalid for custom parser per imports/parser + '`parseForESLint` from parser `' + (typeof parserOrPath === 'string' ? parserOrPath : '`context.languageOptions.parser`') + '` is invalid and will just be ignored' ); } else { - return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined)); + // @ts-expect-error TODO: FIXME + return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined)); } } const ast = parser.parse(content, parserOptions); - return makeParseReturn(ast, keysFromParser(parserPath, parser, undefined)); + // @ts-expect-error TODO: FIXME + return makeParseReturn(ast, keysFromParser(parserOrPath, parser, undefined)); }; - -function getParserPath(path, context) { - const parsers = context.settings['import/parsers']; - if (parsers != null) { - const extension = extname(path); - for (const parserPath in parsers) { - if (parsers[parserPath].indexOf(extension) > -1) { - // use this alternate parser - log('using alt parser:', parserPath); - return parserPath; - } - } - } - // default to use ESLint parser - return context.parserPath; -} diff --git a/utils/pkgDir.d.ts b/utils/pkgDir.d.ts new file mode 100644 index 0000000000..af01e2e9bf --- /dev/null +++ b/utils/pkgDir.d.ts @@ -0,0 +1,3 @@ +declare function pkgDir(cwd: string): string | null; + +export default pkgDir; diff --git a/utils/pkgDir.js b/utils/pkgDir.js index 34412202f1..84c334680a 100644 --- a/utils/pkgDir.js +++ b/utils/pkgDir.js @@ -5,6 +5,7 @@ const pkgUp = require('./pkgUp').default; exports.__esModule = true; +/** @type {import('./pkgDir').default} */ exports.default = function (cwd) { const fp = pkgUp({ cwd }); return fp ? path.dirname(fp) : null; diff --git a/utils/pkgUp.d.ts b/utils/pkgUp.d.ts new file mode 100644 index 0000000000..6382457bec --- /dev/null +++ b/utils/pkgUp.d.ts @@ -0,0 +1,3 @@ +declare function pkgUp(opts?: { cwd?: string }): string | null; + +export default pkgUp; diff --git a/utils/pkgUp.js b/utils/pkgUp.js index 049869719b..076e59fd76 100644 --- a/utils/pkgUp.js +++ b/utils/pkgUp.js @@ -1,4 +1,5 @@ 'use strict'; + exports.__esModule = true; const fs = require('fs'); @@ -6,22 +7,22 @@ const path = require('path'); /** * Derived significantly from package find-up@2.0.0. See license below. - * + * * @copyright Sindre Sorhus * MIT License * * Copyright (c) Sindre Sorhus (https://sindresorhus.com) - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -30,10 +31,13 @@ const path = require('path'); * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ + +/** @type {(filename: string | string[], cwd?: string) => string | null} */ function findUp(filename, cwd) { let dir = path.resolve(cwd || ''); const root = path.parse(dir).root; + /** @type {string[]} */ // @ts-expect-error TS sucks with concat const filenames = [].concat(filename); // eslint-disable-next-line no-constant-condition @@ -51,6 +55,7 @@ function findUp(filename, cwd) { } } +/** @type {import('./pkgUp').default} */ exports.default = function pkgUp(opts) { return findUp('package.json', opts && opts.cwd); }; diff --git a/utils/readPkgUp.d.ts b/utils/readPkgUp.d.ts new file mode 100644 index 0000000000..5fc1668879 --- /dev/null +++ b/utils/readPkgUp.d.ts @@ -0,0 +1,5 @@ +import pkgUp from './pkgUp'; + +declare function readPkgUp(opts?: Parameters[0]): {} | { pkg: string, path: string }; + +export default readPkgUp; diff --git a/utils/readPkgUp.js b/utils/readPkgUp.js index 6a6a1eea3e..08371931f2 100644 --- a/utils/readPkgUp.js +++ b/utils/readPkgUp.js @@ -1,31 +1,33 @@ 'use strict'; + exports.__esModule = true; const fs = require('fs'); const pkgUp = require('./pkgUp').default; +/** @type {(str: string) => string} */ function stripBOM(str) { return str.replace(/^\uFEFF/, ''); } /** * Derived significantly from read-pkg-up@2.0.0. See license below. - * + * * @copyright Sindre Sorhus * MIT License * * Copyright (c) Sindre Sorhus (https://sindresorhus.com) - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -34,6 +36,7 @@ function stripBOM(str) { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ +/** @type {import('./readPkgUp').default} */ exports.default = function readPkgUp(opts) { const fp = pkgUp(opts); diff --git a/utils/resolve.d.ts b/utils/resolve.d.ts new file mode 100644 index 0000000000..bb885bcfaf --- /dev/null +++ b/utils/resolve.d.ts @@ -0,0 +1,30 @@ +import type { Rule } from 'eslint'; + +import type ModuleCache from './ModuleCache'; +import type { ESLintSettings } from './types'; + +export type ResultNotFound = { found: false, path?: undefined }; +export type ResultFound = { found: true, path: string | null }; +export type ResolvedResult = ResultNotFound | ResultFound; + +export type ResolverResolve = (modulePath: string, sourceFile:string, config: unknown) => ResolvedResult; +export type ResolverResolveImport = (modulePath: string, sourceFile:string, config: unknown) => string | undefined; +export type Resolver = { interfaceVersion?: 1 | 2, resolve: ResolverResolve, resolveImport: ResolverResolveImport }; + +declare function resolve( + p: string, + context: Rule.RuleContext, +): ResolvedResult['path']; + +export default resolve; + +declare function fileExistsWithCaseSync( + filepath: string | null, + cacheSettings: ESLintSettings, + strict: boolean +): boolean | ReturnType; + +declare function relative(modulePath: string, sourceFile: string, settings: ESLintSettings): ResolvedResult['path']; + + +export { fileExistsWithCaseSync, relative }; diff --git a/utils/resolve.js b/utils/resolve.js index 4a35c6a472..5a3084351e 100644 --- a/utils/resolve.js +++ b/utils/resolve.js @@ -1,4 +1,5 @@ 'use strict'; + exports.__esModule = true; const fs = require('fs'); @@ -18,16 +19,30 @@ const fileExistsCache = new ModuleCache(); // Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) // Use `Module.createRequire` if available (added in Node v12.2.0) -const createRequire = Module.createRequire || 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; -}; +const createRequire = Module.createRequire + // @ts-expect-error this only exists in older node + || Module.createRequireFromPath + || /** @type {(filename: string) => unknown} */ function (filename) { + const mod = new Module(filename, void null); + mod.filename = filename; + // @ts-expect-error _nodeModulePaths is undocumented + mod.paths = Module._nodeModulePaths(path.dirname(filename)); + + // @ts-expect-error _compile is undocumented + mod._compile(`module.exports = require;`, filename); + + return mod.exports; + }; + +/** @type {(resolver: object) => resolver is import('./resolve').Resolver} */ +function isResolverValid(resolver) { + if ('interfaceVersion' in resolver && resolver.interfaceVersion === 2) { + return 'resolve' in resolver && !!resolver.resolve && typeof resolver.resolve === 'function'; + } + return 'resolveImport' in resolver && !!resolver.resolveImport && typeof resolver.resolveImport === 'function'; +} +/** @type {(target: T, sourceFile?: string | null | undefined) => undefined | ReturnType} */ function tryRequire(target, sourceFile) { let resolved; try { @@ -50,19 +65,70 @@ function tryRequire(target, sourceFile) { return require(resolved); } +/** @type {>(resolvers: string[] | string | { [k: string]: string }, map: T) => T} */ +function resolverReducer(resolvers, map) { + if (Array.isArray(resolvers)) { + resolvers.forEach((r) => resolverReducer(r, map)); + return map; + } + + if (typeof resolvers === 'string') { + map.set(resolvers, null); + return map; + } + + if (typeof resolvers === 'object') { + for (const key in resolvers) { + map.set(key, resolvers[key]); + } + return map; + } + + const err = new Error('invalid resolver config'); + err.name = ERROR_NAME; + throw err; +} + +/** @type {(sourceFile: string) => string} */ +function getBaseDir(sourceFile) { + return pkgDir(sourceFile) || process.cwd(); +} + +/** @type {(name: string, sourceFile: string) => import('./resolve').Resolver} */ +function requireResolver(name, sourceFile) { + // Try to resolve package with conventional name + const resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile) + || tryRequire(name, sourceFile) + || tryRequire(path.resolve(getBaseDir(sourceFile), name)); + + if (!resolver) { + const err = new Error(`unable to load resolver "${name}".`); + err.name = ERROR_NAME; + throw err; + } + if (!isResolverValid(resolver)) { + const err = new Error(`${name} with invalid interface loaded as resolver`); + err.name = ERROR_NAME; + throw err; + } + + return resolver; +} + // https://stackoverflow.com/a/27382838 +/** @type {import('./resolve').fileExistsWithCaseSync} */ exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cacheSettings, strict) { // don't care if the FS is case-sensitive - if (CASE_SENSITIVE_FS) return true; + if (CASE_SENSITIVE_FS) { return true; } // null means it resolved to a builtin - if (filepath === null) return true; - if (filepath.toLowerCase() === process.cwd().toLowerCase() && !strict) return true; + if (filepath === null) { return true; } + if (filepath.toLowerCase() === process.cwd().toLowerCase() && !strict) { return true; } const parsedPath = path.parse(filepath); const dir = parsedPath.dir; let result = fileExistsCache.get(filepath, cacheSettings); - if (result != null) return result; + if (result != null) { return result; } // base case if (dir === '' || parsedPath.root === filepath) { @@ -79,55 +145,51 @@ exports.fileExistsWithCaseSync = function fileExistsWithCaseSync(filepath, cache return result; }; -function relative(modulePath, sourceFile, settings) { - return fullResolve(modulePath, sourceFile, settings).path; -} - +/** @type {import('./types').ESLintSettings | null} */ +let prevSettings = null; +let memoizedHash = ''; +/** @type {(modulePath: string, sourceFile: string, settings: import('./types').ESLintSettings) => import('./resolve').ResolvedResult} */ function fullResolve(modulePath, sourceFile, settings) { // check if this is a bonus core module const coreSet = new Set(settings['import/core-modules']); - if (coreSet.has(modulePath)) return { found: true, path: null }; + if (coreSet.has(modulePath)) { return { found: true, path: null }; } const sourceDir = path.dirname(sourceFile); - const cacheKey = sourceDir + hashObject(settings).digest('hex') + modulePath; + + if (prevSettings !== settings) { + memoizedHash = hashObject(settings).digest('hex'); + prevSettings = settings; + } + + const cacheKey = sourceDir + memoizedHash + modulePath; const cacheSettings = ModuleCache.getSettings(settings); const cachedPath = fileExistsCache.get(cacheKey, cacheSettings); - if (cachedPath !== undefined) return { found: true, path: cachedPath }; + if (cachedPath !== undefined) { return { found: true, path: cachedPath }; } + /** @type {(resolvedPath: string | null) => void} */ function cache(resolvedPath) { fileExistsCache.set(cacheKey, resolvedPath); } + /** @type {(resolver: import('./resolve').Resolver, config: unknown) => import('./resolve').ResolvedResult} */ function withResolver(resolver, config) { - - function v1() { - try { - const resolved = resolver.resolveImport(modulePath, sourceFile, config); - if (resolved === undefined) return { found: false }; - return { found: true, path: resolved }; - } catch (err) { - return { found: false }; - } - } - - function v2() { + if (resolver.interfaceVersion === 2) { return resolver.resolve(modulePath, sourceFile, config); } - switch (resolver.interfaceVersion) { - case 2: - return v2(); - - default: - case 1: - return v1(); + try { + const resolved = resolver.resolveImport(modulePath, sourceFile, config); + if (resolved === undefined) { return { found: false }; } + return { found: true, path: resolved }; + } catch (err) { + return { found: false }; } } - const configResolvers = (settings['import/resolver'] - || { 'node': settings['import/resolve'] }); // backward compatibility + const configResolvers = settings['import/resolver'] + || { node: settings['import/resolve'] }; // backward compatibility const resolvers = resolverReducer(configResolvers, new Map()); @@ -137,7 +199,7 @@ function fullResolve(modulePath, sourceFile, settings) { const resolver = requireResolver(name, sourceFile); const resolved = withResolver(resolver, config); - if (!resolved.found) continue; + if (!resolved.found) { continue; } // else, counts cache(resolved.path); @@ -148,71 +210,22 @@ function fullResolve(modulePath, sourceFile, settings) { // cache(undefined) return { found: false }; } -exports.relative = relative; - -function resolverReducer(resolvers, map) { - if (Array.isArray(resolvers)) { - resolvers.forEach(r => resolverReducer(r, map)); - return map; - } - - if (typeof resolvers === 'string') { - map.set(resolvers, null); - return map; - } - - if (typeof resolvers === 'object') { - for (const key in resolvers) { - map.set(key, resolvers[key]); - } - return map; - } - - const err = new Error('invalid resolver config'); - err.name = ERROR_NAME; - throw err; -} - -function getBaseDir(sourceFile) { - return pkgDir(sourceFile) || process.cwd(); -} -function requireResolver(name, sourceFile) { - // Try to resolve package with conventional name - const resolver = tryRequire(`eslint-import-resolver-${name}`, sourceFile) || - tryRequire(name, sourceFile) || - tryRequire(path.resolve(getBaseDir(sourceFile), name)); - - if (!resolver) { - const err = new Error(`unable to load resolver "${name}".`); - err.name = ERROR_NAME; - throw err; - } - if (!isResolverValid(resolver)) { - const err = new Error(`${name} with invalid interface loaded as resolver`); - err.name = ERROR_NAME; - throw err; - } - - return resolver; -} -function isResolverValid(resolver) { - if (resolver.interfaceVersion === 2) { - return resolver.resolve && typeof resolver.resolve === 'function'; - } else { - return resolver.resolveImport && typeof resolver.resolveImport === 'function'; - } +/** @type {import('./resolve').relative} */ +function relative(modulePath, sourceFile, settings) { + return fullResolve(modulePath, sourceFile, settings).path; } +exports.relative = relative; +/** @type {Set} */ const erroredContexts = new Set(); /** * Given - * @param {string} p - module path - * @param {object} context - ESLint context - * @return {string} - the full module filesystem path; - * null if package is core; - * undefined if not found + * @param p - module path + * @param context - ESLint context + * @return - the full module filesystem path; null if package is core; undefined if not found + * @type {import('./resolve').default} */ function resolve(p, context) { try { @@ -221,8 +234,11 @@ function resolve(p, context) { 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. + // @ts-expect-error this might be an Error let errMessage = err.message; + // @ts-expect-error this might be an Error if (err.name !== ERROR_NAME && err.stack) { + // @ts-expect-error this might be an Error errMessage = err.stack.replace(/^Error: /, ''); } context.report({ diff --git a/utils/tsconfig.json b/utils/tsconfig.json new file mode 100644 index 0000000000..9e6fbc5cc1 --- /dev/null +++ b/utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "ES2017", + "moduleResolution": "node", + "maxNodeModuleJsDepth": 0, + }, + "exclude": [ + "coverage", + ], +} diff --git a/utils/types.d.ts b/utils/types.d.ts new file mode 100644 index 0000000000..e0c4f5749d --- /dev/null +++ b/utils/types.d.ts @@ -0,0 +1,9 @@ +import type { Rule } from 'eslint'; + +export type Extension = `.${string}`; + +export type ESLintSettings = NonNullable & { + 'import/extensions'?: Extension[]; + 'import/parsers'?: { [k: string]: Extension[] }; + 'import/cache'?: { lifetime: number | '∞' | 'Infinity' }; +}; diff --git a/utils/unambiguous.d.ts b/utils/unambiguous.d.ts new file mode 100644 index 0000000000..1679224189 --- /dev/null +++ b/utils/unambiguous.d.ts @@ -0,0 +1,7 @@ +import type { AST } from 'eslint'; + +declare function isModule(ast: AST.Program): boolean; + +declare function test(content: string): boolean; + +export { isModule, test } diff --git a/utils/unambiguous.js b/utils/unambiguous.js index 75f21693b7..20aabd1bd4 100644 --- a/utils/unambiguous.js +++ b/utils/unambiguous.js @@ -1,4 +1,5 @@ 'use strict'; + exports.__esModule = true; const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*=]))|import\(/m; @@ -10,7 +11,7 @@ const pattern = /(^|;)\s*(export|import)((\s+\w)|(\s*[{*=]))|import\(/m; * * Not perfect, just a fast way to disqualify large non-ES6 modules and * avoid a parse. - * @type {RegExp} + * @type {import('./unambiguous').test} */ exports.test = function isMaybeUnambiguousModule(content) { return pattern.test(content); @@ -21,9 +22,8 @@ const unambiguousNodeType = /^(?:(?:Exp|Imp)ort.*Declaration|TSExportAssignment) /** * Given an AST, return true if the AST unambiguously represents a module. - * @param {Program node} ast - * @return {Boolean} + * @type {import('./unambiguous').isModule} */ exports.isModule = function isUnambiguousModule(ast) { - return ast.body && ast.body.some(node => unambiguousNodeType.test(node.type)); + return ast.body && ast.body.some((node) => unambiguousNodeType.test(node.type)); }; diff --git a/utils/visit.d.ts b/utils/visit.d.ts new file mode 100644 index 0000000000..50559aaab0 --- /dev/null +++ b/utils/visit.d.ts @@ -0,0 +1,9 @@ +import type { Node } from 'estree'; + +declare function visit( + node: Node, + keys: { [k in Node['type']]?: (keyof Node)[] }, + visitorSpec: { [k in Node['type'] | `${Node['type']}:Exit`]?: Function } +): void; + +export default visit; diff --git a/utils/visit.js b/utils/visit.js index 77b09850ae..dd0c6248da 100644 --- a/utils/visit.js +++ b/utils/visit.js @@ -1,24 +1,30 @@ 'use strict'; + exports.__esModule = true; +/** @type {import('./visit').default} */ exports.default = function visit(node, keys, visitorSpec) { if (!node || !keys) { return; } const type = node.type; - if (typeof visitorSpec[type] === 'function') { - visitorSpec[type](node); + const visitor = visitorSpec[type]; + if (typeof visitor === 'function') { + visitor(node); } const childFields = keys[type]; if (!childFields) { return; } childFields.forEach((fieldName) => { + // @ts-expect-error TS sucks with concat [].concat(node[fieldName]).forEach((item) => { visit(item, keys, visitorSpec); }); }); - if (typeof visitorSpec[`${type}:Exit`] === 'function') { - visitorSpec[`${type}:Exit`](node); + + const exit = visitorSpec[`${type}:Exit`]; + if (typeof exit === 'function') { + exit(node); } };