├── .eslintignore ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── docs └── rules │ └── order-imports.md ├── eslint.config.mjs ├── gulpfile.js ├── package-lock.json ├── package.json ├── readme.md ├── src ├── index.ts ├── rules │ └── order-imports.ts └── util │ ├── import-type.ts │ └── static-require.ts ├── test ├── rules │ ├── order-imports-2.js │ └── order-imports.js └── utils.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # Compiled 2 | lib/ 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | branches: ['master'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x, 22.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - name: Test 26 | run: | 27 | npm install 28 | npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | test-results -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | src 3 | docs 4 | node_modules 5 | test-results 6 | gulpfile.js 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "semi": true, 5 | "singleQuote": true, 6 | "useTabs": true, 7 | "tabWidth": 4, 8 | "endOfLine": "lf", 9 | "trailingComma": "es5" 10 | } 11 | -------------------------------------------------------------------------------- /docs/rules/order-imports.md: -------------------------------------------------------------------------------- 1 | # Enforce a _configurable_ convention in module import order 2 | 3 | Enforce a convention in the order of `require()` / `import` statements. The default order is as shown in the following example: 4 | 5 | ```js 6 | // 1. "absolute" path modules 7 | import abs from '/absolute-module'; // uncommon 8 | // 2. all non-relative and non-absolute "modules" 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import _ from 'lodash'; 12 | import chalk from 'chalk'; 13 | import foo from 'src/foo'; 14 | // 3. modules from a "parent" directory 15 | import foo from '../foo'; 16 | import qux from '../../foo/qux'; 17 | // 4. "sibling" modules from the same or a sibling's directory 18 | import bar from './bar'; 19 | import baz from './bar/baz'; 20 | // 5. "index" of the current directory 21 | import main from './'; 22 | ``` 23 | 24 | Notes: 25 | 26 | - Unassigned imports are ignored (ex: `import 'polyfill'`), as the order they are imported in may be important. 27 | - Statements using the ES6 `import` syntax must appear before any `require()` statements. 28 | 29 | ## Usage 30 | 31 | To use the rule, update your `eslint` config. 32 | 33 | ```js 34 | { 35 | // .eslintrc.js 36 | plugins: ['eslint-plugin-import-helpers'], 37 | rules: { 38 | 'import-helpers/order-imports': [ 39 | 'warn', 40 | { // example configuration 41 | newlinesBetween: 'always', 42 | groups: [ 43 | 'module', 44 | '/^@shared/', 45 | ['parent', 'sibling', 'index'], 46 | ], 47 | alphabetize: { order: 'asc', ignoreCase: true }, 48 | }, 49 | ], 50 | } 51 | } 52 | ``` 53 | 54 | ## Options 55 | 56 | This rule supports the following options: 57 | 58 | ### `groups: Array>`: 59 | 60 | > The default value is `["absolute", "module", "parent", "sibling", "index"]`. 61 | 62 | Groups dictates how the imports should be grouped and it what order. `groups` is an array. Each value in the array must be a valid string or an array of valid strings. The valid strings are: 63 | 64 | - `'module'` | `'absolute'` | `'parent'` | `'sibling'` | `'index'` | `'type'` 65 | - or a regular expression like string, ex: `/^shared/` 66 | - the wrapping `/` is essential 67 | - in this example, it would match any import paths starting with `'shared'` 68 | - note: files are first categorized as matching a regular expression group before going into another group 69 | 70 | The enforced order is the same as the order of each element in a group. Omitted groups are implicitly grouped together as the last element. Example: 71 | 72 | ```js 73 | [ 74 | 'absolute', // any absolute path modules are first (ex: `/path/to/code.ts`) 75 | 'module', // then normal modules (ex: `lodash/pull`) 76 | ['sibling', 'parent'], // Then sibling and parent types. They can be mingled together 77 | '/^shared/', // any import paths starting with 'shared' 78 | 'index', // Then the index file 79 | ]; 80 | ``` 81 | 82 | You can set the options like this: 83 | 84 | ```js 85 | "import-helpers/order-imports": [ 86 | "error", 87 | {"groups": [ 'module', '/^@shared/', ['parent', 'sibling', 'index'] ]} 88 | ] 89 | ``` 90 | 91 | #### The `type` group 92 | 93 | TypeScript has what are called type imports, e.g., 94 | 95 | ```ts 96 | import type { ImportantType } from './thing'; 97 | ``` 98 | 99 | If you would like to treat these type imports as a completely separate group (instead of sorted according to the file it was imported from), add a `type` group to your `groups` list. 100 | 101 | With the `type` group: 102 | 103 | ```ts 104 | /* eslint import-helpers/order-imports: ["error", {"groups": ['sibling', 'module', 'type']}] */ 105 | import foo from './foo'; 106 | import fs from 'fs'; 107 | import path from 'path'; 108 | import type { ImportantType } from './sibling'; 109 | ``` 110 | 111 | Without the `type` group: 112 | 113 | ```ts 114 | /* eslint import-helpers/order-imports: ["error", {"groups": ['sibling', 'module']}] */ 115 | import foo from './foo'; 116 | import type { ImportantType } from './sibling'; 117 | import fs from 'fs'; 118 | import path from 'path'; 119 | ``` 120 | 121 | ### `newlinesBetween: [ignore|always|always-and-inside-groups|never]`: 122 | 123 | Enforces or forbids new lines between import groups: 124 | 125 | - If set to `ignore`, no errors related to new lines between import groups will be reported (default). 126 | - 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. 127 | - If set to `always-and-inside-groups`, at least one new line between each import statement will be enforced. 128 | - If set to `never`, no new lines are allowed in the entire import section. 129 | 130 | With the default group setting, the following will be valid: 131 | 132 | ```js 133 | /* eslint import-helpers/order-imports: ["error", {"newlinesBetween": "always"}] */ 134 | import fs from 'fs'; 135 | import path from 'path'; 136 | 137 | import sibling from './foo'; 138 | 139 | import index from './'; 140 | ``` 141 | 142 | ```js 143 | /* eslint import-helpers/order-imports: ["error", {"newlinesBetween": "never"}] */ 144 | import fs from 'fs'; 145 | import path from 'path'; 146 | import sibling from './foo'; 147 | import index from './'; 148 | ``` 149 | 150 | ### `alphabetize: object`: 151 | 152 | Sort the order within each group in alphabetical manner: 153 | 154 | - `order`: use `asc` to sort in ascending order, and `desc` to sort in descending order (default: `ignore`). 155 | - `ignoreCase` [boolean]: when `true`, the rule ignores case-sensitivity of the import name (default: `false`). 156 | 157 | Example setting: 158 | 159 | ```js 160 | alphabetize: { 161 | order: 'asc', /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */ 162 | ignoreCase: false, /* case-sensitive. This property does not have any effect if 'order' is set to 'ignore' */ 163 | } 164 | ``` 165 | 166 | This will pass: 167 | 168 | ```js 169 | import Baz from 'Baz'; 170 | import bar from 'bar'; 171 | import foo from 'foo'; 172 | ``` 173 | 174 | This will fail the rule check: 175 | 176 | ```js 177 | import foo from 'foo'; 178 | import bar from 'bar'; 179 | import Baz from 'Baz'; 180 | ``` 181 | 182 | ## Upgrading from v0.14 to v1 183 | 184 | ### `builtin` | `external` | `internal` → `module` 185 | 186 | In v1, `builtin`, `external`, `internal` have all been combined into one group, `module`. This simplifies the logic for this rule and makes it so it ONLY looks at the import strings and doesn't attempt any module resolution itself. The same functionality can be accomplished using regular expression groups. 187 | 188 | If you want to keep the same `builtin` functionality, create a custom regular expression group to replace it, like so. 189 | 190 | ```javascript 191 | // v0.14 192 | groups: ['builtin', 'sibling']; 193 | // v1 194 | groups: [ 195 | '/^(assert|async_hooks|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|http2|https|inspector|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|timers|tls|trace_events|tty|url|util|v8|vm|zli)/', 196 | 'sibling', 197 | ]; 198 | ``` 199 | 200 | If you want to keep the same `internal`/`external` functionality, create a custom regular expression group with your modules names. 201 | 202 | ### `'newlines-between' → 'newlinesBetween'` 203 | 204 | In v1, the `newLinesBetween` configuration option is now in camel case. 205 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import path from 'node:path'; 4 | import { fileURLToPath } from 'node:url'; 5 | import js from '@eslint/js'; 6 | import { FlatCompat } from '@eslint/eslintrc'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all, 14 | }); 15 | 16 | export default [ 17 | { 18 | ignores: ['**/lib/'], 19 | }, 20 | ...compat.extends( 21 | 'eslint:recommended', 22 | 'plugin:eslint-plugin/recommended', 23 | 'plugin:node/recommended', 24 | 'plugin:prettier/recommended' 25 | ), 26 | { 27 | languageOptions: { 28 | globals: { 29 | ...globals.node, 30 | }, 31 | 32 | ecmaVersion: 2021, 33 | sourceType: 'module', 34 | }, 35 | 36 | settings: { 37 | node: { 38 | tryExtensions: ['.js', '.json', '.node', '.ts', '.d.ts'], 39 | }, 40 | }, 41 | 42 | rules: { 43 | 'prettier/prettier': 0, 44 | 'eslint-plugin/prefer-message-ids': 1, 45 | }, 46 | }, 47 | { 48 | files: ['test/**/*.js'], 49 | 50 | languageOptions: { 51 | globals: { 52 | ...globals.mocha, 53 | }, 54 | }, 55 | }, 56 | ...compat.extends('plugin:@typescript-eslint/recommended').map((config) => ({ 57 | ...config, 58 | files: ['**/*.ts'], 59 | })), 60 | { 61 | files: ['**/*.ts'], 62 | 63 | languageOptions: { 64 | parser: tsParser, 65 | }, 66 | 67 | rules: { 68 | 'node/no-unsupported-features/es-syntax': [ 69 | 'error', 70 | { 71 | ignores: ['modules'], 72 | }, 73 | ], 74 | }, 75 | }, 76 | ]; 77 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const ts = require('gulp-typescript'); 3 | const rimraf = require('rimraf'); 4 | 5 | const SRC = 'src/**/?(*.js|*.ts)'; 6 | const DEST = 'lib'; 7 | const tsProject = ts.createProject('tsconfig.json'); 8 | 9 | gulp.task('clean', function (done) { 10 | rimraf.rimraf(DEST).then(() => done()); 11 | }); 12 | 13 | gulp.task( 14 | 'src', 15 | gulp.series('clean', function () { 16 | return gulp.src(SRC).pipe(tsProject()).pipe(gulp.dest(DEST)); 17 | }) 18 | ); 19 | 20 | gulp.task('prepublish', gulp.series('src')); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-import-helpers", 3 | "version": "2.0.1", 4 | "description": "ESLint Rules to Aid with Imports", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "gulp src", 8 | "lint": "npm run lint:js", 9 | "lint:js": "eslint --cache .", 10 | "prepublish": "npm run build", 11 | "test": "npm run build && npm run test-quick", 12 | "test-quick": "cross-env NODE_PATH=./lib nyc -s mocha -R dot --recursive test -t test-results" 13 | }, 14 | "keywords": [ 15 | "eslint", 16 | "eslint-plugin", 17 | "eslintplugin", 18 | "import", 19 | "eslint-plugin-import", 20 | "configurable" 21 | ], 22 | "repository": { 23 | "url": "https://github.com/Tibfib/eslint-plugin-import-helpers.git" 24 | }, 25 | "author": "Will Honey", 26 | "license": "MIT", 27 | "peerDependencies": { 28 | "eslint": "9.x" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^22.7.8", 32 | "@typescript-eslint/eslint-plugin": "^8.11.0", 33 | "@typescript-eslint/parser": "^8.11.0", 34 | "cross-env": "^7.0.3", 35 | "eslint": "^9.13.0", 36 | "eslint-config-prettier": "^9.1.0", 37 | "eslint-plugin-eslint-plugin": "^6.2.0", 38 | "eslint-plugin-node": "^11.1.0", 39 | "eslint-plugin-prettier": "^5.2.1", 40 | "gulp": "^4.0.2", 41 | "gulp-typescript": "^5.0.1", 42 | "mocha": "^10.7.3", 43 | "nyc": "^17.1.0", 44 | "prettier": "^3.3.3", 45 | "rimraf": "^6.0.1", 46 | "typescript": "^5.6.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-import-helpers 2 | 3 | > Originally forked/inspired by [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import) and [this fork](https://github.com/dannysindra/eslint-plugin-import) 4 | 5 | [![npm version](https://badge.fury.io/js/eslint-plugin-import-helpers.svg)](https://badge.fury.io/js/eslint-plugin-import-helpers) 6 | 7 | This package was created to supplement the rules provided by [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import). There are a lot of great rules in there, but we found it missing a few key use cases. 8 | 9 | # Alternatives 10 | 11 | > [!TIP] 12 | > Use Prettier! 13 | 14 | I recommend using [@ianvs/prettier-plugin-sort-imports](https://github.com/IanVS/prettier-plugin-sort-imports) instead of this ESLint plugin for sorting imports. It works really well and I think Prettier is better suited for formatting-related rules. 15 | 16 | # Rules 17 | 18 | #### [`order-imports`] 19 | 20 | Enforce a _configurable_ convention in module import order. See the [`order-imports`] page for configuration details. 21 | 22 | ```javascript 23 | // Given ESLint Config 24 | rules: { 25 | 'import-helpers/order-imports': [ 26 | 'warn', 27 | { 28 | newlinesBetween: 'always', // new line between groups 29 | groups: [ 30 | 'module', 31 | '/^@shared/', 32 | ['parent', 'sibling', 'index'], 33 | ], 34 | alphabetize: { order: 'asc', ignoreCase: true }, 35 | }, 36 | ], 37 | } 38 | 39 | // will fix 40 | import SiblingComponent from './SiblingComponent'; 41 | import lodash from 'lodash'; 42 | import SharedComponent from '@shared/components/SharedComponent'; 43 | import React from 'react'; 44 | 45 | // into 46 | import lodash from 'lodash'; 47 | import React from 'react'; 48 | 49 | import SharedComponent from '@shared/components/SharedComponent'; 50 | 51 | import SiblingComponent from './SiblingComponent'; 52 | ``` 53 | 54 | [`order-imports`]: ./docs/rules/order-imports.md 55 | 56 | # Installation 57 | 58 | ```sh 59 | npm install eslint-plugin-import-helpers -g 60 | ``` 61 | 62 | or if you manage ESLint as a dev dependency: 63 | 64 | ```sh 65 | # inside your project's working tree 66 | npm install eslint-plugin-import-helpers --save-dev 67 | ``` 68 | 69 | To add a rule, update your `.eslintrc.(yml|json|js)`: 70 | 71 | ```js 72 | { 73 | // .eslintrc.js 74 | plugins: ['eslint-plugin-import-helpers'], 75 | rules: { 76 | 'import-helpers/order-imports': [ 77 | 'warn', 78 | { // example configuration 79 | newlinesBetween: 'always', 80 | groups: [ 81 | 'module', 82 | '/^@shared/', 83 | ['parent', 'sibling', 'index'], 84 | ], 85 | alphabetize: { order: 'asc', ignoreCase: true }, 86 | }, 87 | ], 88 | } 89 | } 90 | ``` 91 | 92 | # ESLint Versions 93 | 94 | The 2.0 version supports ESLint v9. It may still work with older versions of ESLint but we no longer test against it. If you need ESLint v8 support, use 1.X. 95 | 96 | # TypeScript 97 | 98 | To use this plugin with TypeScript, you must use the TypeScript parser for ESLint. See [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser) for more details. 99 | 100 | Please note that the import sorter does not currently work with `type` imports. 101 | 102 | # Working with This Repo 103 | 104 | ## Dependencies 105 | 106 | | Name | Version | 107 | | ----------------------------- | ------- | 108 | | [node.js](https://nodejs.org) | 22.x | 109 | 110 | ## Running Tests 111 | 112 | First, `npm install` 113 | Then, `npm test` 114 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import orderImports from './rules/order-imports'; 2 | 3 | export = { 4 | rules: { 5 | 'order-imports': orderImports, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/rules/order-imports.ts: -------------------------------------------------------------------------------- 1 | import { 2 | determineImportType, 3 | isRegularExpressionGroup, 4 | ValidImportType, 5 | KnownImportType, 6 | RegExpGroups, 7 | ImportKind, 8 | } from '../util/import-type'; 9 | import { isStaticRequire } from '../util/static-require'; 10 | 11 | type NewLinesBetweenOption = 'ignore' | 'always' | 'always-and-inside-groups' | 'never'; 12 | const newLinesBetweenOptions: NewLinesBetweenOption[] = ['ignore', 'always', 'always-and-inside-groups', 'never']; 13 | 14 | type AlphabetizeOption = 'ignore' | 'asc' | 'desc'; 15 | type AlphabetizeConfig = { order: AlphabetizeOption; ignoreCase: boolean }; 16 | const alphabetizeOptions: AlphabetizeOption[] = ['ignore', 'asc', 'desc']; 17 | 18 | type Groups = (ValidImportType | ValidImportType[])[]; 19 | const defaultGroups: Groups = ['absolute', 'module', 'parent', 'sibling', 'index']; 20 | const MAX_GROUP_SIZE = 100000; // Higher than the number of imports we would ever expect to see in a single file. 21 | 22 | type RuleOptions = { 23 | groups?: Groups; 24 | newlinesBetween?: NewLinesBetweenOption; 25 | alphabetize?: Partial; 26 | }; 27 | 28 | type ImportType = 'require' | 'import'; 29 | 30 | type NodeOrToken = any; // todo; 31 | 32 | type Ranks = { [group: string]: number }; 33 | type Imported = { name: string; rank: number; node: NodeOrToken }; 34 | 35 | // REPORTING AND FIXING 36 | 37 | function reverse(array: Imported[]) { 38 | return array 39 | .map(function (v) { 40 | return { 41 | name: v.name, 42 | rank: -v.rank, 43 | node: v.node, 44 | }; 45 | }) 46 | .reverse(); 47 | } 48 | 49 | function getTokensOrCommentsAfter(sourceCode, node, count): NodeOrToken[] { 50 | let currentNodeOrToken = node; 51 | const result: NodeOrToken = []; 52 | for (let i = 0; i < count; i++) { 53 | currentNodeOrToken = sourceCode.getTokenOrCommentAfter(currentNodeOrToken); 54 | if (currentNodeOrToken == null) { 55 | break; 56 | } 57 | result.push(currentNodeOrToken); 58 | } 59 | return result; 60 | } 61 | 62 | function getTokensOrCommentsBefore(sourceCode, node, count): NodeOrToken[] { 63 | let currentNodeOrToken = node; 64 | const result: NodeOrToken = []; 65 | for (let i = 0; i < count; i++) { 66 | currentNodeOrToken = sourceCode.getTokenOrCommentBefore(currentNodeOrToken); 67 | if (currentNodeOrToken == null) { 68 | break; 69 | } 70 | result.push(currentNodeOrToken); 71 | } 72 | return result.reverse(); 73 | } 74 | 75 | function takeTokensAfterWhile(sourceCode, node, condition): NodeOrToken[] { 76 | const tokens: NodeOrToken[] = getTokensOrCommentsAfter(sourceCode, node, MAX_GROUP_SIZE); 77 | const result: NodeOrToken = []; 78 | for (let i = 0; i < tokens.length; i++) { 79 | if (condition(tokens[i])) { 80 | result.push(tokens[i]); 81 | } else { 82 | break; 83 | } 84 | } 85 | return result; 86 | } 87 | 88 | function takeTokensBeforeWhile(sourceCode, node, condition): NodeOrToken[] { 89 | const tokens: NodeOrToken[] = getTokensOrCommentsBefore(sourceCode, node, MAX_GROUP_SIZE); 90 | const result: NodeOrToken[] = []; 91 | for (let i = tokens.length - 1; i >= 0; i--) { 92 | if (condition(tokens[i])) { 93 | result.push(tokens[i]); 94 | } else { 95 | break; 96 | } 97 | } 98 | return result.reverse(); 99 | } 100 | 101 | function findOutOfOrder(imported) { 102 | if (imported.length === 0) { 103 | return []; 104 | } 105 | let maxSeenRankNode = imported[0]; 106 | return imported.filter(function (importedModule) { 107 | const res = importedModule.rank < maxSeenRankNode.rank; 108 | if (maxSeenRankNode.rank < importedModule.rank) { 109 | maxSeenRankNode = importedModule; 110 | } 111 | return res; 112 | }); 113 | } 114 | 115 | function findRootNode(node) { 116 | let parent = node; 117 | while (parent.parent != null && parent.parent.body == null) { 118 | parent = parent.parent; 119 | } 120 | return parent; 121 | } 122 | 123 | function findEndOfLineWithComments(sourceCode, node) { 124 | const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node)); 125 | const endOfTokens = 126 | tokensToEndOfLine.length > 0 ? tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1] : node.range[1]; 127 | let result = endOfTokens; 128 | for (let i = endOfTokens; i < sourceCode.text.length; i++) { 129 | if (sourceCode.text[i] === '\n') { 130 | result = i + 1; 131 | break; 132 | } 133 | if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t' && sourceCode.text[i] !== '\r') { 134 | break; 135 | } 136 | result = i + 1; 137 | } 138 | return result; 139 | } 140 | 141 | function commentOnSameLineAs(node): (token: NodeOrToken) => boolean { 142 | return (token) => 143 | (token.type === 'Block' || token.type === 'Line') && 144 | token.loc.start.line === token.loc.end.line && 145 | token.loc.end.line === node.loc.end.line; 146 | } 147 | 148 | function findStartOfLineWithComments(sourceCode, node) { 149 | const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node)); 150 | const startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].range[0] : node.range[0]; 151 | let result = startOfTokens; 152 | for (let i = startOfTokens - 1; i > 0; i--) { 153 | if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t') { 154 | break; 155 | } 156 | result = i; 157 | } 158 | return result; 159 | } 160 | 161 | function isPlainRequireModule(node): boolean { 162 | if (node.type !== 'VariableDeclaration') { 163 | return false; 164 | } 165 | if (node.declarations.length !== 1) { 166 | return false; 167 | } 168 | const decl = node.declarations[0]; 169 | 170 | return ( 171 | decl.id != null && 172 | decl.id.type === 'Identifier' && 173 | decl.init != null && 174 | decl.init.type === 'CallExpression' && 175 | decl.init.callee != null && 176 | decl.init.callee.name === 'require' && 177 | decl.init.arguments != null && 178 | decl.init.arguments.length === 1 && 179 | decl.init.arguments[0].type === 'Literal' 180 | ); 181 | } 182 | 183 | function isPlainImportModule(node: NodeOrToken): boolean { 184 | return node.type === 'ImportDeclaration' && node.specifiers != null && node.specifiers.length > 0; 185 | } 186 | 187 | function canCrossNodeWhileReorder(node: NodeOrToken): boolean { 188 | return isPlainRequireModule(node) || isPlainImportModule(node); 189 | } 190 | 191 | function canReorderItems(firstNode: NodeOrToken, secondNode: NodeOrToken): boolean { 192 | const parent = firstNode.parent; 193 | const firstIndex = parent.body.indexOf(firstNode); 194 | const secondIndex = parent.body.indexOf(secondNode); 195 | const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1); 196 | for (const nodeBetween of nodesBetween) { 197 | if (!canCrossNodeWhileReorder(nodeBetween)) { 198 | return false; 199 | } 200 | } 201 | return true; 202 | } 203 | 204 | function fixOutOfOrder(context, firstNode: NodeOrToken, secondNode: NodeOrToken, order: 'before' | 'after'): void { 205 | const sourceCode = context.getSourceCode(); 206 | 207 | const firstRoot = findRootNode(firstNode.node); 208 | const firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot); 209 | const firstRootEnd = findEndOfLineWithComments(sourceCode, firstRoot); 210 | 211 | const secondRoot = findRootNode(secondNode.node); 212 | const secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot); 213 | const secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot); 214 | const canFix = canReorderItems(firstRoot, secondRoot); 215 | 216 | let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd); 217 | if (newCode[newCode.length - 1] !== '\n') { 218 | newCode = newCode + '\n'; 219 | } 220 | 221 | const message = '`' + secondNode.name + '` import should occur ' + order + ' import of `' + firstNode.name + '`'; 222 | 223 | if (order === 'before') { 224 | context.report({ 225 | node: secondNode.node, 226 | message: message, 227 | fix: 228 | canFix && 229 | ((fixer) => 230 | fixer.replaceTextRange( 231 | [firstRootStart, secondRootEnd], 232 | newCode + sourceCode.text.substring(firstRootStart, secondRootStart) 233 | )), 234 | }); 235 | } else if (order === 'after') { 236 | context.report({ 237 | node: secondNode.node, 238 | message: message, 239 | fix: 240 | canFix && 241 | ((fixer) => 242 | fixer.replaceTextRange( 243 | [secondRootStart, firstRootEnd], 244 | sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode 245 | )), 246 | }); 247 | } 248 | } 249 | 250 | function reportOutOfOrder(context, imported: Imported[], outOfOrder, order: 'before' | 'after'): void { 251 | outOfOrder.forEach(function (imp) { 252 | const found = imported.find(function hasHigherRank(importedItem) { 253 | return importedItem.rank > imp.rank; 254 | }); 255 | fixOutOfOrder(context, found, imp, order); 256 | }); 257 | } 258 | 259 | function makeOutOfOrderReport(context, imported: Imported[]) { 260 | const outOfOrder = findOutOfOrder(imported); 261 | if (!outOfOrder.length) { 262 | return; 263 | } 264 | // There are things to report. Try to minimize the number of reported errors. 265 | const reversedImported = reverse(imported); 266 | const reversedOrder = findOutOfOrder(reversedImported); 267 | if (reversedOrder.length < outOfOrder.length) { 268 | reportOutOfOrder(context, reversedImported, reversedOrder, 'after'); 269 | return; 270 | } 271 | reportOutOfOrder(context, imported, outOfOrder, 'before'); 272 | } 273 | 274 | function mutateRanksToAlphabetize(imported, order, ignoreCase) { 275 | const groupedByRanks = imported.reduce(function (acc, importedItem) { 276 | acc[importedItem.rank] = acc[importedItem.rank] || []; 277 | acc[importedItem.rank].push(importedItem.name); 278 | return acc; 279 | }, {}); 280 | 281 | const groupRanks = Object.keys(groupedByRanks); 282 | 283 | // sort imports locally within their group 284 | groupRanks.forEach(function (groupRank) { 285 | groupedByRanks[groupRank].sort(function (importA, importB) { 286 | return ignoreCase ? importA.localeCompare(importB) : importA < importB ? -1 : importA === importB ? 0 : 1; 287 | }); 288 | 289 | if (order === 'desc') { 290 | groupedByRanks[groupRank].reverse(); 291 | } 292 | }); 293 | 294 | // add decimal ranking to sort within the group 295 | const alphabetizedRanks = groupRanks.sort().reduce(function (acc, groupRank) { 296 | groupedByRanks[groupRank].forEach(function (importedItemName, index) { 297 | acc[importedItemName] = +groupRank + index / MAX_GROUP_SIZE; 298 | }); 299 | return acc; 300 | }, {}); 301 | 302 | // mutate the original group-rank with alphabetized-rank 303 | imported.forEach(function (importedItem) { 304 | importedItem.rank = alphabetizedRanks[importedItem.name]; 305 | }); 306 | } 307 | 308 | function getRegExpGroups(ranks: Ranks): RegExpGroups { 309 | return Object.keys(ranks) 310 | .filter(isRegularExpressionGroup) 311 | .map((rank): [string, RegExp] => [rank, new RegExp(rank.slice(1, rank.length - 1))]); 312 | } 313 | 314 | // DETECTING 315 | 316 | function computeRank( 317 | ranks: Ranks, 318 | regExpGroups, 319 | name: string, 320 | type: ImportType, 321 | importKind: ImportKind, 322 | treatTypesAsGroup: boolean 323 | ): number { 324 | return ( 325 | ranks[determineImportType({ name, regExpGroups, importKind, treatTypesAsGroup })] + 326 | (type === 'import' ? 0 : MAX_GROUP_SIZE) 327 | ); 328 | } 329 | 330 | function registerNode( 331 | node: NodeOrToken, 332 | name: string, 333 | type: ImportType, 334 | ranks, 335 | regExpGroups, 336 | imported: Imported[], 337 | treatTypesAsGroup: boolean 338 | ) { 339 | const rank = computeRank(ranks, regExpGroups, name, type, node.importKind || 'value', treatTypesAsGroup); 340 | if (rank !== -1) { 341 | imported.push({ name, rank, node }); 342 | } 343 | } 344 | 345 | function isInVariableDeclarator(node: NodeOrToken): boolean { 346 | return node && (node.type === 'VariableDeclarator' || isInVariableDeclarator(node.parent)); 347 | } 348 | 349 | const knownTypes: KnownImportType[] = ['absolute', 'module', 'parent', 'sibling', 'index', 'type']; 350 | 351 | // Creates an object with type-rank pairs. 352 | // Example: { index: 0, sibling: 1, parent: 1, module: 2 } 353 | // Will throw an error if it: contains a type that does not exist in the list, does not start and end with '/', or has a duplicate 354 | function convertGroupsToRanks(groups: Groups): Ranks { 355 | const rankObject = groups.reduce(function (res, group, index) { 356 | if (typeof group === 'string') group = [group]; // wrap them all in arrays 357 | group.forEach(function (groupItem: ValidImportType) { 358 | if (!isRegularExpressionGroup(groupItem) && knownTypes.indexOf(groupItem as KnownImportType) === -1) { 359 | throw new Error( 360 | `Incorrect configuration of the rule: Unknown type ${JSON.stringify( 361 | groupItem 362 | )}. For a regular expression, wrap the string in '/', ex: '/shared/'` 363 | ); 364 | } 365 | if (res[groupItem] !== undefined) { 366 | throw new Error('Incorrect configuration of the rule: `' + groupItem + '` is duplicated'); 367 | } 368 | res[groupItem] = index; 369 | }); 370 | return res; 371 | }, {}); 372 | 373 | const omittedTypes = knownTypes.filter(function (type) { 374 | return rankObject[type] === undefined; 375 | }); 376 | 377 | return omittedTypes.reduce(function (res, type) { 378 | res[type] = groups.length; 379 | return res; 380 | }, rankObject); 381 | } 382 | 383 | function fixNewLineAfterImport(context, previousImport) { 384 | const prevRoot = findRootNode(previousImport.node); 385 | const tokensToEndOfLine = takeTokensAfterWhile(context.getSourceCode(), prevRoot, commentOnSameLineAs(prevRoot)); 386 | 387 | let endOfLine = prevRoot.range[1]; 388 | if (tokensToEndOfLine.length > 0) { 389 | endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].range[1]; 390 | } 391 | return (fixer) => fixer.insertTextAfterRange([prevRoot.range[0], endOfLine], '\n'); 392 | } 393 | 394 | function removeNewLineAfterImport(context, currentImport, previousImport) { 395 | const sourceCode = context.getSourceCode(); 396 | const prevRoot = findRootNode(previousImport.node); 397 | const currRoot = findRootNode(currentImport.node); 398 | const rangeToRemove = [ 399 | findEndOfLineWithComments(sourceCode, prevRoot), 400 | findStartOfLineWithComments(sourceCode, currRoot), 401 | ]; 402 | if (/^\s*$/.test(sourceCode.text.substring(rangeToRemove[0], rangeToRemove[1]))) { 403 | return (fixer) => fixer.removeRange(rangeToRemove); 404 | } 405 | return undefined; 406 | } 407 | 408 | function makeNewlinesBetweenReport( 409 | context: any, 410 | imported: Imported[], 411 | newlinesBetweenImports: NewLinesBetweenOption 412 | ): void { 413 | const getNumberOfEmptyLinesBetween = (currentImport: Imported, previousImport: Imported): number => { 414 | const linesBetweenImports = context 415 | .getSourceCode() 416 | .lines.slice(previousImport.node.loc.end.line, currentImport.node.loc.start.line - 1); 417 | 418 | return linesBetweenImports.filter((line: any) => !line.trim().length).length; 419 | }; 420 | let previousImport = imported[0]; 421 | 422 | imported.slice(1).forEach(function (currentImport) { 423 | const emptyLinesBetween: number = getNumberOfEmptyLinesBetween(currentImport, previousImport); 424 | 425 | const currentGroupRank = Math.floor(currentImport.rank); // each group rank is a whole number, within a group, decimals indicate subranking. yeah, not great. 426 | const previousGroupRank = Math.floor(previousImport.rank); 427 | 428 | if (newlinesBetweenImports === 'always' || newlinesBetweenImports === 'always-and-inside-groups') { 429 | if (currentGroupRank !== previousGroupRank && emptyLinesBetween === 0) { 430 | context.report({ 431 | node: previousImport.node, 432 | message: 'There should be at least one empty line between import groups', 433 | fix: fixNewLineAfterImport(context, previousImport), 434 | }); 435 | } else if ( 436 | currentGroupRank === previousGroupRank && 437 | emptyLinesBetween === 0 && 438 | newlinesBetweenImports === 'always-and-inside-groups' 439 | ) { 440 | context.report({ 441 | node: previousImport.node, 442 | message: 'There should be at least one empty line between imports', 443 | fix: fixNewLineAfterImport(context, previousImport), 444 | }); 445 | } else if ( 446 | currentGroupRank === previousGroupRank && 447 | emptyLinesBetween > 0 && 448 | newlinesBetweenImports !== 'always-and-inside-groups' 449 | ) { 450 | context.report({ 451 | node: previousImport.node, 452 | message: 'There should be no empty line within import group', 453 | fix: removeNewLineAfterImport(context, currentImport, previousImport), 454 | }); 455 | } 456 | } else if (emptyLinesBetween > 0) { 457 | context.report({ 458 | node: previousImport.node, 459 | message: 'There should be no empty line between import groups', 460 | fix: removeNewLineAfterImport(context, currentImport, previousImport), 461 | }); 462 | } 463 | 464 | previousImport = currentImport; 465 | }); 466 | } 467 | 468 | function getAlphabetizeConfig(options: RuleOptions): AlphabetizeConfig { 469 | const alphabetize = options.alphabetize || {}; 470 | const order = alphabetize.order || 'ignore'; 471 | const ignoreCase = alphabetize.ignoreCase || false; 472 | 473 | if (typeof order !== 'string') { 474 | throw new Error( 475 | 'Incorrect alphabetize config: `order` property should be ' + 476 | 'a string, but `' + 477 | JSON.stringify(typeof order) + 478 | '` found instead.' 479 | ); 480 | } else if (['ignore', 'asc', 'desc'].indexOf(order) === -1) { 481 | throw new Error( 482 | 'Incorrect alphabetize config: `order` property should be ' + 483 | 'either `ignore`, `asc` or `desc`, but `' + 484 | JSON.stringify(order) + 485 | '` found instead.' 486 | ); 487 | } 488 | 489 | if (typeof ignoreCase !== 'boolean') { 490 | throw new Error( 491 | 'Incorrect alphabetize config: ignoreCase should be ' + 492 | 'a boolean, but `' + 493 | JSON.stringify(typeof ignoreCase) + 494 | '` found instead.' 495 | ); 496 | } 497 | 498 | return { order, ignoreCase }; 499 | } 500 | 501 | export default { 502 | meta: { 503 | type: 'suggestion', 504 | docs: { 505 | url: 'https://github.com/Tibfib/eslint-plugin-import-helpers/blob/master/docs/rules/order-imports.md', 506 | }, 507 | 508 | fixable: 'code', 509 | schema: [ 510 | { 511 | type: 'object', 512 | properties: { 513 | groups: { 514 | type: 'array', 515 | }, 516 | newlinesBetween: { 517 | enum: newLinesBetweenOptions, 518 | }, 519 | alphabetize: { 520 | type: 'object', 521 | properties: { 522 | order: { 523 | enum: alphabetizeOptions, 524 | default: 'ignore', 525 | }, 526 | ignoreCase: { 527 | type: 'boolean', 528 | default: false, 529 | }, 530 | }, 531 | }, 532 | }, 533 | additionalProperties: false, 534 | }, 535 | ], 536 | }, 537 | 538 | create: function importOrderRule(context) { 539 | const options: RuleOptions = context.options[0] || {}; 540 | const newlinesBetweenImports: NewLinesBetweenOption = options.newlinesBetween || 'ignore'; 541 | const groups = options.groups || defaultGroups; 542 | const treatTypesAsGroup = groups.includes('type'); 543 | 544 | let alphabetize: AlphabetizeConfig; 545 | let ranks: Ranks; 546 | let regExpGroups: RegExpGroups; 547 | 548 | try { 549 | alphabetize = getAlphabetizeConfig(options); 550 | ranks = convertGroupsToRanks(groups); 551 | regExpGroups = getRegExpGroups(ranks); 552 | } catch (error) { 553 | // Malformed configuration 554 | return { 555 | Program: function (node) { 556 | context.report({ node, message: error.message }); 557 | }, 558 | }; 559 | } 560 | let imported: Imported[] = []; 561 | 562 | let level = 0; 563 | const incrementLevel = () => level++; 564 | const decrementLevel = () => level--; 565 | 566 | return { 567 | ImportDeclaration: function handleImports(node) { 568 | if (node.specifiers.length) { 569 | // Ignoring unassigned imports 570 | const name: string = node.source.value; 571 | registerNode(node, name, 'import', ranks, regExpGroups, imported, treatTypesAsGroup); 572 | } 573 | }, 574 | CallExpression: function handleRequires(node) { 575 | if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) { 576 | return; 577 | } 578 | const name: string = node.arguments[0].value; 579 | registerNode(node, name, 'require', ranks, regExpGroups, imported, treatTypesAsGroup); 580 | }, 581 | 'Program:exit': function reportAndReset() { 582 | if (alphabetize.order !== 'ignore') { 583 | mutateRanksToAlphabetize(imported, alphabetize.order, alphabetize.ignoreCase); 584 | } 585 | 586 | makeOutOfOrderReport(context, imported); 587 | 588 | if (newlinesBetweenImports !== 'ignore') { 589 | makeNewlinesBetweenReport(context, imported, newlinesBetweenImports); 590 | } 591 | 592 | imported = []; 593 | }, 594 | FunctionDeclaration: incrementLevel, 595 | FunctionExpression: incrementLevel, 596 | ArrowFunctionExpression: incrementLevel, 597 | BlockStatement: incrementLevel, 598 | ObjectExpression: incrementLevel, 599 | 'FunctionDeclaration:exit': decrementLevel, 600 | 'FunctionExpression:exit': decrementLevel, 601 | 'ArrowFunctionExpression:exit': decrementLevel, 602 | 'BlockStatement:exit': decrementLevel, 603 | 'ObjectExpression:exit': decrementLevel, 604 | }; 605 | }, 606 | }; 607 | -------------------------------------------------------------------------------- /src/util/import-type.ts: -------------------------------------------------------------------------------- 1 | export function isAbsolute(name: string): boolean { 2 | return name.indexOf('/') === 0; 3 | } 4 | 5 | // a module is anything that doesn't start with a . or a / or a \ 6 | const moduleRegExp = /^[^/\\.]/; 7 | export function isModule(name: string): boolean { 8 | return moduleRegExp.test(name); 9 | } 10 | 11 | export function isRelativeToParent(name: string): boolean { 12 | return /^\.\.[\\/]/.test(name); 13 | } 14 | 15 | const indexFiles = ['.', './', './index', './index.js', './index.ts']; 16 | export function isIndex(name: string): boolean { 17 | return indexFiles.indexOf(name) !== -1; // todo make this more flexible with different line endings 18 | } 19 | 20 | export function isRelativeToSibling(name: string): boolean { 21 | return /^\.[\\/]/.test(name); 22 | } 23 | 24 | export function isRegularExpressionGroup(group: string): boolean { 25 | return !!group && group[0] === '/' && group[group.length - 1] === '/' && group.length > 1; 26 | } 27 | 28 | export type KnownImportType = 'absolute' | 'module' | 'parent' | 'index' | 'sibling' | 'type'; 29 | export type ValidImportType = KnownImportType | string; // this string should be a string surrounded with '/' 30 | export type EveryImportType = ValidImportType | 'unknown'; 31 | export type RegExpGroups = [string, RegExp][]; // array of tuples of [string, RegExp] 32 | export type ImportKind = 'value' | 'type'; 33 | 34 | export function determineImportType({ 35 | name, 36 | regExpGroups, 37 | importKind, 38 | treatTypesAsGroup = false, 39 | }: { 40 | name: string; 41 | regExpGroups: RegExpGroups; 42 | importKind: ImportKind; 43 | treatTypesAsGroup?: boolean; 44 | }): EveryImportType { 45 | if (treatTypesAsGroup && importKind === 'type') return 'type'; 46 | 47 | const matchingRegExpGroup = regExpGroups.find(([_groupName, regExp]) => regExp.test(name)); 48 | if (matchingRegExpGroup) return matchingRegExpGroup[0]; 49 | 50 | if (isAbsolute(name)) return 'absolute'; 51 | if (isModule(name)) return 'module'; 52 | if (isRelativeToParent(name)) return 'parent'; 53 | if (isIndex(name)) return 'index'; 54 | if (isRelativeToSibling(name)) return 'sibling'; 55 | 56 | return 'unknown'; 57 | } 58 | -------------------------------------------------------------------------------- /src/util/static-require.ts: -------------------------------------------------------------------------------- 1 | export function isStaticRequire(node: any): boolean { 2 | return ( 3 | node && 4 | node.callee && 5 | node.callee.type === 'Identifier' && 6 | node.callee.name === 'require' && 7 | node.arguments.length === 1 && 8 | node.arguments[0].type === 'Literal' && 9 | typeof node.arguments[0].value === 'string' 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /test/rules/order-imports-2.js: -------------------------------------------------------------------------------- 1 | const RuleTester = require('eslint').RuleTester; 2 | 3 | const { test } = require('../utils'); 4 | 5 | const ruleTester = new RuleTester(); 6 | const { default: rule } = require('../../lib/rules/order-imports'); 7 | 8 | ruleTester.run('order', rule, { 9 | valid: [ 10 | // Default order using import 11 | // absolute at top 12 | test({ 13 | name: 'modules starting with _ or @ are sorted with modules', 14 | code: ` 15 | import abs from '/absolute/module'; 16 | 17 | import blah from '_lodash'; 18 | import module from '@module/core'; 19 | import print from '@module/print'; 20 | import async, {foo1} from 'async'; 21 | import fs from 'fs'; 22 | 23 | import relParent1 from '../foo'; 24 | import relParent2, {foo2} from '../foo/bar'; 25 | 26 | import relParent3 from '@shared'; 27 | 28 | import sibling, {foo3} from './foo'; 29 | 30 | import index from './';`, 31 | options: [ 32 | { 33 | groups: ['absolute', 'module', 'parent', '/@shared/', 'sibling', 'index'], 34 | alphabetize: { order: 'asc', ignoreCase: true }, 35 | newlinesBetween: 'always', 36 | }, 37 | ], 38 | }), 39 | test({ 40 | code: ` 41 | import async, {foo1} from 'async'; 42 | import fs from 'fs'; 43 | 44 | import relParent3 from '@shared'; 45 | 46 | import relParent1 from '../foo'; 47 | import relParent2, {foo2} from '../foo/bar'; 48 | import index from './'; 49 | import sibling, {foo3} from './foo';`, 50 | options: [ 51 | { 52 | groups: [['module'], '/@shared/', ['parent', 'sibling', 'index']], 53 | alphabetize: { order: 'asc', ignoreCase: true }, 54 | newlinesBetween: 'always', 55 | }, 56 | ], 57 | }), 58 | test({ 59 | code: ` 60 | import fs from 'fs'; 61 | import async, {foo1} from 'async'; 62 | 63 | import relParent3 from '@shared'; 64 | 65 | import relParent1 from '../foo'; 66 | import relParent2, {foo2} from '../foo/bar'; 67 | 68 | import sibling, {foo3} from './foo'; 69 | 70 | import index from './'; 71 | `, 72 | options: [ 73 | { 74 | groups: ['module', '/^@shared/', 'parent', 'sibling', 'index'], 75 | newlinesBetween: 'always', 76 | }, 77 | ], 78 | }), 79 | test({ 80 | code: ` 81 | import async, {foo1} from 'async'; 82 | 83 | import fs from 'fs'; 84 | 85 | import relParent3 from '@shared'; 86 | 87 | import relParent1 from '../foo'; 88 | 89 | import relParent2, {foo2} from '../foo/bar'; 90 | 91 | import index from './'; 92 | 93 | import sibling, {foo3} from './foo';`, 94 | options: [ 95 | { 96 | groups: [['module'], '/@shared/', ['parent', 'sibling', 'index']], 97 | alphabetize: { order: 'asc', ignoreCase: true }, 98 | newlinesBetween: 'always-and-inside-groups', 99 | }, 100 | ], 101 | }), 102 | // test out "types" 103 | test({ 104 | name: 'type at end', 105 | code: ` 106 | import sib from './sib'; 107 | import type { relative } from './relative'; 108 | `, 109 | languageOptions: { 110 | parser: require('@typescript-eslint/parser'), 111 | }, 112 | options: [{ groups: ['module', 'sibling', 'type'] }], 113 | }), 114 | test({ 115 | name: 'type at beginning', 116 | code: ` 117 | import type { relative } from './relative'; 118 | import sib from './sib'; 119 | `, 120 | languageOptions: { 121 | parser: require('@typescript-eslint/parser'), 122 | }, 123 | options: [{ groups: ['type', 'module', 'sibling'] }], 124 | }), 125 | test({ 126 | name: "explicit no type group means don't treat types special, both of these pass (when alphabetization is ignored) 1", 127 | code: ` 128 | import sib from './sib'; 129 | import type { relative } from './relative'; 130 | `, 131 | languageOptions: { 132 | parser: require('@typescript-eslint/parser'), 133 | }, 134 | options: [{ groups: ['module', 'sibling'] }], 135 | }), 136 | test({ 137 | name: "explicit no type group means don't treat types special, both of these pass (when alphabetization is ignored) 2", 138 | code: ` 139 | import type { relative } from './relative'; 140 | import sib from './sib'; 141 | `, 142 | languageOptions: { 143 | parser: require('@typescript-eslint/parser'), 144 | }, 145 | options: [{ groups: ['module', 'sibling'] }], 146 | }), 147 | test({ 148 | name: "default groups don't have a type group and so types aren't special", 149 | code: ` 150 | import sib from './sib'; 151 | import type { relative } from './relative'; 152 | `, 153 | languageOptions: { 154 | parser: require('@typescript-eslint/parser'), 155 | }, 156 | }), 157 | test({ 158 | name: "default groups don't have a type group and so types aren't special", 159 | code: ` 160 | import type { relative } from './relative'; 161 | import sib from './sib'; 162 | `, 163 | languageOptions: { 164 | parser: require('@typescript-eslint/parser'), 165 | }, 166 | }), 167 | ], 168 | invalid: [ 169 | test({ 170 | code: ` 171 | import type { relative } from './relative'; 172 | import sib from './sib'; 173 | `, 174 | output: ` 175 | import sib from './sib'; 176 | import type { relative } from './relative'; 177 | `, 178 | languageOptions: { 179 | parser: require('@typescript-eslint/parser'), 180 | }, 181 | 182 | options: [{ groups: ['module', 'sibling', 'type'] }], 183 | errors: [{ message: '`./sib` import should occur before import of `./relative`' }], 184 | }), 185 | test({ 186 | name: "Option alphabetize: {order: 'desc', ignoreCase: true}", 187 | code: ` 188 | import foo from 'foo'; 189 | import bar from 'bar'; 190 | import Baz from 'Baz'; 191 | import index from './'; 192 | `, 193 | output: ` 194 | import foo from 'foo'; 195 | import Baz from 'Baz'; 196 | import bar from 'bar'; 197 | import index from './'; 198 | `, 199 | options: [ 200 | { 201 | groups: ['module', 'index'], 202 | alphabetize: { order: 'desc', ignoreCase: true }, 203 | }, 204 | ], 205 | errors: [ 206 | { 207 | message: '`Baz` import should occur before import of `bar`', 208 | }, 209 | ], 210 | }), 211 | // // Multiple errors 212 | // TODO FAILING TEST 213 | // test({ 214 | // code: ` 215 | // var sibling = require('./sibling'); 216 | // var parent = require('../parent'); 217 | // var fs = require('fs'); 218 | // `, 219 | // output: ` 220 | // var fs = require('fs'); 221 | // var parent = require('../parent'); 222 | // var sibling = require('./sibling'); 223 | // `, 224 | // errors: [ 225 | // { 226 | // message: '`../parent` import should occur before import of `./sibling`', 227 | // }, 228 | // { 229 | // message: '`fs` import should occur before import of `./sibling`', 230 | // }, 231 | // ], 232 | // }), 233 | ], 234 | }); 235 | -------------------------------------------------------------------------------- /test/rules/order-imports.js: -------------------------------------------------------------------------------- 1 | const RuleTester = require('eslint').RuleTester; 2 | 3 | const { test } = require('../utils'); 4 | 5 | const ruleTester = new RuleTester(); 6 | const { default: rule } = require('../../lib/rules/order-imports'); 7 | 8 | function generateImports(count) { 9 | const imports = []; 10 | for (let i = 0; i < count; i++) { 11 | imports.push(`import foo${i} from './foo${i}.js';`); 12 | } 13 | return imports.sort().join('\n'); 14 | } 15 | 16 | ruleTester.run('order', rule, { 17 | valid: [ 18 | test({ 19 | name: 'Default order using require', 20 | code: ` 21 | var async = require('async'); 22 | var fs = require('fs'); 23 | var relParent1 = require('../foo'); 24 | var relParent2 = require('../foo/bar'); 25 | var relParent3 = require('../'); 26 | var sibling = require('./foo'); 27 | var index = require('./');`, 28 | }), 29 | test({ 30 | name: 'Default order using import', 31 | code: ` 32 | import abs from '/absolute-path'; 33 | import async, {foo1} from 'async'; 34 | import fs from 'fs'; 35 | import relParent1 from '../foo'; 36 | import relParent2, {foo2} from '../foo/bar'; 37 | import relParent3 from '../'; 38 | import sibling, {foo3} from './foo'; 39 | import index from './';`, 40 | }), 41 | test({ 42 | name: 'Multiple module of the same rank next to each other', 43 | code: ` 44 | var async = require('async'); 45 | var fs = require('fs'); 46 | var fs = require('fs'); 47 | var path = require('path'); 48 | var _ = require('lodash'); 49 | `, 50 | }), 51 | test({ 52 | name: 'Overriding order to be the reverse of the default order', 53 | code: ` 54 | var index = require('./'); 55 | var sibling = require('./foo'); 56 | var relParent3 = require('../'); 57 | var relParent2 = require('../foo/bar'); 58 | var relParent1 = require('../foo'); 59 | var async = require('async'); 60 | var fs = require('fs'); 61 | `, 62 | options: [{ groups: ['index', 'sibling', 'parent', 'module'] }], 63 | }), 64 | test({ 65 | name: 'Ignore dynamic requires', 66 | code: ` 67 | var async = require('async'); 68 | var path = require('path'); 69 | var _ = require('lodash'); 70 | var fs = require('f' + 's');`, 71 | }), 72 | test({ 73 | name: 'Ignore non-require call expressions', 74 | code: ` 75 | var path = require('path'); 76 | var result = add(1, 2); 77 | var _ = require('lodash');`, 78 | }), 79 | test({ 80 | name: 'Ignore requires that are not at the top-level', 81 | code: ` 82 | var index = require('./'); 83 | function foo() { 84 | var fs = require('fs'); 85 | } 86 | () => require('fs'); 87 | if (a) { 88 | require('fs'); 89 | }`, 90 | }), 91 | test({ 92 | name: 'Ignore unknown/invalid cases', 93 | code: ` 94 | var async = require('async'); 95 | var fs = require('fs'); 96 | var unknown1 = require(/unknown1/); 97 | `, 98 | }), 99 | test({ 100 | name: 'Ignoring unassigned values by default (require)', 101 | code: ` 102 | require('./foo'); 103 | require('fs'); 104 | var path = require('path'); 105 | `, 106 | }), 107 | test({ 108 | name: 'Ignoring unassigned values by default (import)', 109 | code: ` 110 | import './foo'; 111 | import 'fs'; 112 | import path from 'path'; 113 | `, 114 | }), 115 | test({ 116 | name: 'No imports', 117 | code: ` 118 | function add(a, b) { 119 | return a + b; 120 | } 121 | var foo; 122 | `, 123 | }), 124 | test({ 125 | name: 'Grouping import types', 126 | code: ` 127 | var async = require('async'); 128 | var fs = require('fs'); 129 | var path = require('path'); 130 | var index = require('./'); 131 | 132 | var sibling = require('./foo'); 133 | var relParent3 = require('../'); 134 | var relParent1 = require('../foo'); 135 | `, 136 | options: [ 137 | { 138 | groups: [ 139 | ['module', 'index'], 140 | ['sibling', 'parent'], 141 | ], 142 | }, 143 | ], 144 | }), 145 | test({ 146 | name: 'Omitted types should implicitly be considered as the last type', 147 | code: ` 148 | var index = require('./'); 149 | var path = require('path'); 150 | `, 151 | options: [ 152 | { 153 | groups: [ 154 | 'index', 155 | ['sibling', 'parent'], 156 | // missing 'module' 157 | ], 158 | }, 159 | ], 160 | }), 161 | test({ 162 | name: 'Mixing require and import should have import up top', 163 | code: ` 164 | import async, {foo1} from 'async'; 165 | import relParent2, {foo2} from '../foo/bar'; 166 | import sibling, {foo3} from './foo'; 167 | var fs = require('fs'); 168 | var relParent1 = require('../foo'); 169 | var relParent3 = require('../'); 170 | var index = require('./'); 171 | `, 172 | }), 173 | test({ 174 | name: "Option: newlinesBetween: 'always'", 175 | code: ` 176 | var fs = require('fs'); 177 | var async = require('async'); 178 | var path = require('path'); 179 | var index = require('./'); 180 | 181 | 182 | 183 | var sibling = require('./foo'); 184 | 185 | 186 | var relParent1 = require('../foo'); 187 | var relParent3 = require('../'); 188 | `, 189 | options: [ 190 | { 191 | groups: [['module', 'index'], ['sibling'], ['parent']], 192 | newlinesBetween: 'always', 193 | }, 194 | ], 195 | }), 196 | test({ 197 | name: "Option: newlinesBetween: 'never'", 198 | code: ` 199 | var async = require('async'); 200 | var fs = require('fs'); 201 | var path = require('path'); 202 | var index = require('./'); 203 | var sibling = require('./foo'); 204 | var relParent1 = require('../foo'); 205 | var relParent3 = require('../'); 206 | `, 207 | options: [ 208 | { 209 | groups: [['module', 'index'], ['sibling'], ['parent']], 210 | newlinesBetween: 'never', 211 | }, 212 | ], 213 | }), 214 | test({ 215 | name: "Option: newlinesBetween: 'ignore'", 216 | code: ` 217 | var fs = require('fs'); 218 | var async = require('async'); 219 | 220 | var index = require('./'); 221 | var path = require('path'); 222 | var sibling = require('./foo'); 223 | 224 | 225 | var relParent1 = require('../foo'); 226 | 227 | var relParent3 = require('../'); 228 | `, 229 | options: [ 230 | { 231 | groups: [['module', 'index'], ['sibling'], ['parent']], 232 | newlinesBetween: 'ignore', 233 | }, 234 | ], 235 | }), 236 | test({ 237 | name: "'ignore' should be the default value for `newlinesBetween`", 238 | code: ` 239 | var fs = require('fs'); 240 | 241 | var async = require('async'); 242 | 243 | var index = require('./'); 244 | var path = require('path'); 245 | var sibling = require('./foo'); 246 | 247 | 248 | var relParent1 = require('../foo'); 249 | 250 | var relParent3 = require('../'); 251 | 252 | `, 253 | options: [ 254 | { 255 | groups: [['module', 'index'], ['sibling'], ['parent']], 256 | }, 257 | ], 258 | }), 259 | test({ 260 | name: "Option newlinesBetween: 'always' with multiline imports #1", 261 | code: ` 262 | import path from 'path'; 263 | import { 264 | I, 265 | Want, 266 | Couple, 267 | Imports, 268 | Here 269 | } from 'bar'; 270 | import external from 'external' 271 | `, 272 | options: [{ newlinesBetween: 'always' }], 273 | }), 274 | test({ 275 | name: "Option newlinesBetween: 'always' with multiline imports #2", 276 | code: ` 277 | import path from 'path'; 278 | import net 279 | from 'net'; 280 | import external from 'external' 281 | 282 | import foo from './foo'; 283 | `, 284 | options: [{ newlinesBetween: 'always' }], 285 | }), 286 | test({ 287 | name: "Option newlinesBetween: 'always' with multiline imports #3", 288 | code: ` 289 | import foo 290 | from '../../../../this/will/be/very/long/path/and/therefore/this/import/has/to/be/in/two/lines'; 291 | 292 | import bar 293 | from './sibling'; 294 | `, 295 | options: [{ newlinesBetween: 'always' }], 296 | }), 297 | test({ 298 | name: "Option newlinesBetween: 'always' with not assigned import #1", 299 | code: ` 300 | import path from 'path'; 301 | 302 | import 'loud-rejection'; 303 | import 'something-else'; 304 | 305 | import foo from './foo'; 306 | `, 307 | options: [{ newlinesBetween: 'always' }], 308 | }), 309 | test({ 310 | name: "Option newlinesBetween: 'never' with not assigned import #2", 311 | code: ` 312 | import path from 'path'; 313 | import 'loud-rejection'; 314 | import 'something-else'; 315 | import foo from './foo'; 316 | `, 317 | options: [{ newlinesBetween: 'never' }], 318 | }), 319 | test({ 320 | name: "Option newlinesBetween: 'always' with not assigned require #1", 321 | code: ` 322 | var path = require('path'); 323 | 324 | require('loud-rejection'); 325 | require('something-else'); 326 | 327 | var foo = require('./foo'); 328 | `, 329 | options: [{ newlinesBetween: 'always' }], 330 | }), 331 | test({ 332 | name: "Option newlinesBetween: 'never' with not assigned require #2", 333 | code: ` 334 | var path = require('path'); 335 | require('loud-rejection'); 336 | require('something-else'); 337 | var foo = require('./foo'); 338 | `, 339 | options: [{ newlinesBetween: 'never' }], 340 | }), 341 | test({ 342 | name: "Option newlinesBetween: 'never' should ignore nested require statement's #1", 343 | code: ` 344 | var some = require('asdas'); 345 | var config = { 346 | port: 4444, 347 | runner: { 348 | server_path: require('runner-binary').path, 349 | 350 | cli_args: { 351 | 'webdriver.chrome.driver': require('browser-binary').path 352 | } 353 | } 354 | } 355 | `, 356 | options: [{ newlinesBetween: 'never' }], 357 | }), 358 | test({ 359 | name: "Option newlinesBetween: 'always' should ignore nested require statement's #2", 360 | code: ` 361 | var some = require('asdas'); 362 | var config = { 363 | port: 4444, 364 | runner: { 365 | server_path: require('runner-binary').path, 366 | cli_args: { 367 | 'webdriver.chrome.driver': require('browser-binary').path 368 | } 369 | } 370 | } 371 | `, 372 | options: [{ newlinesBetween: 'always' }], 373 | }), 374 | // Option: newlinesBetween: 'always-and-inside-groups' 375 | test({ 376 | name: 'should have at least one new line between each import statement', 377 | code: ` 378 | var fs = require('fs'); 379 | 380 | var path = require('path'); 381 | 382 | var util = require('util'); 383 | 384 | var async = require('async'); 385 | 386 | 387 | 388 | var relParent1 = require('../foo'); 389 | 390 | var relParent2 = require('../'); 391 | 392 | var relParent3 = require('../bar'); 393 | 394 | var sibling = require('./foo'); 395 | 396 | var sibling2 = require('./bar'); 397 | 398 | var sibling3 = require('./foobar'); 399 | `, 400 | options: [ 401 | { 402 | newlinesBetween: 'always-and-inside-groups', 403 | }, 404 | ], 405 | }), 406 | test({ 407 | name: "Option alphabetize: {order: 'ignore'}", 408 | code: ` 409 | import foo from 'foo'; 410 | import bar from 'bar'; 411 | 412 | import index from './'; 413 | `, 414 | options: [ 415 | { 416 | groups: ['module', 'index'], 417 | alphabetize: { order: 'ignore' }, 418 | }, 419 | ], 420 | }), 421 | test({ 422 | name: "Option alphabetize: {order: 'asc', ignoreCase: false}", 423 | code: ` 424 | import Baz from 'Baz'; 425 | import bar from 'bar'; 426 | import foo from 'foo'; 427 | 428 | import index from './'; 429 | `, 430 | options: [ 431 | { 432 | groups: ['module', 'index'], 433 | alphabetize: { order: 'asc', ignoreCase: false }, 434 | }, 435 | ], 436 | }), 437 | test({ 438 | name: "Option alphabetize: {order: 'asc', ignoreCase: true}", 439 | code: ` 440 | import bar from 'bar'; 441 | import Baz from 'Baz'; 442 | import foo from 'foo'; 443 | 444 | import index from './'; 445 | `, 446 | options: [ 447 | { 448 | groups: ['module', 'index'], 449 | alphabetize: { order: 'asc', ignoreCase: true }, 450 | }, 451 | ], 452 | }), 453 | test({ 454 | name: "Option alphabetize: {order: 'desc', ignoreCase: false}", 455 | code: ` 456 | import foo from 'foo'; 457 | import bar from 'bar'; 458 | import Baz from 'Baz'; 459 | 460 | import index from './'; 461 | `, 462 | options: [ 463 | { 464 | groups: ['module', 'index'], 465 | alphabetize: { order: 'desc', ignoreCase: false }, 466 | }, 467 | ], 468 | }), 469 | test({ 470 | name: "Option alphabetize: {order: 'desc', ignoreCase: true}", 471 | code: ` 472 | import foo from 'foo'; 473 | import Baz from 'Baz'; 474 | import bar from 'bar'; 475 | 476 | import index from './'; 477 | `, 478 | options: [ 479 | { 480 | groups: ['module', 'index'], 481 | alphabetize: { order: 'desc', ignoreCase: true }, 482 | }, 483 | ], 484 | }), 485 | test({ 486 | name: 'With large number of imports in the same group to ensure no newlines are inserted into group.', 487 | code: generateImports(150), 488 | options: [ 489 | { 490 | newlinesBetween: 'always', 491 | alphabetize: { order: 'asc' }, 492 | }, 493 | ], 494 | }), 495 | ], 496 | invalid: [ 497 | test({ 498 | name: "Option: newlinesBetween: 'always-and-inside-groups' should have at least one new line between each import statement", 499 | code: ` 500 | var fs = require('fs'); 501 | 502 | var path = require('path'); 503 | 504 | var util = require('util'); 505 | var async = require('async'); 506 | 507 | 508 | 509 | var relParent1 = require('../foo'); 510 | `, 511 | output: ` 512 | var fs = require('fs'); 513 | 514 | var path = require('path'); 515 | 516 | var util = require('util'); 517 | 518 | var async = require('async'); 519 | 520 | 521 | 522 | var relParent1 = require('../foo'); 523 | `, 524 | errors: [ 525 | { 526 | message: 'There should be at least one empty line between imports', 527 | }, 528 | ], 529 | options: [ 530 | { 531 | newlinesBetween: 'always-and-inside-groups', 532 | }, 533 | ], 534 | }), 535 | test({ 536 | name: 'fix order with spaces on the end of line', 537 | code: ` 538 | var parent = require('../parent'); 539 | var fs = require('fs');${' '} 540 | `, 541 | output: ` 542 | var fs = require('fs');${' '} 543 | var parent = require('../parent'); 544 | `, 545 | errors: [ 546 | { 547 | message: '`fs` import should occur before import of `../parent`', 548 | }, 549 | ], 550 | }), 551 | test({ 552 | name: 'fix order with comment on the end of line', 553 | code: ` 554 | var parent = require('../parent'); 555 | var fs = require('fs'); /* comment */ 556 | `, 557 | output: ` 558 | var fs = require('fs'); /* comment */ 559 | var parent = require('../parent'); 560 | `, 561 | errors: [ 562 | { 563 | message: '`fs` import should occur before import of `../parent`', 564 | }, 565 | ], 566 | }), 567 | test({ 568 | name: 'fix order with comments at the end and start of line', 569 | code: ` 570 | /* comment1 */ var parent = require('../parent'); /* comment2 */ 571 | /* comment3 */ var fs = require('fs'); /* comment4 */ 572 | `, 573 | output: ` 574 | /* comment3 */ var fs = require('fs'); /* comment4 */ 575 | /* comment1 */ var parent = require('../parent'); /* comment2 */ 576 | `, 577 | errors: [ 578 | { 579 | message: '`fs` import should occur before import of `../parent`', 580 | }, 581 | ], 582 | }), 583 | test({ 584 | name: 'fix order with few comments at the end and start of line', 585 | code: ` 586 | /* comment0 */ /* comment1 */ var parent = require('../parent'); /* comment2 */ 587 | /* comment3 */ var fs = require('fs'); /* comment4 */ 588 | `, 589 | output: ` 590 | /* comment3 */ var fs = require('fs'); /* comment4 */ 591 | /* comment0 */ /* comment1 */ var parent = require('../parent'); /* comment2 */ 592 | `, 593 | errors: [ 594 | { 595 | message: '`fs` import should occur before import of `../parent`', 596 | }, 597 | ], 598 | }), 599 | test({ 600 | name: 'fix order with windows end of lines', 601 | code: 602 | `/* comment0 */ /* comment1 */ var parent = require('../parent'); /* comment2 */` + 603 | `\r\n` + 604 | `/* comment3 */ var fs = require('fs'); /* comment4 */` + 605 | `\r\n`, 606 | output: 607 | `/* comment3 */ var fs = require('fs'); /* comment4 */` + 608 | `\r\n` + 609 | `/* comment0 */ /* comment1 */ var parent = require('../parent'); /* comment2 */` + 610 | `\r\n`, 611 | errors: [ 612 | { 613 | message: '`fs` import should occur before import of `../parent`', 614 | }, 615 | ], 616 | }), 617 | test({ 618 | name: 'fix order with multilines comments at the end and start of line', 619 | code: "/* multiline1\n\ 620 | comment1 */var parent = require('../parent'); /* multiline2\n\ 621 | comment2 */ var fs = require('fs');/* multiline3\n\ 622 | comment3 */", 623 | output: "/* multiline1\n\ 624 | comment1 */ var fs = require('fs');\n\ 625 | var parent = require('../parent'); /* multiline2\n\ 626 | comment2 *//* multiline3\n\ 627 | comment3 */", // the spacing here is really sensitive 628 | errors: [ 629 | { 630 | message: '`fs` import should occur before import of `../parent`', 631 | }, 632 | ], 633 | }), 634 | test({ 635 | name: 'fix order of multiple import', 636 | code: ` 637 | var parent = require('../parent'); 638 | var fs = 639 | require('fs'); 640 | `, 641 | output: ` 642 | var fs = 643 | require('fs'); 644 | var parent = require('../parent'); 645 | `, 646 | errors: [ 647 | { 648 | message: '`fs` import should occur before import of `../parent`', 649 | }, 650 | ], 651 | }), 652 | test({ 653 | name: 'fix order at the end of file', 654 | code: ` 655 | var parent = require('../parent'); 656 | var fs = require('fs');`, 657 | output: 658 | ` 659 | var fs = require('fs'); 660 | var parent = require('../parent');` + '\n', 661 | errors: [ 662 | { 663 | message: '`fs` import should occur before import of `../parent`', 664 | }, 665 | ], 666 | }), 667 | test({ 668 | name: 'module before parent module (import)', 669 | code: ` 670 | import parent from '../parent'; 671 | import fs from 'fs'; 672 | `, 673 | output: ` 674 | import fs from 'fs'; 675 | import parent from '../parent'; 676 | `, 677 | errors: [ 678 | { 679 | message: '`fs` import should occur before import of `../parent`', 680 | }, 681 | ], 682 | }), 683 | test({ 684 | name: 'module before parent (mixed import and require)', 685 | code: ` 686 | var parent = require('../parent'); 687 | import fs from 'fs'; 688 | `, 689 | output: ` 690 | import fs from 'fs'; 691 | var parent = require('../parent'); 692 | `, 693 | errors: [ 694 | { 695 | message: '`fs` import should occur before import of `../parent`', 696 | }, 697 | ], 698 | }), 699 | test({ 700 | name: 'parent before sibling', 701 | code: ` 702 | var sibling = require('./sibling'); 703 | var parent = require('../parent'); 704 | `, 705 | output: ` 706 | var parent = require('../parent'); 707 | var sibling = require('./sibling'); 708 | `, 709 | errors: [ 710 | { 711 | message: '`../parent` import should occur before import of `./sibling`', 712 | }, 713 | ], 714 | }), 715 | test({ 716 | name: 'sibling before index', 717 | code: ` 718 | var index = require('./'); 719 | var sibling = require('./sibling'); 720 | `, 721 | output: ` 722 | var sibling = require('./sibling'); 723 | var index = require('./'); 724 | `, 725 | errors: [ 726 | { 727 | message: '`./sibling` import should occur before import of `./`', 728 | }, 729 | ], 730 | }), 731 | test({ 732 | name: "Uses 'after' wording if it creates less errors", 733 | code: ` 734 | var index = require('./'); 735 | var fs = require('fs'); 736 | var path = require('path'); 737 | var _ = require('lodash'); 738 | var foo = require('foo'); 739 | var bar = require('bar'); 740 | `, 741 | output: ` 742 | var fs = require('fs'); 743 | var path = require('path'); 744 | var _ = require('lodash'); 745 | var foo = require('foo'); 746 | var bar = require('bar'); 747 | var index = require('./'); 748 | `, 749 | errors: [ 750 | { 751 | message: '`./` import should occur after import of `bar`', 752 | }, 753 | ], 754 | }), 755 | test({ 756 | name: 'Overriding order to be the reverse of the default order', 757 | code: ` 758 | var fs = require('fs'); 759 | var index = require('./'); 760 | `, 761 | output: ` 762 | var index = require('./'); 763 | var fs = require('fs'); 764 | `, 765 | options: [{ groups: ['index', 'sibling', 'parent', 'module'] }], 766 | errors: [ 767 | { 768 | message: '`./` import should occur before import of `fs`', 769 | }, 770 | ], 771 | }), 772 | // // member expression of require 773 | test({ 774 | code: ` 775 | var foo = require('./foo').bar; 776 | var fs = require('fs'); 777 | `, 778 | output: null, 779 | errors: [ 780 | { 781 | message: '`fs` import should occur before import of `./foo`', 782 | }, 783 | ], 784 | }), 785 | // // nested member expression of require 786 | test({ 787 | code: ` 788 | var foo = require('./foo').bar.bar.bar; 789 | var fs = require('fs'); 790 | `, 791 | output: null, 792 | errors: [ 793 | { 794 | message: '`fs` import should occur before import of `./foo`', 795 | }, 796 | ], 797 | }), 798 | // // fix near nested member expression of require with newlines 799 | test({ 800 | code: ` 801 | var foo = require('./foo').bar 802 | .bar 803 | .bar; 804 | var fs = require('fs'); 805 | `, 806 | output: null, 807 | errors: [ 808 | { 809 | message: '`fs` import should occur before import of `./foo`', 810 | }, 811 | ], 812 | }), 813 | // // fix nested member expression of require with newlines 814 | test({ 815 | code: ` 816 | var foo = require('./foo'); 817 | var fs = require('fs').bar 818 | .bar 819 | .bar; 820 | `, 821 | output: null, 822 | errors: [ 823 | { 824 | message: '`fs` import should occur before import of `./foo`', 825 | }, 826 | ], 827 | }), 828 | test({ 829 | name: '// Grouping import types', 830 | code: ` 831 | var fs = require('fs'); 832 | var index = require('./'); 833 | var sibling = require('./foo'); 834 | var path = require('path'); 835 | `, 836 | output: ` 837 | var fs = require('fs'); 838 | var index = require('./'); 839 | var path = require('path'); 840 | var sibling = require('./foo'); 841 | `, 842 | options: [ 843 | { 844 | groups: [ 845 | ['module', 'index'], 846 | ['sibling', 'parent'], 847 | ], 848 | }, 849 | ], 850 | errors: [ 851 | { 852 | message: '`path` import should occur before import of `./foo`', 853 | }, 854 | ], 855 | }), 856 | test({ 857 | name: 'Omitted types should implicitly be considered as the last type', 858 | code: ` 859 | var path = require('path'); 860 | var parent = require('../parent'); 861 | var async = require('async'); 862 | `, 863 | output: ` 864 | var path = require('path'); 865 | var async = require('async'); 866 | var parent = require('../parent'); 867 | `, 868 | options: [ 869 | { 870 | groups: [ 871 | 'index', 872 | ['module', 'sibling'], 873 | // missing 'parent' 874 | ], 875 | }, 876 | ], 877 | errors: [ 878 | { 879 | message: '`async` import should occur before import of `../parent`', 880 | }, 881 | ], 882 | }), 883 | test({ 884 | name: 'Setting the order for an unknown type should make the rule trigger an error and do nothing else', 885 | code: ` 886 | var async = require('async'); 887 | var index = require('./'); 888 | `, 889 | options: [{ groups: ['index', ['sibling', 'parent', 'UNKNOWN', 'internal']] }], 890 | errors: [ 891 | { 892 | message: 893 | "Incorrect configuration of the rule: Unknown type \"UNKNOWN\". For a regular expression, wrap the string in '/', ex: '/shared/'", 894 | }, 895 | ], 896 | }), 897 | test({ 898 | name: "Type in an array can't be another array, too much nesting", 899 | code: ` 900 | var async = require('async'); 901 | var index = require('./'); 902 | `, 903 | options: [{ groups: ['index', ['sibling', 'parent', ['builtin'], 'internal']] }], 904 | errors: [ 905 | { 906 | message: 907 | "Incorrect configuration of the rule: Unknown type [\"builtin\"]. For a regular expression, wrap the string in '/', ex: '/shared/'", 908 | }, 909 | ], 910 | }), 911 | test({ 912 | name: ' // No numbers', 913 | code: ` 914 | var async = require('async'); 915 | var index = require('./'); 916 | `, 917 | options: [{ groups: ['index', ['sibling', 'parent', 2, 'internal']] }], 918 | errors: [ 919 | { 920 | message: 921 | "Incorrect configuration of the rule: Unknown type 2. For a regular expression, wrap the string in '/', ex: '/shared/'", 922 | }, 923 | ], 924 | }), 925 | test({ 926 | name: ' // Duplicate', 927 | code: ` 928 | var async = require('async'); 929 | var index = require('./'); 930 | `, 931 | options: [{ groups: ['index', ['sibling', 'parent', 'parent', 'internal']] }], 932 | errors: [ 933 | { 934 | message: 'Incorrect configuration of the rule: `parent` is duplicated', 935 | }, 936 | ], 937 | }), 938 | test({ 939 | name: ' // Mixing require and import should have import up top', 940 | code: ` 941 | import async, {foo1} from 'async'; 942 | import relParent2, {foo2} from '../foo/bar'; 943 | var fs = require('fs'); 944 | var relParent1 = require('../foo'); 945 | var relParent3 = require('../'); 946 | import sibling, {foo3} from './foo'; 947 | var index = require('./'); 948 | `, 949 | output: ` 950 | import async, {foo1} from 'async'; 951 | import relParent2, {foo2} from '../foo/bar'; 952 | import sibling, {foo3} from './foo'; 953 | var fs = require('fs'); 954 | var relParent1 = require('../foo'); 955 | var relParent3 = require('../'); 956 | var index = require('./'); 957 | `, 958 | errors: [ 959 | { 960 | message: '`./foo` import should occur before import of `fs`', 961 | }, 962 | ], 963 | }), 964 | test({ 965 | code: ` 966 | var fs = require('fs'); 967 | import async, {foo1} from 'async'; 968 | import relParent2, {foo2} from '../foo/bar'; 969 | `, 970 | output: ` 971 | import async, {foo1} from 'async'; 972 | import relParent2, {foo2} from '../foo/bar'; 973 | var fs = require('fs'); 974 | `, 975 | errors: [ 976 | { 977 | message: '`fs` import should occur after import of `../foo/bar`', 978 | }, 979 | ], 980 | }), 981 | test({ 982 | name: "Option newlinesBetween: 'never' - should report unnecessary line between groups", 983 | code: ` 984 | var fs = require('fs'); 985 | var index = require('./'); 986 | var async = require('async'); 987 | 988 | var path = require('path'); 989 | 990 | var sibling = require('./foo'); 991 | var relParent1 = require('../foo'); 992 | var relParent3 = require('../'); 993 | `, 994 | output: ` 995 | var fs = require('fs'); 996 | var index = require('./'); 997 | var async = require('async'); 998 | var path = require('path'); 999 | var sibling = require('./foo'); 1000 | var relParent1 = require('../foo'); 1001 | var relParent3 = require('../'); 1002 | `, 1003 | options: [ 1004 | { 1005 | groups: [['module', 'index'], ['sibling'], ['parent']], 1006 | newlinesBetween: 'never', 1007 | }, 1008 | ], 1009 | errors: [ 1010 | { 1011 | line: 4, 1012 | message: 'There should be no empty line between import groups', 1013 | }, 1014 | { 1015 | line: 6, 1016 | message: 'There should be no empty line between import groups', 1017 | }, 1018 | ], 1019 | }), 1020 | test({ 1021 | name: 'Fix newlinesBetween with comments after', 1022 | code: ` 1023 | var fs = require('fs'); /* comment */ 1024 | 1025 | var index = require('./');`, 1026 | output: ` 1027 | var fs = require('fs'); /* comment */ 1028 | var index = require('./');`, 1029 | options: [ 1030 | { 1031 | groups: [['module'], ['index']], 1032 | newlinesBetween: 'never', 1033 | }, 1034 | ], 1035 | errors: [ 1036 | { 1037 | line: 2, 1038 | message: 'There should be no empty line between import groups', 1039 | }, 1040 | ], 1041 | }), 1042 | test({ 1043 | name: 'Cannot fix newlinesBetween with multiline comment after', 1044 | code: ` 1045 | var fs = require('fs'); /* multiline 1046 | comment */ 1047 | 1048 | var index = require('./'); 1049 | `, 1050 | output: null, 1051 | options: [ 1052 | { 1053 | groups: [['module'], ['index']], 1054 | newlinesBetween: 'never', 1055 | }, 1056 | ], 1057 | errors: [ 1058 | { 1059 | line: 2, 1060 | message: 'There should be no empty line between import groups', 1061 | }, 1062 | ], 1063 | }), 1064 | test({ 1065 | name: "Option newlinesBetween: 'always' - should report lack of newline between groups", 1066 | code: ` 1067 | var fs = require('fs'); 1068 | var index = require('./'); 1069 | var path = require('path'); 1070 | var sibling = require('./foo'); 1071 | var relParent1 = require('../foo'); 1072 | var relParent3 = require('../');`, 1073 | output: ` 1074 | var fs = require('fs'); 1075 | var index = require('./'); 1076 | var path = require('path'); 1077 | 1078 | var sibling = require('./foo'); 1079 | 1080 | var relParent1 = require('../foo'); 1081 | var relParent3 = require('../');`, 1082 | options: [ 1083 | { 1084 | groups: [['module', 'index'], ['sibling'], ['parent']], 1085 | newlinesBetween: 'always', 1086 | }, 1087 | ], 1088 | errors: [ 1089 | { 1090 | line: 4, 1091 | message: 'There should be at least one empty line between import groups', 1092 | }, 1093 | { 1094 | line: 5, 1095 | message: 'There should be at least one empty line between import groups', 1096 | }, 1097 | ], 1098 | }), 1099 | test({ 1100 | name: "Option newlinesBetween: 'always' should report unnecessary empty lines space between import groups", 1101 | code: ` 1102 | var fs = require('fs'); 1103 | 1104 | var path = require('path'); 1105 | var index = require('./'); 1106 | 1107 | var sibling = require('./foo'); 1108 | `, 1109 | output: ` 1110 | var fs = require('fs'); 1111 | var path = require('path'); 1112 | var index = require('./'); 1113 | 1114 | var sibling = require('./foo'); 1115 | `, 1116 | options: [ 1117 | { 1118 | groups: [ 1119 | ['module', 'index'], 1120 | ['sibling', 'parent'], 1121 | ], 1122 | newlinesBetween: 'always', 1123 | }, 1124 | ], 1125 | errors: [ 1126 | { 1127 | line: 2, 1128 | message: 'There should be no empty line within import group', 1129 | }, 1130 | ], 1131 | }), 1132 | test({ 1133 | name: "Option newlinesBetween: 'never' cannot fix if there are other statements between imports", 1134 | code: ` 1135 | import path from 'path'; 1136 | import 'loud-rejection'; 1137 | 1138 | import 'something-else'; 1139 | import _ from 'lodash'; 1140 | `, 1141 | output: null, 1142 | options: [{ newlinesBetween: 'never' }], 1143 | errors: [ 1144 | { 1145 | line: 2, 1146 | message: 'There should be no empty line between import groups', 1147 | }, 1148 | ], 1149 | }), 1150 | test({ 1151 | name: "Option newlinesBetween: 'always' should report missing empty lines when using not assigned imports", 1152 | code: ` 1153 | import path from 'path'; 1154 | import 'loud-rejection'; 1155 | import 'something-else'; 1156 | import _ from './relative'; 1157 | `, 1158 | output: ` 1159 | import path from 'path'; 1160 | 1161 | import 'loud-rejection'; 1162 | import 'something-else'; 1163 | import _ from './relative'; 1164 | `, 1165 | options: [{ newlinesBetween: 'always' }], 1166 | errors: [ 1167 | { 1168 | line: 2, 1169 | message: 'There should be at least one empty line between import groups', 1170 | }, 1171 | ], 1172 | }), 1173 | test({ 1174 | name: 'fix missing empty lines with single line comment after', 1175 | code: ` 1176 | import path from 'path'; // comment 1177 | import _ from './relative'; 1178 | `, 1179 | output: ` 1180 | import path from 'path'; // comment 1181 | 1182 | import _ from './relative'; 1183 | `, 1184 | options: [{ newlinesBetween: 'always' }], 1185 | errors: [ 1186 | { 1187 | line: 2, 1188 | message: 'There should be at least one empty line between import groups', 1189 | }, 1190 | ], 1191 | }), 1192 | test({ 1193 | name: 'fix missing empty lines with few line block comment after', 1194 | code: ` 1195 | import path from 'path'; /* comment */ /* comment */ 1196 | import _ from './relative'; 1197 | `, 1198 | output: ` 1199 | import path from 'path'; /* comment */ /* comment */ 1200 | 1201 | import _ from './relative'; 1202 | `, 1203 | options: [{ newlinesBetween: 'always' }], 1204 | errors: [ 1205 | { 1206 | line: 2, 1207 | message: 'There should be at least one empty line between import groups', 1208 | }, 1209 | ], 1210 | }), 1211 | test( 1212 | { 1213 | name: 'fix missing empty lines with single line block comment after', 1214 | code: ` 1215 | import path from 'path'; /* 1 1216 | 2 */ 1217 | import _ from './relative'; 1218 | `, 1219 | output: ` 1220 | import path from 'path'; 1221 | /* 1 1222 | 2 */ 1223 | import _ from './relative'; 1224 | `, 1225 | options: [{ newlinesBetween: 'always' }], 1226 | errors: [ 1227 | { 1228 | line: 2, 1229 | message: 'There should be at least one empty line between import groups', 1230 | }, 1231 | ], 1232 | }, 1233 | { noTrim: true } 1234 | ), 1235 | // reorder fix cannot cross non import or require 1236 | test({ 1237 | output: null, 1238 | code: ` 1239 | var relative = require('./relative'); 1240 | fn_call(); 1241 | var fs = require('fs'); 1242 | `, 1243 | errors: [ 1244 | { 1245 | message: '`fs` import should occur before import of `./relative`', 1246 | }, 1247 | ], 1248 | }), 1249 | // reorder cannot cross non plain requires 1250 | test({ 1251 | output: null, 1252 | code: ` 1253 | var relative = require('./relative'); 1254 | var a = require('./value.js')(a); 1255 | var fs = require('fs'); 1256 | `, 1257 | errors: [ 1258 | { 1259 | message: '`fs` import should occur before import of `./relative`', 1260 | }, 1261 | ], 1262 | }), 1263 | // reorder fixes cannot be applied to non plain requires #1 1264 | test({ 1265 | output: null, 1266 | code: ` 1267 | var relative = require('./relative'); 1268 | var fs = require('fs')(a); 1269 | `, 1270 | errors: [ 1271 | { 1272 | message: '`fs` import should occur before import of `./relative`', 1273 | }, 1274 | ], 1275 | }), 1276 | // reorder fixes cannot be applied to non plain requires #2 1277 | test({ 1278 | output: null, 1279 | code: ` 1280 | var relative = require('./relative')(a); 1281 | var fs = require('fs'); 1282 | `, 1283 | errors: [ 1284 | { 1285 | message: '`fs` import should occur before import of `./relative`', 1286 | }, 1287 | ], 1288 | }), 1289 | // cannot require in case of not assignement require 1290 | test({ 1291 | output: null, 1292 | code: ` 1293 | var relative = require('./relative'); 1294 | require('./aa'); 1295 | var fs = require('fs'); 1296 | `, 1297 | errors: [ 1298 | { 1299 | message: '`fs` import should occur before import of `./relative`', 1300 | }, 1301 | ], 1302 | }), 1303 | // reorder cannot cross function call (import statement) 1304 | test({ 1305 | output: null, 1306 | code: ` 1307 | import relative from './relative'; 1308 | fn_call(); 1309 | import fs from 'fs'; 1310 | `, 1311 | errors: [ 1312 | { 1313 | message: '`fs` import should occur before import of `./relative`', 1314 | }, 1315 | ], 1316 | }), 1317 | // reorder cannot cross variable assignemet (import statement) 1318 | test({ 1319 | output: null, 1320 | code: ` 1321 | import relative from './relative'; 1322 | var a = 1; 1323 | import fs from 'fs'; 1324 | `, 1325 | errors: [ 1326 | { 1327 | message: '`fs` import should occur before import of `./relative`', 1328 | }, 1329 | ], 1330 | }), 1331 | // reorder cannot cross non plain requires (import statement) 1332 | test({ 1333 | output: null, 1334 | code: ` 1335 | import relative from './relative'; 1336 | var a = require('./value.js')(a); 1337 | import fs from 'fs'; 1338 | `, 1339 | errors: [ 1340 | { 1341 | message: '`fs` import should occur before import of `./relative`', 1342 | }, 1343 | ], 1344 | }), 1345 | // cannot reorder in case of not assignement import 1346 | test({ 1347 | output: null, 1348 | code: ` 1349 | import relative from './relative'; 1350 | import './aa'; 1351 | import fs from 'fs'; 1352 | `, 1353 | errors: [ 1354 | { 1355 | message: '`fs` import should occur before import of `./relative`', 1356 | }, 1357 | ], 1358 | }), 1359 | test({ 1360 | name: 'fix incorrect order with @typescript-eslint/parser', 1361 | code: ` 1362 | var relative = require('./relative'); 1363 | var fs = require('fs'); 1364 | `, 1365 | output: ` 1366 | var fs = require('fs'); 1367 | var relative = require('./relative'); 1368 | `, 1369 | languageOptions: { 1370 | parser: require('@typescript-eslint/parser'), 1371 | }, 1372 | errors: [ 1373 | { 1374 | message: '`fs` import should occur before import of `./relative`', 1375 | }, 1376 | ], 1377 | }), 1378 | test({ 1379 | name: "Option alphabetize: {order: 'asc', ignoreCase: false}", 1380 | code: ` 1381 | import bar from 'bar'; 1382 | import Baz from 'Baz'; 1383 | import foo from 'foo'; 1384 | import index from './'; 1385 | `, 1386 | output: ` 1387 | import Baz from 'Baz'; 1388 | import bar from 'bar'; 1389 | import foo from 'foo'; 1390 | import index from './'; 1391 | `, 1392 | options: [ 1393 | { 1394 | groups: ['module', 'index'], 1395 | alphabetize: { order: 'asc', ignoreCase: false }, 1396 | }, 1397 | ], 1398 | errors: [ 1399 | { 1400 | message: '`Baz` import should occur before import of `bar`', 1401 | }, 1402 | ], 1403 | }), 1404 | test({ 1405 | name: "Option alphabetize: {order: 'asc', ignoreCase: true}", 1406 | code: ` 1407 | import Baz from 'Baz'; 1408 | import bar from 'bar'; 1409 | import foo from 'foo'; 1410 | import index from './'; 1411 | `, 1412 | output: ` 1413 | import bar from 'bar'; 1414 | import Baz from 'Baz'; 1415 | import foo from 'foo'; 1416 | import index from './'; 1417 | `, 1418 | options: [ 1419 | { 1420 | groups: ['module', 'index'], 1421 | alphabetize: { order: 'asc', ignoreCase: true }, 1422 | }, 1423 | ], 1424 | errors: [ 1425 | { 1426 | message: '`bar` import should occur before import of `Baz`', 1427 | }, 1428 | ], 1429 | }), 1430 | test({ 1431 | name: "Option alphabetize: {order: 'desc', ignoreCase: false}", 1432 | code: ` 1433 | import foo from 'foo'; 1434 | import Baz from 'Baz'; 1435 | import bar from 'bar'; 1436 | import index from './'; 1437 | `, 1438 | output: ` 1439 | import foo from 'foo'; 1440 | import bar from 'bar'; 1441 | import Baz from 'Baz'; 1442 | import index from './'; 1443 | `, 1444 | options: [ 1445 | { 1446 | groups: ['module', 'index'], 1447 | alphabetize: { order: 'desc', ignoreCase: false }, 1448 | }, 1449 | ], 1450 | errors: [ 1451 | { 1452 | message: '`bar` import should occur before import of `Baz`', 1453 | }, 1454 | ], 1455 | }), 1456 | test({ 1457 | name: "Option alphabetize: {order: 'desc', ignoreCase: true}", 1458 | code: ` 1459 | import foo from 'foo'; 1460 | import bar from 'bar'; 1461 | import Baz from 'Baz'; 1462 | import index from './'; 1463 | `, 1464 | output: ` 1465 | import foo from 'foo'; 1466 | import Baz from 'Baz'; 1467 | import bar from 'bar'; 1468 | import index from './'; 1469 | `, 1470 | options: [ 1471 | { 1472 | groups: ['module', 'index'], 1473 | alphabetize: { order: 'desc', ignoreCase: true }, 1474 | }, 1475 | ], 1476 | errors: [ 1477 | { 1478 | message: '`Baz` import should occur before import of `bar`', 1479 | }, 1480 | ], 1481 | }), 1482 | ], 1483 | }); 1484 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const FILENAME = testFilePath('foo.js'); 4 | 5 | function testFilePath(relativePath) { 6 | return path.join(process.cwd(), './tests/files', relativePath); 7 | } 8 | 9 | function test(t, extraOptions = {}) { 10 | let testParams = { 11 | filename: FILENAME, 12 | ...t, 13 | code: extraOptions.noTrim ? t.code : trimWhitespaceForEachLine(t.code), 14 | languageOptions: Object.assign( 15 | { 16 | sourceType: 'module', 17 | ecmaVersion: 6, 18 | }, 19 | t.languageOptions 20 | ), 21 | }; 22 | 23 | if (testParams.output && !extraOptions.noTrim) { 24 | testParams.output = trimWhitespaceForEachLine(testParams.output); 25 | } 26 | 27 | return testParams; 28 | } 29 | 30 | function trimWhitespaceForEachLine(string = '') { 31 | return string 32 | .split('\n') 33 | .map((line) => line.trim()) 34 | .join('\n'); 35 | } 36 | 37 | module.exports = { 38 | FILENAME, 39 | test, 40 | testFilePath, 41 | }; 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitAny": false, 5 | "removeComments": true, 6 | "sourceMap": true, 7 | "strictNullChecks": true, 8 | "allowJs": true, 9 | "target": "es5", 10 | "lib": ["es6"] 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules", "**/*.spec.ts"] 14 | } 15 | --------------------------------------------------------------------------------