carrick 0.3.85 → 0.3.87
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/dist/contract.d.ts +9 -5
- package/dist/contract.js.map +1 -1
- package/dist/init/hosted.d.ts +27 -1
- package/dist/init/hosted.js +53 -18
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/output.d.ts +5 -3
- package/dist/init/output.js +12 -4
- package/dist/init/output.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/bundler.d.ts +5 -0
- package/sidecar/dist/src/bundler.js +13 -3
- package/sidecar/dist/src/capture/machinery.d.ts +1 -1
- package/sidecar/dist/src/capture/machinery.js +1 -1
- package/sidecar/dist/src/capture/repair-dangling.d.ts +46 -0
- package/sidecar/dist/src/capture/repair-dangling.js +162 -0
- package/sidecar/dist/src/capture/self-check.js +61 -5
- package/sidecar/dist/src/definition-resolver.d.ts +7 -0
- package/sidecar/dist/src/definition-resolver.js +43 -4
- package/sidecar/dist/src/origin.d.ts +45 -0
- package/sidecar/dist/src/origin.js +86 -0
- package/sidecar/dist/src/type-inferrer.d.ts +18 -31
- package/sidecar/dist/src/type-inferrer.js +31 -78
- package/sidecar/dist/src/type-structural-expander.d.ts +38 -6
- package/sidecar/dist/src/type-structural-expander.js +61 -32
- package/templates/skills/carrick-census.md +5 -0
- package/templates/skills/carrick-impact.md +11 -4
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
* function's return type.
|
|
19
19
|
*/
|
|
20
20
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
21
|
-
import * as fs from 'node:fs';
|
|
22
|
-
import * as path from 'node:path';
|
|
23
21
|
import { validateInferRequestItem } from './validators.js';
|
|
22
|
+
import { isExternalOrigin } from './origin.js';
|
|
24
23
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
25
24
|
/**
|
|
26
25
|
* TS/lib globals and primitives that must never be emitted as a deterministic
|
|
@@ -152,75 +151,6 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
|
|
|
152
151
|
'statusText',
|
|
153
152
|
'headers',
|
|
154
153
|
]);
|
|
155
|
-
/**
|
|
156
|
-
* True when a declaration's source file is runtime/library origin rather than
|
|
157
|
-
* user source. Four answers, in order:
|
|
158
|
-
*
|
|
159
|
-
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
160
|
-
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
161
|
-
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
162
|
-
* guess about anyone else's.
|
|
163
|
-
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
164
|
-
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
165
|
-
* 3. An install under a `node_modules` segment, however it entered the
|
|
166
|
-
* program (an import, or a root the loader registered).
|
|
167
|
-
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
168
|
-
* This is the graph-backed answer for a project whose resolution does not
|
|
169
|
-
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
170
|
-
* types from its own cache, a path with no `node_modules` segment, and
|
|
171
|
-
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
172
|
-
* the graph, which the compiler records on the file. Nothing crosses the
|
|
173
|
-
* capture seam; both programs are built with that host. One exclusion: a
|
|
174
|
-
* workspace package reached through a `node_modules` symlink is also
|
|
175
|
-
* marked external by the compiler but is the user's own source, so a file
|
|
176
|
-
* inside the checkout (the nearest `.git` above the service root) that
|
|
177
|
-
* carries no `node_modules` segment stays user source.
|
|
178
|
-
*
|
|
179
|
-
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
180
|
-
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
181
|
-
* the pair on a real program.
|
|
182
|
-
*/
|
|
183
|
-
export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
184
|
-
const file = sourceFile.fileName.replace(/\\/g, '/');
|
|
185
|
-
if (file.includes('/.carrick/deno/')) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
if (program.isSourceFileDefaultLibrary(sourceFile)) {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
if (file.includes('/node_modules/')) {
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
|
|
195
|
-
}
|
|
196
|
-
const checkoutRoots = new Map();
|
|
197
|
-
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
198
|
-
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
199
|
-
* root itself. */
|
|
200
|
-
function checkoutRootOf(repoRoot) {
|
|
201
|
-
const key = path.resolve(repoRoot);
|
|
202
|
-
const cached = checkoutRoots.get(key);
|
|
203
|
-
if (cached)
|
|
204
|
-
return cached;
|
|
205
|
-
let dir = key;
|
|
206
|
-
let root = key;
|
|
207
|
-
for (;;) {
|
|
208
|
-
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
209
|
-
root = dir;
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
const parent = path.dirname(dir);
|
|
213
|
-
if (parent === dir)
|
|
214
|
-
break;
|
|
215
|
-
dir = parent;
|
|
216
|
-
}
|
|
217
|
-
checkoutRoots.set(key, root);
|
|
218
|
-
return root;
|
|
219
|
-
}
|
|
220
|
-
function isInsideCheckout(file, repoRoot) {
|
|
221
|
-
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
222
|
-
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
223
|
-
}
|
|
224
154
|
/**
|
|
225
155
|
* How far the response-helper recovery (carrick#631) descends through nested
|
|
226
156
|
* calls looking for the payload argument. `cors(request, json(payload))` needs
|
|
@@ -304,6 +234,18 @@ export class TypeInferrer {
|
|
|
304
234
|
this.packageOf = options.packageOf;
|
|
305
235
|
this.repoRoot = options.repoRoot;
|
|
306
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* What the structural printer needs to tell the user's own declarations from
|
|
239
|
+
* the runtime's and its packages' — the same program and service root this
|
|
240
|
+
* class asks `isExternalOrigin` about machinery, so the two layers cannot
|
|
241
|
+
* classify one declaration two ways.
|
|
242
|
+
*/
|
|
243
|
+
expandOrigin() {
|
|
244
|
+
return {
|
|
245
|
+
program: this.project.getProgram().compilerObject,
|
|
246
|
+
repoRoot: this.repoRoot,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
307
249
|
/**
|
|
308
250
|
* Infer types for the given requests
|
|
309
251
|
*
|
|
@@ -2610,7 +2552,7 @@ export class TypeInferrer {
|
|
|
2610
2552
|
const fallback = typeNode.getText();
|
|
2611
2553
|
try {
|
|
2612
2554
|
const annotationType = this.unwrapPromiseType(typeNode.getType());
|
|
2613
|
-
const expanded = expandTypeStructural(annotationType,
|
|
2555
|
+
const expanded = expandTypeStructural(annotationType, this.expandOrigin(), { wire });
|
|
2614
2556
|
// Only prefer the structural form when expansion actually inlined an
|
|
2615
2557
|
// object shape; otherwise keep the annotation text (e.g. a bare
|
|
2616
2558
|
// primitive or a library type the expander leaves by name). A wire
|
|
@@ -2618,7 +2560,7 @@ export class TypeInferrer {
|
|
|
2618
2560
|
// `Date` annotation sends a string (carrick#1163).
|
|
2619
2561
|
if (expanded.startsWith('{'))
|
|
2620
2562
|
return expanded;
|
|
2621
|
-
return wire === 'json' && expanded !== expandTypeStructural(annotationType)
|
|
2563
|
+
return wire === 'json' && expanded !== expandTypeStructural(annotationType, this.expandOrigin())
|
|
2622
2564
|
? expanded
|
|
2623
2565
|
: fallback;
|
|
2624
2566
|
}
|
|
@@ -2644,12 +2586,12 @@ export class TypeInferrer {
|
|
|
2644
2586
|
*/
|
|
2645
2587
|
expandResolvedTypeStructural(type, fallback, wire = 'declared') {
|
|
2646
2588
|
try {
|
|
2647
|
-
const expanded = expandTypeStructural(type,
|
|
2589
|
+
const expanded = expandTypeStructural(type, this.expandOrigin(), { wire });
|
|
2648
2590
|
// A wire print that differs from the declared one is the answer even
|
|
2649
2591
|
// without an inlined object: a bare `Date` payload sends a string.
|
|
2650
2592
|
if (wire === 'json' &&
|
|
2651
2593
|
!expanded.includes('{') &&
|
|
2652
|
-
expanded !== expandTypeStructural(type)) {
|
|
2594
|
+
expanded !== expandTypeStructural(type, this.expandOrigin())) {
|
|
2653
2595
|
return expanded;
|
|
2654
2596
|
}
|
|
2655
2597
|
// Prefer the expanded form whenever an object got inlined, not only when
|
|
@@ -3029,7 +2971,16 @@ export class TypeInferrer {
|
|
|
3029
2971
|
* True when an object literal argument is response INIT rather than a body:
|
|
3030
2972
|
* every property it declares is one the standard `ResponseInit` declares
|
|
3031
2973
|
* (`status`, `statusText`, `headers`), and it states at least one of them as
|
|
3032
|
-
* init really does — a
|
|
2974
|
+
* init really does — a status in the HTTP range, or headers.
|
|
2975
|
+
*
|
|
2976
|
+
* The status does NOT have to be a literal code. A route that carries its
|
|
2977
|
+
* outcome in a value writes `new Response(body, { status: result.status })`,
|
|
2978
|
+
* where the source fixes no code and `statedStatusCodes` answers
|
|
2979
|
+
* `'variable'` — status-shaped, just not pinned. Requiring a literal there
|
|
2980
|
+
* left the whole init object reading as a body, so an endpoint whose payload
|
|
2981
|
+
* this layer does not publish (a string body) published `{ status: number }`
|
|
2982
|
+
* as its response contract instead: a wrong contract, served, where an
|
|
2983
|
+
* abstention was the honest answer.
|
|
3033
2984
|
*
|
|
3034
2985
|
* A payload that merely has a `status` member of its own (`{ status: "ok",
|
|
3035
2986
|
* service: "ledger" }`) declares members init does not, or states `status` as
|
|
@@ -3058,7 +3009,7 @@ export class TypeInferrer {
|
|
|
3058
3009
|
statesInit = true;
|
|
3059
3010
|
continue;
|
|
3060
3011
|
}
|
|
3061
|
-
if (name === 'status' &&
|
|
3012
|
+
if (name === 'status' && this.statedStatusCodes(value) !== undefined) {
|
|
3062
3013
|
statesInit = true;
|
|
3063
3014
|
}
|
|
3064
3015
|
}
|
|
@@ -4509,7 +4460,9 @@ export class TypeInferrer {
|
|
|
4509
4460
|
const overrides = { types, applied: new Set(), at };
|
|
4510
4461
|
let text;
|
|
4511
4462
|
try {
|
|
4512
|
-
text = expandTypeStructural(this.unwrapPromiseType(input),
|
|
4463
|
+
text = expandTypeStructural(this.unwrapPromiseType(input), this.expandOrigin(), {
|
|
4464
|
+
overrides,
|
|
4465
|
+
});
|
|
4513
4466
|
}
|
|
4514
4467
|
catch {
|
|
4515
4468
|
return null;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* `type-inferrer.ts` (consumer-side inference), so both paths emit the same
|
|
29
29
|
* structural form rather than a dangling name.
|
|
30
30
|
*/
|
|
31
|
-
import { type Node, type Type } from 'ts-morph';
|
|
31
|
+
import { type Node, type Type, ts } from 'ts-morph';
|
|
32
32
|
/**
|
|
33
33
|
* Bound on the structural-expansion recursion. Deep enough for every realistic
|
|
34
34
|
* request/response shape; a backstop against pathological/recursive types the
|
|
@@ -73,17 +73,49 @@ export interface MemberOverrides {
|
|
|
73
73
|
* signature, never from a list of type names.
|
|
74
74
|
*/
|
|
75
75
|
export type WireFormat = 'declared' | 'json';
|
|
76
|
+
/**
|
|
77
|
+
* The program the walked type belongs to, and the service root inside it.
|
|
78
|
+
*
|
|
79
|
+
* Required, not optional, because the walk cannot decide from a type alone
|
|
80
|
+
* whether a declaration is the user's source or something the runtime
|
|
81
|
+
* installed: a path test answers that only where resolution goes through
|
|
82
|
+
* `node_modules`, and a runtime that serves an npm dependency's types out of
|
|
83
|
+
* its own cache leaves no such segment in the path (carrick#1264). The program
|
|
84
|
+
* carries the resolver's own verdict, so it is what `isExternalOrigin` is
|
|
85
|
+
* asked — the same instrument the inference path uses, so the two layers
|
|
86
|
+
* cannot disagree about which types to inline.
|
|
87
|
+
*/
|
|
88
|
+
export interface ExpandOrigin {
|
|
89
|
+
readonly program: ts.Program;
|
|
90
|
+
readonly repoRoot: string;
|
|
91
|
+
}
|
|
92
|
+
/** Everything `expandTypeStructural` takes besides the type and its origin. */
|
|
93
|
+
export interface ExpandOptions {
|
|
94
|
+
/** Substitutions at named member positions; see `MemberOverrides`. */
|
|
95
|
+
readonly overrides?: MemberOverrides;
|
|
96
|
+
/** Which representation to print; see `WireFormat`. */
|
|
97
|
+
readonly wire?: WireFormat;
|
|
98
|
+
/**
|
|
99
|
+
* Where in the recursion the walk starts. Production callers never set it —
|
|
100
|
+
* depth is walk state — but the `MAX_EXPANSION_DEPTH` backstop is reachable
|
|
101
|
+
* from a shallow type only by starting the walk at the bound, which is how
|
|
102
|
+
* its union ordering is tested.
|
|
103
|
+
*/
|
|
104
|
+
readonly depth?: number;
|
|
105
|
+
}
|
|
76
106
|
/**
|
|
77
107
|
* Recursively render a `Type` as fully-inlined structural text.
|
|
78
108
|
*
|
|
79
109
|
* Named object/interface types are expanded to their member structure;
|
|
80
110
|
* primitives, literals, library types (`Date`, `Promise`, tuples, …) and
|
|
81
|
-
* functions stay by name
|
|
82
|
-
* branch) breaks reference cycles
|
|
83
|
-
*
|
|
84
|
-
*
|
|
111
|
+
* functions stay by name — `origin` is what decides which is which. A cycle
|
|
112
|
+
* set (object type ids on the current branch) breaks reference cycles and
|
|
113
|
+
* `MAX_EXPANSION_DEPTH` is a hard backstop; both are walk state, not caller
|
|
114
|
+
* state. `overrides` substitutes a type at named member positions
|
|
115
|
+
* (`MemberOverrides`); without it the print is unchanged. `wire` picks the
|
|
116
|
+
* representation (`WireFormat`).
|
|
85
117
|
*/
|
|
86
|
-
export declare function expandTypeStructural(type: Type,
|
|
118
|
+
export declare function expandTypeStructural(type: Type, origin: ExpandOrigin, options?: ExpandOptions): string;
|
|
87
119
|
/**
|
|
88
120
|
* The type `JSON.stringify` serialises in place of a value of `type`: the
|
|
89
121
|
* return type of the value's own `toJSON()`, or `undefined` when it declares
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { ts } from 'ts-morph';
|
|
32
32
|
import { canonicalizeUnionsInText, foldBooleanLiterals, } from './type-text-canonicalizer.js';
|
|
33
|
+
import { isExternalOrigin } from './origin.js';
|
|
33
34
|
/**
|
|
34
35
|
* Bound on the structural-expansion recursion. Deep enough for every realistic
|
|
35
36
|
* request/response shape; a backstop against pathological/recursive types the
|
|
@@ -62,13 +63,16 @@ function childCursor(cursor, segment) {
|
|
|
62
63
|
*
|
|
63
64
|
* Named object/interface types are expanded to their member structure;
|
|
64
65
|
* primitives, literals, library types (`Date`, `Promise`, tuples, …) and
|
|
65
|
-
* functions stay by name
|
|
66
|
-
* branch) breaks reference cycles
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* functions stay by name — `origin` is what decides which is which. A cycle
|
|
67
|
+
* set (object type ids on the current branch) breaks reference cycles and
|
|
68
|
+
* `MAX_EXPANSION_DEPTH` is a hard backstop; both are walk state, not caller
|
|
69
|
+
* state. `overrides` substitutes a type at named member positions
|
|
70
|
+
* (`MemberOverrides`); without it the print is unchanged. `wire` picks the
|
|
71
|
+
* representation (`WireFormat`).
|
|
69
72
|
*/
|
|
70
|
-
export function expandTypeStructural(type,
|
|
71
|
-
|
|
73
|
+
export function expandTypeStructural(type, origin, options = {}) {
|
|
74
|
+
const { overrides, wire = 'declared', depth = 0 } = options;
|
|
75
|
+
return expandAt(type, new Set(), depth, overrides ? { overrides, position: '' } : undefined, wire, origin);
|
|
72
76
|
}
|
|
73
77
|
/**
|
|
74
78
|
* The type `JSON.stringify` serialises in place of a value of `type`: the
|
|
@@ -88,7 +92,7 @@ export function jsonWireType(type) {
|
|
|
88
92
|
const declaration = member.getValueDeclaration() ?? member.getDeclarations()[0];
|
|
89
93
|
const memberType = declaration
|
|
90
94
|
? member.getTypeAtLocation(declaration)
|
|
91
|
-
: member
|
|
95
|
+
: memberTypeWithoutDeclaration(member, type);
|
|
92
96
|
const signature = memberType.getCallSignatures()[0];
|
|
93
97
|
if (!signature)
|
|
94
98
|
return undefined;
|
|
@@ -101,7 +105,7 @@ export function jsonWireType(type) {
|
|
|
101
105
|
return undefined;
|
|
102
106
|
return serialised;
|
|
103
107
|
}
|
|
104
|
-
function expandAt(type, seen, depth, at, wire) {
|
|
108
|
+
function expandAt(type, seen, depth, at, wire, origin) {
|
|
105
109
|
if (depth > MAX_EXPANSION_DEPTH)
|
|
106
110
|
return backstopText(type);
|
|
107
111
|
let cursor = at;
|
|
@@ -109,7 +113,7 @@ function expandAt(type, seen, depth, at, wire) {
|
|
|
109
113
|
const replacement = cursor.overrides.types.get(cursor.position);
|
|
110
114
|
if (replacement) {
|
|
111
115
|
cursor.overrides.applied.add(cursor.position);
|
|
112
|
-
return expandAt(replacement, seen, depth, undefined, wire);
|
|
116
|
+
return expandAt(replacement, seen, depth, undefined, wire, origin);
|
|
113
117
|
}
|
|
114
118
|
// Nothing to substitute below here: print exactly as without overrides.
|
|
115
119
|
if (!hasOverrideBelow(cursor))
|
|
@@ -133,10 +137,10 @@ function expandAt(type, seen, depth, at, wire) {
|
|
|
133
137
|
}
|
|
134
138
|
// Unions / intersections: expand each member, in canonical order.
|
|
135
139
|
if (type.isUnion()) {
|
|
136
|
-
return canonicalMembers(type.getUnionTypes(), seen, depth, cursor, wire).join(' | ');
|
|
140
|
+
return canonicalMembers(type.getUnionTypes(), seen, depth, cursor, wire, origin).join(' | ');
|
|
137
141
|
}
|
|
138
142
|
if (type.isIntersection()) {
|
|
139
|
-
return canonicalMembers(type.getIntersectionTypes(), seen, depth, cursor, wire).join(' & ');
|
|
143
|
+
return canonicalMembers(type.getIntersectionTypes(), seen, depth, cursor, wire, origin).join(' & ');
|
|
140
144
|
}
|
|
141
145
|
// Tuples are array-like but must keep their `[a, b]` shape, not be walked
|
|
142
146
|
// as objects (which explodes into `Array.prototype`). Handle before arrays.
|
|
@@ -147,7 +151,7 @@ function expandAt(type, seen, depth, at, wire) {
|
|
|
147
151
|
const element = type.getArrayElementType();
|
|
148
152
|
if (!element)
|
|
149
153
|
return namedText(type);
|
|
150
|
-
const inner = expandAt(element, seen, depth + 1, childCursor(cursor, '<0>'), wire);
|
|
154
|
+
const inner = expandAt(element, seen, depth + 1, childCursor(cursor, '<0>'), wire, origin);
|
|
151
155
|
// Parenthesise a union/intersection element so `(A | B)[]` doesn't misparse
|
|
152
156
|
// as `A | B[]`. Decide from the TYPE, not the string: a single object
|
|
153
157
|
// literal like `{ a: A | B }` is NOT a union and must not be parenthesised,
|
|
@@ -160,12 +164,12 @@ function expandAt(type, seen, depth, at, wire) {
|
|
|
160
164
|
if (wire === 'json') {
|
|
161
165
|
const serialised = jsonWireType(type);
|
|
162
166
|
if (serialised)
|
|
163
|
-
return expandAt(serialised, seen, depth + 1, undefined, wire);
|
|
167
|
+
return expandAt(serialised, seen, depth + 1, undefined, wire, origin);
|
|
164
168
|
}
|
|
165
169
|
// Library / built-in types (Date, Promise, RegExp, …): keep by name, unless
|
|
166
170
|
// a member below has to be substituted — a schema library declares its
|
|
167
171
|
// inferred object types itself, and they are only walkable, not by-name.
|
|
168
|
-
if (!cursor && isLibraryType(type)) {
|
|
172
|
+
if (!cursor && isLibraryType(type, origin)) {
|
|
169
173
|
return namedText(type);
|
|
170
174
|
}
|
|
171
175
|
// Callable/constructable object types (functions): keep by name; their
|
|
@@ -182,7 +186,7 @@ function expandAt(type, seen, depth, at, wire) {
|
|
|
182
186
|
const props = type.getProperties();
|
|
183
187
|
if (props.length === 0)
|
|
184
188
|
return namedText(type);
|
|
185
|
-
const parts = props.map((prop) => expandProperty(prop, nextSeen, depth, cursor, wire));
|
|
189
|
+
const parts = props.map((prop) => expandProperty(prop, type, nextSeen, depth, cursor, wire, origin));
|
|
186
190
|
return `{ ${parts.join('; ')}; }`;
|
|
187
191
|
}
|
|
188
192
|
return namedText(type);
|
|
@@ -241,8 +245,8 @@ function backstopText(type) {
|
|
|
241
245
|
* Ties can only happen between two members that render identically, in which
|
|
242
246
|
* case the joined output is the same whichever way round they go.
|
|
243
247
|
*/
|
|
244
|
-
function canonicalMembers(members, seen, depth, cursor, wire) {
|
|
245
|
-
return orderMembers(members, (member) => expandAt(member, seen, depth + 1, cursor, wire));
|
|
248
|
+
function canonicalMembers(members, seen, depth, cursor, wire, origin) {
|
|
249
|
+
return orderMembers(members, (member) => expandAt(member, seen, depth + 1, cursor, wire, origin));
|
|
246
250
|
}
|
|
247
251
|
/**
|
|
248
252
|
* The canonical order itself, over whatever text `render` gives each member.
|
|
@@ -263,7 +267,7 @@ function orderMembers(members, render) {
|
|
|
263
267
|
return rendered.map((entry) => entry.text);
|
|
264
268
|
}
|
|
265
269
|
/** Render a single property as `name[?]: <expanded>`. */
|
|
266
|
-
function expandProperty(prop, seen, depth, at, wire) {
|
|
270
|
+
function expandProperty(prop, owner, seen, depth, at, wire, origin) {
|
|
267
271
|
const optional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
268
272
|
const propDecl = prop.getDeclarations()[0];
|
|
269
273
|
// Render the key from the declaration's name node so quoted/computed keys
|
|
@@ -274,7 +278,7 @@ function expandProperty(prop, seen, depth, at, wire) {
|
|
|
274
278
|
? prop.getTypeAtLocation(at.overrides.at)
|
|
275
279
|
: propDecl
|
|
276
280
|
? prop.getTypeAtLocation(propDecl)
|
|
277
|
-
: prop
|
|
281
|
+
: memberTypeWithoutDeclaration(prop, owner);
|
|
278
282
|
// A substituted member takes the override's TYPE but keeps this key's
|
|
279
283
|
// optionality, and is looked up before the `undefined` strip below so an
|
|
280
284
|
// override that carries `| undefined` strips like any other optional key.
|
|
@@ -295,13 +299,36 @@ function expandProperty(prop, seen, depth, at, wire) {
|
|
|
295
299
|
propType = nonUndefined[0];
|
|
296
300
|
}
|
|
297
301
|
else if (nonUndefined.length > 1) {
|
|
298
|
-
const inner = canonicalMembers(nonUndefined, seen, depth, cursor, wire).join(' | ');
|
|
302
|
+
const inner = canonicalMembers(nonUndefined, seen, depth, cursor, wire, origin).join(' | ');
|
|
299
303
|
return `${name}?: ${inner}`;
|
|
300
304
|
}
|
|
301
305
|
}
|
|
302
|
-
const inner = expandAt(propType, seen, depth + 1, cursor, wire);
|
|
306
|
+
const inner = expandAt(propType, seen, depth + 1, cursor, wire, origin);
|
|
303
307
|
return `${name}${optional ? '?' : ''}: ${inner}`;
|
|
304
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* The type of a member the CHECKER synthesised, which has no declaration of
|
|
311
|
+
* its own to be read at (carrick#1433).
|
|
312
|
+
*
|
|
313
|
+
* A mapped type's members — what a query builder's projection, a
|
|
314
|
+
* `GetPayload<…>`-style generic or any homomorphic mapping produces — carry no
|
|
315
|
+
* declaration node. `Symbol.getDeclaredType()` answers `any` for such a symbol
|
|
316
|
+
* (it is the DECLARED type of a type symbol, and a value member declares
|
|
317
|
+
* none), so the printed contract lost every field the compiler had resolved:
|
|
318
|
+
* `{ id: string; createdAt: Date }` printed as `{ id: any; createdAt: any }`
|
|
319
|
+
* and the row was demoted for carrying a top type.
|
|
320
|
+
*
|
|
321
|
+
* The member is read at the owning type's own declaration instead — the mapped
|
|
322
|
+
* type node the checker instantiated. A synthesised member's type does not
|
|
323
|
+
* depend on the location it is read at (only narrowing and `this` do, and it
|
|
324
|
+
* has neither), so this is the instantiated member type; it is the same answer
|
|
325
|
+
* the caller's own node gives. With no declaration anywhere to read at, the
|
|
326
|
+
* declared type is still the only thing left to ask for.
|
|
327
|
+
*/
|
|
328
|
+
function memberTypeWithoutDeclaration(prop, owner) {
|
|
329
|
+
const ownerDecl = (owner.getSymbol() ?? owner.getAliasSymbol())?.getDeclarations()?.[0];
|
|
330
|
+
return ownerDecl ? prop.getTypeAtLocation(ownerDecl) : prop.getDeclaredType();
|
|
331
|
+
}
|
|
305
332
|
/**
|
|
306
333
|
* The property key as valid TS text. Uses the declaration's name node so a
|
|
307
334
|
* quoted (`'x-y'`) or computed (`[Symbol.iterator]`) key keeps its syntax;
|
|
@@ -320,24 +347,26 @@ function isTuple(type) {
|
|
|
320
347
|
return ((target.objectFlags ?? 0) & ts.ObjectFlags.Tuple) !== 0;
|
|
321
348
|
}
|
|
322
349
|
/**
|
|
323
|
-
* True for types
|
|
324
|
-
*
|
|
350
|
+
* True for types the runtime or an installed package declares (Date, Promise,
|
|
351
|
+
* RegExp, a framework's own types, …). These stay by name rather than being
|
|
352
|
+
* inlined.
|
|
353
|
+
*
|
|
354
|
+
* Asked of the program (`isExternalOrigin`), not of the path: where resolution
|
|
355
|
+
* does not go through `node_modules` — a runtime serving an npm dependency's
|
|
356
|
+
* types from its own cache — a path test recognises nothing, and the walk
|
|
357
|
+
* inlines a library's internals as if they were the user's contract. An
|
|
358
|
+
* interface that extends `Array<T>` then prints as the whole array prototype,
|
|
359
|
+
* whose signatures carry `thisArg?: any`, and the row is demoted for a top
|
|
360
|
+
* type that is not in the contract at all (carrick#1264).
|
|
325
361
|
*/
|
|
326
|
-
function isLibraryType(type) {
|
|
362
|
+
function isLibraryType(type, origin) {
|
|
327
363
|
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
328
364
|
if (!symbol)
|
|
329
365
|
return false;
|
|
330
366
|
const decls = symbol.getDeclarations();
|
|
331
367
|
if (decls.length === 0)
|
|
332
368
|
return false;
|
|
333
|
-
return decls.some((decl) =>
|
|
334
|
-
const sf = decl.getSourceFile();
|
|
335
|
-
if (sf.isInNodeModules())
|
|
336
|
-
return true;
|
|
337
|
-
return (sf.isDeclarationFile() &&
|
|
338
|
-
// Normalize separators so a Windows `\\` path still matches lib.*.d.ts.
|
|
339
|
-
/(^|\/)lib\.[^/]*\.d\.ts$/.test(sf.getFilePath().replace(/\\/g, '/')));
|
|
340
|
-
});
|
|
369
|
+
return decls.some((decl) => isExternalOrigin(origin.program, decl.getSourceFile().compilerNode, origin.repoRoot));
|
|
341
370
|
}
|
|
342
371
|
/**
|
|
343
372
|
* Non-expanded text for a type. Passes `undefined` as the enclosing node so
|
|
@@ -53,6 +53,11 @@ Report these numbers before the list, per query where the field is per query:
|
|
|
53
53
|
- `hidden_by_threshold` where the answer carries it: how many rows the floor
|
|
54
54
|
removed, and `best` where it names the closest of them. A count above zero is
|
|
55
55
|
the case for one more search at a lower `similarity_threshold`;
|
|
56
|
+
- `hidden_by_lexical_floor` where the answer carries it: how many rows matched a
|
|
57
|
+
word of the query but too little of its word weight to rank, and `best` where
|
|
58
|
+
it names the nearest of them, with the weight it reached against the bar it
|
|
59
|
+
had to clear. A lower `similarity_threshold` does not reach these rows. The
|
|
60
|
+
search that does names the identifier or the rare term itself;
|
|
56
61
|
- `total_without_intent`, which is index-wide: functions carrying no intent
|
|
57
62
|
text, which the search ranked on their name, signature and body tokens
|
|
58
63
|
alone, so a query that names one finds it and a query that describes what
|
|
@@ -57,10 +57,17 @@ For each consumer service the step above listed:
|
|
|
57
57
|
check_compatibility({{SCOPE}}, consumer_service: "<consumer>", producer_service: "<producer>", path: "<path>")
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Pass `path`. Without it a large producer returns hundreds of rows.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
Pass `path`. Without it a large producer returns hundreds of rows.
|
|
61
|
+
|
|
62
|
+
Read `status` before the buckets: `compatible`, `incompatible`,
|
|
63
|
+
`partially_checked`, or `unresolved` where the check compared nothing, with
|
|
64
|
+
`pairs_compared` and `pairs_uncompared` saying how much of the pair it reached.
|
|
65
|
+
A `status` of `unresolved` is not a pass. It means the answer holds no evidence
|
|
66
|
+
either way, and `type_verdicts` says where the gap is.
|
|
67
|
+
|
|
68
|
+
Then read `type_verdicts` (`compatible`, `incompatible`, `unresolved`,
|
|
69
|
+
`not_compared`) and the `issues` rows for this operation. A `not_compared` pair
|
|
70
|
+
has no stored verdict, which is never agreement.
|
|
64
71
|
|
|
65
72
|
## 4. A file you have already edited
|
|
66
73
|
|