carrick 0.3.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +99 -0
- package/README.md +79 -0
- package/bin/carrick.mjs +168 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +37 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +141 -0
- package/dist/contract.js.map +1 -0
- package/dist/diagnostics.d.ts +54 -0
- package/dist/diagnostics.js +199 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hook/post-edit.d.ts +13 -0
- package/dist/hook/post-edit.js +76 -0
- package/dist/hook/post-edit.js.map +1 -0
- package/dist/hook/session-start.d.ts +2 -0
- package/dist/hook/session-start.js +44 -0
- package/dist/hook/session-start.js.map +1 -0
- package/dist/init/identity.d.ts +20 -0
- package/dist/init/identity.js +60 -0
- package/dist/init/identity.js.map +1 -0
- package/dist/init/repos.d.ts +19 -0
- package/dist/init/repos.js +46 -0
- package/dist/init/repos.js.map +1 -0
- package/dist/init/run.d.ts +11 -0
- package/dist/init/run.js +228 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/settings.d.ts +50 -0
- package/dist/init/settings.js +138 -0
- package/dist/init/settings.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.js +35 -0
- package/dist/log.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.js +334 -0
- package/dist/render.js.map +1 -0
- package/dist/root.d.ts +30 -0
- package/dist/root.js +82 -0
- package/dist/root.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +255 -0
- package/dist/server.js.map +1 -0
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +66 -0
- package/dist/templates.js.map +1 -0
- package/package.json +72 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/.lsp.json +14 -0
- package/plugin/hooks/hooks.json +27 -0
- package/sidecar/dist/src/bundler.d.ts +141 -0
- package/sidecar/dist/src/bundler.js +680 -0
- package/sidecar/dist/src/capture/anchors.d.ts +61 -0
- package/sidecar/dist/src/capture/anchors.js +1132 -0
- package/sidecar/dist/src/capture/api.d.ts +378 -0
- package/sidecar/dist/src/capture/api.js +10 -0
- package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
- package/sidecar/dist/src/capture/augmentations.js +60 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
- package/sidecar/dist/src/capture/check-classify.js +189 -0
- package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
- package/sidecar/dist/src/capture/check-deep.js +91 -0
- package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
- package/sidecar/dist/src/capture/check-poison.js +155 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
- package/sidecar/dist/src/capture/check-probe.js +147 -0
- package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
- package/sidecar/dist/src/capture/check-scrub.js +71 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
- package/sidecar/dist/src/capture/check-workspace.js +218 -0
- package/sidecar/dist/src/capture/check.d.ts +39 -0
- package/sidecar/dist/src/capture/check.js +450 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
- package/sidecar/dist/src/capture/deep-walk.js +243 -0
- package/sidecar/dist/src/capture/index.d.ts +36 -0
- package/sidecar/dist/src/capture/index.js +477 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
- package/sidecar/dist/src/capture/lockfile.js +490 -0
- package/sidecar/dist/src/capture/machinery.d.ts +59 -0
- package/sidecar/dist/src/capture/machinery.js +160 -0
- package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
- package/sidecar/dist/src/capture/node-builder.js +123 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
- package/sidecar/dist/src/capture/self-check.d.ts +38 -0
- package/sidecar/dist/src/capture/self-check.js +317 -0
- package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
- package/sidecar/dist/src/capture/specifiers.js +56 -0
- package/sidecar/dist/src/definition-resolver.d.ts +57 -0
- package/sidecar/dist/src/definition-resolver.js +153 -0
- package/sidecar/dist/src/index.d.ts +14 -0
- package/sidecar/dist/src/index.js +564 -0
- package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
- package/sidecar/dist/src/monorepo-builder.js +584 -0
- package/sidecar/dist/src/project-loader.d.ts +130 -0
- package/sidecar/dist/src/project-loader.js +399 -0
- package/sidecar/dist/src/type-inferrer.d.ts +940 -0
- package/sidecar/dist/src/type-inferrer.js +3540 -0
- package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
- package/sidecar/dist/src/type-structural-expander.js +283 -0
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
- package/sidecar/dist/src/types.d.ts +669 -0
- package/sidecar/dist/src/types.js +5 -0
- package/sidecar/dist/src/validators.d.ts +2214 -0
- package/sidecar/dist/src/validators.js +336 -0
- package/sidecar/package.json +6 -0
- package/templates/carrick.json +7 -0
- package/templates/carrick.yml +45 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural type expansion — renders a ts-morph `Type` as fully-inlined
|
|
3
|
+
* structural text, with every named object/interface member expanded to its
|
|
4
|
+
* member structure recursively.
|
|
5
|
+
*
|
|
6
|
+
* `Type.getText()` does NOT inline named members: the compiler prints a
|
|
7
|
+
* referenced type by its symbol name when that symbol is in scope
|
|
8
|
+
* (`total: Money`, not `total: { amountCents: number; currency: string }`).
|
|
9
|
+
* That is fine inside a single project, but a cross-repo bundle carries only
|
|
10
|
+
* the alias lines — no source declarations — so a named reference is a
|
|
11
|
+
* dangling identifier that resolves to `any` downstream. Expanding the shape
|
|
12
|
+
* structurally puts the real members in the bundle so the type checker can
|
|
13
|
+
* compare them.
|
|
14
|
+
*
|
|
15
|
+
* Object/interface types are expanded to their members; primitives, literals,
|
|
16
|
+
* library types (`Date`, `Promise`, tuples, …) and functions stay by name.
|
|
17
|
+
* Bounded recursion + a per-branch cycle set guard against blow-ups; any type
|
|
18
|
+
* that can't be safely expanded falls back to the non-expanded text rather
|
|
19
|
+
* than throwing.
|
|
20
|
+
*
|
|
21
|
+
* Union and intersection members are printed in a canonical order that does not
|
|
22
|
+
* depend on when the checker created each member type — see `orderMembers`
|
|
23
|
+
* (carrick#735), which the depth backstop applies too, and which `namedText`
|
|
24
|
+
* applies as text to everything the walk hands back to the compiler's own
|
|
25
|
+
* print (carrick#775).
|
|
26
|
+
*
|
|
27
|
+
* Shared by `definition-resolver.ts` (bundle alias resolution) and
|
|
28
|
+
* `type-inferrer.ts` (consumer-side inference), so both paths emit the same
|
|
29
|
+
* structural form rather than a dangling name.
|
|
30
|
+
*/
|
|
31
|
+
import { type Type } from 'ts-morph';
|
|
32
|
+
/**
|
|
33
|
+
* Bound on the structural-expansion recursion. Deep enough for every realistic
|
|
34
|
+
* request/response shape; a backstop against pathological/recursive types the
|
|
35
|
+
* cycle set somehow misses.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_EXPANSION_DEPTH = 12;
|
|
38
|
+
/**
|
|
39
|
+
* Recursively render a `Type` as fully-inlined structural text.
|
|
40
|
+
*
|
|
41
|
+
* Named object/interface types are expanded to their member structure;
|
|
42
|
+
* primitives, literals, library types (`Date`, `Promise`, tuples, …) and
|
|
43
|
+
* functions stay by name. The `seen` set (object type ids on the current
|
|
44
|
+
* branch) breaks reference cycles; `depth` is a hard backstop.
|
|
45
|
+
*/
|
|
46
|
+
export declare function expandTypeStructural(type: Type, seen?: Set<number>, depth?: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Non-expanded text for a type. Passes `undefined` as the enclosing node so
|
|
49
|
+
* the compiler can't throw on an invalid node context (tuples and some
|
|
50
|
+
* generic instantiations do), falling back to the bare `getText()` and
|
|
51
|
+
* finally to `unknown` so a single bad type never aborts the whole resolve.
|
|
52
|
+
*
|
|
53
|
+
* This is the ONE place the walk hands a subtree back to the compiler's own
|
|
54
|
+
* print — for a library type, a type with no properties to walk, a tuple, a
|
|
55
|
+
* function, a cycle, or the depth backstop. Everything inside that print is in
|
|
56
|
+
* type-id order, which is creation order, so the unions it contains are put in
|
|
57
|
+
* the same canonical order the walk gives the ones it renders itself
|
|
58
|
+
* (carrick#775). Doing it here rather than at each caller means no bail-out
|
|
59
|
+
* path can print a union one way while the walk prints it another.
|
|
60
|
+
*/
|
|
61
|
+
export declare function namedText(type: Type): string;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural type expansion — renders a ts-morph `Type` as fully-inlined
|
|
3
|
+
* structural text, with every named object/interface member expanded to its
|
|
4
|
+
* member structure recursively.
|
|
5
|
+
*
|
|
6
|
+
* `Type.getText()` does NOT inline named members: the compiler prints a
|
|
7
|
+
* referenced type by its symbol name when that symbol is in scope
|
|
8
|
+
* (`total: Money`, not `total: { amountCents: number; currency: string }`).
|
|
9
|
+
* That is fine inside a single project, but a cross-repo bundle carries only
|
|
10
|
+
* the alias lines — no source declarations — so a named reference is a
|
|
11
|
+
* dangling identifier that resolves to `any` downstream. Expanding the shape
|
|
12
|
+
* structurally puts the real members in the bundle so the type checker can
|
|
13
|
+
* compare them.
|
|
14
|
+
*
|
|
15
|
+
* Object/interface types are expanded to their members; primitives, literals,
|
|
16
|
+
* library types (`Date`, `Promise`, tuples, …) and functions stay by name.
|
|
17
|
+
* Bounded recursion + a per-branch cycle set guard against blow-ups; any type
|
|
18
|
+
* that can't be safely expanded falls back to the non-expanded text rather
|
|
19
|
+
* than throwing.
|
|
20
|
+
*
|
|
21
|
+
* Union and intersection members are printed in a canonical order that does not
|
|
22
|
+
* depend on when the checker created each member type — see `orderMembers`
|
|
23
|
+
* (carrick#735), which the depth backstop applies too, and which `namedText`
|
|
24
|
+
* applies as text to everything the walk hands back to the compiler's own
|
|
25
|
+
* print (carrick#775).
|
|
26
|
+
*
|
|
27
|
+
* Shared by `definition-resolver.ts` (bundle alias resolution) and
|
|
28
|
+
* `type-inferrer.ts` (consumer-side inference), so both paths emit the same
|
|
29
|
+
* structural form rather than a dangling name.
|
|
30
|
+
*/
|
|
31
|
+
import { ts } from 'ts-morph';
|
|
32
|
+
import { canonicalizeUnionsInText } from './type-text-canonicalizer.js';
|
|
33
|
+
/**
|
|
34
|
+
* Bound on the structural-expansion recursion. Deep enough for every realistic
|
|
35
|
+
* request/response shape; a backstop against pathological/recursive types the
|
|
36
|
+
* cycle set somehow misses.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_EXPANSION_DEPTH = 12;
|
|
39
|
+
/**
|
|
40
|
+
* Recursively render a `Type` as fully-inlined structural text.
|
|
41
|
+
*
|
|
42
|
+
* Named object/interface types are expanded to their member structure;
|
|
43
|
+
* primitives, literals, library types (`Date`, `Promise`, tuples, …) and
|
|
44
|
+
* functions stay by name. The `seen` set (object type ids on the current
|
|
45
|
+
* branch) breaks reference cycles; `depth` is a hard backstop.
|
|
46
|
+
*/
|
|
47
|
+
export function expandTypeStructural(type, seen = new Set(), depth = 0) {
|
|
48
|
+
if (depth > MAX_EXPANSION_DEPTH)
|
|
49
|
+
return backstopText(type);
|
|
50
|
+
// Primitives & literals: nothing to inline.
|
|
51
|
+
if (type.isString() ||
|
|
52
|
+
type.isNumber() ||
|
|
53
|
+
type.isBoolean() ||
|
|
54
|
+
type.isBooleanLiteral() ||
|
|
55
|
+
type.isUndefined() ||
|
|
56
|
+
type.isNull() ||
|
|
57
|
+
type.isVoid() ||
|
|
58
|
+
type.isAny() ||
|
|
59
|
+
type.isUnknown() ||
|
|
60
|
+
type.isNever() ||
|
|
61
|
+
type.isStringLiteral() ||
|
|
62
|
+
type.isNumberLiteral() ||
|
|
63
|
+
type.isEnumLiteral()) {
|
|
64
|
+
return namedText(type);
|
|
65
|
+
}
|
|
66
|
+
// Unions / intersections: expand each member, in canonical order.
|
|
67
|
+
if (type.isUnion()) {
|
|
68
|
+
return canonicalMembers(type.getUnionTypes(), seen, depth).join(' | ');
|
|
69
|
+
}
|
|
70
|
+
if (type.isIntersection()) {
|
|
71
|
+
return canonicalMembers(type.getIntersectionTypes(), seen, depth).join(' & ');
|
|
72
|
+
}
|
|
73
|
+
// Tuples are array-like but must keep their `[a, b]` shape, not be walked
|
|
74
|
+
// as objects (which explodes into `Array.prototype`). Handle before arrays.
|
|
75
|
+
if (isTuple(type)) {
|
|
76
|
+
return namedText(type);
|
|
77
|
+
}
|
|
78
|
+
if (type.isArray()) {
|
|
79
|
+
const element = type.getArrayElementType();
|
|
80
|
+
if (!element)
|
|
81
|
+
return namedText(type);
|
|
82
|
+
const inner = expandTypeStructural(element, seen, depth + 1);
|
|
83
|
+
// Parenthesise a union/intersection element so `(A | B)[]` doesn't misparse
|
|
84
|
+
// as `A | B[]`. Decide from the TYPE, not the string: a single object
|
|
85
|
+
// literal like `{ a: A | B }` is NOT a union and must not be parenthesised,
|
|
86
|
+
// and a union led by an object literal (`{ a: string } | null`) MUST be.
|
|
87
|
+
const needsParens = element.isUnion() || element.isIntersection();
|
|
88
|
+
return needsParens ? `(${inner})[]` : `${inner}[]`;
|
|
89
|
+
}
|
|
90
|
+
// Library / built-in types (Date, Promise, RegExp, …): keep by name.
|
|
91
|
+
if (isLibraryType(type)) {
|
|
92
|
+
return namedText(type);
|
|
93
|
+
}
|
|
94
|
+
// Callable/constructable object types (functions): keep by name; their
|
|
95
|
+
// structural form is the signature, which `getText` already renders.
|
|
96
|
+
if (type.getCallSignatures().length > 0 ||
|
|
97
|
+
type.getConstructSignatures().length > 0) {
|
|
98
|
+
return namedText(type);
|
|
99
|
+
}
|
|
100
|
+
if (type.isObject() || type.isInterface()) {
|
|
101
|
+
const id = type.compilerType.id;
|
|
102
|
+
if (id != null && seen.has(id))
|
|
103
|
+
return namedText(type);
|
|
104
|
+
const nextSeen = id != null ? new Set(seen).add(id) : seen;
|
|
105
|
+
const props = type.getProperties();
|
|
106
|
+
if (props.length === 0)
|
|
107
|
+
return namedText(type);
|
|
108
|
+
const parts = props.map((prop) => expandProperty(prop, nextSeen, depth));
|
|
109
|
+
return `{ ${parts.join('; ')}; }`;
|
|
110
|
+
}
|
|
111
|
+
return namedText(type);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The print for a type the recursion bound stopped at (carrick#775).
|
|
115
|
+
*
|
|
116
|
+
* `namedText` is the compiler's own print, and for a union that print is in
|
|
117
|
+
* type-id order — the creation-order artefact `canonicalMembers` exists to
|
|
118
|
+
* remove. Stopping the recursion must not also stop the normalisation: a union
|
|
119
|
+
* at depth 13 is as much a set as one at depth 2, and a diff reader comparing
|
|
120
|
+
* two `expanded_definition` strings cannot tell which depth a member came from.
|
|
121
|
+
*
|
|
122
|
+
* So the members are rendered by NAME — no recursion, which is the whole point
|
|
123
|
+
* of the bound — and put in the same canonical order as the expanded path.
|
|
124
|
+
* Everything else falls through to the compiler's print unchanged.
|
|
125
|
+
*/
|
|
126
|
+
function backstopText(type) {
|
|
127
|
+
if (type.isUnion()) {
|
|
128
|
+
return orderMembers(type.getUnionTypes(), namedText).join(' | ');
|
|
129
|
+
}
|
|
130
|
+
if (type.isIntersection()) {
|
|
131
|
+
return orderMembers(type.getIntersectionTypes(), namedText).join(' & ');
|
|
132
|
+
}
|
|
133
|
+
return namedText(type);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Render every member of a union/intersection and put them in a canonical
|
|
137
|
+
* order (carrick#735).
|
|
138
|
+
*
|
|
139
|
+
* The compiler stores a union's constituents sorted by type id, and ids are
|
|
140
|
+
* handed out in the order the checker CREATES types. A literal type is created
|
|
141
|
+
* the first time some declaration is checked, and it is then interned — so on
|
|
142
|
+
* a tree where `"PENDING" | "TIMED_OUT"` and `"TIMED_OUT" | "PENDING"` are both
|
|
143
|
+
* declared, whichever declaration is reached first decides the printed order of
|
|
144
|
+
* BOTH. Two runs over an unchanged tree can therefore print one type two ways.
|
|
145
|
+
*
|
|
146
|
+
* The order carries no meaning: a union is a set, and the check phase compares
|
|
147
|
+
* these strings by typechecking them, which is order-insensitive. So we impose
|
|
148
|
+
* one — every member sorts by its own rendered text, compared by UTF-16 code
|
|
149
|
+
* unit. That is a pure function of the members, with no dependence on when the
|
|
150
|
+
* checker happened to create any of them.
|
|
151
|
+
*
|
|
152
|
+
* #735 carved out an exception for the intrinsics (`string`, `null`, `number`,
|
|
153
|
+
* …), keeping them ahead of the rest in compiler-id order on the grounds that
|
|
154
|
+
* their ids are fixed before any source file is read, so `string | null` would
|
|
155
|
+
* still print the way the compiler prints it. The second half of that is not
|
|
156
|
+
* true: the compiler's printer does not use id order. On a union of
|
|
157
|
+
* `null | number` the checker's ids give `null` first and `typeToString` prints
|
|
158
|
+
* `number | null`, so keeping id order reproduced neither the compiler's print
|
|
159
|
+
* nor — once carrick#775 put the compiler's own prints under the same rule —
|
|
160
|
+
* the other path's. One rule for every member is the only way a union prints
|
|
161
|
+
* one way wherever it is rendered, and it costs nothing the exception was
|
|
162
|
+
* actually buying. `type-text-canonicalizer.ts` applies the same rule to text.
|
|
163
|
+
*
|
|
164
|
+
* Ties can only happen between two members that render identically, in which
|
|
165
|
+
* case the joined output is the same whichever way round they go.
|
|
166
|
+
*/
|
|
167
|
+
function canonicalMembers(members, seen, depth) {
|
|
168
|
+
return orderMembers(members, (member) => expandTypeStructural(member, seen, depth + 1));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The canonical order itself, over whatever text `render` gives each member.
|
|
172
|
+
* Shared by the expanded path (`canonicalMembers`, which renders structurally)
|
|
173
|
+
* and the depth backstop (`backstopText`, which renders by name), so one union
|
|
174
|
+
* cannot be ordered two ways depending on how deep it sits.
|
|
175
|
+
*/
|
|
176
|
+
function orderMembers(members, render) {
|
|
177
|
+
const rendered = members.map((member, index) => ({
|
|
178
|
+
index,
|
|
179
|
+
text: render(member),
|
|
180
|
+
}));
|
|
181
|
+
rendered.sort((a, b) => {
|
|
182
|
+
if (a.text !== b.text)
|
|
183
|
+
return a.text < b.text ? -1 : 1;
|
|
184
|
+
return a.index - b.index;
|
|
185
|
+
});
|
|
186
|
+
return rendered.map((entry) => entry.text);
|
|
187
|
+
}
|
|
188
|
+
/** Render a single property as `name[?]: <expanded>`. */
|
|
189
|
+
function expandProperty(prop, seen, depth) {
|
|
190
|
+
const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
191
|
+
const propDecl = prop.getDeclarations()[0];
|
|
192
|
+
// Render the key from the declaration's name node so quoted/computed keys
|
|
193
|
+
// ('x-y', "x y", [Symbol.iterator]) survive as valid TS text rather than
|
|
194
|
+
// being unquoted into invalid output; fall back to the bare symbol name.
|
|
195
|
+
const name = renderPropertyName(prop, propDecl);
|
|
196
|
+
let propType = propDecl
|
|
197
|
+
? prop.getTypeAtLocation(propDecl)
|
|
198
|
+
: prop.getDeclaredType();
|
|
199
|
+
// An optional property's type includes `undefined`; the structural label
|
|
200
|
+
// drops it (`note?: string`, not `note?: string | undefined`).
|
|
201
|
+
if (optional && propType.isUnion()) {
|
|
202
|
+
const nonUndefined = propType
|
|
203
|
+
.getUnionTypes()
|
|
204
|
+
.filter((member) => !member.isUndefined());
|
|
205
|
+
if (nonUndefined.length === 1) {
|
|
206
|
+
propType = nonUndefined[0];
|
|
207
|
+
}
|
|
208
|
+
else if (nonUndefined.length > 1) {
|
|
209
|
+
const inner = canonicalMembers(nonUndefined, seen, depth).join(' | ');
|
|
210
|
+
return `${name}?: ${inner}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const inner = expandTypeStructural(propType, seen, depth + 1);
|
|
214
|
+
return `${name}${optional ? '?' : ''}: ${inner}`;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The property key as valid TS text. Uses the declaration's name node so a
|
|
218
|
+
* quoted (`'x-y'`) or computed (`[Symbol.iterator]`) key keeps its syntax;
|
|
219
|
+
* `Symbol.getName()` would drop the quoting and emit invalid output. Falls
|
|
220
|
+
* back to the bare symbol name when there's no usable name node.
|
|
221
|
+
*/
|
|
222
|
+
function renderPropertyName(prop, decl) {
|
|
223
|
+
const node = decl;
|
|
224
|
+
const text = node?.getNameNode?.()?.getText();
|
|
225
|
+
return text && text.length > 0 ? text : prop.getName();
|
|
226
|
+
}
|
|
227
|
+
/** True for tuple types (`[a, b]`), which must not be walked as objects. */
|
|
228
|
+
function isTuple(type) {
|
|
229
|
+
const compiler = type.compilerType;
|
|
230
|
+
const target = compiler.target ?? compiler;
|
|
231
|
+
return ((target.objectFlags ?? 0) & ts.ObjectFlags.Tuple) !== 0;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* True for types declared in `node_modules` or a TS `lib.*.d.ts` (Date,
|
|
235
|
+
* Promise, RegExp, …). These stay by name rather than being inlined.
|
|
236
|
+
*/
|
|
237
|
+
function isLibraryType(type) {
|
|
238
|
+
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
239
|
+
if (!symbol)
|
|
240
|
+
return false;
|
|
241
|
+
const decls = symbol.getDeclarations();
|
|
242
|
+
if (decls.length === 0)
|
|
243
|
+
return false;
|
|
244
|
+
return decls.some((decl) => {
|
|
245
|
+
const sf = decl.getSourceFile();
|
|
246
|
+
if (sf.isInNodeModules())
|
|
247
|
+
return true;
|
|
248
|
+
return (sf.isDeclarationFile() &&
|
|
249
|
+
// Normalize separators so a Windows `\\` path still matches lib.*.d.ts.
|
|
250
|
+
/(^|\/)lib\.[^/]*\.d\.ts$/.test(sf.getFilePath().replace(/\\/g, '/')));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Non-expanded text for a type. Passes `undefined` as the enclosing node so
|
|
255
|
+
* the compiler can't throw on an invalid node context (tuples and some
|
|
256
|
+
* generic instantiations do), falling back to the bare `getText()` and
|
|
257
|
+
* finally to `unknown` so a single bad type never aborts the whole resolve.
|
|
258
|
+
*
|
|
259
|
+
* This is the ONE place the walk hands a subtree back to the compiler's own
|
|
260
|
+
* print — for a library type, a type with no properties to walk, a tuple, a
|
|
261
|
+
* function, a cycle, or the depth backstop. Everything inside that print is in
|
|
262
|
+
* type-id order, which is creation order, so the unions it contains are put in
|
|
263
|
+
* the same canonical order the walk gives the ones it renders itself
|
|
264
|
+
* (carrick#775). Doing it here rather than at each caller means no bail-out
|
|
265
|
+
* path can print a union one way while the walk prints it another.
|
|
266
|
+
*/
|
|
267
|
+
export function namedText(type) {
|
|
268
|
+
return canonicalizeUnionsInText(compilerText(type));
|
|
269
|
+
}
|
|
270
|
+
/** The compiler's print, with the two fallbacks. */
|
|
271
|
+
function compilerText(type) {
|
|
272
|
+
try {
|
|
273
|
+
return type.getText(undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
try {
|
|
277
|
+
return type.getText();
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return 'unknown';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical union order inside text the COMPILER printed (carrick#775, #760).
|
|
3
|
+
*
|
|
4
|
+
* `expandTypeStructural` walks a type and orders every union it walks
|
|
5
|
+
* (`orderMembers`), so the structural path is stable. But the walk stops at
|
|
6
|
+
* several points and hands the rest of the subtree to `namedText` — the
|
|
7
|
+
* compiler's own print:
|
|
8
|
+
*
|
|
9
|
+
* - library types, which are kept by name (`isLibraryType`), and whose print
|
|
10
|
+
* carries their whole generic instantiation with it;
|
|
11
|
+
* - object types with no named properties (an index signature alone), which
|
|
12
|
+
* have nothing to walk;
|
|
13
|
+
* - tuples, functions, cycles the `seen` set breaks, and the depth backstop.
|
|
14
|
+
*
|
|
15
|
+
* Everything inside one of those prints is in the compiler's own union order,
|
|
16
|
+
* which is type-id order, which is CREATION order — the same instability
|
|
17
|
+
* `orderMembers` exists to remove, one level down where the type API can no
|
|
18
|
+
* longer reach it: at that point the members exist only as text.
|
|
19
|
+
*
|
|
20
|
+
* So they are ordered as text, by the same rule the walk uses: every member
|
|
21
|
+
* sorts by its own rendering, compared by UTF-16 code unit. A union is a set,
|
|
22
|
+
* the check phase compares these strings by typechecking them, and a reader
|
|
23
|
+
* diffing two `expanded_definition` strings cannot tell which side of a
|
|
24
|
+
* `namedText` boundary a member came from — so both sides must order them the
|
|
25
|
+
* same way, which is why `orderMembers` no longer keeps intrinsics in compiler
|
|
26
|
+
* id order (the compiler's printer does not use that order either).
|
|
27
|
+
*
|
|
28
|
+
* The rewrite is deliberately timid. Anything whose meaning could depend on
|
|
29
|
+
* order or on a parse this file does not do — a function type (`=>`), a
|
|
30
|
+
* conditional (`?`) — is returned untouched, and untouched is always a legal
|
|
31
|
+
* answer here because the compiler's print is already valid TypeScript.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Put every union in `text` into canonical order, at every nesting level.
|
|
35
|
+
*
|
|
36
|
+
* Returns `text` unchanged whenever the shape is one this file does not parse
|
|
37
|
+
* (see the module comment). Never throws: a type print is arbitrary text, and
|
|
38
|
+
* a definition resolve must not fail because one member confused a scanner.
|
|
39
|
+
*/
|
|
40
|
+
export declare function canonicalizeUnionsInText(text: string): string;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical union order inside text the COMPILER printed (carrick#775, #760).
|
|
3
|
+
*
|
|
4
|
+
* `expandTypeStructural` walks a type and orders every union it walks
|
|
5
|
+
* (`orderMembers`), so the structural path is stable. But the walk stops at
|
|
6
|
+
* several points and hands the rest of the subtree to `namedText` — the
|
|
7
|
+
* compiler's own print:
|
|
8
|
+
*
|
|
9
|
+
* - library types, which are kept by name (`isLibraryType`), and whose print
|
|
10
|
+
* carries their whole generic instantiation with it;
|
|
11
|
+
* - object types with no named properties (an index signature alone), which
|
|
12
|
+
* have nothing to walk;
|
|
13
|
+
* - tuples, functions, cycles the `seen` set breaks, and the depth backstop.
|
|
14
|
+
*
|
|
15
|
+
* Everything inside one of those prints is in the compiler's own union order,
|
|
16
|
+
* which is type-id order, which is CREATION order — the same instability
|
|
17
|
+
* `orderMembers` exists to remove, one level down where the type API can no
|
|
18
|
+
* longer reach it: at that point the members exist only as text.
|
|
19
|
+
*
|
|
20
|
+
* So they are ordered as text, by the same rule the walk uses: every member
|
|
21
|
+
* sorts by its own rendering, compared by UTF-16 code unit. A union is a set,
|
|
22
|
+
* the check phase compares these strings by typechecking them, and a reader
|
|
23
|
+
* diffing two `expanded_definition` strings cannot tell which side of a
|
|
24
|
+
* `namedText` boundary a member came from — so both sides must order them the
|
|
25
|
+
* same way, which is why `orderMembers` no longer keeps intrinsics in compiler
|
|
26
|
+
* id order (the compiler's printer does not use that order either).
|
|
27
|
+
*
|
|
28
|
+
* The rewrite is deliberately timid. Anything whose meaning could depend on
|
|
29
|
+
* order or on a parse this file does not do — a function type (`=>`), a
|
|
30
|
+
* conditional (`?`) — is returned untouched, and untouched is always a legal
|
|
31
|
+
* answer here because the compiler's print is already valid TypeScript.
|
|
32
|
+
*/
|
|
33
|
+
/** Bracket pairs whose interiors are descended into. */
|
|
34
|
+
const CLOSERS = { '{': '}', '<': '>', '(': ')', '[': ']' };
|
|
35
|
+
/**
|
|
36
|
+
* Put every union in `text` into canonical order, at every nesting level.
|
|
37
|
+
*
|
|
38
|
+
* Returns `text` unchanged whenever the shape is one this file does not parse
|
|
39
|
+
* (see the module comment). Never throws: a type print is arbitrary text, and
|
|
40
|
+
* a definition resolve must not fail because one member confused a scanner.
|
|
41
|
+
*/
|
|
42
|
+
export function canonicalizeUnionsInText(text) {
|
|
43
|
+
try {
|
|
44
|
+
return canonicalizeType(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Canonicalise one TYPE fragment — not a `key: value` member. */
|
|
51
|
+
function canonicalizeType(text) {
|
|
52
|
+
const trimmed = text.trim();
|
|
53
|
+
if (trimmed.length === 0)
|
|
54
|
+
return text;
|
|
55
|
+
// A function type or a conditional type is left exactly as printed: both
|
|
56
|
+
// carry a top-level `|` that is not a union separator at this level
|
|
57
|
+
// (`() => a | b` is one function returning a union, `T extends U ? a : b`
|
|
58
|
+
// has unions inside its branches), and neither is worth the parse.
|
|
59
|
+
if (hasTopLevel(trimmed, ['=>', '?']))
|
|
60
|
+
return text;
|
|
61
|
+
const members = splitTopLevel(trimmed, '|');
|
|
62
|
+
if (members.length > 1) {
|
|
63
|
+
return orderTextMembers(members.map((m) => canonicalizeType(m))).join(' | ');
|
|
64
|
+
}
|
|
65
|
+
return descendIntoBrackets(trimmed);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Rewrite the interiors of the bracket groups in a fragment that is not itself
|
|
69
|
+
* a union: an object body's member types, a generic argument list, a tuple's
|
|
70
|
+
* elements, a parenthesised type.
|
|
71
|
+
*/
|
|
72
|
+
function descendIntoBrackets(text) {
|
|
73
|
+
let out = '';
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < text.length) {
|
|
76
|
+
const ch = text[i];
|
|
77
|
+
const quoted = readQuoted(text, i);
|
|
78
|
+
if (quoted !== null) {
|
|
79
|
+
out += text.slice(i, quoted);
|
|
80
|
+
i = quoted;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const closer = CLOSERS[ch];
|
|
84
|
+
if (closer === undefined) {
|
|
85
|
+
out += ch;
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const end = matchBracket(text, i);
|
|
90
|
+
if (end === -1) {
|
|
91
|
+
// Unbalanced (a truncated print): copy the rest verbatim.
|
|
92
|
+
out += text.slice(i);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
const inner = text.slice(i + 1, end);
|
|
96
|
+
out += ch + canonicalizeBody(inner, ch) + closer;
|
|
97
|
+
i = end + 1;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Canonicalise the inside of one bracket group.
|
|
103
|
+
*
|
|
104
|
+
* `{ ... }` is a list of `key: type` members separated by `;` (or `,`), and
|
|
105
|
+
* only the type half may be rewritten — a key is not a type, and `a: X | Y`
|
|
106
|
+
* split on `|` would produce the nonsense member `a: X`. Every other bracket
|
|
107
|
+
* holds a comma-separated list of types.
|
|
108
|
+
*
|
|
109
|
+
* Each piece is spliced back at its own offsets, so a body with nothing to
|
|
110
|
+
* reorder comes out byte-identical to the compiler's print — whitespace,
|
|
111
|
+
* separators and all.
|
|
112
|
+
*/
|
|
113
|
+
function canonicalizeBody(inner, opener) {
|
|
114
|
+
const separator = opener === '{' ? ';' : ',';
|
|
115
|
+
const spans = splitTopLevelSpans(inner, separator);
|
|
116
|
+
let out = '';
|
|
117
|
+
let cursor = 0;
|
|
118
|
+
for (const span of spans) {
|
|
119
|
+
const piece = inner.slice(span.start, span.end);
|
|
120
|
+
const trimmed = piece.trim();
|
|
121
|
+
if (trimmed.length > 0) {
|
|
122
|
+
const rewritten = canonicalizeMember(trimmed);
|
|
123
|
+
if (rewritten !== trimmed) {
|
|
124
|
+
const at = span.start + piece.indexOf(trimmed);
|
|
125
|
+
out += inner.slice(cursor, at) + rewritten;
|
|
126
|
+
cursor = at + trimmed.length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out + inner.slice(cursor);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* One list element: `key: type`, `name?: type`, `[x: string]: type`, or a bare
|
|
134
|
+
* type. Splits at the element's own `:` — the one at depth zero, which an
|
|
135
|
+
* index signature's `[x: string]` and a nested object both sit below — and
|
|
136
|
+
* rewrites only what follows it.
|
|
137
|
+
*/
|
|
138
|
+
function canonicalizeMember(member) {
|
|
139
|
+
const colon = indexOfTopLevel(member, ':');
|
|
140
|
+
if (colon === -1)
|
|
141
|
+
return canonicalizeType(member);
|
|
142
|
+
const key = member.slice(0, colon + 1);
|
|
143
|
+
const value = member.slice(colon + 1);
|
|
144
|
+
const leading = value.length - value.trimStart().length;
|
|
145
|
+
const trailing = value.length - value.trimEnd().length;
|
|
146
|
+
return (key +
|
|
147
|
+
value.slice(0, leading) +
|
|
148
|
+
canonicalizeType(value.trim()) +
|
|
149
|
+
value.slice(value.length - trailing));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The canonical order, as text: every member by its own rendering, compared by
|
|
153
|
+
* UTF-16 code unit. This is `orderMembers` in `type-structural-expander.ts`,
|
|
154
|
+
* over the renderings that are all this side has — the two must agree member
|
|
155
|
+
* for member, or one union prints two ways depending on whether the walk or
|
|
156
|
+
* the compiler rendered it.
|
|
157
|
+
*/
|
|
158
|
+
function orderTextMembers(members) {
|
|
159
|
+
return members
|
|
160
|
+
.map((text, index) => ({ text: text.trim(), index }))
|
|
161
|
+
.sort((a, b) => {
|
|
162
|
+
if (a.text !== b.text)
|
|
163
|
+
return a.text < b.text ? -1 : 1;
|
|
164
|
+
return a.index - b.index;
|
|
165
|
+
})
|
|
166
|
+
.map((entry) => entry.text);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The spans between depth-zero occurrences of `separator`. Depth counts the
|
|
170
|
+
* four bracket pairs; quoted runs are skipped whole, so a separator inside a
|
|
171
|
+
* string literal never splits.
|
|
172
|
+
*/
|
|
173
|
+
function splitTopLevelSpans(text, separator) {
|
|
174
|
+
const spans = [];
|
|
175
|
+
let start = 0;
|
|
176
|
+
let depth = 0;
|
|
177
|
+
let i = 0;
|
|
178
|
+
while (i < text.length) {
|
|
179
|
+
const quoted = readQuoted(text, i);
|
|
180
|
+
if (quoted !== null) {
|
|
181
|
+
i = quoted;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const ch = text[i];
|
|
185
|
+
if (CLOSERS[ch] !== undefined) {
|
|
186
|
+
depth++;
|
|
187
|
+
}
|
|
188
|
+
else if (ch === '}' || ch === ')' || ch === ']' || isTypeArgClose(text, i)) {
|
|
189
|
+
depth = Math.max(0, depth - 1);
|
|
190
|
+
}
|
|
191
|
+
else if (ch === separator && depth === 0) {
|
|
192
|
+
spans.push({ start, end: i });
|
|
193
|
+
start = i + 1;
|
|
194
|
+
}
|
|
195
|
+
i++;
|
|
196
|
+
}
|
|
197
|
+
spans.push({ start, end: text.length });
|
|
198
|
+
return spans;
|
|
199
|
+
}
|
|
200
|
+
/** Split at every depth-zero occurrence of `separator`. */
|
|
201
|
+
function splitTopLevel(text, separator) {
|
|
202
|
+
return splitTopLevelSpans(text, separator).map((span) => text.slice(span.start, span.end));
|
|
203
|
+
}
|
|
204
|
+
/** Index of the first depth-zero `char`, or -1. */
|
|
205
|
+
function indexOfTopLevel(text, char) {
|
|
206
|
+
const parts = splitTopLevel(text, char);
|
|
207
|
+
return parts.length > 1 ? parts[0].length : -1;
|
|
208
|
+
}
|
|
209
|
+
/** True when any of `tokens` occurs at depth zero. */
|
|
210
|
+
function hasTopLevel(text, tokens) {
|
|
211
|
+
for (const token of tokens) {
|
|
212
|
+
if (token === '=>') {
|
|
213
|
+
// Split on '>' would fight the generic-close rule; look for the arrow
|
|
214
|
+
// directly, at depth zero, by walking with the same scanner.
|
|
215
|
+
if (indexOfTopLevelArrow(text) !== -1)
|
|
216
|
+
return true;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (indexOfTopLevel(text, token) !== -1)
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
/** Index of a depth-zero `=>`, or -1. */
|
|
225
|
+
function indexOfTopLevelArrow(text) {
|
|
226
|
+
let depth = 0;
|
|
227
|
+
let i = 0;
|
|
228
|
+
while (i < text.length) {
|
|
229
|
+
const quoted = readQuoted(text, i);
|
|
230
|
+
if (quoted !== null) {
|
|
231
|
+
i = quoted;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const ch = text[i];
|
|
235
|
+
if (ch === '=' && text[i + 1] === '>') {
|
|
236
|
+
if (depth === 0)
|
|
237
|
+
return i;
|
|
238
|
+
i += 2;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (CLOSERS[ch] !== undefined)
|
|
242
|
+
depth++;
|
|
243
|
+
else if (ch === '}' || ch === ')' || ch === ']' || isTypeArgClose(text, i))
|
|
244
|
+
depth = Math.max(0, depth - 1);
|
|
245
|
+
i++;
|
|
246
|
+
}
|
|
247
|
+
return -1;
|
|
248
|
+
}
|
|
249
|
+
/** A `>` that closes a type-argument list, rather than the tail of an arrow. */
|
|
250
|
+
function isTypeArgClose(text, i) {
|
|
251
|
+
return text[i] === '>' && text[i - 1] !== '=';
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* If a quoted run (string literal or template literal) starts at `i`, the
|
|
255
|
+
* index just past its closing quote; otherwise null. Escapes are honoured so a
|
|
256
|
+
* `"a\"|b"` literal is never split.
|
|
257
|
+
*/
|
|
258
|
+
function readQuoted(text, i) {
|
|
259
|
+
const quote = text[i];
|
|
260
|
+
if (quote !== '"' && quote !== "'" && quote !== '`')
|
|
261
|
+
return null;
|
|
262
|
+
let j = i + 1;
|
|
263
|
+
while (j < text.length) {
|
|
264
|
+
if (text[j] === '\\') {
|
|
265
|
+
j += 2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (text[j] === quote)
|
|
269
|
+
return j + 1;
|
|
270
|
+
j++;
|
|
271
|
+
}
|
|
272
|
+
return text.length;
|
|
273
|
+
}
|
|
274
|
+
/** Index of the bracket closing the one at `open`, or -1 when unbalanced. */
|
|
275
|
+
function matchBracket(text, open) {
|
|
276
|
+
const closer = CLOSERS[text[open]];
|
|
277
|
+
let depth = 0;
|
|
278
|
+
let i = open;
|
|
279
|
+
while (i < text.length) {
|
|
280
|
+
const quoted = readQuoted(text, i);
|
|
281
|
+
if (quoted !== null) {
|
|
282
|
+
i = quoted;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const ch = text[i];
|
|
286
|
+
if (ch === text[open]) {
|
|
287
|
+
depth++;
|
|
288
|
+
}
|
|
289
|
+
else if (ch === closer && !(closer === '>' && text[i - 1] === '=')) {
|
|
290
|
+
depth--;
|
|
291
|
+
if (depth === 0)
|
|
292
|
+
return i;
|
|
293
|
+
}
|
|
294
|
+
i++;
|
|
295
|
+
}
|
|
296
|
+
return -1;
|
|
297
|
+
}
|