Skip to content

Commit b05fb54

Browse files
authored
refactor(sort-imports): extract and test internal functions
1 parent 8108a6e commit b05fb54

12 files changed

Lines changed: 645 additions & 305 deletions

rules/sort-imports.ts

Lines changed: 94 additions & 297 deletions
Large diffs are not rendered by default.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { builtinModules } from 'node:module'
2+
3+
import type { ReadClosestTsConfigByPathValue } from './read-closest-ts-config-by-path'
4+
import type { RegexOption } from '../../types/common-options'
5+
6+
import { getTypescriptImport } from './get-typescript-import'
7+
import { matches } from '../../utils/matches'
8+
9+
export let computeCommonPredefinedGroups = ({
10+
tsConfigOutput,
11+
filename,
12+
options,
13+
name,
14+
}: {
15+
options: {
16+
internalPattern: RegexOption[]
17+
environment: 'node' | 'bun'
18+
}
19+
tsConfigOutput: ReadClosestTsConfigByPathValue | null
20+
filename: string
21+
name: string
22+
}): string[] => {
23+
let matchesInternalPattern = (value: string): boolean | number =>
24+
options.internalPattern.some(pattern => matches(value, pattern))
25+
26+
let internalExternalGroup = matchesInternalPattern(name)
27+
? 'internal'
28+
: getInternalOrExternalGroup({
29+
tsConfigOutput,
30+
filename,
31+
name,
32+
})
33+
34+
let predefinedGroups: string[] = []
35+
36+
if (isIndex(name)) {
37+
predefinedGroups.push('index')
38+
}
39+
40+
if (isSibling(name)) {
41+
predefinedGroups.push('sibling')
42+
}
43+
44+
if (isParent(name)) {
45+
predefinedGroups.push('parent')
46+
}
47+
48+
if (internalExternalGroup === 'internal') {
49+
predefinedGroups.push('internal')
50+
}
51+
52+
if (isCoreModule(name, options.environment)) {
53+
predefinedGroups.push('builtin')
54+
}
55+
56+
if (internalExternalGroup === 'external') {
57+
predefinedGroups.push('external')
58+
}
59+
60+
return predefinedGroups
61+
}
62+
63+
let bunModules = new Set([
64+
'detect-libc',
65+
'bun:sqlite',
66+
'bun:test',
67+
'bun:wrap',
68+
'bun:ffi',
69+
'bun:jsc',
70+
'undici',
71+
'bun',
72+
'ws',
73+
])
74+
let nodeBuiltinModules = new Set(builtinModules)
75+
let builtinPrefixOnlyModules = new Set(['node:sqlite', 'node:test', 'node:sea'])
76+
let isCoreModule = (value: string, environment: 'node' | 'bun'): boolean => {
77+
let valueToCheck = value.startsWith('node:') ? value.split('node:')[1] : value
78+
return (
79+
(!!valueToCheck && nodeBuiltinModules.has(valueToCheck)) ||
80+
builtinPrefixOnlyModules.has(value) ||
81+
(environment === 'bun' ? bunModules.has(value) : false)
82+
)
83+
}
84+
85+
let isParent = (value: string): boolean => value.startsWith('..')
86+
87+
let isSibling = (value: string): boolean => value.startsWith('./')
88+
89+
let isIndex = (value: string): boolean =>
90+
[
91+
'./index.d.js',
92+
'./index.d.ts',
93+
'./index.js',
94+
'./index.ts',
95+
'./index',
96+
'./',
97+
'.',
98+
].includes(value)
99+
100+
let getInternalOrExternalGroup = ({
101+
tsConfigOutput,
102+
filename,
103+
name,
104+
}: {
105+
tsConfigOutput: ReadClosestTsConfigByPathValue | null
106+
filename: string
107+
name: string
108+
}): 'internal' | 'external' | null => {
109+
let typescriptImport = getTypescriptImport()
110+
if (!typescriptImport) {
111+
return !name.startsWith('.') && !name.startsWith('/') ? 'external' : null
112+
}
113+
114+
let isRelativeImport = typescriptImport.isExternalModuleNameRelative(name)
115+
if (isRelativeImport) {
116+
return null
117+
}
118+
if (!tsConfigOutput) {
119+
return 'external'
120+
}
121+
122+
let resolution = typescriptImport.resolveModuleName(
123+
name,
124+
filename,
125+
tsConfigOutput.compilerOptions,
126+
typescriptImport.sys,
127+
tsConfigOutput.cache,
128+
)
129+
// If the module can't be resolved, assume it is external.
130+
if (typeof resolution.resolvedModule?.isExternalLibraryImport !== 'boolean') {
131+
return 'external'
132+
}
133+
134+
return resolution.resolvedModule.isExternalLibraryImport
135+
? 'external'
136+
: 'internal'
137+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { GroupsOptions } from '../../types/common-options'
2+
3+
import { isNewlinesBetweenOption } from '../../utils/is-newlines-between-option'
4+
5+
export let isSideEffectOnlyGroup = (
6+
group: GroupsOptions<string>[0],
7+
): boolean => {
8+
if (isNewlinesBetweenOption(group)) {
9+
return false
10+
}
11+
if (typeof group === 'string') {
12+
return group === 'side-effect' || group === 'side-effect-style'
13+
}
14+
15+
return group.every(isSideEffectOnlyGroup)
16+
}

rules/sort-imports/read-closest-ts-config-by-path.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { getTypescriptImport } from './get-typescript-import'
1010
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-estree/src/parseSettings/getProjectConfigFiles.ts
1111
*/
1212

13-
interface ReadClosestTsConfigByPathValue {
13+
export interface ReadClosestTsConfigByPathValue {
1414
compilerOptions: ts.CompilerOptions
1515
cache: ts.ModuleResolutionCache
1616
}

rules/sort-imports/types.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type {
2+
DeprecatedCustomGroupsOption,
3+
PartitionByCommentOption,
4+
SpecialCharactersOption,
5+
NewlinesBetweenOption,
6+
FallbackSortOption,
7+
GroupsOptions,
8+
OrderOption,
9+
RegexOption,
10+
TypeOption,
11+
} from '../../types/common-options'
12+
import type { SortingNode } from '../../types/sorting-node'
13+
14+
export type Options = [
15+
Partial<{
16+
customGroups: {
17+
value?: DeprecatedCustomGroupsOption
18+
type?: DeprecatedCustomGroupsOption
19+
}
20+
partitionByComment: PartitionByCommentOption
21+
specialCharacters: SpecialCharactersOption
22+
locales: NonNullable<Intl.LocalesArgument>
23+
newlinesBetween: NewlinesBetweenOption
24+
fallbackSort: FallbackSortOption
25+
internalPattern: RegexOption[]
26+
groups: GroupsOptions<Group>
27+
environment: 'node' | 'bun'
28+
partitionByNewLine: boolean
29+
sortSideEffects: boolean
30+
tsconfigRootDir?: string
31+
maxLineLength?: number
32+
ignoreCase: boolean
33+
order: OrderOption
34+
type: TypeOption
35+
alphabet: string
36+
}>,
37+
]
38+
39+
export type Group =
40+
| 'side-effect-style'
41+
| 'external-type'
42+
| 'internal-type'
43+
| 'builtin-type'
44+
| 'sibling-type'
45+
| 'parent-type'
46+
| 'side-effect'
47+
| 'index-type'
48+
| 'internal'
49+
| 'external'
50+
| 'sibling'
51+
| 'unknown'
52+
| 'builtin'
53+
| 'parent'
54+
| 'object'
55+
| 'index'
56+
| 'style'
57+
| 'type'
58+
| string
59+
60+
export interface SortImportsSortingNode extends SortingNode {
61+
isIgnored: boolean
62+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { GroupsOptions } from '../../types/common-options'
2+
import type { Group } from './types'
3+
4+
import { isSideEffectOnlyGroup } from './is-side-effect-only-group'
5+
6+
export let validateSideEffectsConfiguration = ({
7+
sortSideEffects,
8+
groups,
9+
}: {
10+
groups: GroupsOptions<Group>
11+
sortSideEffects: boolean
12+
}): void => {
13+
if (sortSideEffects) {
14+
return
15+
}
16+
/**
17+
* Ensure that if `sortSideEffects: false`, no side effect group is in a
18+
* nested group with non-side-effect groups.
19+
*/
20+
let hasInvalidGroup = groups
21+
.filter(group => Array.isArray(group))
22+
.some(
23+
nestedGroup =>
24+
!isSideEffectOnlyGroup(nestedGroup) &&
25+
nestedGroup.some(
26+
subGroup =>
27+
subGroup === 'side-effect' || subGroup === 'side-effect-style',
28+
),
29+
)
30+
if (hasInvalidGroup) {
31+
throw new Error(
32+
"Side effect groups cannot be nested with non side effect groups when 'sortSideEffects' is 'false'.",
33+
)
34+
}
35+
}

test/rules/sort-imports.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { createModuleResolutionCache } from 'typescript'
1111
import { RuleTester as EslintRuleTester } from 'eslint'
1212
import dedent from 'dedent'
1313

14-
import type { MESSAGE_ID, Options } from '../../rules/sort-imports'
14+
import type { Options } from '../../rules/sort-imports/types'
15+
import type { MESSAGE_ID } from '../../rules/sort-imports'
1516

1617
import * as readClosestTsConfigUtilities from '../../rules/sort-imports/read-closest-ts-config-by-path'
1718
import * as getTypescriptImportUtilities from '../../rules/sort-imports/get-typescript-import'

0 commit comments

Comments
 (0)