diff --git a/CHANGELOG.md b/CHANGELOG.md index ba7f30845..a6143e915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel - support `parseForESLint` from custom parser ([#1435], thanks [@JounQin]) - [`no-extraneous-dependencies`]: Implement support for [bundledDependencies](https://npm.github.io/using-pkgs-docs/package-json/types/bundleddependencies.html) ([#1436], thanks [@schmidsi])) - [`no-unused-modules`]: add flow type support ([#1542], thanks [@rfermann]) +- [`order`]: Adds support for pathGroups to allow ordering by defined patterns ([#795], [#1386], thanks [@Mairu]) ### Fixed - [`default`]: make error message less confusing ([#1470], thanks [@golopot]) @@ -644,6 +645,7 @@ for info on changes for earlier releases. [#1401]: https://github.com/benmosher/eslint-plugin-import/pull/1401 [#1393]: https://github.com/benmosher/eslint-plugin-import/pull/1393 [#1389]: https://github.com/benmosher/eslint-plugin-import/pull/1389 +[#1386]: https://github.com/benmosher/eslint-plugin-import/pull/1386 [#1377]: https://github.com/benmosher/eslint-plugin-import/pull/1377 [#1375]: https://github.com/benmosher/eslint-plugin-import/pull/1375 [#1372]: https://github.com/benmosher/eslint-plugin-import/pull/1372 @@ -788,6 +790,7 @@ for info on changes for earlier releases. [#863]: https://github.com/benmosher/eslint-plugin-import/issues/863 [#842]: https://github.com/benmosher/eslint-plugin-import/issues/842 [#839]: https://github.com/benmosher/eslint-plugin-import/issues/839 +[#795]: https://github.com/benmosher/eslint-plugin-import/issues/795 [#793]: https://github.com/benmosher/eslint-plugin-import/issues/793 [#720]: https://github.com/benmosher/eslint-plugin-import/issues/720 [#717]: https://github.com/benmosher/eslint-plugin-import/issues/717 @@ -1025,3 +1028,4 @@ for info on changes for earlier releases. [@Taranys]: https://github.com/Taranys [@maxmalov]: https://github.com/maxmalov [@marcusdarmstrong]: https://github.com/marcusdarmstrong +[@Mairu]: https://github.com/Mairu diff --git a/docs/rules/order.md b/docs/rules/order.md index d71643048..94c0115e1 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -94,6 +94,32 @@ You can set the options like this: "import/order": ["error", {"groups": ["index", "sibling", "parent", "internal", "external", "builtin"]}] ``` +### `pathGroups: [array of objects]`: + +To be able so group by paths mostly needed with aliases pathGroups can be defined. + +Properties of the objects + +| property | required | type | description | +|----------------|:--------:|--------|---------------| +| pattern | x | string | minimatch pattern for the paths to be in this group (will not be used for builtins or externals) | +| patternOptions | | object | options for minimatch, default: { nocomment: true } | +| group | x | string | one of the allowed groups, the pathGroup will be positioned relative to this group | +| position | | string | defines where around the group the pathGroup will be positioned, can be 'after' or 'before', if not provided pathGroup will be positioned like the group | + +```json +{ + "import/order": ["error", { + "pathGroups": [ + { + "pattern": "~/**", + "group": "external" + } + ] + }] +} +``` + ### `newlines-between: [ignore|always|always-and-inside-groups|never]`: diff --git a/src/rules/order.js b/src/rules/order.js index 920345ff2..9daeb5e8a 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -1,5 +1,6 @@ 'use strict' +import minimatch from 'minimatch' import importType from '../core/importType' import isStaticRequire from '../core/staticRequire' import docsUrl from '../docsUrl' @@ -244,9 +245,29 @@ function makeOutOfOrderReport(context, imported) { // DETECTING +function computePathRank(ranks, pathGroups, path, maxPosition) { + for (let i = 0, l = pathGroups.length; i < l; i++) { + const { pattern, patternOptions, group, position = 1 } = pathGroups[i] + if (minimatch(path, pattern, patternOptions || { nocomment: true })) { + return ranks[group] + (position / maxPosition) + } + } +} + function computeRank(context, ranks, name, type) { - return ranks[importType(name, context)] + - (type === 'import' ? 0 : 100) + const impType = importType(name, context) + let rank + if (impType !== 'builtin' && impType !== 'external') { + rank = computePathRank(ranks.groups, ranks.pathGroups, name, ranks.maxPosition) + } + if (!rank) { + rank = ranks.groups[impType] + } + if (type !== 'import') { + rank += 100 + } + + return rank } function registerNode(context, node, name, type, ranks, imported) { @@ -294,6 +315,49 @@ function convertGroupsToRanks(groups) { }, rankObject) } +function convertPathGroupsForRanks(pathGroups) { + const after = {} + const before = {} + + const transformed = pathGroups.map((pathGroup, index) => { + const { group, position: positionString } = pathGroup + let position = 0 + if (positionString === 'after') { + if (!after[group]) { + after[group] = 1 + } + position = after[group]++ + } else if (positionString === 'before') { + if (!before[group]) { + before[group] = [] + } + before[group].push(index) + } + + return Object.assign({}, pathGroup, { position }) + }) + + let maxPosition = 1 + + Object.keys(before).forEach((group) => { + const groupLength = before[group].length + before[group].forEach((groupIndex, index) => { + transformed[groupIndex].position = -1 * (groupLength - index) + }) + maxPosition = Math.max(maxPosition, groupLength) + }) + + Object.keys(after).forEach((key) => { + const groupNextPosition = after[key] + maxPosition = Math.max(maxPosition, groupNextPosition - 1) + }) + + return { + pathGroups: transformed, + maxPosition: maxPosition > 10 ? Math.pow(10, Math.ceil(Math.log10(maxPosition))) : 10, + } +} + function fixNewLineAfterImport(context, previousImport) { const prevRoot = findRootNode(previousImport.node) const tokensToEndOfLine = takeTokensAfterWhile( @@ -378,6 +442,29 @@ module.exports = { groups: { type: 'array', }, + pathGroups: { + type: 'array', + items: { + type: 'object', + properties: { + pattern: { + type: 'string', + }, + patternOptions: { + type: 'object', + }, + group: { + type: 'string', + enum: types, + }, + position: { + type: 'string', + enum: ['after', 'before'], + }, + }, + required: ['pattern', 'group'], + }, + }, 'newlines-between': { enum: [ 'ignore', @@ -398,7 +485,12 @@ module.exports = { let ranks try { - ranks = convertGroupsToRanks(options.groups || defaultGroups) + const { pathGroups, maxPosition } = convertPathGroupsForRanks(options.pathGroups || []) + ranks = { + groups: convertGroupsToRanks(options.groups || defaultGroups), + pathGroups, + maxPosition, + } } catch (error) { // Malformed configuration return { diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index ff71bbed2..669dc2dd0 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -204,6 +204,79 @@ ruleTester.run('order', rule, { }, ], }), + + // Using pathGroups to customize ordering, position 'after' + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { Input } from '~/components/Input'; + import { Button } from '#/components/Button'; + import { add } from './helper';`, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '#/**', group: 'external', position: 'after' }, + ], + }], + }), + // pathGroup without position means "equal" with group + test({ + code: ` + import fs from 'fs'; + import { Input } from '~/components/Input'; + import async from 'async'; + import { Button } from '#/components/Button'; + import _ from 'lodash'; + import { add } from './helper';`, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external' }, + { pattern: '#/**', group: 'external' }, + ], + }], + }), + // Using pathGroups to customize ordering, position 'before' + test({ + code: ` + import fs from 'fs'; + + import { Input } from '~/components/Input'; + + import { Button } from '#/components/Button'; + + import _ from 'lodash'; + + import { add } from './helper';`, + options: [{ + 'newlines-between': 'always', + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'before' }, + { pattern: '#/**', group: 'external', position: 'before' }, + ], + }], + }), + // Using pathGroups to customize ordering, with patternOptions + test({ + code: ` + import fs from 'fs'; + + import _ from 'lodash'; + + import { Input } from '~/components/Input'; + + import { Button } from '!/components/Button'; + + import { add } from './helper';`, + options: [{ + 'newlines-between': 'always', + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '!/**', patternOptions: { nonegate: true }, group: 'external', position: 'after' }, + ], + }], + }), + // Option: newlines-between: 'always' test({ code: ` @@ -573,7 +646,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], }), - // fix order of multile import + // fix order of multiline import test({ code: ` var async = require('async'); @@ -1396,6 +1469,153 @@ ruleTester.run('order', rule, { '`./local2` import should occur after import of `global4`', ], }), + + // pathGroup with position 'after' + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { add } from './helper'; + import { Input } from '~/components/Input'; + `, + output: ` + import fs from 'fs'; + import _ from 'lodash'; + import { Input } from '~/components/Input'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + ], + }], + errors: [{ + ruleId: 'order', + message: '`~/components/Input` import should occur before import of `./helper`', + }], + }), + // pathGroup without position + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { add } from './helper'; + import { Input } from '~/components/Input'; + import async from 'async'; + `, + output: ` + import fs from 'fs'; + import _ from 'lodash'; + import { Input } from '~/components/Input'; + import async from 'async'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external' }, + ], + }], + errors: [{ + ruleId: 'order', + message: '`./helper` import should occur after import of `async`', + }], + }), + // pathGroup with position 'before' + test({ + code: ` + import fs from 'fs'; + import _ from 'lodash'; + import { add } from './helper'; + import { Input } from '~/components/Input'; + `, + output: ` + import fs from 'fs'; + import { Input } from '~/components/Input'; + import _ from 'lodash'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'before' }, + ], + }], + errors: [{ + ruleId: 'order', + message: '`~/components/Input` import should occur before import of `lodash`', + }], + }), + // multiple pathGroup with different positions for same group, fix for 'after' + test({ + code: ` + import fs from 'fs'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Output } from '~/components/Output'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + import { Export } from '-/components/Export'; + `, + output: ` + import fs from 'fs'; + import { Export } from '-/components/Export'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Output } from '~/components/Output'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '#/**', group: 'external', position: 'after' }, + { pattern: '-/**', group: 'external', position: 'before' }, + { pattern: '$/**', group: 'external', position: 'before' }, + ], + }], + errors: [ + { + ruleId: 'order', + message: '`-/components/Export` import should occur before import of `$/components/Import`', + }, + ], + }), + + // multiple pathGroup with different positions for same group, fix for 'before' + test({ + code: ` + import fs from 'fs'; + import { Export } from '-/components/Export'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + import { Output } from '~/components/Output'; + `, + output: ` + import fs from 'fs'; + import { Export } from '-/components/Export'; + import { Import } from '$/components/Import'; + import _ from 'lodash'; + import { Output } from '~/components/Output'; + import { Input } from '#/components/Input'; + import { add } from './helper'; + `, + options: [{ + pathGroups: [ + { pattern: '~/**', group: 'external', position: 'after' }, + { pattern: '#/**', group: 'external', position: 'after' }, + { pattern: '-/**', group: 'external', position: 'before' }, + { pattern: '$/**', group: 'external', position: 'before' }, + ], + }], + errors: [ + { + ruleId: 'order', + message: '`~/components/Output` import should occur before import of `#/components/Input`', + }, + ], + }), + // reorder fix cannot cross non import or require test(withoutAutofixOutput({ code: ` @@ -1469,7 +1689,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), - // cannot require in case of not assignement require + // cannot require in case of not assignment require test(withoutAutofixOutput({ code: ` var async = require('async'); @@ -1493,7 +1713,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), - // reorder cannot cross variable assignemet (import statement) + // reorder cannot cross variable assignment (import statement) test(withoutAutofixOutput({ code: ` import async from 'async'; @@ -1517,7 +1737,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], })), - // cannot reorder in case of not assignement import + // cannot reorder in case of not assignment import test(withoutAutofixOutput({ code: ` import async from 'async';