Skip to content

Commit 15289cc

Browse files
committed
Allow secondary alphabetical sort of imports
* Define comparators that take into account rank and name
1 parent edbb570 commit 15289cc

File tree

5 files changed

+3888
-29
lines changed

5 files changed

+3888
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
88
- [`prefer-default-export`] handles re-exported default exports ([#609])
99
- Fix crash when using `newline-after-import` with decorators ([#592])
1010
- Properly report `newline-after-import` when next line is a decorator
11+
- [`order`] allows secondary alphabetical sort ([#389])
1112

1213
## [2.0.1] - 2016-10-06
1314
### Fixed

docs/rules/order.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,42 @@ import index from './';
141141
import sibling from './foo';
142142
```
143143

144+
### `sort: [ignore|alphabetical]`:
145+
146+
Enforces alphabetical sorting within import groups:
147+
148+
- If set to `ignore`, no errors related to order within import groups will be reported (default).
149+
- If set to `alphabetical`, imports within a group must be alphabetized. Imports across groups will not be compared.
150+
151+
With the default group setting, the following will be invalid:
152+
153+
```js
154+
/* eslint import/order: ["error", {"sort": "alphabetical"}] */
155+
import path from 'path';
156+
import fs from 'fs';
157+
import index from './';
158+
import sibling from './foo';
159+
```
160+
161+
while this will be valid:
162+
163+
```js
164+
/* eslint import/order: ["error", {"sort": "alphabetical"}] */
165+
import fs from 'fs';
166+
import path from 'path';
167+
import index from './';
168+
import sibling from './foo';
169+
```
170+
171+
```js
172+
/* eslint import/order: ["error", {"sort": "ignore"}] */
173+
/* eslint import/order: ["error"] */
174+
import path from 'path';
175+
import fs from 'fs';
176+
import index from './';
177+
import sibling from './foo';
178+
```
179+
144180
## Related
145181

146182
- [`import/external-module-folders`] setting

src/rules/order.js

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,54 @@ const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index']
77

88
// REPORTING
99

10-
function reverse(array) {
11-
return array.map(function (v) {
12-
return {
13-
name: v.name,
14-
rank: -v.rank,
15-
node: v.node,
16-
}
17-
}).reverse()
18-
}
19-
20-
function findOutOfOrder(imported) {
10+
function findOutOfOrder(imported, comparator) {
2111
if (imported.length === 0) {
2212
return []
2313
}
2414
let maxSeenRankNode = imported[0]
2515
return imported.filter(function (importedModule) {
26-
const res = importedModule.rank < maxSeenRankNode.rank
27-
if (maxSeenRankNode.rank < importedModule.rank) {
16+
const res = comparator(importedModule, maxSeenRankNode)
17+
if (comparator(maxSeenRankNode, importedModule)) {
2818
maxSeenRankNode = importedModule
2919
}
3020
return res
3121
})
3222
}
3323

34-
function reportOutOfOrder(context, imported, outOfOrder, order) {
35-
outOfOrder.forEach(function (imp) {
36-
const found = imported.find(function hasHigherRank(importedItem) {
37-
return importedItem.rank > imp.rank
38-
})
24+
function reportOutOfOrder(context, sortedImports, outOfOrder, order, comparator) {
25+
// Pass in imports pre-sorted to ensure `found` is correct position
26+
for (let imp of outOfOrder) {
27+
const found = sortedImports.find(importedItem => comparator(importedItem, imp))
28+
3929
context.report(imp.node, '`' + imp.name + '` import should occur ' + order +
4030
' import of `' + found.name + '`')
41-
})
31+
}
4232
}
4333

44-
function makeOutOfOrderReport(context, imported) {
45-
const outOfOrder = findOutOfOrder(imported)
46-
if (!outOfOrder.length) {
34+
function makeOutOfOrderReport(context, imported, forwardSortComparator, reverseSortComparator) {
35+
const outOfOrder = findOutOfOrder(imported, reverseSortComparator)
36+
if (outOfOrder.length === 0) {
4737
return
4838
}
4939
// There are things to report. Try to minimize the number of reported errors.
50-
const reversedImported = reverse(imported)
51-
const reversedOrder = findOutOfOrder(reversedImported)
40+
const reversedImported = [...imported].reverse()
41+
const reversedOrder = findOutOfOrder(reversedImported, forwardSortComparator)
42+
const sortedImports = [...imported].sort(forwardSortComparator)
5243
if (reversedOrder.length < outOfOrder.length) {
53-
reportOutOfOrder(context, reversedImported, reversedOrder, 'after')
54-
return
44+
reportOutOfOrder(context,
45+
sortedImports.reverse(),
46+
reversedOrder,
47+
'after',
48+
reverseSortComparator
49+
)
50+
} else {
51+
reportOutOfOrder(context,
52+
sortedImports,
53+
outOfOrder,
54+
'before',
55+
forwardSortComparator
56+
)
5557
}
56-
reportOutOfOrder(context, imported, outOfOrder, 'before')
5758
}
5859

5960
// DETECTING
@@ -158,6 +159,9 @@ module.exports = {
158159
'newlines-between': {
159160
enum: [ 'ignore', 'always', 'never' ],
160161
},
162+
'sort': {
163+
enum: [ 'ignore', 'alphabetical' ],
164+
},
161165
},
162166
additionalProperties: false,
163167
},
@@ -189,6 +193,20 @@ module.exports = {
189193
level--
190194
}
191195

196+
function determineComparators(alphabetize) {
197+
let forwardSortComparator, reverseSortComparator
198+
if (alphabetize) {
199+
forwardSortComparator = (a, b) => a.rank > b.rank ||
200+
(a.rank === b.rank && a.name > b.name)
201+
reverseSortComparator = (a, b) => a.rank < b.rank ||
202+
(a.rank === b.rank && a.name < b.name)
203+
} else {
204+
forwardSortComparator = (a, b) => a.rank > b.rank
205+
reverseSortComparator = (a, b) => a.rank < b.rank
206+
}
207+
return [forwardSortComparator, reverseSortComparator]
208+
}
209+
192210
return {
193211
ImportDeclaration: function handleImports(node) {
194212
if (node.specifiers.length) { // Ignoring unassigned imports
@@ -204,7 +222,9 @@ module.exports = {
204222
registerNode(context, node, name, 'require', ranks, imported)
205223
},
206224
'Program:exit': function reportAndReset() {
207-
makeOutOfOrderReport(context, imported)
225+
const alphabetize = (options['sort'] === 'alphabetical')
226+
let [forwardSortComparator, reverseSortComparator] = determineComparators(alphabetize)
227+
makeOutOfOrderReport(context, imported, forwardSortComparator, reverseSortComparator)
208228

209229
if (newlinesBetweenImports !== 'ignore') {
210230
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports)

tests/src/rules/order.js

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,47 @@ ruleTester.run('order', rule, {
376376
`,
377377
options: [{ 'newlines-between': 'always' }]
378378
}),
379+
// Alphabetical order
380+
test({
381+
code: `
382+
import fs from 'fs';
383+
import path from 'path';
384+
`,
385+
options: [{ 'sort': 'alphabetical' }],
386+
}),
387+
// Alphabetical order with duplicate import
388+
test({
389+
code: `
390+
import fs from 'fs';
391+
import fs from 'fs';
392+
`,
393+
options: [{ 'sort': 'alphabetical' }],
394+
}),
395+
// Alphabetical order with multiple groups
396+
test({
397+
code: `
398+
import fs from 'fs';
399+
import path from 'path';
400+
import async from 'async';
401+
`,
402+
options: [{ 'sort': 'alphabetical' }],
403+
}),
404+
// Ignore alphabetical order
405+
test({
406+
code: `
407+
import path from 'path';
408+
import fs from 'fs';
409+
`,
410+
options: [{ 'sort': 'ignore' }],
411+
}),
412+
// Ignore alphabetical order across groups
413+
test({
414+
code: `
415+
import fs from 'fs';
416+
import async from 'async';
417+
`,
418+
options: [{ 'sort': 'alphabetical' }],
419+
}),
379420
],
380421
invalid: [
381422
// builtin before external module (require)
@@ -456,7 +497,7 @@ ruleTester.run('order', rule, {
456497
message: '`async` import should occur before import of `./sibling`',
457498
}, {
458499
ruleId: 'order',
459-
message: '`fs` import should occur before import of `./sibling`',
500+
message: '`fs` import should occur before import of `async`',
460501
}],
461502
}),
462503
// Uses 'after' wording if it creates less errors
@@ -760,5 +801,92 @@ ruleTester.run('order', rule, {
760801
},
761802
],
762803
}),
804+
// Bad alphabetical order
805+
test({
806+
code: `
807+
import path from 'path';
808+
import fs from 'fs';
809+
`,
810+
options: [{ 'sort': 'alphabetical' }],
811+
errors: [
812+
{
813+
ruleId: 'order',
814+
message: '`fs` import should occur before import of `path`',
815+
},
816+
],
817+
}),
818+
// Bad alphabetical order with duplicate import
819+
test({
820+
code: `
821+
import fs from 'fs';
822+
import path from 'path';
823+
import fs from 'fs';
824+
`,
825+
options: [{ 'sort': 'alphabetical' }],
826+
errors: [
827+
{
828+
ruleId: 'order',
829+
message: '`fs` import should occur before import of `path`',
830+
},
831+
],
832+
}),
833+
// Bad alphabetical order reverse
834+
test({
835+
code: `
836+
import path from 'path';
837+
import index from './';
838+
import fs from 'fs';
839+
`,
840+
options: [
841+
{
842+
groups: [
843+
['builtin', 'index'],
844+
['sibling'],
845+
['parent', 'external'],
846+
],
847+
'sort': 'alphabetical',
848+
}
849+
],
850+
errors: [
851+
{
852+
ruleId: 'order',
853+
message: '`path` import should occur after import of `fs`',
854+
},
855+
],
856+
}),
857+
// Good alphabetical order with incorrect group order
858+
test({
859+
code: `
860+
import async from 'async'
861+
import fs from 'fs';
862+
import path from 'path';
863+
`,
864+
options: [{ 'sort': 'alphabetical' }],
865+
errors: [
866+
{
867+
ruleId: 'order',
868+
message: '`async` import should occur after import of `path`',
869+
},
870+
],
871+
}),
872+
// Bad alphabetical order with incorrect group order
873+
test({
874+
code: `
875+
import async from 'async'
876+
import path from 'path';
877+
import fs from 'fs';
878+
`,
879+
options: [{ 'sort': 'alphabetical' }],
880+
errors: [
881+
{
882+
ruleId: 'order',
883+
message: '`path` import should occur before import of `async`',
884+
},
885+
{
886+
ruleId: 'order',
887+
message: '`fs` import should occur before import of `path`',
888+
},
889+
],
890+
}),
763891
],
764892
})

0 commit comments

Comments
 (0)