From ba167f235cb0627cc650540fe47add0ea4ed496a Mon Sep 17 00:00:00 2001 From: "Xunnamius (Romulus)" Date: Mon, 30 Dec 2024 17:12:49 -0800 Subject: [PATCH] feat: [import/order] collapse excess spacing for aesthetically pleasing imports via `consolidateIslands` --- docs/rules/order.md | 286 +++- src/rules/order.js | 123 +- tests/src/rules/order.js | 2874 +++++++++++++++++++++++++++++++++++--- 3 files changed, 3081 insertions(+), 202 deletions(-) diff --git a/docs/rules/order.md b/docs/rules/order.md index 3dee4b6de..4a52b823e 100644 --- a/docs/rules/order.md +++ b/docs/rules/order.md @@ -108,6 +108,7 @@ This rule supports the following options (none of which are required): - [`warnOnUnassignedImports`][5] - [`sortTypesGroup`][7] - [`newlines-between-types`][27] + - [`consolidateIslands`][25] --- @@ -184,7 +185,7 @@ Sometimes [the predefined groups][18] are not fine-grained enough, especially wh `pathGroups` defines one or more [`PathGroup`][13]s relative to a predefined group. Imports are associated with a [`PathGroup`][13] based on path matching against the import specifier (using [minimatch][14]). -> \[!IMPORTANT] +> [!IMPORTANT] > > Note that, by default, imports grouped as `"builtin"`, `"external"`, or `"object"` will not be considered for further `pathGroups` matching unless they are removed from [`pathGroupsExcludedImportTypes`][9]. @@ -224,7 +225,7 @@ Default: `["builtin", "external", "object"]` By default, imports in certain [groups][18] are excluded from being matched against [`pathGroups`][8] to prevent overeager sorting. Use `pathGroupsExcludedImportTypes` to modify which groups are excluded. -> \[!TIP] +> [!TIP] > > If using imports with custom specifier aliases (e.g. > you're using `eslint-import-resolver-alias`, `paths` in `tsconfig.json`, etc) that [end up @@ -254,7 +255,7 @@ Use `pathGroupsExcludedImportTypes` to modify which groups are excluded. Valid values: `boolean` \ Default: `true` -> \[!CAUTION] +> [!CAUTION] > > Currently, `distinctGroup` defaults to `true`. However, in a later update, the > default will change to `false`. @@ -371,7 +372,7 @@ Default: `{ order: "ignore", orderImportKind: "ignore", caseInsensitive: false } Determine the sort order of imports within each [predefined group][18] or [`PathGroup`][8] alphabetically based on specifier. -> \[!NOTE] +> [!NOTE] > > Imports will be alphabetized based on their _specifiers_, not by their > identifiers. For example, `const a = require('z');` will come _after_ `const z = require('a');` when `alphabetize` is set to `{ order: "asc" }`. @@ -502,7 +503,7 @@ Default: `false` Warn when "unassigned" imports are out of order. Unassigned imports are imports with no corresponding identifiers (e.g. `import './my/thing.js'` or `require('./side-effects.js')`). -> \[!NOTE] +> [!NOTE] > > These warnings are not fixable with `--fix` since unassigned imports might be used for their [side-effects][31], > and changing the order of such imports cannot be done safely. @@ -540,7 +541,7 @@ import './styles.css'; Valid values: `boolean` \ Default: `false` -> \[!NOTE] +> [!NOTE] > > This setting is only meaningful when `"type"` is included in [`groups`][18]. @@ -598,7 +599,7 @@ The same example will pass. Valid values: `"ignore" | "always" | "always-and-inside-groups" | "never"` \ Default: the value of [`newlines-between`][20] -> \[!NOTE] +> [!NOTE] > > This setting is only meaningful when [`sortTypesGroup`][7] is enabled. @@ -656,9 +657,9 @@ The same example will pass. Note the new line after `import type E from './';` but before `import a from "fs";`. This new line separates the type-only imports from the normal imports. Its existence is governed by [`newlines-between-types`][27] and _not `newlines-between`_. -> \[!IMPORTANT] +> [!IMPORTANT] > -> In certain situations, `consolidateIslands: true` will take precedence over `newlines-between-types: "never"`, if used, when it comes to the new line separating type-only imports from normal imports. +> In certain situations, [`consolidateIslands: true`][25] will take precedence over `newlines-between-types: "never"`, if used, when it comes to the new line separating type-only imports from normal imports. The next example will pass even though there's a new line preceding the normal import and [`newlines-between`][20] is set to `"never"`: @@ -722,6 +723,272 @@ import d from "./bar.js"; import e from "./"; ``` +### `consolidateIslands` + +Valid values: `"inside-groups" | "never"` \ +Default: `"never"` + +> [!NOTE] +> +> This setting is only meaningful when [`newlines-between`][20] and/or [`newlines-between-types`][27] is set to `"always-and-inside-groups"`. + +When set to `"inside-groups"`, this ensures imports spanning multiple lines are separated from other imports with a new line while single-line imports are grouped together (and the space between them consolidated) if they belong to the same [group][18] or [`pathGroups`][8]. + +> [!IMPORTANT] +> +> When all of the following are true: +> +> - [`sortTypesGroup`][7] is set to `true` +> - `consolidateIslands` is set to `"inside-groups"` +> - [`newlines-between`][20] is set to `"always-and-inside-groups"` when [`newlines-between-types`][27] is set to `"never"` (or vice-versa) +> +> Then [`newlines-between`][20]/[`newlines-between-types`][27] will yield to +> `consolidateIslands` and allow new lines to separate multi-line imports +> regardless of the `"never"` setting. +> +> This configuration is useful, for instance, to keep single-line type-only +> imports stacked tightly together at the bottom of your import block to +> preserve space while still logically organizing normal imports for quick and +> pleasant reference. + +#### Example + +Given the following settings: + +```jsonc +{ + "import/order": ["error", { + "newlines-between": "always-and-inside-groups", + "consolidateIslands": "inside-groups" + }] +} +``` + +This will fail the rule check: + +```ts +var fs = require('fs'); +var path = require('path'); +var { util1, util2, util3 } = require('util'); +var async = require('async'); +var relParent1 = require('../foo'); +var { + relParent21, + relParent22, + relParent23, + relParent24, +} = require('../'); +var relParent3 = require('../bar'); +var { sibling1, + sibling2, sibling3 } = require('./foo'); +var sibling2 = require('./bar'); +var sibling3 = require('./foobar'); +``` + +While this will succeed (and is what `--fix` would yield): + +```ts +var fs = require('fs'); +var path = require('path'); +var { util1, util2, util3 } = require('util'); + +var async = require('async'); + +var relParent1 = require('../foo'); + +var { + relParent21, + relParent22, + relParent23, + relParent24, +} = require('../'); + +var relParent3 = require('../bar'); + +var { sibling1, + sibling2, sibling3 } = require('./foo'); + +var sibling2 = require('./bar'); +var sibling3 = require('./foobar'); +``` + +Note the intragroup "islands" of grouped single-line imports, as well as multi-line imports, are surrounded by new lines. At the same time, note the typical new lines separating different groups are still maintained thanks to [`newlines-between`][20]. + +The same holds true for the next example; when given the following settings: + +```jsonc +{ + "import/order": ["error", { + "alphabetize": { "order": "asc" }, + "groups": ["external", "internal", "index", "type"], + "pathGroups": [ + { + "pattern": "dirA/**", + "group": "internal", + "position": "after" + }, + { + "pattern": "dirB/**", + "group": "internal", + "position": "before" + }, + { + "pattern": "dirC/**", + "group": "internal" + } + ], + "newlines-between": "always-and-inside-groups", + "newlines-between-types": "never", + "pathGroupsExcludedImportTypes": [], + "sortTypesGroup": true, + "consolidateIslands": "inside-groups" + }] +} +``` + +> [!IMPORTANT] +> +> **Pay special attention to the value of [`pathGroupsExcludedImportTypes`][9]** in this example's settings. +> Without it, the successful example below would fail. +> This is because the imports with specifiers starting with "dirA/", "dirB/", and "dirC/" are all [considered part of the `"external"` group](#how-imports-are-grouped), and imports in that group are excluded from [`pathGroups`][8] matching by default. +> +> The fix is to remove `"external"` (and, in this example, the others) from [`pathGroupsExcludedImportTypes`][9]. + +This will fail the rule check: + +```ts +import c from 'Bar'; +import d from 'bar'; +import { + aa, + bb, + cc, + dd, + ee, + ff, + gg +} from 'baz'; +import { + hh, + ii, + jj, + kk, + ll, + mm, + nn +} from 'fizz'; +import a from 'foo'; +import b from 'dirA/bar'; +import index from './'; +import type { AA, + BB, CC } from 'abc'; +import type { Z } from 'fizz'; +import type { + A, + B +} from 'foo'; +import type { C2 } from 'dirB/Bar'; +import type { + D2, + X2, + Y2 +} from 'dirB/bar'; +import type { E2 } from 'dirB/baz'; +import type { C3 } from 'dirC/Bar'; +import type { + D3, + X3, + Y3 +} from 'dirC/bar'; +import type { E3 } from 'dirC/baz'; +import type { F3 } from 'dirC/caz'; +import type { C1 } from 'dirA/Bar'; +import type { + D1, + X1, + Y1 +} from 'dirA/bar'; +import type { E1 } from 'dirA/baz'; +import type { F } from './index.js'; +import type { G } from './aaa.js'; +import type { H } from './bbb'; +``` + +While this will succeed (and is what `--fix` would yield): + +```ts +import c from 'Bar'; +import d from 'bar'; + +import { + aa, + bb, + cc, + dd, + ee, + ff, + gg +} from 'baz'; + +import { + hh, + ii, + jj, + kk, + ll, + mm, + nn +} from 'fizz'; + +import a from 'foo'; + +import b from 'dirA/bar'; + +import index from './'; + +import type { AA, + BB, CC } from 'abc'; + +import type { Z } from 'fizz'; + +import type { + A, + B +} from 'foo'; + +import type { C2 } from 'dirB/Bar'; + +import type { + D2, + X2, + Y2 +} from 'dirB/bar'; + +import type { E2 } from 'dirB/baz'; +import type { C3 } from 'dirC/Bar'; + +import type { + D3, + X3, + Y3 +} from 'dirC/bar'; + +import type { E3 } from 'dirC/baz'; +import type { F3 } from 'dirC/caz'; +import type { C1 } from 'dirA/Bar'; + +import type { + D1, + X1, + Y1 +} from 'dirA/bar'; + +import type { E1 } from 'dirA/baz'; +import type { F } from './index.js'; +import type { G } from './aaa.js'; +import type { H } from './bbb'; +``` + ## Related - [`import/external-module-folders`][29] @@ -747,6 +1014,7 @@ import e from "./"; [21]: https://eslint.org/docs/latest/rules/no-multiple-empty-lines [22]: https://prettier.io [23]: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#type-modifiers-on-import-names +[25]: #consolidateislands [27]: #newlines-between-types [28]: ../../README.md#importinternal-regex [29]: ../../README.md#importexternal-module-folders diff --git a/src/rules/order.js b/src/rules/order.js index 17e8735e1..b34efd027 100644 --- a/src/rules/order.js +++ b/src/rules/order.js @@ -555,7 +555,17 @@ function computeRank(context, ranks, importEntry, excludedImportTypes, isSorting function registerNode(context, importEntry, ranks, imported, excludedImportTypes, isSortingTypesGroup) { const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesGroup); if (rank !== -1) { - imported.push({ ...importEntry, rank }); + let importNode = importEntry.node; + + if (importEntry.type === 'require' && importNode.parent.parent.type === 'VariableDeclaration') { + importNode = importNode.parent.parent; + } + + imported.push({ + ...importEntry, + rank, + isMultiline: importNode.loc.end.line !== importNode.loc.start.line, + }); } } @@ -681,7 +691,7 @@ function removeNewLineAfterImport(context, currentImport, previousImport) { return undefined; } -function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesGroup) { +function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports_, newlinesBetweenTypeOnlyImports_, distinctGroup, isSortingTypesGroup, isConsolidatingSpaceBetweenImports) { const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => { const linesBetweenImports = getSourceCode(context).lines.slice( previousImport.node.loc.end.line, @@ -707,34 +717,61 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, ne const isTypeOnlyImport = currentImport.node.importKind === 'type'; const isPreviousImportTypeOnlyImport = previousImport.node.importKind === 'type'; - const isNormalImportFollowingTypeOnlyImportAndRelevant = !isTypeOnlyImport && isPreviousImportTypeOnlyImport && isSortingTypesGroup; + const isNormalImportNextToTypeOnlyImportAndRelevant = isTypeOnlyImport !== isPreviousImportTypeOnlyImport && isSortingTypesGroup; const isTypeOnlyImportAndRelevant = isTypeOnlyImport && isSortingTypesGroup; - const isNotIgnored = isTypeOnlyImportAndRelevant + // In the special case where newlinesBetweenImports and consolidateIslands + // want the opposite thing, consolidateIslands wins + const newlinesBetweenImports = isSortingTypesGroup + && isConsolidatingSpaceBetweenImports + && (previousImport.isMultiline || currentImport.isMultiline) + && newlinesBetweenImports_ === 'never' + ? 'always-and-inside-groups' + : newlinesBetweenImports_; + + // In the special case where newlinesBetweenTypeOnlyImports and + // consolidateIslands want the opposite thing, consolidateIslands wins + const newlinesBetweenTypeOnlyImports = isSortingTypesGroup + && isConsolidatingSpaceBetweenImports + && (isNormalImportNextToTypeOnlyImportAndRelevant + || previousImport.isMultiline + || currentImport.isMultiline) + && newlinesBetweenTypeOnlyImports_ === 'never' + ? 'always-and-inside-groups' + : newlinesBetweenTypeOnlyImports_; + + const isNotIgnored = isTypeOnlyImportAndRelevant && newlinesBetweenTypeOnlyImports !== 'ignore' || !isTypeOnlyImportAndRelevant && newlinesBetweenImports !== 'ignore'; if (isNotIgnored) { - const shouldAssertNewlineBetweenGroups = (isTypeOnlyImportAndRelevant || isNormalImportFollowingTypeOnlyImportAndRelevant) + const shouldAssertNewlineBetweenGroups = (isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant) && (newlinesBetweenTypeOnlyImports === 'always' || newlinesBetweenTypeOnlyImports === 'always-and-inside-groups') - || !isTypeOnlyImportAndRelevant && !isNormalImportFollowingTypeOnlyImportAndRelevant + || !isTypeOnlyImportAndRelevant && !isNormalImportNextToTypeOnlyImportAndRelevant && (newlinesBetweenImports === 'always' || newlinesBetweenImports === 'always-and-inside-groups'); - const shouldAssertNoNewlineWithinGroup = (isTypeOnlyImportAndRelevant || isNormalImportFollowingTypeOnlyImportAndRelevant) + const shouldAssertNoNewlineWithinGroup = (isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant) && newlinesBetweenTypeOnlyImports !== 'always-and-inside-groups' - || !isTypeOnlyImportAndRelevant && !isNormalImportFollowingTypeOnlyImportAndRelevant + || !isTypeOnlyImportAndRelevant && !isNormalImportNextToTypeOnlyImportAndRelevant && newlinesBetweenImports !== 'always-and-inside-groups'; - const shouldAssertNoNewlineBetweenGroup = !isSortingTypesGroup - || !isNormalImportFollowingTypeOnlyImportAndRelevant + const shouldAssertNoNewlineBetweenGroup = !isSortingTypesGroup + || !isNormalImportNextToTypeOnlyImportAndRelevant || newlinesBetweenTypeOnlyImports === 'never'; + const isTheNewlineBetweenImportsInTheSameGroup = distinctGroup && currentImport.rank === previousImport.rank + || !distinctGroup && !isStartOfDistinctGroup; + + // Let's try to cut down on linting errors sent to the user + let alreadyReported = false; + if (shouldAssertNewlineBetweenGroups) { if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) { if (distinctGroup || !distinctGroup && isStartOfDistinctGroup) { + alreadyReported = true; context.report({ node: previousImport.node, message: 'There should be at least one empty line between import groups', @@ -742,10 +779,8 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, ne }); } } else if (emptyLinesBetween > 0 && shouldAssertNoNewlineWithinGroup) { - if ( - distinctGroup && currentImport.rank === previousImport.rank - || !distinctGroup && !isStartOfDistinctGroup - ) { + if (isTheNewlineBetweenImportsInTheSameGroup) { + alreadyReported = true; context.report({ node: previousImport.node, message: 'There should be no empty line within import group', @@ -754,12 +789,41 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, ne } } } else if (emptyLinesBetween > 0 && shouldAssertNoNewlineBetweenGroup) { + alreadyReported = true; context.report({ node: previousImport.node, message: 'There should be no empty line between import groups', fix: removeNewLineAfterImport(context, currentImport, previousImport), }); } + + if (!alreadyReported && isConsolidatingSpaceBetweenImports) { + if (emptyLinesBetween === 0 && currentImport.isMultiline) { + context.report({ + node: previousImport.node, + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + fix: fixNewLineAfterImport(context, previousImport), + }); + } else if (emptyLinesBetween === 0 && previousImport.isMultiline) { + context.report({ + node: previousImport.node, + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + fix: fixNewLineAfterImport(context, previousImport), + }); + } else if ( + emptyLinesBetween > 0 + && !previousImport.isMultiline + && !currentImport.isMultiline + && isTheNewlineBetweenImportsInTheSameGroup + ) { + context.report({ + node: previousImport.node, + message: + 'There should be no empty lines between this single-line import and the single-line import that follows it', + fix: removeNewLineAfterImport(context, currentImport, previousImport), + }); + } + } } previousImport = currentImport; @@ -842,6 +906,12 @@ module.exports = { 'never', ], }, + consolidateIslands: { + enum: [ + 'inside-groups', + 'never', + ], + }, sortTypesGroup: { type: 'boolean', default: false, @@ -901,6 +971,18 @@ module.exports = { }, required: ['sortTypesGroup'], }, + consolidateIslands: { + anyOf: [{ + properties: { + 'newlines-between': { enum: ['always-and-inside-groups'] }, + }, + required: ['newlines-between'], + }, { + properties: { + 'newlines-between-types': { enum: ['always-and-inside-groups'] }, + }, + required: ['newlines-between-types'], + }] }, }, }, ], @@ -912,6 +994,7 @@ module.exports = { const newlinesBetweenTypeOnlyImports = options['newlines-between-types'] || newlinesBetweenImports; const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']); const sortTypesGroup = options.sortTypesGroup; + const consolidateIslands = options.consolidateIslands || 'never'; const named = { types: 'mixed', @@ -1174,7 +1257,17 @@ module.exports = { 'Program:exit'() { importMap.forEach((imported) => { if (newlinesBetweenImports !== 'ignore' || newlinesBetweenTypeOnlyImports !== 'ignore') { - makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesGroup); + makeNewlinesBetweenReport( + context, + imported, + newlinesBetweenImports, + newlinesBetweenTypeOnlyImports, + distinctGroup, + isSortingTypesGroup, + consolidateIslands === 'inside-groups' + && (newlinesBetweenImports === 'always-and-inside-groups' + || newlinesBetweenTypeOnlyImports === 'always-and-inside-groups'), + ); } if (alphabetize.order !== 'ignore') { diff --git a/tests/src/rules/order.js b/tests/src/rules/order.js index bde928f42..fa592c9b1 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -530,7 +530,7 @@ ruleTester.run('order', rule, { }, ], }), - // Option newlines-between: 'always' with multiline imports #1 + // Option newlines-between: 'always' with multi-line imports #1 test({ code: ` import path from 'path'; @@ -546,7 +546,7 @@ ruleTester.run('order', rule, { `, options: [{ 'newlines-between': 'always' }], }), - // Option newlines-between: 'always' with multiline imports #2 + // Option newlines-between: 'always' with multi-line imports #2 test({ code: ` import path from 'path'; @@ -557,7 +557,7 @@ ruleTester.run('order', rule, { `, options: [{ 'newlines-between': 'always' }], }), - // Option newlines-between: 'always' with multiline imports #3 + // Option newlines-between: 'always' with multi-line imports #3 test({ code: ` import foo @@ -671,6 +671,33 @@ ruleTester.run('order', rule, { }, ], }), + // Option newlines-between: 'always-and-inside-groups' and consolidateIslands: true + test({ + code: ` + var fs = require('fs'); + var path = require('path'); + var util = require('util'); + + var async = require('async'); + + var relParent1 = require('../foo'); + + var { + relParent2 } = require('../'); + + var relParent3 = require('../bar'); + + var sibling = require('./foo'); + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + consolidateIslands: 'inside-groups', + }, + ], + }), // Option alphabetize: {order: 'ignore'} test({ code: ` @@ -1343,19 +1370,19 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], }), - // fix order with multilines comments at the end and start of line + // fix order with multi-lines comments at the end and start of line test({ code: ` - /* multiline1 - comment1 */ var async = require('async'); /* multiline2 - comment2 */ var fs = require('fs'); /* multiline3 + /* multi-line1 + comment1 */ var async = require('async'); /* multi-line2 + comment2 */ var fs = require('fs'); /* multi-line3 comment3 */ `, output: ` - /* multiline1 + /* multi-line1 comment1 */ var fs = require('fs');` + ' ' + ` - var async = require('async'); /* multiline2 - comment2 *//* multiline3 + var async = require('async'); /* multi-line2 + comment2 *//* multi-line3 comment3 */ `, errors: [{ @@ -1376,7 +1403,7 @@ ruleTester.run('order', rule, { message: '`fs` import should occur before import of `async`', }], }), - // fix order of multiline import + // fix order of multi-line import test({ code: ` var async = require('async'); @@ -1901,10 +1928,10 @@ ruleTester.run('order', rule, { }, ], }), - // Cannot fix newlines-between with multiline comment after + // Cannot fix newlines-between with multi-line comment after test(withoutAutofixOutput({ code: ` - var fs = require('fs'); /* multiline + var fs = require('fs'); /* multi-line comment */ var index = require('./'); @@ -2952,7 +2979,7 @@ ruleTester.run('order', rule, { message: '`A.C` export should occur after export of `A.B`', }], }), - // multiline named specifiers & trailing commas + // multi-line named specifiers & trailing commas test({ code: ` const { @@ -3032,6 +3059,156 @@ ruleTester.run('order', rule, { }], }), ], + // Option newlines-between: 'always-and-inside-groups' and consolidateIslands: true + test({ + code: ` + var fs = require('fs'); + var path = require('path'); + var { util1, util2, util3 } = require('util'); + var async = require('async'); + var relParent1 = require('../foo'); + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + var relParent3 = require('../bar'); + var { sibling1, + sibling2, sibling3 } = require('./foo'); + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + output: ` + var fs = require('fs'); + var path = require('path'); + var { util1, util2, util3 } = require('util'); + + var async = require('async'); + + var relParent1 = require('../foo'); + + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + + var relParent3 = require('../bar'); + + var { sibling1, + sibling2, sibling3 } = require('./foo'); + + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: 'There should be at least one empty line between import groups', + line: 4, + }, + { + message: 'There should be at least one empty line between import groups', + line: 5, + }, + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 6, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 12, + }, + { + message: 'There should be at least one empty line between import groups', + line: 13, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 15, + }, + ], + }), + test({ + code: ` + var fs = require('fs'); + + var path = require('path'); + + var { util1, util2, util3 } = require('util'); + + var async = require('async'); + + var relParent1 = require('../foo'); + + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + + var relParent3 = require('../bar'); + + var { sibling1, + sibling2, sibling3 } = require('./foo'); + + var sibling2 = require('./bar'); + + var sibling3 = require('./foobar'); + `, + output: ` + var fs = require('fs'); + var path = require('path'); + var { util1, util2, util3 } = require('util'); + + var async = require('async'); + + var relParent1 = require('../foo'); + + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + + var relParent3 = require('../bar'); + + var { sibling1, + sibling2, sibling3 } = require('./foo'); + + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: 'There should be no empty lines between this single-line import and the single-line import that follows it', + line: 2, + }, + { + message: 'There should be no empty lines between this single-line import and the single-line import that follows it', + line: 4, + }, + { + message: 'There should be no empty lines between this single-line import and the single-line import that follows it', + line: 24, + }, + ], + }), ].filter(Boolean), }); @@ -3767,261 +3944,2289 @@ context('TypeScript', function () { }, ], }), - ), - invalid: [].concat( - // Option alphabetize: {order: 'asc'} + // Option: sortTypesGroup: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'inside-groups' test({ code: ` - import b from 'bar'; import c from 'Bar'; - import type { C } from 'Bar'; - import a from 'foo'; - import type { A } from 'foo'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; - import index from './'; - `, - output: ` - import c from 'Bar'; - import type { C } from 'Bar'; - import b from 'bar'; import a from 'foo'; - import type { A } from 'foo'; + + import b from 'dirA/bar'; import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + import type { H } from './bbb'; `, ...parserConfig, options: [ { - groups: ['external', 'index'], alphabetize: { order: 'asc' }, - }, - ], - errors: [ - { - message: semver.satisfies(eslintPkg.version, '< 3') - ? '`bar` import should occur after type import of `Bar`' - : /(`bar` import should occur after type import of `Bar`)|(`Bar` type import should occur before import of `bar`)/, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'always-and-inside-groups', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', }, ], }), - // Option alphabetize: {order: 'desc'} + // Option: sortTypesGroup: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'never' (default) test({ code: ` - import a from 'foo'; - import type { A } from 'foo'; import c from 'Bar'; - import type { C } from 'Bar'; - import b from 'bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; - import index from './'; - `, - output: ` import a from 'foo'; - import type { A } from 'foo'; - import b from 'bar'; - import c from 'Bar'; - import type { C } from 'Bar'; + + import b from 'dirA/bar'; import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + import type { H } from './bbb'; `, ...parserConfig, options: [ { - groups: ['external', 'index'], - alphabetize: { order: 'desc' }, - }, - ], - errors: [ - { - message: semver.satisfies(eslintPkg.version, '< 3') - ? '`bar` import should occur before import of `Bar`' - : /(`bar` import should occur before import of `Bar`)|(`Bar` import should occur after import of `bar`)/, + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'always-and-inside-groups', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'never', }, ], }), - // Option alphabetize: {order: 'asc'} with type group + // Ensure consolidateIslands: 'inside-groups', newlines-between: 'always-and-inside-groups', and newlines-between-types: 'never' do not fight for dominance test({ code: ` - import b from 'bar'; - import c from 'Bar'; - import a from 'foo'; - - import index from './'; - - import type { A } from 'foo'; - import type { C } from 'Bar'; - `, - output: ` - import c from 'Bar'; - import b from 'bar'; - import a from 'foo'; - - import index from './'; - - import type { C } from 'Bar'; - import type { A } from 'foo'; + import makeVanillaYargs from 'yargs/yargs'; + + import { createDebugLogger } from 'multiverse+rejoinder'; + + import { globalDebuggerNamespace } from 'rootverse+bfe:src/constant.ts'; + import { ErrorMessage, type KeyValueEntry } from 'rootverse+bfe:src/error.ts'; + + import { + $artificiallyInvoked, + $canonical, + $exists, + $genesis + } from 'rootverse+bfe:src/symbols.ts'; + + import type { + Entries, + LiteralUnion, + OmitIndexSignature, + Promisable, + StringKeyOf + } from 'type-fest'; `, ...parserConfig, options: [ { - groups: ['external', 'index', 'type'], - alphabetize: { order: 'asc' }, - }, + alphabetize: { + order: 'asc', + orderImportKind: 'asc', + caseInsensitive: true, + }, + named: { + enabled: true, + types: 'types-last', + }, + groups: [ + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + ['object', 'type'], + ], + pathGroups: [ + { + pattern: 'multiverse{*,*/**}', + group: 'external', + position: 'after', + }, + { + pattern: 'rootverse{*,*/**}', + group: 'external', + position: 'after', + }, + { + pattern: 'universe{*,*/**}', + group: 'external', + position: 'after', + }, + ], + distinctGroup: true, + pathGroupsExcludedImportTypes: ['builtin', 'object'], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'never', + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + }), + // Ensure consolidateIslands: 'inside-groups', newlines-between: 'never', and newlines-between-types: 'always-and-inside-groups' do not fight for dominance + test({ + code: ` + import makeVanillaYargs from 'yargs/yargs'; + import { createDebugLogger } from 'multiverse+rejoinder'; + import { globalDebuggerNamespace } from 'rootverse+bfe:src/constant.ts'; + import { ErrorMessage, type KeyValueEntry } from 'rootverse+bfe:src/error.ts'; + import { $artificiallyInvoked } from 'rootverse+bfe:src/symbols.ts'; + + import type { + Entries, + LiteralUnion, + OmitIndexSignature, + Promisable, + StringKeyOf + } from 'type-fest'; + `, + ...parserConfig, + options: [ + { + alphabetize: { + order: 'asc', + orderImportKind: 'asc', + caseInsensitive: true, + }, + named: { + enabled: true, + types: 'types-last', + }, + groups: [ + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + ['object', 'type'], + ], + pathGroups: [ + { + pattern: 'multiverse{*,*/**}', + group: 'external', + position: 'after', + }, + { + pattern: 'rootverse{*,*/**}', + group: 'external', + position: 'after', + }, + { + pattern: 'universe{*,*/**}', + group: 'external', + position: 'after', + }, + ], + distinctGroup: true, + pathGroupsExcludedImportTypes: ['builtin', 'object'], + 'newlines-between': 'never', + 'newlines-between-types': 'always-and-inside-groups', + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + }), + test({ + code: ` + import c from 'Bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + import b from 'dirA/bar'; + import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'never', + 'newlines-between-types': 'always-and-inside-groups', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + }), + test({ + code: ` + import assert from 'assert'; + import { isNativeError } from 'util/types'; + + import { runNoRejectOnBadExit } from '@-xun/run'; + import { TrialError } from 'named-app-errors'; + import { resolve as resolverLibrary } from 'resolve.exports'; + + import { ${supportsImportTypeSpecifiers ? 'toAbsolutePath, type AbsolutePath' : 'AbsolutePath, toAbsolutePath'} } from 'rootverse+project-utils:src/fs.ts'; + + import type { PackageJson } from 'type-fest'; + // Some comment about remembering to do something + import type { XPackageJson } from 'rootverse:src/assets/config/_package.json.ts'; + `, + ...parserConfig, + options: [ + { + alphabetize: { + order: 'asc', + orderImportKind: 'asc', + caseInsensitive: true, + }, + named: { + enabled: true, + types: 'types-last', + }, + groups: [ + 'builtin', + 'external', + 'internal', + ['parent', 'sibling', 'index'], + ['object', 'type'], + ], + pathGroups: [ + { + pattern: 'rootverse{*,*/**}', + group: 'external', + position: 'after', + }, + ], + distinctGroup: true, + pathGroupsExcludedImportTypes: ['builtin', 'object'], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'never', + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + }), + + // Documentation passing example #1 for newlines-between + test({ + code: ` + import fs from 'fs'; + import path from 'path'; + + import sibling from './foo'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + 'newlines-between': 'always', + }, + ], + }), + // Documentation passing example #2 for newlines-between + test({ + code: ` + import fs from 'fs'; + + import path from 'path'; + + import sibling from './foo'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + }, + ], + }), + // Documentation passing example #3 for newlines-between + test({ + code: ` + import fs from 'fs'; + import path from 'path'; + import sibling from './foo'; + import index from './'; + `, + ...parserConfig, + options: [ + { + 'newlines-between': 'never', + }, + ], + }), + // Documentation passing example #1 for alphabetize + test({ + code: ` + import blist2 from 'blist'; + import blist from 'BList'; + import * as classnames from 'classnames'; + import aTypes from 'prop-types'; + import React, { PureComponent } from 'react'; + import { compose, apply } from 'xcompose'; + `, + ...parserConfig, + options: [ + { + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + }), + // (not an example, but we also test caseInsensitive: false for completeness) + test({ + code: ` + import blist from 'BList'; + import blist2 from 'blist'; + import * as classnames from 'classnames'; + import aTypes from 'prop-types'; + import React, { PureComponent } from 'react'; + import { compose, apply } from 'xcompose'; + `, + ...parserConfig, + options: [ + { + alphabetize: { + order: 'asc', + caseInsensitive: false, + }, + }, + ], + }), + // Documentation passing example #1 for named + test({ + code: ` + import { apply, compose } from 'xcompose'; + `, + ...parserConfig, + options: [ + { + named: true, + alphabetize: { + order: 'asc', + }, + }, + ], + }), + // Documentation passing example #1 for warnOnUnassignedImports + test({ + code: ` + import fs from 'fs'; + import path from 'path'; + import './styles.css'; + `, + ...parserConfig, + options: [ + { + warnOnUnassignedImports: true, + }, + ], + }), + // Documentation passing example #1 for sortTypesGroup + test({ + code: ` + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; + `, + ...parserConfig, + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + alphabetize: { order: 'asc' }, + sortTypesGroup: true, + }, + ], + }), + // (not an example, but we also test the reverse for completeness) + test({ + code: ` + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; + + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + `, + ...parserConfig, + options: [ + { + groups: ['builtin', 'parent', 'sibling', 'index', 'type'], + sortTypesGroup: true, + }, + ], + }), + // Documentation passing example #1 for newlines-between-types + test({ + code: ` + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + + import c from "../foo.js"; + + import d from "./bar.js"; + + import e from "./"; + `, + ...parserConfig, + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + sortTypesGroup: true, + 'newlines-between': 'always', + 'newlines-between-types': 'ignore', + }, + ], + }), + // (not an example, but we also test the reverse for completeness) + test({ + code: ` + import a from "fs"; + import b from "path"; + + import c from "../foo.js"; + + import d from "./bar.js"; + + import e from "./"; + + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + `, + ...parserConfig, + options: [ + { + groups: ['builtin', 'parent', 'sibling', 'index', 'type'], + sortTypesGroup: true, + 'newlines-between': 'always', + 'newlines-between-types': 'ignore', + }, + ], + }), + // Documentation passing example #2 for newlines-between-types + test({ + code: ` + import type A from "fs"; + import type B from "path"; + + import type C from "../foo.js"; + + import type D from "./bar.js"; + + import type E from './'; + + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; + `, + ...parserConfig, + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + sortTypesGroup: true, + 'newlines-between': 'never', + 'newlines-between-types': 'always', + }, + ], + }), + // (not an example, but we also test the reverse for completeness) + test({ + code: ` + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; + + import type A from "fs"; + import type B from "path"; + + import type C from "../foo.js"; + + import type D from "./bar.js"; + + import type E from './'; + `, + ...parserConfig, + options: [ + { + groups: ['builtin', 'parent', 'sibling', 'index', 'type'], + sortTypesGroup: true, + 'newlines-between': 'never', + 'newlines-between-types': 'always', + }, + ], + }), + // Documentation passing example #1 for consolidateIslands + test({ + code: ` + var fs = require('fs'); + var path = require('path'); + var { util1, util2, util3 } = require('util'); + + var async = require('async'); + + var relParent1 = require('../foo'); + + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + + var relParent3 = require('../bar'); + + var { sibling1, + sibling2, sibling3 } = require('./foo'); + + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + ...parserConfig, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + consolidateIslands: 'inside-groups', + }, + ], + }), + // Documentation passing example #2 for consolidateIslands + test({ + code: ` + import c from 'Bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + import type { F } from './index.js'; + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'never', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + }), + // (not an example, but we also test the reverse for completeness) + test({ + code: ` + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + import type { F } from './index.js'; + import type { G } from './aaa.js'; + import type { H } from './bbb'; + + import c from 'Bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['type', 'external', 'internal', 'index'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'never', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + }), + ), + invalid: [].concat( + // Option alphabetize: {order: 'asc'} + test({ + code: ` + import b from 'bar'; + import c from 'Bar'; + import type { C } from 'Bar'; + import a from 'foo'; + import type { A } from 'foo'; + + import index from './'; + `, + output: ` + import c from 'Bar'; + import type { C } from 'Bar'; + import b from 'bar'; + import a from 'foo'; + import type { A } from 'foo'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + groups: ['external', 'index'], + alphabetize: { order: 'asc' }, + }, + ], + errors: [ + { + message: semver.satisfies(eslintPkg.version, '< 3') + ? '`bar` import should occur after type import of `Bar`' + : /(`bar` import should occur after type import of `Bar`)|(`Bar` type import should occur before import of `bar`)/, + }, + ], + }), + // Option alphabetize: {order: 'desc'} + test({ + code: ` + import a from 'foo'; + import type { A } from 'foo'; + import c from 'Bar'; + import type { C } from 'Bar'; + import b from 'bar'; + + import index from './'; + `, + output: ` + import a from 'foo'; + import type { A } from 'foo'; + import b from 'bar'; + import c from 'Bar'; + import type { C } from 'Bar'; + + import index from './'; + `, + ...parserConfig, + options: [ + { + groups: ['external', 'index'], + alphabetize: { order: 'desc' }, + }, + ], + errors: [ + { + message: semver.satisfies(eslintPkg.version, '< 3') + ? '`bar` import should occur before import of `Bar`' + : /(`bar` import should occur before import of `Bar`)|(`Bar` import should occur after import of `bar`)/, + }, + ], + }), + // Option alphabetize: {order: 'asc'} with type group + test({ + code: ` + import b from 'bar'; + import c from 'Bar'; + import a from 'foo'; + + import index from './'; + + import type { A } from 'foo'; + import type { C } from 'Bar'; + `, + output: ` + import c from 'Bar'; + import b from 'bar'; + import a from 'foo'; + + import index from './'; + + import type { C } from 'Bar'; + import type { A } from 'foo'; + `, + ...parserConfig, + options: [ + { + groups: ['external', 'index', 'type'], + alphabetize: { order: 'asc' }, + }, + ], + errors: semver.satisfies(eslintPkg.version, '< 3') ? [ + { message: '`Bar` import should occur before import of `bar`' }, + { message: '`Bar` type import should occur before type import of `foo`' }, + ] : [ + { message: /(`Bar` import should occur before import of `bar`)|(`bar` import should occur after import of `Bar`)/ }, + { message: /(`Bar` type import should occur before type import of `foo`)|(`foo` type import should occur after type import of `Bar`)/ }, + ], + }), + // Option alphabetize: {order: 'desc'} with type group + test({ + code: ` + import a from 'foo'; + import c from 'Bar'; + import b from 'bar'; + + import index from './'; + + import type { C } from 'Bar'; + import type { A } from 'foo'; + `, + output: ` + import a from 'foo'; + import b from 'bar'; + import c from 'Bar'; + + import index from './'; + + import type { A } from 'foo'; + import type { C } from 'Bar'; + `, + ...parserConfig, + options: [ + { + groups: ['external', 'index', 'type'], + alphabetize: { order: 'desc' }, + }, + ], + errors: semver.satisfies(eslintPkg.version, '< 3') ? [ + { message: '`bar` import should occur before import of `Bar`' }, + { message: '`foo` type import should occur before type import of `Bar`' }, + ] : [ + { message: /(`bar` import should occur before import of `Bar`)|(`Bar` import should occur after import of `bar`)/ }, + { message: /(`foo` type import should occur before type import of `Bar`)|(`Bar` type import should occur after import of type `foo`)/ }, + ], + }), + // warns for out of order unassigned imports (warnOnUnassignedImports enabled) + test(withoutAutofixOutput({ + code: ` + import './local1'; + import global from 'global1'; + import local from './local2'; + import 'global2'; + `, + errors: [ + { + message: '`global1` import should occur before import of `./local1`', + }, + { + message: '`global2` import should occur before import of `./local1`', + }, + ], + options: [{ warnOnUnassignedImports: true }], + })), + // fix cannot move below unassigned import (warnOnUnassignedImports enabled) + test(withoutAutofixOutput({ + code: ` + import local from './local'; + + import 'global1'; + + import global2 from 'global2'; + import global3 from 'global3'; + `, + errors: [{ + message: '`./local` import should occur after import of `global3`', + }], + options: [{ warnOnUnassignedImports: true }], + })), + // Imports inside module declaration + test({ + code: ` + import type { ParsedPath } from 'path'; + import type { CopyOptions } from 'fs'; + + declare module 'my-module' { + import type { ParsedPath } from 'path'; + import type { CopyOptions } from 'fs'; + } + `, + output: ` + import type { CopyOptions } from 'fs'; + import type { ParsedPath } from 'path'; + + declare module 'my-module' { + import type { CopyOptions } from 'fs'; + import type { ParsedPath } from 'path'; + } + `, + errors: [ + { message: '`fs` type import should occur before type import of `path`' }, + { message: '`fs` type import should occur before type import of `path`' }, + ], + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + }, + ], + }), + // named import order + test({ + code: ` + import { type Z, A } from "./Z"; + import type N, { E, D } from "./Z"; + import type { L, G } from "./Z"; + `, + output: ` + import { A, type Z } from "./Z"; + import type N, { D, E } from "./Z"; + import type { G, L } from "./Z"; + `, + ...parserConfig, + options: [{ + named: true, + alphabetize: { order: 'asc' }, + }], + errors: [ + { message: `\`A\` import should occur before${supportsImportTypeSpecifiers ? ' type' : ''} import of \`Z\`` }, + { message: '`D` import should occur before import of `E`' }, + { message: '`G` import should occur before import of `L`' }, + ], + }), + test({ + code: ` + const { B, /* Hello World */ A } = require("./Z"); + export { B, A } from "./Z"; + `, + output: ` + const { /* Hello World */ A, B } = require("./Z"); + export { A, B } from "./Z"; + `, + ...parserConfig, + options: [{ + named: true, + alphabetize: { order: 'asc' }, + }], + errors: [{ + message: '`A` import should occur before import of `B`', + }, { + message: '`A` export should occur before export of `B`', + }], + }), + // Options: sortTypesGroup + newlines-between-types example #1 from the documentation (fail) + test({ + code: ` + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + + import c from "../foo.js"; + + import d from "./bar.js"; + + import e from "./"; + `, + output: ` + import type A from "fs"; + import type B from "path"; + + import type C from "../foo.js"; + + import type D from "./bar.js"; + + import type E from './'; + + import a from "fs"; + import b from "path"; + + import c from "../foo.js"; + + import d from "./bar.js"; + + import e from "./"; + `, + ...parserConfig, + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + sortTypesGroup: true, + 'newlines-between': 'always', + }, + ], + errors: [ + { + message: 'There should be at least one empty line between import groups', + line: 3, + }, + { + message: 'There should be at least one empty line between import groups', + line: 4, + }, + { + message: 'There should be at least one empty line between import groups', + line: 5, + }, + ], + }), + // Options: sortTypesGroup + newlines-between-types example #2 from the documentation (fail) + test({ + code: ` + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + + import c from "../foo.js"; + + import d from "./bar.js"; + + import e from "./"; + `, + output: ` + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + import a from "fs"; + import b from "path"; + + import c from "../foo.js"; + + import d from "./bar.js"; + + import e from "./"; + `, + ...parserConfig, + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + sortTypesGroup: true, + 'newlines-between': 'always', + 'newlines-between-types': 'never', + }, + ], + errors: [ + { + message: 'There should be no empty line between import groups', + line: 6, + }, + ], + }), + // Option: sortTypesGroup: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'inside-groups' with all newlines + test({ + code: ` + import c from 'Bar'; + + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + + import type { H } from './bbb'; + `, + output: ` + import c from 'Bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'always-and-inside-groups', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: + 'There should be no empty lines between this single-line import and the single-line import that follows it', + line: 2, + }, + { + message: + 'There should be no empty lines between this single-line import and the single-line import that follows it', + line: 60, + }, + { + message: + 'There should be no empty lines between this single-line import and the single-line import that follows it', + line: 76, + }, + ], + }), + // Option: sortTypesGroup: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'inside-groups' with no newlines + test({ + code: ` + import c from 'Bar'; + import d from 'bar'; + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + import a from 'foo'; + import b from 'dirA/bar'; + import index from './'; + import type { AA, + BB, CC } from 'abc'; + import type { Z } from 'fizz'; + import type { + A, + B + } from 'foo'; + import type { C2 } from 'dirB/Bar'; + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + import type { E2 } from 'dirB/baz'; + import type { C3 } from 'dirC/Bar'; + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + import type { C1 } from 'dirA/Bar'; + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + import type { E1 } from 'dirA/baz'; + import type { F } from './index.js'; + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + output: ` + import c from 'Bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'always-and-inside-groups', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, ], - errors: semver.satisfies(eslintPkg.version, '< 3') ? [ - { message: '`Bar` import should occur before import of `bar`' }, - { message: '`Bar` type import should occur before type import of `foo`' }, - ] : [ - { message: /(`Bar` import should occur before import of `bar`)|(`bar` import should occur after import of `Bar`)/ }, - { message: /(`Bar` type import should occur before type import of `foo`)|(`foo` type import should occur after type import of `Bar`)/ }, + errors: [ + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 3, + }, + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 4, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 13, + }, + { + message: 'There should be at least one empty line between import groups', + line: 22, + }, + { + message: 'There should be at least one empty line between import groups', + line: 23, + }, + { + message: 'There should be at least one empty line between import groups', + line: 24, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 25, + }, + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 27, + }, + { + message: 'There should be at least one empty line between import groups', + line: 28, + }, + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 32, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 33, + }, + { + message: 'There should be at least one empty line between import groups', + line: 38, + }, + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 39, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 40, + }, + { + message: 'There should be at least one empty line between import groups', + line: 46, + }, + { + message: 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 47, + }, + { + message: 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 48, + }, + { + message: 'There should be at least one empty line between import groups', + line: 53, + }, + { + message: 'There should be at least one empty line between import groups', + line: 54, + }, ], }), - // Option alphabetize: {order: 'desc'} with type group + // Option: sortTypesGroup: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'never' (default) test({ code: ` + import c from 'Bar'; + import d from 'bar'; + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; import a from 'foo'; + import b from 'dirA/bar'; + import index from './'; + import type { AA, + BB, CC } from 'abc'; + import type { Z } from 'fizz'; + import type { + A, + B + } from 'foo'; + import type { C2 } from 'dirB/Bar'; + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + import type { E2 } from 'dirB/baz'; + import type { C3 } from 'dirC/Bar'; + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + import type { C1 } from 'dirA/Bar'; + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + import type { E1 } from 'dirA/baz'; + import type { F } from './index.js'; + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + output: ` import c from 'Bar'; - import b from 'bar'; + import d from 'bar'; + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + import a from 'foo'; + + import b from 'dirA/bar'; import index from './'; - import type { C } from 'Bar'; - import type { A } from 'foo'; + import type { AA, + BB, CC } from 'abc'; + import type { Z } from 'fizz'; + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + import type { E2 } from 'dirB/baz'; + + import type { C3 } from 'dirC/Bar'; + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + + import type { C1 } from 'dirA/Bar'; + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + import type { E1 } from 'dirA/baz'; + + import type { F } from './index.js'; + + import type { G } from './aaa.js'; + import type { H } from './bbb'; `, - output: ` - import a from 'foo'; - import b from 'bar'; - import c from 'Bar'; + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'always-and-inside-groups', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'never', + }, + ], + errors: [ + { + message: 'There should be at least one empty line between import groups', + line: 22, + }, + { + message: 'There should be at least one empty line between import groups', + line: 23, + }, + { + message: 'There should be at least one empty line between import groups', + line: 24, + }, + { + message: 'There should be at least one empty line between import groups', + line: 28, + }, + { + message: 'There should be at least one empty line between import groups', + line: 38, + }, + { + message: 'There should be at least one empty line between import groups', + line: 46, + }, + { + message: 'There should be at least one empty line between import groups', + line: 53, + }, + { + message: 'There should be at least one empty line between import groups', + line: 54, + }, + + ], + }), + // Documentation failing example #1 for newlines-between + test({ + code: ` + import fs from 'fs'; + import path from 'path'; + import sibling from './foo'; import index from './'; + `, + output: ` + import fs from 'fs'; + import path from 'path'; - import type { A } from 'foo'; - import type { C } from 'Bar'; + import sibling from './foo'; + + import index from './'; `, ...parserConfig, options: [ { - groups: ['external', 'index', 'type'], - alphabetize: { order: 'desc' }, + 'newlines-between': 'always', }, ], - errors: semver.satisfies(eslintPkg.version, '< 3') ? [ - { message: '`bar` import should occur before import of `Bar`' }, - { message: '`foo` type import should occur before type import of `Bar`' }, - ] : [ - { message: /(`bar` import should occur before import of `Bar`)|(`Bar` import should occur after import of `bar`)/ }, - { message: /(`foo` type import should occur before type import of `Bar`)|(`Bar` type import should occur after import of type `foo`)/ }, + errors: [ + { + message: 'There should be at least one empty line between import groups', + line: 3, + }, + { + message: 'There should be at least one empty line between import groups', + line: 4, + }, ], }), - // warns for out of order unassigned imports (warnOnUnassignedImports enabled) - test(withoutAutofixOutput({ + // Documentation failing example #2 for newlines-between + test({ code: ` - import './local1'; - import global from 'global1'; - import local from './local2'; - import 'global2'; + import fs from 'fs'; + + import path from 'path'; + import sibling from './foo'; + import index from './'; + `, + output: ` + import fs from 'fs'; + + import path from 'path'; + + import sibling from './foo'; + + import index from './'; `, + ...parserConfig, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + }, + ], errors: [ { - message: '`global1` import should occur before import of `./local1`', + message: 'There should be at least one empty line between import groups', + line: 4, }, { - message: '`global2` import should occur before import of `./local1`', + message: 'There should be at least one empty line between import groups', + line: 5, }, ], - options: [{ warnOnUnassignedImports: true }], - })), - // fix cannot move below unassigned import (warnOnUnassignedImports enabled) - test(withoutAutofixOutput({ + }), + // Documentation failing example #3 for newlines-between + test({ code: ` - import local from './local'; + import fs from 'fs'; + import path from 'path'; - import 'global1'; + import sibling from './foo'; - import global2 from 'global2'; - import global3 from 'global3'; + import index from './'; `, - errors: [{ - message: '`./local` import should occur after import of `global3`', - }], - options: [{ warnOnUnassignedImports: true }], - })), - // Imports inside module declaration + output: ` + import fs from 'fs'; + import path from 'path'; + import sibling from './foo'; + import index from './'; + `, + ...parserConfig, + options: [ + { + 'newlines-between': 'never', + }, + ], + errors: [ + { + message: 'There should be no empty line between import groups', + line: 3, + }, + { + message: 'There should be no empty line between import groups', + line: 5, + }, + ], + }), + // Multiple errors + ...semver.satisfies(eslintPkg.version, '< 3.0.0') ? [] : [ + // Documentation failing example #1 for alphabetize + test({ + code: ` + import React, { PureComponent } from 'react'; + import aTypes from 'prop-types'; + import { compose, apply } from 'xcompose'; + import * as classnames from 'classnames'; + import blist2 from 'blist'; + import blist from 'BList'; + `, + // The reason why this output does not match the success example after being fixed is because eslint will leave overlapping errors alone, so only one import gets reordered when fixes are applied + output: ` + import aTypes from 'prop-types'; + import React, { PureComponent } from 'react'; + import { compose, apply } from 'xcompose'; + import * as classnames from 'classnames'; + import blist2 from 'blist'; + import blist from 'BList'; + `, + ...parserConfig, + options: [ + { + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + errors: [ + { + message: '`prop-types` import should occur before import of `react`', + line: 3, + }, + { + message: '`classnames` import should occur before import of `react`', + line: 5, + }, + { + message: '`blist` import should occur before import of `react`', + line: 6, + }, + { + message: '`BList` import should occur before import of `react`', + line: 7, + }, + ], + }), + ], + // Documentation failing example #1 for named test({ code: ` - import type { ParsedPath } from 'path'; - import type { CopyOptions } from 'fs'; - - declare module 'my-module' { - import type { ParsedPath } from 'path'; - import type { CopyOptions } from 'fs'; - } + import { compose, apply } from 'xcompose'; `, output: ` - import type { CopyOptions } from 'fs'; - import type { ParsedPath } from 'path'; - - declare module 'my-module' { - import type { CopyOptions } from 'fs'; - import type { ParsedPath } from 'path'; - } + import { apply, compose } from 'xcompose'; `, - errors: [ - { message: '`fs` type import should occur before type import of `path`' }, - { message: '`fs` type import should occur before type import of `path`' }, - ], ...parserConfig, options: [ { - alphabetize: { order: 'asc' }, + named: true, + alphabetize: { + order: 'asc', + }, + }, + ], + errors: [ + { + message: '`apply` import should occur before import of `compose`', + line: 2, }, ], }), - // named import order + // Documentation failing example #1 for warnOnUnassignedImports test({ code: ` - import { type Z, A } from "./Z"; - import type N, { E, D } from "./Z"; - import type { L, G } from "./Z"; - `, - output: ` - import { A, type Z } from "./Z"; - import type N, { D, E } from "./Z"; - import type { G, L } from "./Z"; + import fs from 'fs'; + import './styles.css'; + import path from 'path'; `, + // Should not be fixed (eslint@>=9 expects null) + output: semver.satisfies(eslintPkg.version, '< 9.0.0') ? ` + import fs from 'fs'; + import './styles.css'; + import path from 'path'; + ` : null, ...parserConfig, - options: [{ - named: true, - alphabetize: { order: 'asc' }, - }], + options: [ + { + warnOnUnassignedImports: true, + }, + ], errors: [ - { message: `\`A\` import should occur before${supportsImportTypeSpecifiers ? ' type' : ''} import of \`Z\`` }, - { message: '`D` import should occur before import of `E`' }, - { message: '`G` import should occur before import of `L`' }, + { + message: '`path` import should occur before import of `./styles.css`', + line: 4, + }, ], }), + // Documentation failing example #1 for sortTypesGroup test({ code: ` - const { B, /* Hello World */ A } = require("./Z"); - export { B, A } from "./Z"; + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; `, - output: ` - const { /* Hello World */ A, B } = require("./Z"); - export { A, B } from "./Z"; + // This is the "correct" behavior, but it's the wrong outcome (expectedly) + output: semver.satisfies(eslintPkg.version, '< 3.0.0') + // eslint@2 apparently attempts to fix multiple errors in one pass, + // which results in different erroneous output + ? ` + import type E from './'; + import type A from "fs"; + import type B from "path"; + import type C from "../foo.js"; + import type D from "./bar.js"; + + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; + ` : ` + import type C from "../foo.js"; + import type A from "fs"; + import type B from "path"; + import type D from "./bar.js"; + import type E from './'; + + import a from "fs"; + import b from "path"; + import c from "../foo.js"; + import d from "./bar.js"; + import e from "./"; `, ...parserConfig, - options: [{ - named: true, - alphabetize: { order: 'asc' }, - }], - errors: [{ - message: '`A` import should occur before import of `B`', - }, { - message: '`A` export should occur before export of `B`', - }], + options: [ + { + groups: ['type', 'builtin', 'parent', 'sibling', 'index'], + alphabetize: { order: 'asc' }, + }, + ], + errors: [ + { + message: '`../foo.js` type import should occur before type import of `fs`', + line: 4, + }, + { + message: '`./bar.js` type import should occur before type import of `fs`', + line: 5, + }, + { + message: '`./` type import should occur before type import of `fs`', + line: 6, + }, + ], }), - - // Options: sortTypesGroup + newlines-between-types example #1 from the documentation (fail) + // Documentation failing example #1 for newlines-between-types test({ code: ` import type A from "fs"; @@ -4081,8 +6286,7 @@ context('TypeScript', function () { }, ], }), - - // Options: sortTypesGroup + newlines-between-types example #2 from the documentation (fail) + // Documentation failing example #2 for newlines-between-types test({ code: ` import type A from "fs"; @@ -4131,6 +6335,320 @@ context('TypeScript', function () { }, ], }), + // Documentation failing example #1 for consolidateIslands + test({ + code: ` + var fs = require('fs'); + var path = require('path'); + var { util1, util2, util3 } = require('util'); + var async = require('async'); + var relParent1 = require('../foo'); + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + var relParent3 = require('../bar'); + var { sibling1, + sibling2, sibling3 } = require('./foo'); + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + output: ` + var fs = require('fs'); + var path = require('path'); + var { util1, util2, util3 } = require('util'); + + var async = require('async'); + + var relParent1 = require('../foo'); + + var { + relParent21, + relParent22, + relParent23, + relParent24, + } = require('../'); + + var relParent3 = require('../bar'); + + var { sibling1, + sibling2, sibling3 } = require('./foo'); + + var sibling2 = require('./bar'); + var sibling3 = require('./foobar'); + `, + ...parserConfig, + options: [ + { + 'newlines-between': 'always-and-inside-groups', + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: 'There should be at least one empty line between import groups', + line: 4, + }, + { + message: 'There should be at least one empty line between import groups', + line: 5, + }, + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 6, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 12, + }, + { + message: 'There should be at least one empty line between import groups', + line: 13, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 15, + }, + ], + }), + // Documentation failing example #2 for consolidateIslands + test({ + code: ` + import c from 'Bar'; + import d from 'bar'; + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + import a from 'foo'; + import b from 'dirA/bar'; + import index from './'; + import type { AA, + BB, CC } from 'abc'; + import type { Z } from 'fizz'; + import type { + A, + B + } from 'foo'; + import type { C2 } from 'dirB/Bar'; + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + import type { E2 } from 'dirB/baz'; + import type { C3 } from 'dirC/Bar'; + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + import type { C1 } from 'dirA/Bar'; + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + import type { E1 } from 'dirA/baz'; + import type { F } from './index.js'; + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + output: ` + import c from 'Bar'; + import d from 'bar'; + + import { + aa, + bb, + cc, + dd, + ee, + ff, + gg + } from 'baz'; + + import { + hh, + ii, + jj, + kk, + ll, + mm, + nn + } from 'fizz'; + + import a from 'foo'; + + import b from 'dirA/bar'; + + import index from './'; + + import type { AA, + BB, CC } from 'abc'; + + import type { Z } from 'fizz'; + + import type { + A, + B + } from 'foo'; + + import type { C2 } from 'dirB/Bar'; + + import type { + D2, + X2, + Y2 + } from 'dirB/bar'; + + import type { E2 } from 'dirB/baz'; + import type { C3 } from 'dirC/Bar'; + + import type { + D3, + X3, + Y3 + } from 'dirC/bar'; + + import type { E3 } from 'dirC/baz'; + import type { F3 } from 'dirC/caz'; + import type { C1 } from 'dirA/Bar'; + + import type { + D1, + X1, + Y1 + } from 'dirA/bar'; + + import type { E1 } from 'dirA/baz'; + import type { F } from './index.js'; + import type { G } from './aaa.js'; + import type { H } from './bbb'; + `, + ...parserConfig, + options: [ + { + alphabetize: { order: 'asc' }, + groups: ['external', 'internal', 'index', 'type'], + pathGroups: [ + { + pattern: 'dirA/**', + group: 'internal', + position: 'after', + }, + { + pattern: 'dirB/**', + group: 'internal', + position: 'before', + }, + { + pattern: 'dirC/**', + group: 'internal', + }, + ], + 'newlines-between': 'always-and-inside-groups', + 'newlines-between-types': 'never', + pathGroupsExcludedImportTypes: [], + sortTypesGroup: true, + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 3, + }, + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 4, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 13, + }, + { + message: 'There should be at least one empty line between import groups', + line: 22, + }, + { + message: 'There should be at least one empty line between import groups', + line: 23, + }, + { + message: 'There should be at least one empty line between import groups', + line: 24, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 25, + }, + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 27, + }, + { + message: 'There should be at least one empty line between import groups', + line: 28, + }, + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 32, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 33, + }, + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 39, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 40, + }, + { + message: + 'There should be at least one empty line between this import and the multi-line import that follows it', + line: 47, + }, + { + message: + 'There should be at least one empty line between this multi-line import and the import that follows it', + line: 48, + }, + ], + }), supportsExportTypeSpecifiers ? [ test({