Skip to content

[New] add sort-by-length option to order rule #2051

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/rules/order.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,41 @@ import React, { PureComponent } from 'react';
import { compose, apply } from 'xcompose';
```

### `sort-by-length: {order: asc|desc|ignore, caseInsensitive: true|false}`:

Sort the order within each group in import string length based on **import path**:

Example setting:
```js
'sort-by-length': {
order: 'asc', /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */
}
```

This will fail the rule check:

```js
/* eslint import/order: ["error", {"sort-by-length": {"order": "desc"}}] */
import React, { PureComponent } from 'react';
import aTypes from 'prop-types';
import { compose, apply } from 'xcompose';
import * as classnames from 'classnames';
import blist from 'BList';
```

While this will pass:

```js
/* eslint import/order: ["error", {"sort-by-length": {"order": "desc"}}] */
import React, { PureComponent } from 'react';
import { compose, apply } from 'xcompose';
import * as classnames from 'classnames';
import aTypes from 'prop-types';
import blist from 'BList';
```

**NOTE**: Do not use this rule with `alphabetize`

## Related

- [`import/external-module-folders`] setting
Expand Down
81 changes: 79 additions & 2 deletions src/rules/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,53 @@ function mutateRanksToAlphabetize(imported, alphabetizeOptions) {
});
}

function mutateRanksByLength(imported, sortByLengthOptions) {
const groupedByRanks = imported.reduce(function(acc, importedItem) {
if (!Array.isArray(acc[importedItem.rank])) {
acc[importedItem.rank] = [];
}
const value = importedItem.value;
const sourceLength = importedItem.source.range[1] - importedItem.source.range[0];
let specifiersLength = importedItem.specifiers.reduce((acc, specifier) => {
return acc + (specifier.range[1] - specifier.range[0]);
}, 0)
if (importedItem.specifiers.some((specifier) => specifier.type === 'ImportSpecifier')) {
specifiersLength += 4
}
const len = specifiersLength + sourceLength;
acc[importedItem.rank].push({
value,
len
});
return acc;
}, {});

const groupRanks = Object.keys(groupedByRanks);

const sorterFn = getSorter(sortByLengthOptions.order === 'asc');
const comparator = (a, b) => sorterFn(a.len, b.len);

// sort imports locally within their group
groupRanks.forEach(function(groupRank) {
groupedByRanks[groupRank].sort(comparator);
});

// assign globally unique rank to each import
let newRank = 0;
const sortedByLengthRanks = groupRanks.sort().reduce(function(acc, groupRank) {
groupedByRanks[groupRank].forEach(function(importedItemName) {
acc[importedItemName.value] = parseInt(groupRank, 10) + newRank;
newRank += 1;
});
return acc;
}, {});

// mutate the original group-rank with length-rank
imported.forEach(function(importedItem) {
importedItem.rank = sortedByLengthRanks[importedItem.value];
});
}

// DETECTING

function computePathRank(ranks, pathGroups, path, maxPosition) {
Expand Down Expand Up @@ -337,7 +384,7 @@ function isModuleLevelRequire(node) {
let n = node;
// Handle cases like `const baz = require('foo').bar.baz`
// and `const foo = require('foo')()`
while (
while (
(n.parent.type === 'MemberExpression' && n.parent.object === n) ||
(n.parent.type === 'CallExpression' && n.parent.callee === n)
) {
Expand All @@ -346,7 +393,7 @@ function isModuleLevelRequire(node) {
return (
n.parent.type === 'VariableDeclarator' &&
n.parent.parent.type === 'VariableDeclaration' &&
n.parent.parent.parent.type === 'Program'
n.parent.parent.parent.type === 'Program'
);
}

Expand Down Expand Up @@ -503,6 +550,13 @@ function getAlphabetizeConfig(options) {
return { order, caseInsensitive };
}

function getSortByLengthConfig(options) {
const sortByConfig = options['sort-by-length'] || {};
const order = sortByConfig.order || 'ignore';

return { order };
}

module.exports = {
meta: {
type: 'suggestion',
Expand Down Expand Up @@ -566,6 +620,16 @@ module.exports = {
},
additionalProperties: false,
},
'sort-by-length': {
type: 'object',
properties: {
order: {
enum: ['ignore', 'asc', 'desc'],
default: 'ignore',
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
Expand All @@ -577,6 +641,7 @@ module.exports = {
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
const pathGroupsExcludedImportTypes = new Set(options['pathGroupsExcludedImportTypes'] || ['builtin', 'external', 'object']);
const alphabetize = getAlphabetizeConfig(options);
const sortByLength = getSortByLengthConfig(options);
let ranks;

try {
Expand All @@ -600,13 +665,17 @@ module.exports = {
ImportDeclaration: function handleImports(node) {
if (node.specifiers.length) { // Ignoring unassigned imports
const name = node.source.value;
const specifiers = node.specifiers
const source = node.source
registerNode(
context,
{
node,
value: name,
displayName: name,
type: 'import',
specifiers,
source
},
ranks,
imported,
Expand Down Expand Up @@ -667,10 +736,18 @@ module.exports = {
makeNewlinesBetweenReport(context, imported, newlinesBetweenImports);
}

if (alphabetize.order !== 'ignore' && sortByLength.order !== 'ignore') {
throw new Error('Incorrect configuration of the rule: Can\'t sort by length and alphabet at the same time');
}

if (alphabetize.order !== 'ignore') {
mutateRanksToAlphabetize(imported, alphabetize);
}

if (sortByLength.order !== 'ignore') {
mutateRanksByLength(imported, sortByLength);
}

makeOutOfOrderReport(context, imported);

imported = [];
Expand Down
61 changes: 59 additions & 2 deletions tests/src/rules/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,63 @@ ruleTester.run('order', rule, {
},
],
}),
// Option sort-by-length: {order: 'ignore'}
test({
code: `
import longModule from 'long-module-name';
import short from 'short-module';

import index from './';
`,
options: [{
groups: ['external', 'index'],
'sort-by-length': { order: 'ignore' },
}],
}),
// Option sort-by-length: {order: 'asc'}
test({
code: `
import short from 'short-module';
import longModule from 'long-module-name';

import index from './';
`,
options: [{
groups: ['external', 'index'],
'sort-by-length': { order: 'asc' },
}],
}),
// Option sort-by-length: {order: 'desc'}
test({
code: `
import longModule from 'long-module-name';
import short from 'short-module';

import index from './';
`,
options: [{
groups: ['external', 'index'],
'sort-by-length': { order: 'desc' },
}],
}),
// Sort by length with different import types
test({
code: `
import { foo , bar } from 'module-name/path/to/specific/un-exported/file';
import { export1 as alias } from 'module-name-1';
import { export1, export2 } from 'module-name-2';
import defaultExport from 'module-name-3';
import { export3 } from 'module-4';
import * as name from 'module-5';

import index from './';
import './module';
`,
options: [{
groups: ['external', 'index'],
'sort-by-length': { order: 'desc' },
}],
}),
...flatMap(getTSParsers, parser => [
// Order of the `import ... = require(...)` syntax
test({
Expand Down Expand Up @@ -854,10 +911,10 @@ ruleTester.run('order', rule, {
test({
code:
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n` +
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n`,
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n`,
output:
`/* comment3 */ var fs = require('fs'); /* comment4 */` + `\r\n` +
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n`,
`/* comment0 */ /* comment1 */ var async = require('async'); /* comment2 */` + `\r\n`,
errors: [{
message: '`fs` import should occur before import of `async`',
}],
Expand Down