diff --git a/src/rules/order.js b/src/rules/order.js index e60e5a0bb..524bfe892 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, isSortingTypesAmongThemselves) { const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves); 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, isSortingTypesAmongThemselves) { +function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports_, distinctGroup, isSortingTypesAmongThemselves, isConsolidatingSpaceBetweenImports) { const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => { const linesBetweenImports = getSourceCode(context).lines.slice( previousImport.node.loc.end.line, @@ -707,12 +717,24 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, ne const isTypeOnlyImport = currentImport.node.importKind === 'type'; const isPreviousImportTypeOnlyImport = previousImport.node.importKind === 'type'; - const isNormalImportFollowingTypeOnlyImportAndRelevant = - !isTypeOnlyImport && isPreviousImportTypeOnlyImport && isSortingTypesAmongThemselves; + const isNormalImportNextToTypeOnlyImportAndRelevant = + isTypeOnlyImport !== isPreviousImportTypeOnlyImport && isSortingTypesAmongThemselves; const isTypeOnlyImportAndRelevant = isTypeOnlyImport && isSortingTypesAmongThemselves; + // In the special case where newlinesBetweenTypeOnlyImports and + // consolidateIslands want the opposite thing, consolidateIslands wins + const newlinesBetweenTypeOnlyImports = + newlinesBetweenTypeOnlyImports_ === 'never' && + isConsolidatingSpaceBetweenImports && + isSortingTypesAmongThemselves && + (isNormalImportNextToTypeOnlyImportAndRelevant || + previousImport.isMultiline || + currentImport.isMultiline) + ? 'always-and-inside-groups' + : newlinesBetweenTypeOnlyImports_; + const isNotIgnored = (isTypeOnlyImportAndRelevant && newlinesBetweenTypeOnlyImports !== 'ignore') || @@ -720,27 +742,34 @@ function makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, ne if(isNotIgnored) { const shouldAssertNewlineBetweenGroups = - ((isTypeOnlyImportAndRelevant || isNormalImportFollowingTypeOnlyImportAndRelevant) && + ((isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant) && (newlinesBetweenTypeOnlyImports === 'always' || newlinesBetweenTypeOnlyImports === 'always-and-inside-groups')) || - ((!isTypeOnlyImportAndRelevant && !isNormalImportFollowingTypeOnlyImportAndRelevant) && + ((!isTypeOnlyImportAndRelevant && !isNormalImportNextToTypeOnlyImportAndRelevant) && (newlinesBetweenImports === 'always' || newlinesBetweenImports === 'always-and-inside-groups')); const shouldAssertNoNewlineWithinGroup = - ((isTypeOnlyImportAndRelevant || isNormalImportFollowingTypeOnlyImportAndRelevant) && + ((isTypeOnlyImportAndRelevant || isNormalImportNextToTypeOnlyImportAndRelevant) && (newlinesBetweenTypeOnlyImports !== 'always-and-inside-groups')) || - ((!isTypeOnlyImportAndRelevant && !isNormalImportFollowingTypeOnlyImportAndRelevant) && + ((!isTypeOnlyImportAndRelevant && !isNormalImportNextToTypeOnlyImportAndRelevant) && (newlinesBetweenImports !== 'always-and-inside-groups')); const shouldAssertNoNewlineBetweenGroup = !isSortingTypesAmongThemselves || - !isNormalImportFollowingTypeOnlyImportAndRelevant || + !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', @@ -748,10 +777,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', @@ -760,12 +787,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; @@ -848,6 +904,12 @@ module.exports = { 'never', ], }, + consolidateIslands: { + enum: [ + 'inside-groups', + 'never', + ], + }, sortTypesAmongThemselves: { type: 'boolean', default: false, @@ -910,6 +972,7 @@ module.exports = { const newlinesBetweenTypeOnlyImports = options['newlines-between-types'] || newlinesBetweenImports; const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']); const sortTypesAmongThemselves = options.sortTypesAmongThemselves; + const consolidateIslands = options.consolidateIslands || 'never'; const named = { types: 'mixed', @@ -1172,7 +1235,17 @@ module.exports = { 'Program:exit'() { importMap.forEach((imported) => { if (newlinesBetweenImports !== 'ignore' || newlinesBetweenTypeOnlyImports !== 'ignore') { - makeNewlinesBetweenReport(context, imported, newlinesBetweenImports, newlinesBetweenTypeOnlyImports, distinctGroup, isSortingTypesAmongThemselves); + makeNewlinesBetweenReport( + context, + imported, + newlinesBetweenImports, + newlinesBetweenTypeOnlyImports, + distinctGroup, + isSortingTypesAmongThemselves, + 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 f0f333d51..ce8b4488c 100644 --- a/tests/src/rules/order.js +++ b/tests/src/rules/order.js @@ -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: ` @@ -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 multiline import that follows it', + line: 6, + }, + { + message: 'There should be at least one empty line between this multiline 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 multiline 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 singleline import and the singleline import that follows it', + line: 2, + }, + { + message: 'There should be no empty lines between this singleline import and the singleline import that follows it', + line: 4, + }, + { + message: 'There should be no empty lines between this singleline import and the singleline import that follows it', + line: 24, + }, + ], + }), ].filter(Boolean), }); @@ -3737,97 +3914,431 @@ context('TypeScript', function () { } ], }), - // Ensure the rule doesn't choke and die on absolute paths trying to pass NaN around + // Option: sortTypesAmongThemselves: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'inside-groups' test({ code: ` - import fs from 'node:fs'; + 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 '@scoped/package'; - import type { B } from 'node:fs'; + import a from 'foo'; - import type { A1 } from '/bad/bad/bad/bad'; - import './a/b/c'; - import type { A2 } from '/bad/bad/bad/bad'; - import type { A3 } from '/bad/bad/bad/bad'; - import type { D1 } from '/bad/bad/not/good'; - import type { D2 } from '/bad/bad/not/good'; - import type { D3 } from '/bad/bad/not/good'; + import b from 'dirA/bar'; - import type { C } from '@something/else'; + import index from './'; - import type { E } from './index.js'; + 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: ['builtin', 'type', 'unknown', 'external'], + 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: [], sortTypesAmongThemselves: true, - 'newlines-between': 'always' + consolidateIslands: 'inside-groups', }, ], }), - ), - invalid: [].concat( - // Option alphabetize: {order: 'asc'} + // Option: sortTypesAmongThemselves: true and newlines-between-types: 'always-and-inside-groups' and consolidateIslands: 'never' (default) 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: [], + sortTypesAmongThemselves: true, + consolidateIslands: 'never', }, ], }), - // Option alphabetize: {order: 'desc'} + // Ensure the rule doesn't choke and die on absolute paths trying to pass NaN around 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 fs from 'node:fs'; - 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 '@scoped/package'; + import type { B } from 'node:fs'; - import index from './'; + import type { A1 } from '/bad/bad/bad/bad'; + import './a/b/c'; + import type { A2 } from '/bad/bad/bad/bad'; + import type { A3 } from '/bad/bad/bad/bad'; + import type { D1 } from '/bad/bad/not/good'; + import type { D2 } from '/bad/bad/not/good'; + import type { D3 } from '/bad/bad/not/good'; + + import type { C } from '@something/else'; + + import type { E } from './index.js'; `, ...parserConfig, options: [ { - groups: ['external', 'index'], + alphabetize: { order: 'asc' }, + groups: ['builtin', 'type', 'unknown', 'external'], + sortTypesAmongThemselves: true, + 'newlines-between': 'always' + }, + ], + }), + // Ensure consolidateOptions: 'inside-groups', newlines-between: 'always-and-inside-groups', and newlines-between-types: 'never' 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, + $canonical, + $exists, + $genesis + } 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': 'always-and-inside-groups', + 'newlines-between-types': 'never', + sortTypesAmongThemselves: true, + consolidateIslands: 'inside-groups' + }, + ], + }), + test({ + code: ` + import assert from 'node:assert'; + import { isNativeError } from 'node:util/types'; + + import { runNoRejectOnBadExit } from '@-xun/run'; + import { TrialError } from 'named-app-errors'; + import { resolve as resolverLibrary } from 'resolve.exports'; + + import { toAbsolutePath, type AbsolutePath } 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', + sortTypesAmongThemselves: 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' }, }, ], @@ -4129,6 +4640,639 @@ context('TypeScript', function () { }, ], }), + // Option: sortTypesAmongThemselves: 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: [], + sortTypesAmongThemselves: true, + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: + 'There should be no empty lines between this singleline import and the singleline import that follows it', + line: 2 + }, + { + message: + 'There should be no empty lines between this singleline import and the singleline import that follows it', + line: 60 + }, + { + message: + 'There should be no empty lines between this singleline import and the singleline import that follows it', + line: 76 + } + ], + }), + // Option: sortTypesAmongThemselves: 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: [], + sortTypesAmongThemselves: true, + consolidateIslands: 'inside-groups', + }, + ], + errors: [ + { + message: 'There should be at least one empty line between this import and the multiline import that follows it', + line: 3 + }, + { + message: 'There should be at least one empty line between this import and the multiline import that follows it', + line: 4, + }, + { + message: 'There should be at least one empty line between this multiline 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 multiline import and the import that follows it', + line: 25, + }, + { + message: 'There should be at least one empty line between this import and the multiline 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 multiline import that follows it', + line: 32, + }, + { + message: 'There should be at least one empty line between this multiline 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 multiline import that follows it', + line: 39, + }, + { + message: 'There should be at least one empty line between this multiline 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 multiline import that follows it', + line: 47, + }, + { + message: 'There should be at least one empty line between this multiline 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: sortTypesAmongThemselves: 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 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: [], + sortTypesAmongThemselves: 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, + }, + + ], + }), supportsExportTypeSpecifiers ? [ test({