diff --git a/docs/rules/order.md b/docs/rules/order.md index 7d91efd6f5..a85d3d1827 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -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 diff --git a/src/rules/order.js b/src/rules/order.js index bad382945c..9b0d3624b9 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -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) { @@ -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) ) { @@ -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' ); } @@ -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', @@ -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, }, @@ -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 { @@ -600,6 +665,8 @@ 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, { @@ -607,6 +674,8 @@ module.exports = { value: name, displayName: name, type: 'import', + specifiers, + source }, ranks, imported, @@ -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 = []; diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index 42f99c2db8..e95361db13 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -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({ @@ -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`', }],