carrick 0.3.72 → 0.3.75

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.
Files changed (38) hide show
  1. package/dist/contract.d.ts +11 -2
  2. package/dist/contract.js +5 -0
  3. package/dist/contract.js.map +1 -1
  4. package/dist/init/run.js +1 -1
  5. package/dist/init/run.js.map +1 -1
  6. package/dist/render.js +11 -3
  7. package/dist/render.js.map +1 -1
  8. package/package.json +6 -6
  9. package/sidecar/dist/src/capture/anchors.d.ts +15 -0
  10. package/sidecar/dist/src/capture/anchors.js +70 -5
  11. package/sidecar/dist/src/capture/api.d.ts +39 -4
  12. package/sidecar/dist/src/capture/check-classify.d.ts +2 -0
  13. package/sidecar/dist/src/capture/check-classify.js +28 -0
  14. package/sidecar/dist/src/capture/check-deep.js +1 -1
  15. package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
  16. package/sidecar/dist/src/capture/check-probe.js +16 -0
  17. package/sidecar/dist/src/capture/deep-walk.d.ts +33 -6
  18. package/sidecar/dist/src/capture/deep-walk.js +81 -28
  19. package/sidecar/dist/src/capture/index.js +16 -6
  20. package/sidecar/dist/src/capture/installed-package.d.ts +58 -0
  21. package/sidecar/dist/src/capture/installed-package.js +311 -0
  22. package/sidecar/dist/src/capture/lockfile.d.ts +8 -0
  23. package/sidecar/dist/src/capture/lockfile.js +1 -1
  24. package/sidecar/dist/src/capture/node-builder.d.ts +27 -0
  25. package/sidecar/dist/src/capture/node-builder.js +107 -1
  26. package/sidecar/dist/src/capture/paths-rewrite.d.ts +23 -6
  27. package/sidecar/dist/src/capture/paths-rewrite.js +43 -11
  28. package/sidecar/dist/src/capture/self-check.js +12 -3
  29. package/sidecar/dist/src/capture/unresolved.d.ts +28 -0
  30. package/sidecar/dist/src/capture/unresolved.js +107 -0
  31. package/sidecar/dist/src/type-inferrer.d.ts +108 -11
  32. package/sidecar/dist/src/type-inferrer.js +513 -90
  33. package/sidecar/dist/src/type-structural-expander.d.ts +23 -2
  34. package/sidecar/dist/src/type-structural-expander.js +55 -17
  35. package/sidecar/dist/src/type-text-canonicalizer.d.ts +14 -0
  36. package/sidecar/dist/src/type-text-canonicalizer.js +23 -2
  37. package/sidecar/dist/src/validators.d.ts +10 -0
  38. package/sidecar/dist/src/validators.js +1 -0
@@ -0,0 +1,28 @@
1
+ /**
2
+ * What an anchor's source program could not resolve (carrick#1164).
3
+ *
4
+ * The node builder and declaration emit both print TypeScript's
5
+ * unresolved-reference placeholder as the keyword `any`. After that the stub
6
+ * text cannot say whether an `any` member was written by the author or left
7
+ * behind by an import that did not resolve on the scanned checkout — a
8
+ * dependency that was not installed, a generated module that was never
9
+ * generated. The self-check reads only that text, so it used to label both
10
+ * `declared`, and a reader told "declared that way in the source" stops
11
+ * looking where the fix is to install or generate the missing module.
12
+ *
13
+ * The source program still holds the placeholder, so the capture records the
14
+ * placeholder's member paths at anchor time, together with the module
15
+ * specifiers the anchor's file can reach that did not resolve, and the
16
+ * self-check labels the matching findings. Seam: node builtins + `typescript`.
17
+ */
18
+ import ts from 'typescript';
19
+ import { type UnresolvedAtAnchor } from './deep-walk.js';
20
+ /**
21
+ * The unresolved placeholders inside `type` (read at `location` in `sourceFile`),
22
+ * or `undefined` when it has none — the common case, which costs one walk.
23
+ *
24
+ * `pathPrefix` maps the anchor type's member paths onto the paths of the alias
25
+ * the surface declares when the two differ: a symbol anchor restoring array
26
+ * depth prints `import('./m').Row[]`, whose members sit under `<0>`.
27
+ */
28
+ export declare function unresolvedAtAnchor(program: ts.Program, sourceFile: ts.SourceFile, type: ts.Type, location: ts.Node, pathPrefix?: string): UnresolvedAtAnchor | undefined;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * What an anchor's source program could not resolve (carrick#1164).
3
+ *
4
+ * The node builder and declaration emit both print TypeScript's
5
+ * unresolved-reference placeholder as the keyword `any`. After that the stub
6
+ * text cannot say whether an `any` member was written by the author or left
7
+ * behind by an import that did not resolve on the scanned checkout — a
8
+ * dependency that was not installed, a generated module that was never
9
+ * generated. The self-check reads only that text, so it used to label both
10
+ * `declared`, and a reader told "declared that way in the source" stops
11
+ * looking where the fix is to install or generate the missing module.
12
+ *
13
+ * The source program still holds the placeholder, so the capture records the
14
+ * placeholder's member paths at anchor time, together with the module
15
+ * specifiers the anchor's file can reach that did not resolve, and the
16
+ * self-check labels the matching findings. Seam: node builtins + `typescript`.
17
+ */
18
+ import ts from 'typescript';
19
+ import { findUnresolvedPlaceholders } from './deep-walk.js';
20
+ /** Files the specifier walk visits from one anchor before it stops. */
21
+ const MAX_REACHABLE_FILES = 256;
22
+ /** Per-program memo: source file name -> unresolved specifiers it reaches. */
23
+ const reachableCache = new WeakMap();
24
+ /**
25
+ * The unresolved placeholders inside `type` (read at `location` in `sourceFile`),
26
+ * or `undefined` when it has none — the common case, which costs one walk.
27
+ *
28
+ * `pathPrefix` maps the anchor type's member paths onto the paths of the alias
29
+ * the surface declares when the two differ: a symbol anchor restoring array
30
+ * depth prints `import('./m').Row[]`, whose members sit under `<0>`.
31
+ */
32
+ export function unresolvedAtAnchor(program, sourceFile, type, location, pathPrefix = '') {
33
+ const checker = program.getTypeChecker();
34
+ const paths = findUnresolvedPlaceholders(type, program, checker, location).map((path) => prefixPath(pathPrefix, path));
35
+ if (paths.length === 0)
36
+ return undefined;
37
+ return { paths, specifiers: unresolvedSpecifiersReachableFrom(program, sourceFile) };
38
+ }
39
+ /** Join a walk path under a prefix in the walk's own notation. */
40
+ function prefixPath(prefix, path) {
41
+ if (prefix === '')
42
+ return path;
43
+ if (path === '')
44
+ return prefix;
45
+ return /^[<[(]/.test(path) ? `${prefix}${path}` : `${prefix}.${path}`;
46
+ }
47
+ /**
48
+ * Module specifiers, as written, that do not resolve from `sourceFile` or from
49
+ * any source module it imports, breadth-first so the nearest come first, with
50
+ * relative specifiers ahead of package names. Installed packages and the
51
+ * default library are not descended into.
52
+ */
53
+ function unresolvedSpecifiersReachableFrom(program, sourceFile) {
54
+ let cache = reachableCache.get(program);
55
+ if (!cache) {
56
+ cache = new Map();
57
+ reachableCache.set(program, cache);
58
+ }
59
+ const cached = cache.get(sourceFile.fileName);
60
+ if (cached)
61
+ return cached;
62
+ const checker = program.getTypeChecker();
63
+ const unresolved = new Set();
64
+ const visited = new Set([sourceFile.fileName]);
65
+ const queue = [sourceFile];
66
+ while (queue.length > 0 && visited.size <= MAX_REACHABLE_FILES) {
67
+ const file = queue.shift();
68
+ for (const literal of moduleSpecifiersOf(file)) {
69
+ const module = checker.getSymbolAtLocation(literal);
70
+ if (!module) {
71
+ unresolved.add(literal.text);
72
+ continue;
73
+ }
74
+ // An ambient `declare module 'x'` resolves to a module declaration, not
75
+ // a file: resolved, with nothing further to walk.
76
+ const target = module.declarations?.find(ts.isSourceFile);
77
+ if (!target ||
78
+ visited.has(target.fileName) ||
79
+ program.isSourceFileDefaultLibrary(target) ||
80
+ program.isSourceFileFromExternalLibrary(target)) {
81
+ continue;
82
+ }
83
+ visited.add(target.fileName);
84
+ queue.push(target);
85
+ }
86
+ }
87
+ const ordered = [...unresolved].sort((a, b) => Number(!a.startsWith('.')) - Number(!b.startsWith('.')));
88
+ cache.set(sourceFile.fileName, ordered);
89
+ return ordered;
90
+ }
91
+ /** The string-literal module specifiers of a file's imports and re-exports. */
92
+ function moduleSpecifiersOf(file) {
93
+ const out = [];
94
+ for (const statement of file.statements) {
95
+ let specifier;
96
+ if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) {
97
+ specifier = statement.moduleSpecifier;
98
+ }
99
+ else if (ts.isImportEqualsDeclaration(statement) &&
100
+ ts.isExternalModuleReference(statement.moduleReference)) {
101
+ specifier = statement.moduleReference.expression;
102
+ }
103
+ if (specifier && ts.isStringLiteral(specifier))
104
+ out.push(specifier);
105
+ }
106
+ return out;
107
+ }
@@ -135,6 +135,57 @@ export declare class TypeInferrer {
135
135
  */
136
136
  private resolveParamTarget;
137
137
  private inferResponseBody;
138
+ /**
139
+ * The wire representation a request's printed type takes. A route response
140
+ * is serialised as JSON by every sender this layer reads a payload out of,
141
+ * so it prints `toJSON()` results rather than the objects that declare them
142
+ * (carrick#1163). Everything else prints the declared type.
143
+ */
144
+ private wireFormatFor;
145
+ /** An inferred type for a payload read out of response sends. */
146
+ private inferredFromRecoveredPayload;
147
+ /**
148
+ * carrick#1161: type a route response from every send in the handler the
149
+ * locator anchors, rather than from the one send it names.
150
+ *
151
+ * The analyzer reports one response expression per row, and on a handler
152
+ * that guards before it succeeds that is the guard's body. So the located
153
+ * expression selects the HANDLER: the function that contains it, on a row
154
+ * whose line is a route registration. Its returned sends are enumerated
155
+ * (conditional branches split), each is classified by the status it states
156
+ * (`responseSiteStatus`), the error and redirect branches are dropped, and
157
+ * the success bodies are joined exactly as the payload walk joins return
158
+ * statements.
159
+ *
160
+ * Returns `undefined` when this does not apply, which leaves the caller's
161
+ * own reading of the located expression untouched:
162
+ * - the row's line is not a route registration;
163
+ * - the located expression is neither a send nor the argument of one;
164
+ * - that send is not one the handler returns;
165
+ * - it is a success send and the only one that survives, so the join would
166
+ * be the located body anyway;
167
+ * - no joined body is the located one (a text body beside JSON ones).
168
+ *
169
+ * When the located send is an error or redirect branch and no success body
170
+ * survives, the route sends no body a contract can describe: an explicit
171
+ * `unknown` with its reason, so no later locator re-run reads the error body
172
+ * or the redirect location back in.
173
+ */
174
+ private inferFromResponseSites;
175
+ /**
176
+ * True when `candidate` is a response send: a call or `new` whose result is
177
+ * transport machinery (by the extraction config's verified rule or the
178
+ * structural check), or whose callee this handler's own returns prove is a
179
+ * serialiser (`calleesProvenSerialiser`, which holds on a bare checkout).
180
+ */
181
+ private isResponseSend;
182
+ /**
183
+ * The send `node` is an argument of, looking through the wrappers that do
184
+ * not change a payload (parentheses, `as`, `satisfies`, `!`, `await`) and a
185
+ * `JSON.stringify` around the body. `undefined` when the parent call is not
186
+ * a send or `node` is its callee.
187
+ */
188
+ private sendReceivingArgument;
138
189
  private inferCallResult;
139
190
  private inferVariable;
140
191
  private inferExpression;
@@ -524,24 +575,45 @@ export declare class TypeInferrer {
524
575
  */
525
576
  private statesResponseInit;
526
577
  /**
527
- * True when an argument states a >= 400 status: an options object carrying
528
- * `status`/`statusCode`, or a bare status code (`send(body, 404)`).
578
+ * What a response send states about its HTTP status (carrick#1161).
529
579
  *
530
- * Read from the AST first: `{ status: 400 }` in an argument position widens
531
- * to `{ status: number }`, so the literal only survives syntactically. The
532
- * type check behind it catches `as const` and hoisted option objects.
580
+ * Read in order, and the first that states anything decides:
581
+ *
582
+ * 1. an argument past the body that carries a status: a numeric literal
583
+ * (`send(body, 404)`), an options object (`{ status: 401 }`), or any
584
+ * expression whose TYPE is a status literal or a union of them
585
+ * (`let code: 400 | 500`). A numeric argument whose type is a plain
586
+ * `number` states a status the source does not fix, which is
587
+ * `variable`: the branch fails open into the union and is logged.
588
+ * 2. the send call's own RESULT type, when a member of it is typed as a
589
+ * status literal. A framework that types its sends records the status
590
+ * there, and a redirect helper's default `302` is only visible there.
591
+ * A result whose status member spans success and error codes (the
592
+ * default of a typed `json(body, status?)`) says nothing either way.
593
+ *
594
+ * A status in the FIRST argument is a field of the body and is never read.
595
+ * No method or framework name is consulted anywhere.
533
596
  */
534
- private statesErrorStatus;
597
+ private responseSiteStatus;
535
598
  /**
536
- * The HTTP status an argument states, or `undefined`.
599
+ * The HTTP status codes an argument states, `'variable'` when it is a
600
+ * status-shaped value the source does not fix, or `undefined` when it says
601
+ * nothing about a status.
537
602
  *
538
603
  * Read from the AST first: `{ status: 400 }` in an argument position widens
539
604
  * to `{ status: number }`, so the literal only survives syntactically. The
540
- * type check behind it catches `as const` and hoisted option objects. Only
541
- * values in the HTTP range count — an arbitrary number named `status` on a
542
- * domain object (`{ status: 2 }`) states nothing about transport.
605
+ * type read behind it catches `as const`, hoisted option objects and
606
+ * literal-typed variables. Only values in the HTTP range count: an
607
+ * arbitrary number named `status` on a domain object (`{ status: 2 }`)
608
+ * states nothing about transport.
609
+ */
610
+ private statedStatusCodes;
611
+ /**
612
+ * Status codes a send call's result type records: the members (of the
613
+ * result, or of each part of an intersection result) typed as a status
614
+ * literal or a union of them. `undefined` when none is.
543
615
  */
544
- private statedStatusCode;
616
+ private sendResultStatusCodes;
545
617
  /**
546
618
  * Peel the wrappers that do not change an expression's payload: parentheses
547
619
  * and `await`. Unlike `unwrapExpressionNode` this KEEPS `as`/`satisfies`,
@@ -937,6 +1009,31 @@ export declare class TypeInferrer {
937
1009
  * question (`schemaContract`), not this walk's.
938
1010
  */
939
1011
  private middlewareBodySchemaValues;
1012
+ /**
1013
+ * The contract of the schema a handler validates an untyped body read with
1014
+ * (carrick#1166), or null.
1015
+ *
1016
+ * `read` is the located expression: the read call, or the binding that holds
1017
+ * it. Every call in the enclosing function that takes that value as an
1018
+ * argument is a candidate, and the schema is the call's receiver
1019
+ * (`Schema.safeParse(body)`) or one of its other arguments
1020
+ * (`parse(Schema, body)`). A candidate counts only when it declares schema
1021
+ * types (`schemaOutputType` / `schemaInputType`), so a logger or a service
1022
+ * call the body is also handed to is never read as a contract. No method or
1023
+ * library name is matched.
1024
+ */
1025
+ private schemaConsumingRead;
1026
+ /**
1027
+ * The part a validated read names when that part is not a body, or
1028
+ * `undefined` (carrick#1166).
1029
+ *
1030
+ * The read is a call whose only argument is a string literal, and the
1031
+ * registration carries a validator middleware binding that same literal.
1032
+ * The part names match through the source's own literal, so no framework
1033
+ * vocabulary is assumed beyond `REQUEST_BODY_PARTS`, which already decides
1034
+ * what a body is for the middleware anchor.
1035
+ */
1036
+ private nonBodyValidatedPart;
940
1037
  /**
941
1038
  * The request contract behind a VALIDATED READ: a call inside a registered
942
1039
  * handler whose type is exactly the parsed output of a body schema the