Skip to content

Commit cdd174d

Browse files
authored
Merge pull request #50 from Tibfib/will/type-imports
Add support for a `type` group.
2 parents c7533f2 + 0d9bf41 commit cdd174d

File tree

9 files changed

+300
-122
lines changed

9 files changed

+300
-122
lines changed

.eslintrc.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ module.exports = {
1818
tryExtensions: ['.js', '.json', '.node', '.ts', '.d.ts'],
1919
},
2020
},
21-
rules: {},
21+
rules: {
22+
'prettier/prettier': 0,
23+
'eslint-plugin/prefer-message-ids': 1,
24+
},
2225
overrides: [
2326
{
2427
files: ['test/**/*.js'],

docs/rules/order-imports.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ This rule supports the following options:
6161
6262
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:
6363

64-
- `'module'` | `'absolute'` | `'parent'` | `'sibling'` | `'index'`
64+
- `'module'` | `'absolute'` | `'parent'` | `'sibling'` | `'index'` | `'type'`
6565
- or a regular expression like string, ex: `/^shared/`
6666
- the wrapping `/` is essential
6767
- in this example, it would match any import paths starting with `'shared'`
6868
- note: files are first categorized as matching a regular expression group before going into another group
6969

70-
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:
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:
7171

7272
```js
7373
[
@@ -88,6 +88,36 @@ You can set the options like this:
8888
]
8989
```
9090

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+
91121
### `newlinesBetween: [ignore|always|always-and-inside-groups|never]`:
92122

93123
Enforces or forbids new lines between import groups:

package-lock.json

Lines changed: 33 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-import-helpers",
3-
"version": "1.2.1",
3+
"version": "1.3.0",
44
"description": "ESLint Rules to Aid with Imports",
55
"main": "lib/index.js",
66
"scripts": {
@@ -9,7 +9,7 @@
99
"lint:js": "eslint --cache .",
1010
"prepublish": "npm run build",
1111
"test": "npm run build && npm run test-quick",
12-
"test-quick": "NODE_PATH=./lib nyc -s mocha -R dot --recursive test -t test-results"
12+
"test-quick": "cross-env NODE_PATH=./lib nyc -s mocha -R dot --recursive test -t test-results"
1313
},
1414
"keywords": [
1515
"eslint",
@@ -24,13 +24,17 @@
2424
},
2525
"author": "Will Honey",
2626
"license": "MIT",
27+
"engines": {
28+
"node": "> 14"
29+
},
2730
"peerDependencies": {
2831
"eslint": "5.x - 8.x"
2932
},
3033
"devDependencies": {
3134
"@types/node": "^16.11.46",
3235
"@typescript-eslint/eslint-plugin": "^5.31.0",
3336
"@typescript-eslint/parser": "^5.31.0",
37+
"cross-env": "^7.0.3",
3438
"eslint": "^8.20.0",
3539
"eslint-config-prettier": "^8.5.0",
3640
"eslint-plugin-eslint-plugin": "^5.0.1",

src/rules/order-imports.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ValidImportType,
55
KnownImportType,
66
RegExpGroups,
7+
ImportKind,
78
} from '../util/import-type';
89
import { isStaticRequire } from '../util/static-require';
910

@@ -312,12 +313,30 @@ function getRegExpGroups(ranks: Ranks): RegExpGroups {
312313

313314
// DETECTING
314315

315-
function computeRank(ranks: Ranks, regExpGroups, name: string, type: ImportType): number {
316-
return ranks[determineImportType(name, regExpGroups)] + (type === 'import' ? 0 : MAX_GROUP_SIZE);
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+
);
317328
}
318329

319-
function registerNode(node: NodeOrToken, name: string, type: ImportType, ranks, regExpGroups, imported: Imported[]) {
320-
const rank = computeRank(ranks, regExpGroups, name, type);
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);
321340
if (rank !== -1) {
322341
imported.push({ name, rank, node });
323342
}
@@ -327,7 +346,7 @@ function isInVariableDeclarator(node: NodeOrToken): boolean {
327346
return node && (node.type === 'VariableDeclarator' || isInVariableDeclarator(node.parent));
328347
}
329348

330-
const knownTypes: KnownImportType[] = ['absolute', 'module', 'parent', 'sibling', 'index'];
349+
const knownTypes: KnownImportType[] = ['absolute', 'module', 'parent', 'sibling', 'index', 'type'];
331350

332351
// Creates an object with type-rank pairs.
333352
// Example: { index: 0, sibling: 1, parent: 1, module: 2 }
@@ -519,14 +538,16 @@ export default {
519538
create: function importOrderRule(context) {
520539
const options: RuleOptions = context.options[0] || {};
521540
const newlinesBetweenImports: NewLinesBetweenOption = options.newlinesBetween || 'ignore';
541+
const groups = options.groups || defaultGroups;
542+
const treatTypesAsGroup = groups.includes('type');
522543

523544
let alphabetize: AlphabetizeConfig;
524545
let ranks: Ranks;
525546
let regExpGroups: RegExpGroups;
526547

527548
try {
528549
alphabetize = getAlphabetizeConfig(options);
529-
ranks = convertGroupsToRanks(options.groups || defaultGroups);
550+
ranks = convertGroupsToRanks(groups);
530551
regExpGroups = getRegExpGroups(ranks);
531552
} catch (error) {
532553
// Malformed configuration
@@ -547,15 +568,15 @@ export default {
547568
if (node.specifiers.length) {
548569
// Ignoring unassigned imports
549570
const name: string = node.source.value;
550-
registerNode(node, name, 'import', ranks, regExpGroups, imported);
571+
registerNode(node, name, 'import', ranks, regExpGroups, imported, treatTypesAsGroup);
551572
}
552573
},
553574
CallExpression: function handleRequires(node) {
554575
if (level !== 0 || !isStaticRequire(node) || !isInVariableDeclarator(node.parent)) {
555576
return;
556577
}
557578
const name: string = node.arguments[0].value;
558-
registerNode(node, name, 'require', ranks, regExpGroups, imported);
579+
registerNode(node, name, 'require', ranks, regExpGroups, imported, treatTypesAsGroup);
559580
},
560581
'Program:exit': function reportAndReset() {
561582
if (alphabetize.order !== 'ignore') {

src/util/import-type.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,25 @@ export function isRegularExpressionGroup(group: string): boolean {
2525
return !!group && group[0] === '/' && group[group.length - 1] === '/' && group.length > 1;
2626
}
2727

28-
export type KnownImportType = 'absolute' | 'module' | 'parent' | 'index' | 'sibling';
28+
export type KnownImportType = 'absolute' | 'module' | 'parent' | 'index' | 'sibling' | 'type';
2929
export type ValidImportType = KnownImportType | string; // this string should be a string surrounded with '/'
3030
export type EveryImportType = ValidImportType | 'unknown';
3131
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';
3246

33-
export function determineImportType(name: string, regExpGroups: RegExpGroups): EveryImportType {
3447
const matchingRegExpGroup = regExpGroups.find(([_groupName, regExp]) => regExp.test(name));
3548
if (matchingRegExpGroup) return matchingRegExpGroup[0];
3649

test/rules/order-imports-2.js

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ ruleTester.run('order', rule, {
99
valid: [
1010
// Default order using import
1111
// absolute at top
12-
// modules starting with _ or @ are sorted with modules
1312
test({
13+
name: 'modules starting with _ or @ are sorted with modules',
1414
code: `
1515
import abs from '/absolute/module';
1616
@@ -89,7 +89,7 @@ ruleTester.run('order', rule, {
8989
import relParent2, {foo2} from '../foo/bar';
9090
9191
import index from './';
92-
92+
9393
import sibling, {foo3} from './foo';`,
9494
options: [
9595
{
@@ -99,10 +99,77 @@ ruleTester.run('order', rule, {
9999
},
100100
],
101101
}),
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+
parser: require.resolve('@typescript-eslint/parser'),
110+
options: [{ groups: ['module', 'sibling', 'type'] }],
111+
}),
112+
test({
113+
name: 'type at beginning',
114+
code: `
115+
import type { relative } from './relative';
116+
import sib from './sib';
117+
`,
118+
parser: require.resolve('@typescript-eslint/parser'),
119+
options: [{ groups: ['type', 'module', 'sibling'] }],
120+
}),
121+
test({
122+
name: "explicit no type group means don't treat types special, both of these pass (when alphabetization is ignored) 1",
123+
code: `
124+
import sib from './sib';
125+
import type { relative } from './relative';
126+
`,
127+
parser: require.resolve('@typescript-eslint/parser'),
128+
options: [{ groups: ['module', 'sibling'] }],
129+
}),
130+
test({
131+
name: "explicit no type group means don't treat types special, both of these pass (when alphabetization is ignored) 2",
132+
code: `
133+
import type { relative } from './relative';
134+
import sib from './sib';
135+
`,
136+
parser: require.resolve('@typescript-eslint/parser'),
137+
options: [{ groups: ['module', 'sibling'] }],
138+
}),
139+
test({
140+
name: "default groups don't have a type group and so types aren't special",
141+
code: `
142+
import sib from './sib';
143+
import type { relative } from './relative';
144+
`,
145+
parser: require.resolve('@typescript-eslint/parser'),
146+
}),
147+
test({
148+
name: "default groups don't have a type group and so types aren't special",
149+
code: `
150+
import type { relative } from './relative';
151+
import sib from './sib';
152+
`,
153+
parser: require.resolve('@typescript-eslint/parser'),
154+
}),
102155
],
103156
invalid: [
104-
// Option alphabetize: {order: 'desc', ignoreCase: true}
105157
test({
158+
code: `
159+
import type { relative } from './relative';
160+
import sib from './sib';
161+
`,
162+
output: `
163+
import sib from './sib';
164+
import type { relative } from './relative';
165+
`,
166+
parser: require.resolve('@typescript-eslint/parser'),
167+
168+
options: [{ groups: ['module', 'sibling', 'type'] }],
169+
errors: [{ message: '`./sib` import should occur before import of `./relative`' }],
170+
}),
171+
test({
172+
name: "Option alphabetize: {order: 'desc', ignoreCase: true}",
106173
code: `
107174
import foo from 'foo';
108175
import bar from 'bar';
@@ -127,9 +194,7 @@ ruleTester.run('order', rule, {
127194
},
128195
],
129196
}),
130-
131197
// // Multiple errors
132-
133198
// TODO FAILING TEST
134199
// test({
135200
// code: `

0 commit comments

Comments
 (0)