Skip to content

Commit a133e5f

Browse files
aaronadamsCAstropho
authored andcommitted
[New] order: new alphabetize.orderImportKind option to sort imports with same path based on their kind (type, typeof)
Fixes import-js#2339 Co-authored-by: Aaron Adams <[email protected]> Co-authored-by: stropho <[email protected]>
1 parent 395e26b commit a133e5f

File tree

4 files changed

+176
-34
lines changed

4 files changed

+176
-34
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1414
- [`order`]: Add `distinctGroup` option ([#2395], thanks [@hyperupcall])
1515
- [`no-extraneous-dependencies`]: Add `includeInternal` option ([#2541], thanks [@bdwain])
1616
- [`no-extraneous-dependencies`]: Add `includeTypes` option ([#2543], thanks [@bdwain])
17+
- [`order`]: new `alphabetize.orderImportKind` option to sort imports with same path based on their kind (`type`, `typeof`) ([#2544], thanks [@stropho])
1718

1819
### Fixed
1920
- [`order`]: move nested imports closer to main import entry ([#2396], thanks [@pri1311])

docs/rules/order.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -267,11 +267,12 @@ import index from './';
267267
import sibling from './foo';
268268
```
269269

270-
### `alphabetize: {order: asc|desc|ignore, caseInsensitive: true|false}`:
270+
### `alphabetize: {order: asc|desc|ignore, orderImportKind: asc|desc|ignore, caseInsensitive: true|false}`:
271271

272272
Sort the order within each group in alphabetical manner based on **import path**:
273273

274274
- `order`: use `asc` to sort in ascending order, and `desc` to sort in descending order (default: `ignore`).
275+
- `orderImportKind`: use `asc` to sort in ascending order various import kinds, e.g. imports prefixed with `type` or `typeof`, with same import path. Use `desc` to sort in descending order (default: `ignore`).
275276
- `caseInsensitive`: use `true` to ignore case, and `false` to consider case (default: `false`).
276277

277278
Example setting:

src/rules/order.js

+59-25
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,16 @@ function canReorderItems(firstNode, secondNode) {
187187
return true;
188188
}
189189

190+
function makeImportDescription(node) {
191+
if (node.node.importKind === 'type') {
192+
return 'type import';
193+
}
194+
if (node.node.importKind === 'typeof') {
195+
return 'typeof import';
196+
}
197+
return 'import';
198+
}
199+
190200
function fixOutOfOrder(context, firstNode, secondNode, order) {
191201
const sourceCode = context.getSourceCode();
192202

@@ -204,7 +214,9 @@ function fixOutOfOrder(context, firstNode, secondNode, order) {
204214
newCode = newCode + '\n';
205215
}
206216

207-
const message = `\`${secondNode.displayName}\` import should occur ${order} import of \`${firstNode.displayName}\``;
217+
const firstImport = `${makeImportDescription(firstNode)} of \`${firstNode.displayName}\``;
218+
const secondImport = `\`${secondNode.displayName}\` ${makeImportDescription(secondNode)}`;
219+
const message = `${secondImport} should occur ${order} ${firstImport}`;
208220

209221
if (order === 'before') {
210222
context.report({
@@ -253,42 +265,62 @@ function makeOutOfOrderReport(context, imported) {
253265
reportOutOfOrder(context, imported, outOfOrder, 'before');
254266
}
255267

256-
function getSorter(ascending) {
257-
const multiplier = ascending ? 1 : -1;
268+
const compareString = (a, b) => {
269+
if (a < b) {
270+
return -1;
271+
} else if (a > b) {
272+
return 1;
273+
}
274+
return 0;
275+
};
276+
277+
/** Some parsers (languages without types) don't provide ImportKind */
278+
const DEAFULT_IMPORT_KIND = 'value';
279+
const getNormalizedValue = (node, toLowerCase) => {
280+
const value = node.value;
281+
return toLowerCase ? String(value).toLowerCase() : value;
282+
};
283+
284+
function getSorter(alphabetizeOptions) {
285+
const multiplier = alphabetizeOptions.order === 'asc' ? 1 : -1;
286+
const orderImportKind = alphabetizeOptions.orderImportKind;
287+
const multiplierImportKind = orderImportKind !== 'ignore' &&
288+
(alphabetizeOptions.orderImportKind === 'asc' ? 1 : -1);
258289

259-
return function importsSorter(importA, importB) {
290+
return function importsSorter(nodeA, nodeB) {
291+
const importA = getNormalizedValue(nodeA, alphabetizeOptions.caseInsensitive);
292+
const importB = getNormalizedValue(nodeB, alphabetizeOptions.caseInsensitive);
260293
let result = 0;
261294

262295
if (!includes(importA, '/') && !includes(importB, '/')) {
263-
if (importA < importB) {
264-
result = -1;
265-
} else if (importA > importB) {
266-
result = 1;
267-
} else {
268-
result = 0;
269-
}
296+
result = compareString(importA, importB);
270297
} else {
271298
const A = importA.split('/');
272299
const B = importB.split('/');
273300
const a = A.length;
274301
const b = B.length;
275302

276303
for (let i = 0; i < Math.min(a, b); i++) {
277-
if (A[i] < B[i]) {
278-
result = -1;
279-
break;
280-
} else if (A[i] > B[i]) {
281-
result = 1;
282-
break;
283-
}
304+
result = compareString(A[i], B[i]);
305+
if (result) break;
284306
}
285307

286308
if (!result && a !== b) {
287309
result = a < b ? -1 : 1;
288310
}
289311
}
290312

291-
return result * multiplier;
313+
result = result * multiplier;
314+
315+
// In case the paths are equal (result === 0), sort them by importKind
316+
if (!result && multiplierImportKind) {
317+
result = multiplierImportKind * compareString(
318+
nodeA.node.importKind || DEAFULT_IMPORT_KIND,
319+
nodeB.node.importKind || DEAFULT_IMPORT_KIND,
320+
);
321+
}
322+
323+
return result;
292324
};
293325
}
294326

@@ -303,14 +335,11 @@ function mutateRanksToAlphabetize(imported, alphabetizeOptions) {
303335

304336
const groupRanks = Object.keys(groupedByRanks);
305337

306-
const sorterFn = getSorter(alphabetizeOptions.order === 'asc');
307-
const comparator = alphabetizeOptions.caseInsensitive
308-
? (a, b) => sorterFn(String(a.value).toLowerCase(), String(b.value).toLowerCase())
309-
: (a, b) => sorterFn(a.value, b.value);
338+
const sorterFn = getSorter(alphabetizeOptions);
310339

311340
// sort imports locally within their group
312341
groupRanks.forEach(function (groupRank) {
313-
groupedByRanks[groupRank].sort(comparator);
342+
groupedByRanks[groupRank].sort(sorterFn);
314343
});
315344

316345
// assign globally unique rank to each import
@@ -546,9 +575,10 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, di
546575
function getAlphabetizeConfig(options) {
547576
const alphabetize = options.alphabetize || {};
548577
const order = alphabetize.order || 'ignore';
578+
const orderImportKind = alphabetize.orderImportKind || 'ignore';
549579
const caseInsensitive = alphabetize.caseInsensitive || false;
550580

551-
return { order, caseInsensitive };
581+
return { order, orderImportKind, caseInsensitive };
552582
}
553583

554584
// TODO, semver-major: Change the default of "distinctGroup" from true to false
@@ -619,6 +649,10 @@ module.exports = {
619649
enum: ['ignore', 'asc', 'desc'],
620650
default: 'ignore',
621651
},
652+
orderImportKind: {
653+
enum: ['ignore', 'asc', 'desc'],
654+
default: 'ignore',
655+
},
622656
},
623657
additionalProperties: false,
624658
},

tests/src/rules/order.js

+114-8
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@ import { RuleTester } from 'eslint';
44
import eslintPkg from 'eslint/package.json';
55
import semver from 'semver';
66
import flatMap from 'array.prototype.flatmap';
7+
import { resolve } from 'path';
8+
import { default as babelPresetFlow } from 'babel-preset-flow';
9+
710

811
const ruleTester = new RuleTester();
12+
const flowRuleTester = new RuleTester({
13+
parser: resolve(__dirname, '../../../node_modules/babel-eslint'),
14+
parserOptions: {
15+
babelOptions: {
16+
configFile: false,
17+
babelrc: false,
18+
presets: [babelPresetFlow],
19+
},
20+
},
21+
});
922
const rule = require('rules/order');
1023

1124
function withoutAutofixOutput(test) {
@@ -1080,6 +1093,19 @@ ruleTester.run('order', rule, {
10801093
},
10811094
],
10821095
}),
1096+
// orderImportKind option that is not used
1097+
test({
1098+
code: `
1099+
import B from './B';
1100+
import b from './b';
1101+
`,
1102+
options: [
1103+
{
1104+
'alphabetize': { order: 'asc', orderImportKind: 'asc', 'caseInsensitive': true },
1105+
},
1106+
],
1107+
}),
1108+
10831109
],
10841110
invalid: [
10851111
// builtin before external module (require)
@@ -2931,8 +2957,8 @@ context('TypeScript', function () {
29312957
errors: [
29322958
{
29332959
message: semver.satisfies(eslintPkg.version, '< 3')
2934-
? '`bar` import should occur after import of `Bar`'
2935-
: /(`bar` import should occur after import of `Bar`)|(`Bar` import should occur before import of `bar`)/,
2960+
? '`bar` import should occur after type import of `Bar`'
2961+
: /(`bar` import should occur after type import of `Bar`)|(`Bar` type import should occur before import of `bar`)/,
29362962
},
29372963
],
29382964
}),
@@ -3002,10 +3028,10 @@ context('TypeScript', function () {
30023028
],
30033029
errors: semver.satisfies(eslintPkg.version, '< 3') ? [
30043030
{ message: '`Bar` import should occur before import of `bar`' },
3005-
{ message: '`Bar` import should occur before import of `foo`' },
3031+
{ message: '`Bar` type import should occur before type import of `foo`' },
30063032
] : [
30073033
{ message: /(`Bar` import should occur before import of `bar`)|(`bar` import should occur after import of `Bar`)/ },
3008-
{ message: /(`Bar` import should occur before import of `foo`)|(`foo` import should occur after import of `Bar`)/ },
3034+
{ message: /(`Bar` type import should occur before type import of `foo`)|(`foo` type import should occur after type import of `Bar`)/ },
30093035
],
30103036
}),
30113037
// Option alphabetize: {order: 'desc'} with type group
@@ -3039,10 +3065,10 @@ context('TypeScript', function () {
30393065
],
30403066
errors: semver.satisfies(eslintPkg.version, '< 3') ? [
30413067
{ message: '`bar` import should occur before import of `Bar`' },
3042-
{ message: '`foo` import should occur before import of `Bar`' },
3068+
{ message: '`foo` type import should occur before type import of `Bar`' },
30433069
] : [
30443070
{ message: /(`bar` import should occur before import of `Bar`)|(`Bar` import should occur after import of `bar`)/ },
3045-
{ message: /(`foo` import should occur before import of `Bar`)|(`Bar` import should occur after import of `foo`)/ },
3071+
{ message: /(`foo` type import should occur before type import of `Bar`)|(`Bar` type import should occur after import of type `foo`)/ },
30463072
],
30473073
}),
30483074
// warns for out of order unassigned imports (warnOnUnassignedImports enabled)
@@ -3113,9 +3139,9 @@ context('TypeScript', function () {
31133139
}
31143140
`,
31153141
errors: [{
3116-
message: '`fs` import should occur before import of `path`',
3142+
message: '`fs` type import should occur before type import of `path`',
31173143
},{
3118-
message: '`fs` import should occur before import of `path`',
3144+
message: '`fs` type import should occur before type import of `path`',
31193145
}],
31203146
...parserConfig,
31213147
options: [
@@ -3128,3 +3154,83 @@ context('TypeScript', function () {
31283154
});
31293155
});
31303156
});
3157+
3158+
flowRuleTester.run('order', rule, {
3159+
valid: [
3160+
test({
3161+
options: [
3162+
{
3163+
alphabetize: { order: 'asc', orderImportKind: 'asc' },
3164+
},
3165+
],
3166+
code: `
3167+
import type {Bar} from 'common';
3168+
import typeof {foo} from 'common';
3169+
import {bar} from 'common';
3170+
`,
3171+
})],
3172+
invalid: [
3173+
test({
3174+
options: [
3175+
{
3176+
alphabetize: { order: 'asc', orderImportKind: 'asc' },
3177+
},
3178+
],
3179+
code: `
3180+
import type {Bar} from 'common';
3181+
import {bar} from 'common';
3182+
import typeof {foo} from 'common';
3183+
`,
3184+
output: `
3185+
import type {Bar} from 'common';
3186+
import typeof {foo} from 'common';
3187+
import {bar} from 'common';
3188+
`,
3189+
errors: [{
3190+
message: '`common` typeof import should occur before import of `common`',
3191+
}],
3192+
}),
3193+
test({
3194+
options: [
3195+
{
3196+
alphabetize: { order: 'asc', orderImportKind: 'desc' },
3197+
},
3198+
],
3199+
code: `
3200+
import type {Bar} from 'common';
3201+
import {bar} from 'common';
3202+
import typeof {foo} from 'common';
3203+
`,
3204+
output: `
3205+
import {bar} from 'common';
3206+
import typeof {foo} from 'common';
3207+
import type {Bar} from 'common';
3208+
`,
3209+
errors: [{
3210+
message: '`common` type import should occur after typeof import of `common`',
3211+
}],
3212+
}),
3213+
test({
3214+
options: [
3215+
{
3216+
alphabetize: { order: 'asc', orderImportKind: 'asc' },
3217+
},
3218+
],
3219+
code: `
3220+
import type {Bar} from './local/sub';
3221+
import {bar} from './local/sub';
3222+
import {baz} from './local-sub';
3223+
import typeof {foo} from './local/sub';
3224+
`,
3225+
output: `
3226+
import type {Bar} from './local/sub';
3227+
import typeof {foo} from './local/sub';
3228+
import {bar} from './local/sub';
3229+
import {baz} from './local-sub';
3230+
`,
3231+
errors: [{
3232+
message: '`./local/sub` typeof import should occur before import of `./local/sub`',
3233+
}],
3234+
}),
3235+
],
3236+
});

0 commit comments

Comments
 (0)