Skip to content

Commit

Permalink
Implement autofix for eslint sort imports rule (adobe#6766)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Jul 25, 2024
1 parent 976604a commit 48af5db
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 60 deletions.
126 changes: 78 additions & 48 deletions packages/dev/eslint-plugin-rsp-rules/rules/sort-imports.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,75 +16,105 @@ module.exports = {
},
create: function (context) {
const sourceCode = context.getSourceCode();
let previousDeclaration = null;

/**
* Gets the local name of the first imported module.
* @param {ASTNode} node - the ImportDeclaration node.
* @returns {?string} the local name of the first imported module.
*/
function getFirstLocalMemberName(node) {
if (node.specifiers[0]) {
if (node.type === 'ImportDeclaration' && node.specifiers[0]) {
return node.specifiers[0].local.name.toLowerCase();
}
return null;

}

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

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

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

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

// Sort the array into the desired order
.sort((specifierA, specifierB) => {
const aName = getSortableName(specifierA);
const bName = getSortableName(specifierB);
return aName > bName ? 1 : -1;
})
return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
}, '')
);
}
});
} else if (lastImportDeclaration) {
let currentLocalMemberName = getFirstLocalMemberName(statement);
let previousLocalMemberName = getFirstLocalMemberName(lastImportDeclaration);
if (previousLocalMemberName &&
currentLocalMemberName &&
currentLocalMemberName < previousLocalMemberName
) {
context.report({
node,
message: 'Imports should be sorted alphabetically.',
fix(fixer) {
let allImports = [];
for (let statement of node.body) {
if (statement.type === 'ImportDeclaration') {
allImports.push(statement);
} else {
// Do not replace if there are other statements between imports.
break;
}
}

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

return sourceText + sourceCode.getText(specifier) + textAfterSpecifier;
}, '')
);
return fixer.replaceTextRange(
[allImports[0].range[0], allImports[allImports.length - 1].range[1]],
sortedImports.reduce((sourceText, statement, index) => {
const textAfterStatement = index === allImports.length - 1
? ''
: sourceCode.getText().slice(allImports[index].range[1], allImports[index + 1].range[0]);
return sourceText + sourceCode.getText(statement) + textAfterStatement;
}, '')
);
}
});
}
}
});
}

previousDeclaration = node;
lastImportDeclaration = statement;
}
});
}
};
}
Expand Down
24 changes: 12 additions & 12 deletions packages/dev/eslint-plugin-rsp-rules/test/sort-imports.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,19 @@ import {B} from 'bar';
code: "import {B, A} from 'foo';",
output: "import {A, B} from 'foo';",
errors: 1
}
},
// we don't support this case yet
// {
// code: `
// import {B} from 'bar';
// import {A} from 'foo';
// `,
// output: `
// import {A} from 'foo';
// import {B} from 'bar';
// `,
// errors: 1
// }
{
code: `
import {B} from 'bar';
import {A} from 'foo';
`,
output: `
import {A} from 'foo';
import {B} from 'bar';
`,
errors: 1
}
]
}
);

0 comments on commit 48af5db

Please sign in to comment.