Skip to content

Commit 48af5db

Browse files
authored
Implement autofix for eslint sort imports rule (#6766)
1 parent 976604a commit 48af5db

File tree

2 files changed

+90
-60
lines changed

2 files changed

+90
-60
lines changed

packages/dev/eslint-plugin-rsp-rules/rules/sort-imports.js

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,105 @@ module.exports = {
1616
},
1717
create: function (context) {
1818
const sourceCode = context.getSourceCode();
19-
let previousDeclaration = null;
2019

2120
/**
2221
* Gets the local name of the first imported module.
2322
* @param {ASTNode} node - the ImportDeclaration node.
2423
* @returns {?string} the local name of the first imported module.
2524
*/
2625
function getFirstLocalMemberName(node) {
27-
if (node.specifiers[0]) {
26+
if (node.type === 'ImportDeclaration' && node.specifiers[0]) {
2827
return node.specifiers[0].local.name.toLowerCase();
2928
}
3029
return null;
31-
3230
}
3331

3432
return {
35-
ImportDeclaration(node) {
36-
if (previousDeclaration) {
37-
let currentLocalMemberName = getFirstLocalMemberName(node),
38-
previousLocalMemberName = getFirstLocalMemberName(previousDeclaration);
33+
Program(node) {
34+
let lastImportDeclaration = null;
35+
node.body.forEach((statement, i) => {
36+
if (statement.type === 'ImportDeclaration') {
37+
const importSpecifiers = statement.specifiers.filter(specifier => specifier.type === 'ImportSpecifier');
38+
const getSortableName = specifier => specifier.local.name.toLowerCase();
39+
const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
3940

40-
if (previousLocalMemberName &&
41-
currentLocalMemberName &&
42-
currentLocalMemberName < previousLocalMemberName
43-
) {
44-
context.report({
45-
node,
46-
message: 'Imports should be sorted alphabetically.'
47-
});
48-
}
49-
}
41+
if (firstUnsortedIndex !== -1) {
42+
context.report({
43+
node: importSpecifiers[firstUnsortedIndex],
44+
message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
45+
data: {memberName: importSpecifiers[firstUnsortedIndex].local.name},
46+
fix(fixer) {
47+
return fixer.replaceTextRange(
48+
[importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
49+
importSpecifiers
50+
// Clone the importSpecifiers array to avoid mutating it
51+
.slice()
5052

51-
const importSpecifiers = node.specifiers.filter(specifier => specifier.type === 'ImportSpecifier');
52-
const getSortableName = specifier => specifier.local.name.toLowerCase();
53-
const firstUnsortedIndex = importSpecifiers.map(getSortableName).findIndex((name, index, array) => array[index - 1] > name);
53+
// Sort the array into the desired order
54+
.sort((specifierA, specifierB) => {
55+
const aName = getSortableName(specifierA);
56+
const bName = getSortableName(specifierB);
57+
return aName > bName ? 1 : -1;
58+
})
5459

55-
if (firstUnsortedIndex !== -1) {
56-
context.report({
57-
node: importSpecifiers[firstUnsortedIndex],
58-
message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.",
59-
data: {memberName: importSpecifiers[firstUnsortedIndex].local.name},
60-
fix(fixer) {
61-
return fixer.replaceTextRange(
62-
[importSpecifiers[0].range[0], importSpecifiers[importSpecifiers.length - 1].range[1]],
63-
importSpecifiers
64-
// Clone the importSpecifiers array to avoid mutating it
65-
.slice()
60+
// Build a string out of the sorted list of import specifiers and the text between the originals
61+
.reduce((sourceText, specifier, index) => {
62+
const textAfterSpecifier = index === importSpecifiers.length - 1
63+
? ''
64+
: sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
6665

67-
// Sort the array into the desired order
68-
.sort((specifierA, specifierB) => {
69-
const aName = getSortableName(specifierA);
70-
const bName = getSortableName(specifierB);
71-
return aName > bName ? 1 : -1;
72-
})
66+
return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
67+
}, '')
68+
);
69+
}
70+
});
71+
} else if (lastImportDeclaration) {
72+
let currentLocalMemberName = getFirstLocalMemberName(statement);
73+
let previousLocalMemberName = getFirstLocalMemberName(lastImportDeclaration);
74+
if (previousLocalMemberName &&
75+
currentLocalMemberName &&
76+
currentLocalMemberName < previousLocalMemberName
77+
) {
78+
context.report({
79+
node,
80+
message: 'Imports should be sorted alphabetically.',
81+
fix(fixer) {
82+
let allImports = [];
83+
for (let statement of node.body) {
84+
if (statement.type === 'ImportDeclaration') {
85+
allImports.push(statement);
86+
} else {
87+
// Do not replace if there are other statements between imports.
88+
break;
89+
}
90+
}
7391

74-
// Build a string out of the sorted list of import specifiers and the text between the originals
75-
.reduce((sourceText, specifier, index) => {
76-
const textAfterSpecifier = index === importSpecifiers.length - 1
77-
? ''
78-
: sourceCode.getText().slice(importSpecifiers[index].range[1], importSpecifiers[index + 1].range[0]);
92+
let sortedImports = allImports.slice().sort((a, b) => {
93+
let aName = getFirstLocalMemberName(a);
94+
let bName = getFirstLocalMemberName(b);
95+
if (aName === bName) {
96+
return 0;
97+
}
98+
return aName < bName ? -1 : 1;
99+
});
79100

80-
return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
81-
}, '')
82-
);
101+
return fixer.replaceTextRange(
102+
[allImports[0].range[0], allImports[allImports.length - 1].range[1]],
103+
sortedImports.reduce((sourceText, statement, index) => {
104+
const textAfterStatement = index === allImports.length - 1
105+
? ''
106+
: sourceCode.getText().slice(allImports[index].range[1], allImports[index + 1].range[0]);
107+
return sourceText + sourceCode.getText(statement) + textAfterStatement;
108+
}, '')
109+
);
110+
}
111+
});
112+
}
83113
}
84-
});
85-
}
86114

87-
previousDeclaration = node;
115+
lastImportDeclaration = statement;
116+
}
117+
});
88118
}
89119
};
90120
}

packages/dev/eslint-plugin-rsp-rules/test/sort-imports.test.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,19 @@ import {B} from 'bar';
4242
code: "import {B, A} from 'foo';",
4343
output: "import {A, B} from 'foo';",
4444
errors: 1
45-
}
45+
},
4646
// we don't support this case yet
47-
// {
48-
// code: `
49-
// import {B} from 'bar';
50-
// import {A} from 'foo';
51-
// `,
52-
// output: `
53-
// import {A} from 'foo';
54-
// import {B} from 'bar';
55-
// `,
56-
// errors: 1
57-
// }
47+
{
48+
code: `
49+
import {B} from 'bar';
50+
import {A} from 'foo';
51+
`,
52+
output: `
53+
import {A} from 'foo';
54+
import {B} from 'bar';
55+
`,
56+
errors: 1
57+
}
5858
]
5959
}
6060
);

0 commit comments

Comments
 (0)