carrick 0.3.86 → 0.3.88
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 +100 -8
- package/dist/init/hosted.js +234 -33
- 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/dist/init/run.js +12 -6
- package/dist/init/run.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 +8 -30
- package/sidecar/dist/src/type-inferrer.js +20 -76
- 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
|
@@ -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
|
|