Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Add support for TypeScript import assignments" #158

Merged
merged 1 commit into from Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 1 addition & 16 deletions README.md
Expand Up @@ -269,15 +269,6 @@ import g from ".";
import h from "./constants";
import i from "./styles";

// TypeScript import assignments.
import J = require("../parent");
import K = require("./sibling");
export import L = require("an-npm-package");
import M = require("different-npm-package");
import N = Namespace;
export import O = Namespace.A.B.C;
import P = Namespace.A.C;

// Different types of exports:
export { a } from "../..";
export { b } from "/";
Expand Down Expand Up @@ -389,8 +380,6 @@ Side effect imports have `\u0000` _prepended_ to their `from` string (starts wit

Type imports have `\u0000` _appended_ to their `from` string (ends with `\u0000`). You can match them with `"\\u0000$"` – but you probably need more than that to avoid them also being matched by other regexes.

TypeScript import assignments have `\u0001` (for `import A = require("A")`) or `\u0002` (for `import A = B.C.D`) prepended to their `from` string (starts with `\u0001` or `\u0002`). It is _not_ possible to distinguish `export import A =` and `import A =`.

All imports that match the same regex are sorted internally as mentioned in [Sort order].

This is the default value for the `groups` option:
Expand All @@ -410,8 +399,6 @@ This is the default value for the `groups` option:
// Relative imports.
// Anything that starts with a dot.
["^\\."],
// TypeScript import assignments.
["^\\u0001", "^\\u0002"],
];
```

Expand Down Expand Up @@ -530,8 +517,6 @@ The final whitespace rule is that this plugin puts one import/export per line. I

No. This is intentional to keep things simple. Use some other sorting rule, such as [import/order], for sorting `require`. Or consider migrating your code using `require` to `import`. `import` is well supported these days.

The only `require`-like thing supported is TypeScript import assignments like `import Thing = require("something")`. They’re much easier to support since they are very restricted: The thing to the left of the `=` has to be a single identifier, and inside `require()` there has to be a single string literal. This makes it sortable as if it was `import Thing from "something"`.

### Why sort on `from`?

Some other import sorting rules sort based on the first name after `import`, rather than the string after `from`. This plugin intentionally sorts on the `from` string to be `git diff` friendly.
Expand Down Expand Up @@ -707,7 +692,7 @@ Use [custom grouping], setting the `groups` option to only have a single inner a
For example, here’s the default value but changed to a single inner array:

```js
[["^\\u0000", "^node:", "^@?\\w", "^", "^\\.", "^\\u0001", "^\\u0002"]];
[["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]];
```

(By default, each string is in its _own_ array (that’s 5 inner arrays) – causing a blank line between each.)
Expand Down
11 changes: 4 additions & 7 deletions examples/.eslintrc.js
Expand Up @@ -103,7 +103,7 @@ module.exports = {
"error",
{
// The default grouping, but with no blank lines.
groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\.", "^\\u0001", "^\\u0002"]],
groups: [["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]],
},
],
},
Expand All @@ -115,7 +115,7 @@ module.exports = {
"error",
{
// The default grouping, but in reverse.
groups: [["^\\u0001", "^\\u0002"], ["^\\."], ["^"], ["^@?\\w"], ["^node:"], ["^\\u0000"]],
groups: [["^\\."], ["^"], ["^@?\\w"], ["^node:"], ["^\\u0000"]],
},
],
},
Expand All @@ -128,7 +128,7 @@ module.exports = {
"error",
{
// The default grouping, but with type imports first as a separate group.
groups: [["^.*\\u0000$"], ["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."], ["^\\u0001", "^\\u0002"]],
groups: [["^.*\\u0000$"], ["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."]],
},
],
},
Expand All @@ -141,7 +141,7 @@ module.exports = {
"error",
{
// The default grouping, but with type imports last as a separate group.
groups: [["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."], ["^\\u0001", "^\\u0002"], ["^.+\\u0000$"]],
groups: [["^\\u0000"], ["^node:"], ["^@?\\w"], ["^"], ["^\\."], ["^.+\\u0000$"]],
},
],
},
Expand All @@ -162,7 +162,6 @@ module.exports = {
["^@?\\w"],
["^"],
["^\\."],
["^\\u0001", "^\\u0002"],
],
},
],
Expand All @@ -183,7 +182,6 @@ module.exports = {
["^@?\\w"],
["^"],
["^\\."],
["^\\u0001", "^\\u0002"],
["^node:.*\\u0000$", "^@?\\w.*\\u0000$", "^[^.].*\\u0000$", "^\\..*\\u0000$"],
],
},
Expand All @@ -204,7 +202,6 @@ module.exports = {
["^@?\\w.*\\u0000$", "^@?\\w"],
["(?<=\\u0000)$", "^"],
["^\\..*\\u0000$", "^\\."],
["^\\u0001", "^\\u0002"],
],
},
],
Expand Down
9 changes: 0 additions & 9 deletions examples/readme-order.prettier.ts
Expand Up @@ -25,15 +25,6 @@ import g from ".";
import h from "./constants";
import i from "./styles";

// TypeScript import assignments.
import J = require("../parent");
import K = require("./sibling");
export import L = require("an-npm-package");
import M = require("different-npm-package");
import N = Namespace;
export import O = Namespace.A.B.C;
import P = Namespace.A.C;

// Different types of exports:
export { a } from "../..";
export { b } from "/";
Expand Down
71 changes: 14 additions & 57 deletions src/imports.js
Expand Up @@ -16,8 +16,6 @@ const defaultGroups = [
// Relative imports.
// Anything that starts with a dot.
["^\\."],
// TypeScript import assignments.
["^\\u0001", "^\\u0002"],
];

module.exports = {
Expand Down Expand Up @@ -58,7 +56,7 @@ module.exports = {
const parents = new Set();

return {
"ImportDeclaration,TSImportEqualsDeclaration": (node) => {
ImportDeclaration: (node) => {
parents.add(node.parent);
},

Expand Down Expand Up @@ -99,16 +97,14 @@ function makeSortedItems(items, outerGroups) {

for (const item of items) {
const { originalSource } = item.source;
const sourceWithControlCharacter = getSourceWithControlCharacter(
originalSource,
item
);
const source = item.isSideEffectImport
? `\0${originalSource}`
: item.source.kind !== "value"
? `${originalSource}\0`
: originalSource;
const [matchedGroup] = shared
.flatMap(itemGroups, (groups) =>
groups.map((group) => [
group,
group.regex.exec(sourceWithControlCharacter),
])
groups.map((group) => [group, group.regex.exec(source)])
)
.reduce(
([group, longestMatch], [nextGroup, nextMatch]) =>
Expand All @@ -134,41 +130,14 @@ function makeSortedItems(items, outerGroups) {
);
}

function getSourceWithControlCharacter(originalSource, item) {
if (item.isSideEffectImport) {
return `\0${originalSource}`;
}
switch (item.source.kind) {
case shared.KIND_VALUE:
return originalSource;
case shared.KIND_TS_IMPORT_ASSIGNMENT_REQUIRE:
return `\u0001${originalSource}`;
case shared.KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE:
return `\u0002${originalSource}`;
default: // `type` and `typeof`.
return `${originalSource}\u0000`;
}
}

// Exclude "ImportDefaultSpecifier" – the "def" in `import def, {a, b}`.
function getSpecifiers(importNode) {
switch (importNode.type) {
case "ImportDeclaration":
return importNode.specifiers.filter((node) => isImportSpecifier(node));
case "TSImportEqualsDeclaration":
return [];
// istanbul ignore next
default:
throw new Error(`Unsupported import node type: ${importNode.type}`);
}
return importNode.specifiers.filter((node) => isImportSpecifier(node));
}

// Full import statement.
function isImport(node) {
return (
node.type === "ImportDeclaration" ||
node.type === "TSImportEqualsDeclaration"
);
return node.type === "ImportDeclaration";
}

// import def, { a, b as c, type d } from "A"
Expand All @@ -181,21 +150,9 @@ function isImportSpecifier(node) {
// But not: import {} from "setup"
// And not: import type {} from "setup"
function isSideEffectImport(importNode, sourceCode) {
switch (importNode.type) {
case "ImportDeclaration":
return (
importNode.specifiers.length === 0 &&
(!importNode.importKind ||
importNode.importKind === shared.KIND_VALUE) &&
!shared.isPunctuator(
sourceCode.getFirstToken(importNode, { skip: 1 }),
"{"
)
);
case "TSImportEqualsDeclaration":
return false;
// istanbul ignore next
default:
throw new Error(`Unsupported import node type: ${importNode.type}`);
}
return (
importNode.specifiers.length === 0 &&
(!importNode.importKind || importNode.importKind === "value") &&
!shared.isPunctuator(sourceCode.getFirstToken(importNode, { skip: 1 }), "{")
);
}
90 changes: 9 additions & 81 deletions src/shared.js
Expand Up @@ -164,7 +164,7 @@ function getImportExportItems(
const [start] = all[0].range;
const [, end] = all[all.length - 1].range;

const source = getSource(sourceCode, node);
const source = getSource(node);

return {
node,
Expand Down Expand Up @@ -795,8 +795,8 @@ function isNewline(node) {
return node.type === "Newline";
}

function getSource(sourceCode, node) {
const [source, kind] = getSourceTextAndKind(sourceCode, node);
function getSource(node) {
const source = node.source.value;

return {
// Sort by directory level rather than by string length.
Expand All @@ -806,7 +806,7 @@ function getSource(sourceCode, node) {
// Make `../` sort after `../../` but before `../a` etc.
// Why a comma? See the next comment.
.replace(/^[./]*\/$/, "$&,")
// Make `.` and `/` sort before any other punctuation.
// Make `.` and `/` sort before any other punctation.
// The default order is: _ - , x x x . x x x / x x x
// We’re changing it to: . / , x x x _ x x x - x x x
.replace(/[./_-]/g, (char) => {
Expand All @@ -825,85 +825,16 @@ function getSource(sourceCode, node) {
}
}),
originalSource: source,
kind,
kind: getImportExportKind(node),
};
}

function getSourceTextAndKind(sourceCode, node) {
switch (node.type) {
case "ImportDeclaration":
case "ExportNamedDeclaration":
case "ExportAllDeclaration":
return [node.source.value, getImportExportKind(node)];
case "TSImportEqualsDeclaration":
return getSourceTextAndKindFromModuleReference(
sourceCode,
node.moduleReference
);
// istanbul ignore next
default:
throw new Error(`Unsupported import/export node type: ${node.type}`);
}
}

const KIND_VALUE = "value";
const KIND_TS_IMPORT_ASSIGNMENT_REQUIRE = "z_require";
const KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE = "z_namespace";

function getSourceTextAndKindFromModuleReference(sourceCode, node) {
switch (node.type) {
case "TSExternalModuleReference":
// Only string literals inside `require()` are allowed by
// TypeScript, but the parser supports anything. Sorting
// is defined for string literals only. For other expressions,
// we just make sure not to crash.
switch (node.expression.type) {
case "Literal":
return [
typeof node.expression.value === "string"
? node.expression.value
: node.expression.raw,
KIND_TS_IMPORT_ASSIGNMENT_REQUIRE,
];
default: {
const [start, end] = node.expression.range;
return [
sourceCode.text.slice(start, end),
KIND_TS_IMPORT_ASSIGNMENT_REQUIRE,
];
}
}
case "TSQualifiedName":
return [
getSourceTextFromTSQualifiedName(sourceCode, node),
KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE,
];
case "Identifier":
return [node.name, KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE];
// istanbul ignore next
default:
throw new Error(`Unsupported module reference node type: ${node.type}`);
}
}

function getSourceTextFromTSQualifiedName(sourceCode, node) {
switch (node.left.type) {
case "Identifier":
return `${node.left.name}.${node.right.name}`;
case "TSQualifiedName":
return `${getSourceTextFromTSQualifiedName(sourceCode, node.left)}.${
node.right.name
}`;
// istanbul ignore next
default:
throw new Error(`Unsupported TS qualified name node type: ${node.type}`);
}
}

function getImportExportKind(node) {
// `type` and `typeof` imports, as well as `type` exports (there are no
// `typeof` exports).
return node.importKind || node.exportKind || KIND_VALUE;
// `typeof` exports). In Flow, import specifiers can also have a kind. Default
// to "value" (like TypeScript) to make regular imports/exports come after the
// type imports/exports.
return node.importKind || node.exportKind || "value";
}

// Like `Array.prototype.findIndex`, but searches from the end.
Expand Down Expand Up @@ -936,9 +867,6 @@ module.exports = {
getImportExportItems,
getSourceCode,
isPunctuator,
KIND_TS_IMPORT_ASSIGNMENT_NAMESPACE,
KIND_TS_IMPORT_ASSIGNMENT_REQUIRE,
KIND_VALUE,
maybeReportSorting,
printSortedItems,
printWithSortedSpecifiers,
Expand Down
9 changes: 0 additions & 9 deletions test/__snapshots__/examples.test.js.snap
Expand Up @@ -496,15 +496,6 @@ import g from ".";
import h from "./constants";
import i from "./styles";

// TypeScript import assignments.
import J = require("../parent");
import K = require("./sibling");
export import L = require("an-npm-package");
import M = require("different-npm-package");
import N = Namespace;
export import O = Namespace.A.B.C;
import P = Namespace.A.C;

// Different types of exports:
export { a } from "../..";
export { b } from "/";
Expand Down