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.
@@ -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. The `seen` set (object type ids on the current
66
- * branch) breaks reference cycles; `depth` is a hard backstop. `overrides`
67
- * substitutes a type at named member positions (`MemberOverrides`); without
68
- * it the print is unchanged. `wire` picks the representation (`WireFormat`).
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, seen = new Set(), depth = 0, overrides, wire = 'declared') {
71
- return expandAt(type, seen, depth, overrides ? { overrides, position: '' } : undefined, wire);
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.getDeclaredType();
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.getDeclaredType();
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 declared in `node_modules` or a TS `lib.*.d.ts` (Date,
324
- * Promise, RegExp, …). These stay by name rather than being inlined.
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. Read
61
- `type_verdicts` (`compatible`, `incompatible`, `unresolved`, `not_compared`) and
62
- the `issues` rows for this operation. A `not_compared` pair has no stored
63
- verdict, which is never agreement.
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