Skip to content

Commit 6a4156d

Browse files
randallreedjrljharb
authored andcommitted
[Fix] order: allow secondary alphabetical sorting
Fixes import-js#389.
1 parent 37554fe commit 6a4156d

File tree

4 files changed

+121
-53
lines changed

4 files changed

+121
-53
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
99
### Added
1010
- Add ESLint 5 support ([#1122], thanks [@ai] and [@ljharb])
1111
- Add [`no-relative-parent-imports`] rule: disallow relative imports from parent directories ([#1093], thanks [@chrislloyd])
12+
- [`order`] allows secondary alphabetical sort ([#389])
1213

1314
### Fixed
1415
- `namespace` rule: ensure it works in eslint 5/ecmaVersion 2018 (thanks [@ljharb])

docs/rules/order.md

+35
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,41 @@ import index from './';
164164
import sibling from './foo';
165165
```
166166

167+
### `sort: [ignore|alphabetical]`:
168+
169+
Enforces alphabetical sorting within import groups:
170+
171+
- If set to `ignore`, no errors related to order within import groups will be reported (default).
172+
- If set to `alphabetical`, imports within a group must be alphabetized. Imports across groups will not be compared. Alphabetical sort ignores capitalization.
173+
174+
With the default group setting, the following will be invalid:
175+
176+
```js
177+
/* eslint import/order: ["error", {"sort": "alphabetical"}] */
178+
import path from 'path';
179+
import fs from 'fs';
180+
import index from './';
181+
import sibling from './foo';
182+
```
183+
184+
while this will be valid:
185+
186+
```js
187+
/* eslint import/order: ["error", {"sort": "alphabetical"}] */
188+
import fs from 'fs';
189+
import path from 'path';
190+
import index from './';
191+
import sibling from './foo';
192+
```
193+
194+
```js
195+
/* eslint import/order: ["error", {"sort": "ignore"}] */
196+
import path from 'path';
197+
import fs from 'fs';
198+
import index from './';
199+
import sibling from './foo';
200+
```
201+
167202
## Related
168203

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

src/rules/order.js

+51-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index']
88

99
// REPORTING AND FIXING
1010

11+
1112
function reverse(array) {
1213
return array.map(function (v) {
1314
return {
@@ -72,14 +73,14 @@ function takeTokensBeforeWhile(sourceCode, node, condition) {
7273
return result.reverse()
7374
}
7475

75-
function findOutOfOrder(imported) {
76+
function findOutOfOrder(imported, comparator) {
7677
if (imported.length === 0) {
7778
return []
7879
}
7980
let maxSeenRankNode = imported[0]
8081
return imported.filter(function (importedModule) {
81-
const res = importedModule.rank < maxSeenRankNode.rank
82-
if (maxSeenRankNode.rank < importedModule.rank) {
82+
const res = comparator(importedModule, maxSeenRankNode)
83+
if (comparator(maxSeenRankNode, importedModule)) {
8384
maxSeenRankNode = importedModule
8485
}
8586
return res
@@ -215,32 +216,58 @@ function fixOutOfOrder(context, firstNode, secondNode, order) {
215216
}
216217
}
217218

218-
function reportOutOfOrder(context, imported, outOfOrder, order) {
219-
outOfOrder.forEach(function (imp) {
220-
const found = imported.find(function hasHigherRank(importedItem) {
221-
return importedItem.rank > imp.rank
222-
})
223-
fixOutOfOrder(context, found, imp, order)
224-
})
219+
function reportOutOfOrder(context, sortedImports, outOfOrder, order, comparator) {
220+
// Pass in imports pre-sorted to ensure `found` is correct position
221+
for (let imp of outOfOrder) {
222+
const found = sortedImports.find(importedItem => comparator(importedItem, imp))
223+
224+
context.report(imp.node, '`' + imp.name + '` import should occur ' + order +
225+
' import of `' + found.name + '`')
226+
}
225227
}
226228

227-
function makeOutOfOrderReport(context, imported) {
228-
const outOfOrder = findOutOfOrder(imported)
229+
function makeOutOfOrderReport(context, imported, forwardSortComparator, reverseSortComparator) {
230+
const outOfOrder = findOutOfOrder(imported, reverseSortComparator)
229231
if (!outOfOrder.length) {
230232
return
231233
}
232234
// There are things to report. Try to minimize the number of reported errors.
233-
const reversedImported = reverse(imported)
234-
const reversedOrder = findOutOfOrder(reversedImported)
235+
const reversedImported = [...imported].reverse()
236+
const reversedOrder = findOutOfOrder(reversedImported, forwardSortComparator)
237+
const sortedImports = [...imported].sort(forwardSortComparator)
235238
if (reversedOrder.length < outOfOrder.length) {
236-
reportOutOfOrder(context, reversedImported, reversedOrder, 'after')
237-
return
239+
reportOutOfOrder(context,
240+
sortedImports.reverse(),
241+
reversedOrder,
242+
'after',
243+
reverseSortComparator
244+
)
245+
} else {
246+
reportOutOfOrder(context,
247+
sortedImports,
248+
outOfOrder,
249+
'before',
250+
forwardSortComparator
251+
)
238252
}
239-
reportOutOfOrder(context, imported, outOfOrder, 'before')
240253
}
241254

242255
// DETECTING
243256

257+
function determineComparators(alphabetize) {
258+
let forwardSortComparator, reverseSortComparator
259+
if (alphabetize) {
260+
forwardSortComparator = (a, b) => a.rank > b.rank ||
261+
(a.rank === b.rank && (a.name.toLowerCase() > b.name.toLowerCase()))
262+
reverseSortComparator = (a, b) => a.rank < b.rank ||
263+
(a.rank === b.rank && (a.name.toLowerCase() < b.name.toLowerCase()))
264+
} else {
265+
forwardSortComparator = (a, b) => a.rank > b.rank
266+
reverseSortComparator = (a, b) => a.rank < b.rank
267+
}
268+
return [forwardSortComparator, reverseSortComparator]
269+
}
270+
244271
function computeRank(context, ranks, name, type) {
245272
return ranks[importType(name, context)] +
246273
(type === 'import' ? 0 : 100)
@@ -382,13 +409,16 @@ module.exports = {
382409
'never',
383410
],
384411
},
412+
'sort': {
413+
enum: [ 'ignore', 'alphabetical' ],
414+
},
385415
},
386416
additionalProperties: false,
387417
},
388418
],
389419
},
390420

391-
create: function importOrderRule (context) {
421+
create: function importOrderRule(context) {
392422
const options = context.options[0] || {}
393423
const newlinesBetweenImports = options['newlines-between'] || 'ignore'
394424
let ranks
@@ -428,7 +458,9 @@ module.exports = {
428458
registerNode(context, node, name, 'require', ranks, imported)
429459
},
430460
'Program:exit': function reportAndReset() {
431-
makeOutOfOrderReport(context, imported)
461+
const alphabetize = (options['sort'] === 'alphabetical')
462+
const [forwardSortComparator, reverseSortComparator] = determineComparators(alphabetize)
463+
makeOutOfOrderReport(context, imported, forwardSortComparator, reverseSortComparator)
432464

433465
if (newlinesBetweenImports !== 'ignore') {
434466
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports)

tests/src/rules/order.js

+34-34
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { test } from '../utils'
33
import { RuleTester } from 'eslint'
44

55
const ruleTester = new RuleTester()
6-
, rule = require('rules/order')
6+
, rule = require('rules/order')
77

88
function withoutAutofixOutput(test) {
99
return Object.assign({}, test, { output: test.code })
@@ -21,7 +21,7 @@ ruleTester.run('order', rule, {
2121
var relParent3 = require('../');
2222
var sibling = require('./foo');
2323
var index = require('./');`,
24-
}),
24+
}),
2525
// Default order using import
2626
test({
2727
code: `
@@ -32,7 +32,7 @@ ruleTester.run('order', rule, {
3232
import relParent3 from '../';
3333
import sibling, {foo3} from './foo';
3434
import index from './';`,
35-
}),
35+
}),
3636
// Multiple module of the same rank next to each other
3737
test({
3838
code: `
@@ -41,7 +41,7 @@ ruleTester.run('order', rule, {
4141
var path = require('path');
4242
var _ = require('lodash');
4343
var async = require('async');`,
44-
}),
44+
}),
4545
// Overriding order to be the reverse of the default order
4646
test({
4747
code: `
@@ -136,9 +136,9 @@ ruleTester.run('order', rule, {
136136
var relParent1 = require('../foo');
137137
`,
138138
options: [{groups: [
139-
['builtin', 'index'],
140-
['sibling', 'parent', 'external'],
141-
]}],
139+
['builtin', 'index'],
140+
['sibling', 'parent', 'external'],
141+
]}],
142142
}),
143143
// Omitted types should implicitly be considered as the last type
144144
test({
@@ -147,10 +147,10 @@ ruleTester.run('order', rule, {
147147
var path = require('path');
148148
`,
149149
options: [{groups: [
150-
'index',
151-
['sibling', 'parent', 'external'],
152-
// missing 'builtin'
153-
]}],
150+
'index',
151+
['sibling', 'parent', 'external'],
152+
// missing 'builtin'
153+
]}],
154154
}),
155155
// Mixing require and import should have import up top
156156
test({
@@ -486,12 +486,12 @@ ruleTester.run('order', rule, {
486486
// fix order with windows end of lines
487487
test({
488488
code:
489-
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n` +
490-
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n`
489+
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n` +
490+
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n`
491491
,
492492
output:
493-
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n` +
494-
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n`
493+
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n` +
494+
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n`
495495
,
496496
errors: [{
497497
ruleId: 'order',
@@ -740,9 +740,9 @@ ruleTester.run('order', rule, {
740740
var sibling = require('./foo');
741741
`,
742742
options: [{groups: [
743-
['builtin', 'index'],
744-
['sibling', 'parent', 'external'],
745-
]}],
743+
['builtin', 'index'],
744+
['sibling', 'parent', 'external'],
745+
]}],
746746
errors: [{
747747
ruleId: 'order',
748748
message: '`path` import should occur before import of `./foo`',
@@ -759,10 +759,10 @@ ruleTester.run('order', rule, {
759759
var path = require('path');
760760
`,
761761
options: [{groups: [
762-
'index',
763-
['sibling', 'parent', 'external', 'internal'],
764-
// missing 'builtin'
765-
]}],
762+
'index',
763+
['sibling', 'parent', 'external', 'internal'],
764+
// missing 'builtin'
765+
]}],
766766
errors: [{
767767
ruleId: 'order',
768768
message: '`async` import should occur before import of `path`',
@@ -776,9 +776,9 @@ ruleTester.run('order', rule, {
776776
var index = require('./');
777777
`,
778778
options: [{groups: [
779-
'index',
780-
['sibling', 'parent', 'UNKNOWN', 'internal'],
781-
]}],
779+
'index',
780+
['sibling', 'parent', 'UNKNOWN', 'internal'],
781+
]}],
782782
errors: [{
783783
ruleId: 'order',
784784
message: 'Incorrect configuration of the rule: Unknown type `"UNKNOWN"`',
@@ -791,9 +791,9 @@ ruleTester.run('order', rule, {
791791
var index = require('./');
792792
`,
793793
options: [{groups: [
794-
'index',
795-
['sibling', 'parent', ['builtin'], 'internal'],
796-
]}],
794+
'index',
795+
['sibling', 'parent', ['builtin'], 'internal'],
796+
]}],
797797
errors: [{
798798
ruleId: 'order',
799799
message: 'Incorrect configuration of the rule: Unknown type `["builtin"]`',
@@ -806,9 +806,9 @@ ruleTester.run('order', rule, {
806806
var index = require('./');
807807
`,
808808
options: [{groups: [
809-
'index',
810-
['sibling', 'parent', 2, 'internal'],
811-
]}],
809+
'index',
810+
['sibling', 'parent', 2, 'internal'],
811+
]}],
812812
errors: [{
813813
ruleId: 'order',
814814
message: 'Incorrect configuration of the rule: Unknown type `2`',
@@ -821,9 +821,9 @@ ruleTester.run('order', rule, {
821821
var index = require('./');
822822
`,
823823
options: [{groups: [
824-
'index',
825-
['sibling', 'parent', 'parent', 'internal'],
826-
]}],
824+
'index',
825+
['sibling', 'parent', 'parent', 'internal'],
826+
]}],
827827
errors: [{
828828
ruleId: 'order',
829829
message: 'Incorrect configuration of the rule: `parent` is duplicated',

0 commit comments

Comments
 (0)