Skip to content

Commit

Permalink
feat: [import/order] allow intragroup sorting of type-only imports vi…
Browse files Browse the repository at this point in the history
…a `sortTypesAmongThemselves`

Closes import-js#2912 import-js#2347 import-js#2441
Subsumes import-js#2615
  • Loading branch information
Xunnamius committed Nov 16, 2024
1 parent a20d843 commit fba43f1
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 5 deletions.
43 changes: 43 additions & 0 deletions docs/rules/order.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,49 @@ import path from 'path';
import './styles.css';
```

### `sortTypesAmongThemselves: true|false`

Sort type-only imports separately from normal non-type imports.

When enabled, the intragroup sort order of type-only imports will mirror the intergroup ordering of normal imports as defined by `group`, `pathGroups`, etc.

> This setting is only meaningful when `"type"` is included in `groups`.

Given the following settings:

```ts
{
groups: ['type', 'builtin', 'parent', 'sibling', 'index']
}
```

This example will fail the rule check:

```ts
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 "./";
```

However, if we add `sortTypesAmongThemselves: true`:

```ts
{
groups: ['type', 'builtin', 'parent', 'sibling', 'index'],
sortTypesAmongThemselves: true
}
```

The same example will pass.

## Related

- [`import/external-module-folders`] setting
Expand Down
33 changes: 28 additions & 5 deletions src/rules/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,31 +513,43 @@ function computePathRank(ranks, pathGroups, path, maxPosition) {
}
}

function computeRank(context, ranks, importEntry, excludedImportTypes) {
function computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves) {
let impType;
let rank;

const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1;
const isTypeOnlyImport = importEntry.node.importKind === 'type';
const isExcludedFromPathRank = isTypeOnlyImport && isTypeGroupInGroups && excludedImportTypes.has('type')

if (importEntry.type === 'import:object') {
impType = 'object';
} else if (importEntry.node.importKind === 'type' && ranks.omittedTypes.indexOf('type') === -1) {
} else if (isTypeOnlyImport && isTypeGroupInGroups && !isSortingTypesAmongThemselves) {
impType = 'type';
} else {
impType = importType(importEntry.value, context);
}
if (!excludedImportTypes.has(impType)) {

if (!excludedImportTypes.has(impType) && !isExcludedFromPathRank) {
rank = computePathRank(ranks.groups, ranks.pathGroups, importEntry.value, ranks.maxPosition);
}

if (typeof rank === 'undefined') {
rank = ranks.groups[impType];
}

if (isTypeOnlyImport && isSortingTypesAmongThemselves) {
rank = ranks.groups['type'] + rank / 10;
}

if (importEntry.type !== 'import' && !importEntry.type.startsWith('import:')) {
rank += 100;
}

return rank;
}

function registerNode(context, importEntry, ranks, imported, excludedImportTypes) {
const rank = computeRank(context, ranks, importEntry, excludedImportTypes);
function registerNode(context, importEntry, ranks, imported, excludedImportTypes, isSortingTypesAmongThemselves) {
const rank = computeRank(context, ranks, importEntry, excludedImportTypes, isSortingTypesAmongThemselves);
if (rank !== -1) {
imported.push({ ...importEntry, rank });
}
Expand Down Expand Up @@ -781,6 +793,10 @@ module.exports = {
'never',
],
},
sortTypesAmongThemselves: {
type: 'boolean',
default: false,
},
named: {
default: false,
oneOf: [{
Expand Down Expand Up @@ -837,6 +853,7 @@ module.exports = {
const options = context.options[0] || {};
const newlinesBetweenImports = options['newlines-between'] || 'ignore';
const pathGroupsExcludedImportTypes = new Set(options.pathGroupsExcludedImportTypes || ['builtin', 'external', 'object']);
const sortTypesAmongThemselves = options.sortTypesAmongThemselves;

const named = {
types: 'mixed',
Expand Down Expand Up @@ -879,6 +896,9 @@ module.exports = {
const importMap = new Map();
const exportMap = new Map();

const isTypeGroupInGroups = ranks.omittedTypes.indexOf('type') === -1;
const isSortingTypesAmongThemselves = isTypeGroupInGroups && sortTypesAmongThemselves;

function getBlockImports(node) {
if (!importMap.has(node)) {
importMap.set(node, []);
Expand Down Expand Up @@ -932,6 +952,7 @@ module.exports = {
ranks,
getBlockImports(node.parent),
pathGroupsExcludedImportTypes,
isSortingTypesAmongThemselves
);

if (named.import) {
Expand Down Expand Up @@ -983,6 +1004,7 @@ module.exports = {
ranks,
getBlockImports(node.parent),
pathGroupsExcludedImportTypes,
isSortingTypesAmongThemselves
);
},
CallExpression(node) {
Expand All @@ -1005,6 +1027,7 @@ module.exports = {
ranks,
getBlockImports(block),
pathGroupsExcludedImportTypes,
isSortingTypesAmongThemselves
);
},
...named.require && {
Expand Down
182 changes: 182 additions & 0 deletions tests/src/rules/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -3285,6 +3285,188 @@ context('TypeScript', function () {
}],
}),
] : [],
// Option sortTypesAmongThemselves: false (default)
test({
code: `
import c from 'Bar';
import a from 'foo';
import type { C } from 'dirA/Bar';
import b from 'dirA/bar';
import type { D } from 'dirA/bar';
import index from './';
import type { AA } from 'abc';
import type { A } from 'foo';
`,
...parserConfig,
options: [
{
alphabetize: { order: 'asc' },
groups: ['external', 'internal', 'index', 'type'],
pathGroups: [
{
pattern: 'dirA/**',
group: 'internal',
},
],
'newlines-between': 'always',
pathGroupsExcludedImportTypes: [],
},
],
}),
test({
code: `
import c from 'Bar';
import a from 'foo';
import type { C } from 'dirA/Bar';
import b from 'dirA/bar';
import type { D } from 'dirA/bar';
import index from './';
import type { AA } from 'abc';
import type { A } from 'foo';
`,
...parserConfig,
options: [
{
alphabetize: { order: 'asc' },
groups: ['external', 'internal', 'index', 'type'],
pathGroups: [
{
pattern: 'dirA/**',
group: 'internal',
},
],
'newlines-between': 'always',
pathGroupsExcludedImportTypes: [],
sortTypesAmongThemselves: false,
},
],
}),
// Option sortTypesAmongThemselves: true and 'type' in pathGroupsExcludedImportTypes
test({
code: `
import c from 'Bar';
import a from 'foo';
import b from 'dirA/bar';
import index from './';
import type { AA } from 'abc';
import type { C } from 'dirA/Bar';
import type { D } from 'dirA/bar';
import type { A } from 'foo';
`,
...parserConfig,
options: [
{
alphabetize: { order: 'asc' },
groups: ['external', 'internal', 'index', 'type'],
pathGroups: [
{
pattern: 'dirA/**',
group: 'internal',
},
],
'newlines-between': 'always',
pathGroupsExcludedImportTypes: ['type'],
sortTypesAmongThemselves: true,
},
],
}),
// Option sortTypesAmongThemselves: true and 'type' omitted from groups
test({
code: `
import c from 'Bar';
import type { AA } from 'abc';
import a from 'foo';
import type { A } from 'foo';
import type { C } from 'dirA/Bar';
import b from 'dirA/bar';
import type { D } from 'dirA/bar';
import index from './';
`,
...parserConfig,
options: [
{
alphabetize: { order: 'asc' },
groups: ['external', 'internal', 'index'],
pathGroups: [
{
pattern: 'dirA/**',
group: 'internal',
},
],
'newlines-between': 'always',
pathGroupsExcludedImportTypes: [],
// Becomes a no-op without "type" in groups
sortTypesAmongThemselves: true,
},
],
}),
test({
code: `
import c from 'Bar';
import type { AA } from 'abc';
import a from 'foo';
import type { A } from 'foo';
import type { C } from 'dirA/Bar';
import b from 'dirA/bar';
import type { D } from 'dirA/bar';
import index from './';
`,
...parserConfig,
options: [
{
alphabetize: { order: 'asc' },
groups: ['external', 'internal', 'index'],
pathGroups: [
{
pattern: 'dirA/**',
group: 'internal',
},
],
'newlines-between': 'always',
pathGroupsExcludedImportTypes: [],
},
],
}),
// Option: sortTypesAmongThemselves: true puts type imports in the same order as regular imports (from issue #2441, PR #2615)
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',
caseInsensitive: true,
},
sortTypesAmongThemselves: true,
},
],
}),
),
invalid: [].concat(
// Option alphabetize: {order: 'asc'}
Expand Down

0 comments on commit fba43f1

Please sign in to comment.