Skip to content

Commit 590ce8e

Browse files
authored
feat(sort-imports): add multiline and singleline modifiers
1 parent 5032f1d commit 590ce8e

File tree

9 files changed

+150
-3
lines changed

9 files changed

+150
-3
lines changed

docs/content/rules/sort-imports.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ The list of modifiers is sorted from most to least important:
345345
- `'default'` — Imports containing the `default` specifier.
346346
- `'wildcard'` — Imports containing the wildcard (`* as`) specifier.
347347
- `'named'` — Imports containing at least one named specifier.
348+
- `'multiline'` — Imports on multiple lines.
349+
- `'singleline'` — Imports on a single line.
348350

349351
#### Important notes
350352

rules/sort-imports.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { sortNodesByDependencies } from '../utils/sort-nodes-by-dependencies'
4040
import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
4141
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
4242
import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
43+
import { isNodeOnSingleLine } from '../utils/is-node-on-single-line'
4344
import { singleCustomGroupJsonSchema } from './sort-imports/types'
4445
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
4546
import { allModifiers, allSelectors } from './sort-imports/types'
@@ -219,6 +220,12 @@ export default createEslintRule<Options, MessageId>({
219220
modifiers.push('named')
220221
}
221222

223+
if (isNodeOnSingleLine(node)) {
224+
modifiers.push('singleline')
225+
} else {
226+
modifiers.push('multiline')
227+
}
228+
222229
group ??=
223230
computeGroupExceptUnknown({
224231
selectors,

rules/sort-imports/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ export interface SortImportsSortingNode extends SortingNodeWithDependencies {
211211
*/
212212
export type Modifier =
213213
| SideEffectModifier
214+
| SinglelineModifier
215+
| MultilineModifier
214216
| WildcardModifier
215217
| TsEqualsModifier
216218
| RequireModifier
@@ -264,9 +266,15 @@ type ParentTypeSelector = 'parent-type'
264266
/** Modifier for side-effect imports. */
265267
type SideEffectModifier = 'side-effect'
266268

269+
/** Modifier for single-line imports. */
270+
type SinglelineModifier = 'singleline'
271+
267272
/** @deprecated Since v4.12.0. Will be removed in v5.0.0. */
268273
type IndexTypeSelector = 'index-type'
269274

275+
/** Modifier for multiline imports. */
276+
type MultilineModifier = 'multiline'
277+
270278
/** Modifier for TypeScript import-equals declarations. */
271279
type TsEqualsModifier = 'ts-equals'
272280

@@ -366,9 +374,11 @@ export let allDeprecatedSelectors: Selector[] = [
366374
*/
367375
export let allModifiers: Modifier[] = [
368376
'default',
377+
'multiline',
369378
'named',
370379
'require',
371380
'side-effect',
381+
'singleline',
372382
'ts-equals',
373383
'type',
374384
'value',

rules/sort-jsx-props.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
3131
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
3232
import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
3333
import { singleCustomGroupJsonSchema } from './sort-jsx-props/types'
34+
import { isNodeOnSingleLine } from '../utils/is-node-on-single-line'
3435
import { allModifiers, allSelectors } from './sort-jsx-props/types'
3536
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
3637
import { createEslintRule } from '../utils/create-eslint-rule'
@@ -115,7 +116,7 @@ export default createEslintRule<Options, MessageId>({
115116
if (attribute.value === null) {
116117
modifiers.push('shorthand')
117118
}
118-
if (attribute.loc.start.line !== attribute.loc.end.line) {
119+
if (!isNodeOnSingleLine(attribute)) {
119120
modifiers.push('multiline')
120121
}
121122
selectors.push('prop')

rules/sort-object-types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
4545
import { isMemberOptional } from './sort-object-types/is-member-optional'
4646
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
4747
import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
48+
import { isNodeOnSingleLine } from '../utils/is-node-on-single-line'
4849
import { isNodeFunctionType } from '../utils/is-node-function-type'
4950
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
5051
import { createEslintRule } from '../utils/create-eslint-rule'
@@ -225,7 +226,7 @@ export function sortObjectTypeElements<MessageIds extends string>({
225226
selectors.push('method')
226227
}
227228

228-
if (typeElement.loc.start.line !== typeElement.loc.end.line) {
229+
if (!isNodeOnSingleLine(typeElement)) {
229230
modifiers.push('multiline')
230231
}
231232

rules/sort-objects.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines'
4141
import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled'
4242
import { doesCustomGroupMatch } from '../utils/does-custom-group-match'
4343
import { UnreachableCaseError } from '../utils/unreachable-case-error'
44+
import { isNodeOnSingleLine } from '../utils/is-node-on-single-line'
4445
import { sortNodesByGroups } from '../utils/sort-nodes-by-groups'
4546
import { createEslintRule } from '../utils/create-eslint-rule'
4647
import { reportAllErrors } from '../utils/report-all-errors'
@@ -266,7 +267,7 @@ export default createEslintRule<Options, MessageId>({
266267

267268
selectors.push('member')
268269

269-
if (property.loc.start.line !== property.loc.end.line) {
270+
if (!isNodeOnSingleLine(property)) {
270271
modifiers.push('multiline')
271272
}
272273

test/rules/sort-imports.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2635,6 +2635,76 @@ describe('sort-imports', () => {
26352635
})
26362636
})
26372637

2638+
it('prioritizes named imports over named multiline imports', async () => {
2639+
await invalid({
2640+
errors: [
2641+
{
2642+
data: {
2643+
rightGroup: 'named-import',
2644+
leftGroup: 'external',
2645+
right: './z',
2646+
left: 'f',
2647+
},
2648+
messageId: 'unexpectedImportsGroupOrder',
2649+
},
2650+
],
2651+
options: [
2652+
{
2653+
...options,
2654+
groups: ['named-import', 'external', 'multiline-import'],
2655+
},
2656+
],
2657+
output: dedent`
2658+
import {
2659+
a,
2660+
b,
2661+
} from "./z"
2662+
2663+
import f from 'f'
2664+
`,
2665+
code: dedent`
2666+
import f from 'f'
2667+
2668+
import {
2669+
a,
2670+
b,
2671+
} from "./z"
2672+
`,
2673+
})
2674+
})
2675+
2676+
it('prioritizes named imports over named singleline imports', async () => {
2677+
await invalid({
2678+
errors: [
2679+
{
2680+
data: {
2681+
rightGroup: 'named-import',
2682+
leftGroup: 'external',
2683+
right: './z',
2684+
left: 'f',
2685+
},
2686+
messageId: 'unexpectedImportsGroupOrder',
2687+
},
2688+
],
2689+
options: [
2690+
{
2691+
...options,
2692+
groups: ['named-import', 'external', 'singleline-import'],
2693+
},
2694+
],
2695+
output: dedent`
2696+
import { a } from "./z"
2697+
2698+
import f from 'f'
2699+
`,
2700+
code: dedent`
2701+
import f from 'f'
2702+
2703+
import { a } from "./z"
2704+
`,
2705+
})
2706+
})
2707+
26382708
it.each([
26392709
['filters on element name pattern with string', 'hello'],
26402710
['filters on element name pattern with array', ['noMatch', 'hello']],
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { TSESTree } from '@typescript-eslint/types'
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { isNodeOnSingleLine } from '../../utils/is-node-on-single-line'
6+
7+
describe('isNodeOnSingleLine', () => {
8+
it('should return true if the node is on a single line', () => {
9+
let node = createNode({
10+
startLine: 1,
11+
endLine: 1,
12+
})
13+
14+
let result = isNodeOnSingleLine(node)
15+
16+
expect(result).toBeTruthy()
17+
})
18+
19+
it('should return false if the node is on multiple lines', () => {
20+
let node = createNode({
21+
startLine: 1,
22+
endLine: 2,
23+
})
24+
25+
let result = isNodeOnSingleLine(node)
26+
27+
expect(result).toBeFalsy()
28+
})
29+
30+
function createNode({
31+
startLine,
32+
endLine,
33+
}: {
34+
startLine: number
35+
endLine: number
36+
}): TSESTree.Node {
37+
return {
38+
loc: {
39+
start: { line: startLine, column: 0 },
40+
end: { line: endLine, column: 0 },
41+
},
42+
} as TSESTree.Node
43+
}
44+
})

utils/is-node-on-single-line.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { TSESTree } from '@typescript-eslint/types'
2+
3+
/**
4+
* Returns if a node is on a single line.
5+
*
6+
* @param node - The node to check.
7+
* @returns True if the node is on a single line, false otherwise.
8+
*/
9+
export function isNodeOnSingleLine(node: TSESTree.Node): boolean {
10+
return node.loc.start.line === node.loc.end.line
11+
}

0 commit comments

Comments
 (0)