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,1132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor resolution for the surface entry: guards for addressable anchors
|
|
3
|
+
* (symbol / handler_return) and the locator + node-builder path for
|
|
4
|
+
* anonymous inferred types. Produces the final alias line for each anchor,
|
|
5
|
+
* or a recorded demotion -- never a silently wrong line.
|
|
6
|
+
*/
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { printTypeForDestination } from './node-builder.js';
|
|
10
|
+
import { typeIsOrContainsMachinery } from './machinery.js';
|
|
11
|
+
/** Repo-root-relative source file -> extensionless specifier from entryDir. */
|
|
12
|
+
export function entryRelativeSpecifier(entryDir, repoRoot, sourceFile) {
|
|
13
|
+
const target = path
|
|
14
|
+
.join(repoRoot, sourceFile)
|
|
15
|
+
.replace(/\.(ts|tsx|mts|cts)$/, '');
|
|
16
|
+
let rel = path.relative(entryDir, target).split(path.sep).join('/');
|
|
17
|
+
if (!rel.startsWith('.'))
|
|
18
|
+
rel = `./${rel}`;
|
|
19
|
+
return rel;
|
|
20
|
+
}
|
|
21
|
+
function moduleExport(checker, sourceFile, name) {
|
|
22
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
23
|
+
if (!moduleSymbol)
|
|
24
|
+
return undefined;
|
|
25
|
+
return checker
|
|
26
|
+
.getExportsOfModule(moduleSymbol)
|
|
27
|
+
.find((s) => s.getName() === name);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve one anchor against the analysis program. `placeholder` is the
|
|
31
|
+
* anchor's own placeholder alias declaration in the entry file -- the
|
|
32
|
+
* destination node the node builder anchors to (banked correction 1).
|
|
33
|
+
*/
|
|
34
|
+
export function resolveAnchor(program, request, args) {
|
|
35
|
+
const checker = program.getTypeChecker();
|
|
36
|
+
const demote = (reason) => ({
|
|
37
|
+
request,
|
|
38
|
+
aliasText: 'unknown',
|
|
39
|
+
serialization: 'structural_fallback',
|
|
40
|
+
failureReason: reason,
|
|
41
|
+
});
|
|
42
|
+
if (request.kind === 'literal') {
|
|
43
|
+
const text = request.type_text.trim();
|
|
44
|
+
if (!text) {
|
|
45
|
+
return demote('empty literal type text');
|
|
46
|
+
}
|
|
47
|
+
const bareIdentifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text);
|
|
48
|
+
const siblingSpec = bareIdentifier
|
|
49
|
+
? args.siblingSymbolSpecs?.get(text)
|
|
50
|
+
: undefined;
|
|
51
|
+
return {
|
|
52
|
+
request,
|
|
53
|
+
aliasText: siblingSpec ? `import('${siblingSpec}').${text}` : text,
|
|
54
|
+
// Literal anchors ARE the legacy-text tier (WP3 wiring of the design's
|
|
55
|
+
// structural_fallback): hand-produced type text riding the surface.
|
|
56
|
+
// The self-check still classifies decay; the fidelity metric counts
|
|
57
|
+
// them at this tier so the legacy dependence stays measurable and
|
|
58
|
+
// ratchetable. Demotions are distinguished by failureReason.
|
|
59
|
+
serialization: 'structural_fallback',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const sourceAbs = path.join(args.repoRoot, request.source_file);
|
|
63
|
+
const sourceFile = program.getSourceFile(sourceAbs);
|
|
64
|
+
if (!sourceFile) {
|
|
65
|
+
return demote(`source file not in program: ${request.source_file}`);
|
|
66
|
+
}
|
|
67
|
+
const spec = entryRelativeSpecifier(args.entryDir, args.repoRoot, request.source_file);
|
|
68
|
+
if (request.kind === 'symbol') {
|
|
69
|
+
const exported = moduleExport(checker, sourceFile, request.symbol_name);
|
|
70
|
+
if (!exported) {
|
|
71
|
+
return demote(`'${request.symbol_name}' is not an export of ${request.source_file}`);
|
|
72
|
+
}
|
|
73
|
+
// The anchor is the ELEMENT symbol; array_depth restores the use-site's
|
|
74
|
+
// `[]` levels (#248/#306) so an array-vs-scalar mismatch stays visible.
|
|
75
|
+
const arraySuffix = '[]'.repeat(Math.max(0, request.array_depth ?? 0));
|
|
76
|
+
// #438/#439: an LLM symbol anchor may name a schema VALUE const
|
|
77
|
+
// (`export const ZFooSchema = z.object({...})`) whose sibling
|
|
78
|
+
// `export type TFoo = z.infer<typeof ZFooSchema>` was the intended anchor.
|
|
79
|
+
// A value has no type-space meaning, so `import('./m').ZFooSchema` in TYPE
|
|
80
|
+
// position raises TS2694 in the stub tree and (before #438 part 2) poisons
|
|
81
|
+
// every producer pair. Guard structurally on symbol meaning, never on the
|
|
82
|
+
// name: resolve re-exports, then test for any type-space flag.
|
|
83
|
+
const resolvedExport = resolveSymbolAliases(checker, exported);
|
|
84
|
+
if (!symbolHasTypeMeaning(resolvedExport)) {
|
|
85
|
+
// #439 part 2: re-aim at a sibling type alias defined via a `typeof`
|
|
86
|
+
// type-query of this const (covers `z.infer<typeof C>` and equivalents;
|
|
87
|
+
// matched structurally, no library names). Guard A is the fallback.
|
|
88
|
+
const reaimed = reaimValueSymbolToTypeSibling(checker, sourceFile, {
|
|
89
|
+
request,
|
|
90
|
+
valueSymbol: resolvedExport,
|
|
91
|
+
spec,
|
|
92
|
+
arraySuffix,
|
|
93
|
+
});
|
|
94
|
+
if (reaimed)
|
|
95
|
+
return reaimed;
|
|
96
|
+
// #438 part 1: value-only export with no type sibling. Demote so the
|
|
97
|
+
// existing demotion/backfill path engages (honest `unknown`) instead of
|
|
98
|
+
// emitting a surface line that references a value in type position.
|
|
99
|
+
return demote(`'${request.symbol_name}' is a value export with no type-space meaning, ` +
|
|
100
|
+
`and no sibling type alias derives from it (e.g. \`typeof ${request.symbol_name}\`); ` +
|
|
101
|
+
'demoted so the surface line stays valid');
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
request,
|
|
105
|
+
aliasText: `import('${spec}').${request.symbol_name}${arraySuffix}`,
|
|
106
|
+
serialization: 'emitted',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (request.kind === 'handler_return') {
|
|
110
|
+
// Design-doc Capture step 1 guards, all three verified failure modes.
|
|
111
|
+
const exported = moduleExport(checker, sourceFile, request.symbol_name);
|
|
112
|
+
if (!exported) {
|
|
113
|
+
return demote(`handler '${request.symbol_name}' is not an export of ${request.source_file}`);
|
|
114
|
+
}
|
|
115
|
+
const declaration = exported.valueDeclaration ?? exported.declarations?.[0];
|
|
116
|
+
const handlerType = checker.getTypeOfSymbolAtLocation(exported, declaration ?? sourceFile);
|
|
117
|
+
const callSignatures = handlerType.getCallSignatures();
|
|
118
|
+
if (callSignatures.length === 0) {
|
|
119
|
+
return demote(`handler '${request.symbol_name}' has no call signatures`);
|
|
120
|
+
}
|
|
121
|
+
if (callSignatures.length > 1) {
|
|
122
|
+
// ReturnType<> silently resolves the LAST overload only.
|
|
123
|
+
return demote(`handler '${request.symbol_name}' is an overload set (${callSignatures.length} signatures)`);
|
|
124
|
+
}
|
|
125
|
+
if ((callSignatures[0].getTypeParameters() ?? []).length > 0) {
|
|
126
|
+
// Type parameters erase to their constraint/unknown under ReturnType<>.
|
|
127
|
+
return demote(`handler '${request.symbol_name}' is generic`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
request,
|
|
131
|
+
aliasText: `Awaited<ReturnType<typeof import('${spec}').${request.symbol_name}>>`,
|
|
132
|
+
serialization: 'emitted',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// kind === 'infer'
|
|
136
|
+
// #498: a `param_name` anchor targets what a handler RECEIVES. Resolve the
|
|
137
|
+
// parameter before anything else and treat the outcome as final — the
|
|
138
|
+
// expression locator below resolves the enclosing registration call, whose
|
|
139
|
+
// return type (`void`, a subscription handle) is not the payload, and it
|
|
140
|
+
// self-checks clean, so falling through would ship a confidently wrong
|
|
141
|
+
// contract instead of an honest `unknown`.
|
|
142
|
+
if (request.param_name !== undefined) {
|
|
143
|
+
const param = locateHandlerParam(sourceFile, request, request.param_name);
|
|
144
|
+
if (!param) {
|
|
145
|
+
return demote(`no handler parameter '${request.param_name}' resolved in ` +
|
|
146
|
+
`${request.source_file} (${paramLocatorHints(request)})`);
|
|
147
|
+
}
|
|
148
|
+
return finishInferAnchor(program, sourceFile, request, param, args.placeholder, undefined);
|
|
149
|
+
}
|
|
150
|
+
let located = locateNode(sourceFile, request);
|
|
151
|
+
if (!located) {
|
|
152
|
+
return demote(locatorFailureReason(request));
|
|
153
|
+
}
|
|
154
|
+
// #439 part 1: a producer anchor whose locator landed inside a fluent
|
|
155
|
+
// builder chain's config-descriptor argument (an all-literal metadata
|
|
156
|
+
// object) must never capture that descriptor as the request type. Re-aim at
|
|
157
|
+
// the chain's payload/schema argument, or demote when it cannot be picked
|
|
158
|
+
// unambiguously — never leave the descriptor captured (the artifact behind
|
|
159
|
+
// v1's false-incompatible verdicts; kept coupled with #438's containment).
|
|
160
|
+
let reaimNote;
|
|
161
|
+
const builderReaim = reaimBuilderChainPayload(checker, located);
|
|
162
|
+
if (builderReaim.kind === 'demote') {
|
|
163
|
+
return demote(builderReaim.reason);
|
|
164
|
+
}
|
|
165
|
+
if (builderReaim.kind === 'reaim') {
|
|
166
|
+
located = builderReaim.node;
|
|
167
|
+
reaimNote = builderReaim.note;
|
|
168
|
+
}
|
|
169
|
+
return finishInferAnchor(program, sourceFile, request, located, args.placeholder, reaimNote);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Shared tail of the infer paths (locator-resolved node and #498
|
|
173
|
+
* parameter-resolved node alike): read the type at the node, unwrap the
|
|
174
|
+
* transport layer, run the #433 recovery and the carrick#371 machinery guard,
|
|
175
|
+
* and print through the node builder.
|
|
176
|
+
*/
|
|
177
|
+
function finishInferAnchor(program, sourceFile, request, located, placeholder, reaimNote) {
|
|
178
|
+
const checker = program.getTypeChecker();
|
|
179
|
+
const demote = (reason) => ({
|
|
180
|
+
request,
|
|
181
|
+
aliasText: 'unknown',
|
|
182
|
+
serialization: 'structural_fallback',
|
|
183
|
+
failureReason: reason,
|
|
184
|
+
});
|
|
185
|
+
let type = checker.getTypeAtLocation(located);
|
|
186
|
+
if ((request.unwrap ?? 'awaited') === 'awaited') {
|
|
187
|
+
type = checker.getAwaitedType(type) ?? type;
|
|
188
|
+
}
|
|
189
|
+
// #433: the checker gave us nothing (whole top type) — try the syntactic
|
|
190
|
+
// recovery for an inline literal type argument of an unresolvable generic
|
|
191
|
+
// annotation. The literal is dependency-free source syntax; only the outer
|
|
192
|
+
// generic needed the missing package.
|
|
193
|
+
if (isTopType(type)) {
|
|
194
|
+
const literalNode = recoverInlineLiteralTypeArgument(checker, sourceFile, request, located);
|
|
195
|
+
if (literalNode) {
|
|
196
|
+
const recovered = checker.getTypeAtLocation(literalNode);
|
|
197
|
+
if (!isTopType(recovered))
|
|
198
|
+
type = recovered;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// carrick#766: a LINE-ONLY anchor whose type came back a bare top type has
|
|
202
|
+
// no evidence on either side, so it must abstain.
|
|
203
|
+
//
|
|
204
|
+
// The two halves of an infer anchor are the locator and the type at it. A
|
|
205
|
+
// span, an expression text or a parameter name is the scanner NAMING the
|
|
206
|
+
// payload: whatever that node's type turns out to be — including a whole
|
|
207
|
+
// `any` decayed through an uninstalled dependency — is a located fact about
|
|
208
|
+
// the payload, and the deep-any walk, the check's IsAny gate and the literal
|
|
209
|
+
// backfill are all built on it being published. Those anchors are untouched
|
|
210
|
+
// here.
|
|
211
|
+
//
|
|
212
|
+
// A line alone names nothing. `firstExpressionOnLine` picks whatever comes
|
|
213
|
+
// first on the line, which on a re-export statement is the first exported
|
|
214
|
+
// binding. When THAT resolves to a top type the capture holds no payload and
|
|
215
|
+
// no type, and `export type <alias> = any;` states "a type was inferred and
|
|
216
|
+
// it collapsed" — a claim the scan cannot back. The honest word is `unknown`
|
|
217
|
+
// ("no contract stated here"), with the node the line resolved as the reason.
|
|
218
|
+
//
|
|
219
|
+
// Live shape: a route whose handlers are built by a framework factory and
|
|
220
|
+
// re-exported at the bottom of the file. Both of the file's operations
|
|
221
|
+
// anchor at the export statement, the v1 walk abstains there (carrick#771),
|
|
222
|
+
// and the alias falls to this line-only locator, which resolves the first
|
|
223
|
+
// exported binding's identifier.
|
|
224
|
+
if (isTopType(type) && isLineOnly(request)) {
|
|
225
|
+
return {
|
|
226
|
+
request,
|
|
227
|
+
aliasText: 'unknown',
|
|
228
|
+
serialization: 'structural_fallback',
|
|
229
|
+
abstainReason: `line-only locator resolved ` +
|
|
230
|
+
`${describeNode(sourceFile, request.source_file, located)} whose type is a ` +
|
|
231
|
+
`bare top type ('${checker.typeToString(type)}'); the anchor names no ` +
|
|
232
|
+
`payload span, expression or parameter, so it abstains rather than ` +
|
|
233
|
+
`publish a top type as a contract`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// carrick#371 fail-closed guard: a producer anchor whose resolved type IS or
|
|
237
|
+
// CONTAINS framework machinery (a raw `Response`/`Request`, a wrapper envelope
|
|
238
|
+
// `{ response: Response; error }`, a wrapper function `(req) => Promise<Response>`)
|
|
239
|
+
// must never be emitted as a comparable contract — that manufactures a false
|
|
240
|
+
// compat mismatch against the consumer's real payload. This is the capture
|
|
241
|
+
// fallback for the case the v1-inferrer machinery guard leaves behind: when v1
|
|
242
|
+
// abstains on the envelope, `derive_capture_anchors` re-routes the alias to
|
|
243
|
+
// this locator-based infer anchor, which then resolves the WRAPPER FUNCTION
|
|
244
|
+
// type. No source locator points at a distinct clean payload here (the payload
|
|
245
|
+
// lives inside the wrapper's response builder), so degrade to `unknown` —
|
|
246
|
+
// abstain, never a concrete verdict off the machinery.
|
|
247
|
+
if (typeIsOrContainsMachinery(checker, type, located)) {
|
|
248
|
+
return demote('resolved type is or contains framework machinery (Response/Request-shaped); ' +
|
|
249
|
+
'degraded to unknown rather than emit a wrapper envelope as a response contract');
|
|
250
|
+
}
|
|
251
|
+
if (!placeholder) {
|
|
252
|
+
return demote('internal: no placeholder destination for infer anchor');
|
|
253
|
+
}
|
|
254
|
+
const printed = printTypeForDestination(program, type, placeholder);
|
|
255
|
+
if (!printed.text) {
|
|
256
|
+
return demote(printed.failure ?? 'node builder print failed');
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
request,
|
|
260
|
+
aliasText: printed.text,
|
|
261
|
+
serialization: 'node_builder',
|
|
262
|
+
...(reaimNote ? { reaimNote } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* True when the anchor carries a LINE and nothing else — no payload span, no
|
|
267
|
+
* expression text, no parameter name. Such an anchor states where to look, not
|
|
268
|
+
* what to look at, so a top type resolved from it is not a fact about any
|
|
269
|
+
* payload (carrick#766).
|
|
270
|
+
*/
|
|
271
|
+
function isLineOnly(request) {
|
|
272
|
+
return (request.span_start == null &&
|
|
273
|
+
request.span_end == null &&
|
|
274
|
+
!request.expression_text &&
|
|
275
|
+
!request.param_name);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* `<kind> at <file>:<line> (`<text>`)` for a node a locator resolved — what a
|
|
279
|
+
* reader needs to see why an alias abstained. The path is the REPO-RELATIVE
|
|
280
|
+
* one off the request, not `sourceFile.fileName`: the reason is published on
|
|
281
|
+
* the capture record, and an absolute path there is the scanner's checkout
|
|
282
|
+
* directory. The text is collapsed to one line and truncated, since a resolved
|
|
283
|
+
* node can be a whole function body.
|
|
284
|
+
*/
|
|
285
|
+
function describeNode(sourceFile, sourceFileRel, node) {
|
|
286
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
287
|
+
const raw = node.getText(sourceFile).replace(/\s+/g, ' ').trim();
|
|
288
|
+
const text = raw.length > 80 ? `${raw.slice(0, 77)}...` : raw;
|
|
289
|
+
return `${ts.SyntaxKind[node.kind]} at ${sourceFileRel}:${line} (\`${text}\`)`;
|
|
290
|
+
}
|
|
291
|
+
function isTopType(type) {
|
|
292
|
+
return ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) !== 0);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* The checker's intrinsic ERROR type: resolution of the reference FAILED
|
|
296
|
+
* (missing package on a bare checkout, unresolvable symbol). Distinct from a
|
|
297
|
+
* legitimate `any`, whose intrinsic name is `any` — an author-written or
|
|
298
|
+
* alias-resolved `any` must never trigger the syntactic recovery.
|
|
299
|
+
* `intrinsicName` is internal but stable since TS 1.x (same standing as the
|
|
300
|
+
* internals node-builder.ts already mirrors).
|
|
301
|
+
*/
|
|
302
|
+
function isErrorType(type) {
|
|
303
|
+
if (!(type.flags & ts.TypeFlags.Any))
|
|
304
|
+
return false;
|
|
305
|
+
const name = type.intrinsicName;
|
|
306
|
+
return name === 'error' || name === 'unresolved';
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* #433 syntactic recovery: an infer anchor whose checker type decayed to a
|
|
310
|
+
* whole top type may sit under a DECLARED annotation of the form
|
|
311
|
+
* `SomeGeneric<{ ...literal... }>` where only the outer generic failed to
|
|
312
|
+
* resolve (error-typed; its package is absent on a bare checkout) while the
|
|
313
|
+
* literal type argument is dependency-free source syntax referencing only
|
|
314
|
+
* locally-resolvable symbols. Trigger is purely structural — no framework
|
|
315
|
+
* names anywhere:
|
|
316
|
+
*
|
|
317
|
+
* 1. Data-flow path: the payload node (the located node, or the anchor's
|
|
318
|
+
* expression_text relocated) is an argument of a call — or IS a send
|
|
319
|
+
* call — whose callee chain roots at an identifier whose declaration
|
|
320
|
+
* carries the annotation (`res.json(payload)` -> `res`).
|
|
321
|
+
* 2. Registration path: the located node is a call registering inline
|
|
322
|
+
* callbacks; the callbacks' parameter annotations are scanned.
|
|
323
|
+
*
|
|
324
|
+
* Either path recovers ONLY when exactly one literal type argument is in
|
|
325
|
+
* scope — ambiguity refuses recovery and the alias keeps decaying honestly.
|
|
326
|
+
* The recovered literal node's own type is printed through the normal
|
|
327
|
+
* node-builder path, so the alias still faces the real self-check.
|
|
328
|
+
*/
|
|
329
|
+
function recoverInlineLiteralTypeArgument(checker, sourceFile, request, located) {
|
|
330
|
+
const candidates = [located];
|
|
331
|
+
if (request.expression_text) {
|
|
332
|
+
const byText = nodeByExpressionText(sourceFile, request.expression_text, request.line_number);
|
|
333
|
+
if (byText && byText !== located)
|
|
334
|
+
candidates.push(byText);
|
|
335
|
+
}
|
|
336
|
+
// Data-flow first: the payload's own call names the annotated receiver.
|
|
337
|
+
for (const node of candidates) {
|
|
338
|
+
const call = governingCall(node);
|
|
339
|
+
if (!call)
|
|
340
|
+
continue;
|
|
341
|
+
const root = calleeRootIdentifier(call.expression);
|
|
342
|
+
if (!root)
|
|
343
|
+
continue;
|
|
344
|
+
const annotation = declaredAnnotationOf(checker, root);
|
|
345
|
+
if (!annotation || !isErroredGenericReference(checker, annotation))
|
|
346
|
+
continue;
|
|
347
|
+
// The payload carrier is identified; its annotation's verdict is FINAL —
|
|
348
|
+
// never fall through to the registration scan, which could pick a
|
|
349
|
+
// different parameter's literal for this payload.
|
|
350
|
+
const literals = literalTypeArguments(annotation);
|
|
351
|
+
if (literals.length !== 1)
|
|
352
|
+
return undefined;
|
|
353
|
+
return literalResolvesLocally(checker, literals[0]) ? literals[0] : undefined;
|
|
354
|
+
}
|
|
355
|
+
// Registration shape: the located call passes inline callbacks whose
|
|
356
|
+
// parameters carry the annotations. Ambiguity is counted BEFORE any
|
|
357
|
+
// resolvability filtering: two literal-carrying annotations refuse even
|
|
358
|
+
// when one of them would be rejected later — filtering first could crown
|
|
359
|
+
// the wrong parameter's literal.
|
|
360
|
+
for (const node of candidates) {
|
|
361
|
+
if (!ts.isCallExpression(node))
|
|
362
|
+
continue;
|
|
363
|
+
const literals = [];
|
|
364
|
+
for (const arg of node.arguments) {
|
|
365
|
+
if (!isFunctionArgument(arg))
|
|
366
|
+
continue;
|
|
367
|
+
for (const param of arg.parameters) {
|
|
368
|
+
if (param.type && isErroredGenericReference(checker, param.type)) {
|
|
369
|
+
literals.push(...literalTypeArguments(param.type));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (literals.length !== 1)
|
|
374
|
+
continue;
|
|
375
|
+
return literalResolvesLocally(checker, literals[0]) ? literals[0] : undefined;
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The call whose ARGUMENT list carries `node` (payload in argument position),
|
|
381
|
+
* or `node` itself when it is a call (the send call). Calls that register
|
|
382
|
+
* callbacks are excluded: their receiver is the framework app/router value,
|
|
383
|
+
* not a payload carrier — those go through the registration path instead.
|
|
384
|
+
*/
|
|
385
|
+
function governingCall(node) {
|
|
386
|
+
if (ts.isCallExpression(node)) {
|
|
387
|
+
return node.arguments.some(isFunctionArgument) ? undefined : node;
|
|
388
|
+
}
|
|
389
|
+
const parent = node.parent;
|
|
390
|
+
if (parent !== undefined &&
|
|
391
|
+
ts.isCallExpression(parent) &&
|
|
392
|
+
node !== parent.expression &&
|
|
393
|
+
parent.arguments.includes(node)) {
|
|
394
|
+
return parent.arguments.some(isFunctionArgument) ? undefined : parent;
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
function isFunctionArgument(arg) {
|
|
399
|
+
return ts.isArrowFunction(arg) || ts.isFunctionExpression(arg);
|
|
400
|
+
}
|
|
401
|
+
/** Root identifier of a callee chain: `res.status(500).json` -> `res`. */
|
|
402
|
+
function calleeRootIdentifier(expr) {
|
|
403
|
+
let current = expr;
|
|
404
|
+
while (ts.isPropertyAccessExpression(current) ||
|
|
405
|
+
ts.isElementAccessExpression(current) ||
|
|
406
|
+
ts.isCallExpression(current) ||
|
|
407
|
+
ts.isNonNullExpression(current) ||
|
|
408
|
+
ts.isParenthesizedExpression(current)) {
|
|
409
|
+
current = current.expression;
|
|
410
|
+
}
|
|
411
|
+
return ts.isIdentifier(current) ? current : undefined;
|
|
412
|
+
}
|
|
413
|
+
/** The explicit type annotation on the identifier's declaration, if any. */
|
|
414
|
+
function declaredAnnotationOf(checker, identifier) {
|
|
415
|
+
const symbol = checker.getSymbolAtLocation(identifier);
|
|
416
|
+
const decl = symbol?.valueDeclaration ?? symbol?.declarations?.[0];
|
|
417
|
+
if (!decl)
|
|
418
|
+
return undefined;
|
|
419
|
+
if (ts.isParameter(decl) ||
|
|
420
|
+
ts.isVariableDeclaration(decl) ||
|
|
421
|
+
ts.isPropertySignature(decl) ||
|
|
422
|
+
ts.isPropertyDeclaration(decl)) {
|
|
423
|
+
return decl.type;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* A generic type reference whose OUTER resolution failed: the annotation is
|
|
429
|
+
* `Name<...args>` and its own type is the checker's error type. A resolvable
|
|
430
|
+
* generic (however it resolves) never triggers recovery; its checker answer
|
|
431
|
+
* is the truth.
|
|
432
|
+
*/
|
|
433
|
+
function isErroredGenericReference(checker, annotation) {
|
|
434
|
+
return (ts.isTypeReferenceNode(annotation) &&
|
|
435
|
+
(annotation.typeArguments?.length ?? 0) > 0 &&
|
|
436
|
+
isErrorType(checker.getTypeFromTypeNode(annotation)));
|
|
437
|
+
}
|
|
438
|
+
/** Type-literal type arguments of a generic reference (parens unwrapped). */
|
|
439
|
+
function literalTypeArguments(annotation) {
|
|
440
|
+
const literals = [];
|
|
441
|
+
for (const arg of annotation.typeArguments ?? []) {
|
|
442
|
+
let node = arg;
|
|
443
|
+
while (ts.isParenthesizedTypeNode(node))
|
|
444
|
+
node = node.type;
|
|
445
|
+
if (ts.isTypeLiteralNode(node))
|
|
446
|
+
literals.push(node);
|
|
447
|
+
}
|
|
448
|
+
return literals;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Every type reference inside the literal must itself RESOLVE. A literal
|
|
452
|
+
* leaning on a third-party type (`{ thing: LibThing }` with the lib absent)
|
|
453
|
+
* is not the tractable class — recovering it would bake `any` at a member;
|
|
454
|
+
* it keeps decaying honestly instead. Author-written `any`/`unknown`
|
|
455
|
+
* keywords are allowed through: they are the source's truth, and the
|
|
456
|
+
* self-check's deep walk still owns that verdict.
|
|
457
|
+
*/
|
|
458
|
+
function literalResolvesLocally(checker, literal) {
|
|
459
|
+
let resolves = true;
|
|
460
|
+
const visit = (node) => {
|
|
461
|
+
if (!resolves)
|
|
462
|
+
return;
|
|
463
|
+
if ((ts.isTypeReferenceNode(node) ||
|
|
464
|
+
ts.isImportTypeNode(node) ||
|
|
465
|
+
ts.isTypeQueryNode(node) ||
|
|
466
|
+
ts.isExpressionWithTypeArguments(node)) &&
|
|
467
|
+
isErrorType(checker.getTypeFromTypeNode(node))) {
|
|
468
|
+
resolves = false;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
node.forEachChild(visit);
|
|
472
|
+
};
|
|
473
|
+
visit(literal);
|
|
474
|
+
return resolves;
|
|
475
|
+
}
|
|
476
|
+
// ===========================================================================
|
|
477
|
+
// carrick#498: handler-parameter anchors.
|
|
478
|
+
//
|
|
479
|
+
// A subscriber's contract is what its handler RECEIVES, so its anchor carries
|
|
480
|
+
// a `param_name` locator instead of an expression. Resolving it needs the
|
|
481
|
+
// handler function, and the only locator upstream can supply for an anonymous
|
|
482
|
+
// inline handler is a line. Everything below is STRUCTURAL — argument
|
|
483
|
+
// position and binding shape, never a method name, library, or topic string.
|
|
484
|
+
// ===========================================================================
|
|
485
|
+
/**
|
|
486
|
+
* v1's `findFunctionByLine` tolerance, mirrored so the two paths agree on how
|
|
487
|
+
* far an anchor may be from the function it names — and, since carrick#770, on
|
|
488
|
+
* what a forward match may cross (see [`separatedFromAnchor`]).
|
|
489
|
+
*/
|
|
490
|
+
const HANDLER_LINE_TOLERANCE = 2;
|
|
491
|
+
/**
|
|
492
|
+
* The payload parameter named by `paramName` on the handler this anchor
|
|
493
|
+
* points at, or undefined when neither the handler nor the parameter can be
|
|
494
|
+
* picked. The caller demotes on undefined: an unresolved parameter must never
|
|
495
|
+
* fall back to the expression locator.
|
|
496
|
+
*/
|
|
497
|
+
function locateHandlerParam(sourceFile, request, paramName) {
|
|
498
|
+
for (const fn of handlerCandidates(sourceFile, request)) {
|
|
499
|
+
const target = resolveParamTarget(fn, paramName);
|
|
500
|
+
if (target)
|
|
501
|
+
return target;
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Handler functions this anchor could be naming, most specific first:
|
|
507
|
+
*
|
|
508
|
+
* 1. the LAST function-typed argument of a call starting on the anchor's
|
|
509
|
+
* line — the handler slot of a registration call, structurally (a
|
|
510
|
+
* registration passes its routing key first and its handler last).
|
|
511
|
+
* Several calls can start on one line (`wrap(bus.subscribe(t, h), g)`),
|
|
512
|
+
* so they are ordered INNERMOST first: the innermost is the registration
|
|
513
|
+
* the anchor's own line most nearly denotes, and it is the same
|
|
514
|
+
* innermost-wins tie-break the v1 inferrer's `findFunctionByLine` uses;
|
|
515
|
+
* 2. any function whose declaration starts within
|
|
516
|
+
* `HANDLER_LINE_TOLERANCE` lines of the anchor, innermost first — the
|
|
517
|
+
* locator may name a binding on the handler's own signature lines rather
|
|
518
|
+
* than the registration line;
|
|
519
|
+
* 3. the innermost function ENCLOSING the node the expression locator
|
|
520
|
+
* resolves — covers a locator that landed in the handler body.
|
|
521
|
+
*/
|
|
522
|
+
function handlerCandidates(sourceFile, request) {
|
|
523
|
+
const out = [];
|
|
524
|
+
const push = (fn) => {
|
|
525
|
+
if (fn && !out.includes(fn))
|
|
526
|
+
out.push(fn);
|
|
527
|
+
};
|
|
528
|
+
const line = request.line_number;
|
|
529
|
+
if (line !== undefined) {
|
|
530
|
+
for (const call of callsStartingOnLine(sourceFile, line)) {
|
|
531
|
+
const handlerArgs = call.arguments.filter(isFunctionArgument);
|
|
532
|
+
push(handlerArgs[handlerArgs.length - 1]);
|
|
533
|
+
}
|
|
534
|
+
for (const fn of functionsNearLine(sourceFile, line))
|
|
535
|
+
push(fn);
|
|
536
|
+
}
|
|
537
|
+
push(enclosingFunction(locateNode(sourceFile, request)));
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
function callsStartingOnLine(sourceFile, line) {
|
|
541
|
+
const calls = [];
|
|
542
|
+
const visit = (node) => {
|
|
543
|
+
if (ts.isCallExpression(node) && startLineOf(sourceFile, node) === line) {
|
|
544
|
+
calls.push(node);
|
|
545
|
+
}
|
|
546
|
+
node.forEachChild(visit);
|
|
547
|
+
};
|
|
548
|
+
visit(sourceFile);
|
|
549
|
+
// Innermost (smallest span) first: a pre-order walk would otherwise hand
|
|
550
|
+
// back the OUTER call of a same-line nest, whose handler is not the one the
|
|
551
|
+
// anchor's line denotes.
|
|
552
|
+
return calls.sort((a, b) => a.getEnd() - a.getStart() - (b.getEnd() - b.getStart()));
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Functions declared within the line tolerance of `line`, ordered innermost
|
|
556
|
+
* (smallest span) first so a nested handler wins over its enclosing function.
|
|
557
|
+
*
|
|
558
|
+
* The tolerance is one-directional in effect, the same rule the v1 inferrer's
|
|
559
|
+
* `findFunctionByLine` applies (carrick#766/#770). Looking BACK is free: a
|
|
560
|
+
* function starting before the anchor is one the anchor sits inside or just
|
|
561
|
+
* after. Looking FORWARD reaches a handler that starts a line or two into the
|
|
562
|
+
* registration the anchor names — and, unguarded, also reaches the next
|
|
563
|
+
* declaration in the file when the anchor lands on a statement that declares no
|
|
564
|
+
* function at all. Nothing but trivia may sit between the anchor and the
|
|
565
|
+
* function it is taken to name.
|
|
566
|
+
*/
|
|
567
|
+
function functionsNearLine(sourceFile, line) {
|
|
568
|
+
const found = [];
|
|
569
|
+
const visit = (node) => {
|
|
570
|
+
if (isHandlerFunctionLike(node) &&
|
|
571
|
+
Math.abs(startLineOf(sourceFile, node) - line) <= HANDLER_LINE_TOLERANCE) {
|
|
572
|
+
found.push(node);
|
|
573
|
+
}
|
|
574
|
+
node.forEachChild(visit);
|
|
575
|
+
};
|
|
576
|
+
visit(sourceFile);
|
|
577
|
+
const windowStatements = statementsOpeningInWindow(sourceFile, line);
|
|
578
|
+
return found
|
|
579
|
+
.filter((fn) => startLineOf(sourceFile, fn) <= line ||
|
|
580
|
+
!separatedFromAnchor(sourceFile, windowStatements, fn))
|
|
581
|
+
.sort((a, b) => a.getEnd() - a.getStart() - (b.getEnd() - b.getStart()));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Statements opening inside the forward window `[line, line + tolerance]`.
|
|
585
|
+
*
|
|
586
|
+
* Statement LIST members, not every node the checker calls a statement: the
|
|
587
|
+
* members of a source file, a block, a module block or a case clause are the
|
|
588
|
+
* things that can stand between an anchor and a later declaration. Collected
|
|
589
|
+
* from the lists themselves because the raw compiler API has no total
|
|
590
|
+
* "is this a statement" predicate the way ts-morph does.
|
|
591
|
+
*/
|
|
592
|
+
function statementsOpeningInWindow(sourceFile, line) {
|
|
593
|
+
const found = [];
|
|
594
|
+
const consider = (statements) => {
|
|
595
|
+
for (const statement of statements) {
|
|
596
|
+
const start = startLineOf(sourceFile, statement);
|
|
597
|
+
if (start >= line && start <= line + HANDLER_LINE_TOLERANCE) {
|
|
598
|
+
found.push(statement);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
const visit = (node) => {
|
|
603
|
+
if (ts.isSourceFile(node) ||
|
|
604
|
+
ts.isBlock(node) ||
|
|
605
|
+
ts.isModuleBlock(node) ||
|
|
606
|
+
ts.isCaseClause(node) ||
|
|
607
|
+
ts.isDefaultClause(node)) {
|
|
608
|
+
consider(node.statements);
|
|
609
|
+
}
|
|
610
|
+
node.forEachChild(visit);
|
|
611
|
+
};
|
|
612
|
+
visit(sourceFile);
|
|
613
|
+
return found;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Whether a statement opening in the forward window stands between the anchor
|
|
617
|
+
* and `fn` — it begins on an earlier line than `fn` and does not contain it.
|
|
618
|
+
*
|
|
619
|
+
* `broker.register('key', (payload) => …)` spread over three lines is one
|
|
620
|
+
* statement that CONTAINS its handler, so the handler is reachable.
|
|
621
|
+
* `export { handler };` followed by `function buildQuery(payload)` is two
|
|
622
|
+
* statements, and the second is not what the first names.
|
|
623
|
+
*/
|
|
624
|
+
function separatedFromAnchor(sourceFile, windowStatements, fn) {
|
|
625
|
+
const fnStart = fn.getStart(sourceFile);
|
|
626
|
+
const fnLine = startLineOf(sourceFile, fn);
|
|
627
|
+
return windowStatements.some((statement) => startLineOf(sourceFile, statement) < fnLine &&
|
|
628
|
+
!(statement.getStart(sourceFile) <= fnStart &&
|
|
629
|
+
statement.getEnd() >= fn.getEnd()));
|
|
630
|
+
}
|
|
631
|
+
function startLineOf(sourceFile, node) {
|
|
632
|
+
return (sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1);
|
|
633
|
+
}
|
|
634
|
+
function isHandlerFunctionLike(node) {
|
|
635
|
+
return (ts.isArrowFunction(node) ||
|
|
636
|
+
ts.isFunctionExpression(node) ||
|
|
637
|
+
ts.isFunctionDeclaration(node) ||
|
|
638
|
+
ts.isMethodDeclaration(node));
|
|
639
|
+
}
|
|
640
|
+
function enclosingFunction(node) {
|
|
641
|
+
let current = node;
|
|
642
|
+
while (current) {
|
|
643
|
+
if (isHandlerFunctionLike(current))
|
|
644
|
+
return current;
|
|
645
|
+
current = current.parent;
|
|
646
|
+
}
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* The three binding shapes a `function_param` locator can name, mirroring the
|
|
651
|
+
* v1 inferrer's `resolveParamTarget` so both anchor paths answer identically:
|
|
652
|
+
*
|
|
653
|
+
* 1. an exact parameter name (`(payload) => …` <- "payload");
|
|
654
|
+
* 2. a whole destructured binding pattern under whitespace normalisation
|
|
655
|
+
* (`({ id, total }) => …` <- "{ id, total }") — the pattern's own type IS
|
|
656
|
+
* the payload;
|
|
657
|
+
* 3. one named binding element inside a destructured parameter
|
|
658
|
+
* (`({ payload }) => …` <- "payload") — the payload is a property of an
|
|
659
|
+
* envelope parameter, and the checker projects the element's type.
|
|
660
|
+
*/
|
|
661
|
+
function resolveParamTarget(fn, paramName) {
|
|
662
|
+
const params = fn.parameters;
|
|
663
|
+
for (const param of params) {
|
|
664
|
+
if (ts.isIdentifier(param.name) && param.name.text === paramName) {
|
|
665
|
+
return param;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const wanted = paramName.replace(/\s+/g, ' ').trim();
|
|
669
|
+
for (const param of params) {
|
|
670
|
+
if ((ts.isObjectBindingPattern(param.name) ||
|
|
671
|
+
ts.isArrayBindingPattern(param.name)) &&
|
|
672
|
+
param.name.getText().replace(/\s+/g, ' ').trim() === wanted) {
|
|
673
|
+
return param;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const param of params) {
|
|
677
|
+
if (!ts.isObjectBindingPattern(param.name))
|
|
678
|
+
continue;
|
|
679
|
+
for (const element of param.name.elements) {
|
|
680
|
+
if (ts.isIdentifier(element.name) && element.name.text === paramName) {
|
|
681
|
+
return element;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
function paramLocatorHints(request) {
|
|
688
|
+
return request.line_number !== undefined
|
|
689
|
+
? `line ${request.line_number}`
|
|
690
|
+
: 'no line hint';
|
|
691
|
+
}
|
|
692
|
+
function locatorFailureReason(request) {
|
|
693
|
+
const hints = [];
|
|
694
|
+
if (request.span_start !== undefined)
|
|
695
|
+
hints.push(`span ${request.span_start}-${request.span_end}`);
|
|
696
|
+
if (request.expression_text)
|
|
697
|
+
hints.push(`expression '${request.expression_text.slice(0, 40)}'`);
|
|
698
|
+
if (request.line_number !== undefined)
|
|
699
|
+
hints.push(`line ${request.line_number}`);
|
|
700
|
+
return `no node located in ${request.source_file} (${hints.join(', ') || 'no locator hints'})`;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Locate the target node: tightest expression covering the byte span when
|
|
704
|
+
* given; else the expression matching expression_text on/after line_number;
|
|
705
|
+
* else the first expression starting on line_number.
|
|
706
|
+
*/
|
|
707
|
+
export function locateNode(sourceFile, request) {
|
|
708
|
+
if (request.span_start !== undefined && request.span_end !== undefined) {
|
|
709
|
+
const bySpan = tightestCoveringNode(sourceFile, request.span_start, request.span_end);
|
|
710
|
+
if (bySpan)
|
|
711
|
+
return bySpan;
|
|
712
|
+
}
|
|
713
|
+
if (request.expression_text) {
|
|
714
|
+
const byText = nodeByExpressionText(sourceFile, request.expression_text, request.line_number);
|
|
715
|
+
if (byText)
|
|
716
|
+
return byText;
|
|
717
|
+
}
|
|
718
|
+
if (request.line_number !== undefined) {
|
|
719
|
+
return firstExpressionOnLine(sourceFile, request.line_number);
|
|
720
|
+
}
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
function isPreferredTarget(node) {
|
|
724
|
+
return (ts.isExpression(node) ||
|
|
725
|
+
ts.isVariableDeclaration(node) ||
|
|
726
|
+
ts.isParameter(node) ||
|
|
727
|
+
ts.isPropertyAssignment(node));
|
|
728
|
+
}
|
|
729
|
+
function tightestCoveringNode(sourceFile, start, end) {
|
|
730
|
+
let best;
|
|
731
|
+
const visit = (node) => {
|
|
732
|
+
if (node.getStart(sourceFile) <= start && node.getEnd() >= end) {
|
|
733
|
+
if (isPreferredTarget(node))
|
|
734
|
+
best = node;
|
|
735
|
+
node.forEachChild(visit);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
node.forEachChild(visit);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
visit(sourceFile);
|
|
742
|
+
return best;
|
|
743
|
+
}
|
|
744
|
+
function nodeByExpressionText(sourceFile, text, fromLine) {
|
|
745
|
+
const wanted = text.replace(/\s+/g, ' ').trim();
|
|
746
|
+
let best;
|
|
747
|
+
const visit = (node) => {
|
|
748
|
+
if (best)
|
|
749
|
+
return;
|
|
750
|
+
if (isPreferredTarget(node)) {
|
|
751
|
+
const nodeText = node.getText(sourceFile).replace(/\s+/g, ' ').trim();
|
|
752
|
+
if (nodeText === wanted) {
|
|
753
|
+
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
754
|
+
if (fromLine === undefined || line >= fromLine) {
|
|
755
|
+
best = node;
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
node.forEachChild(visit);
|
|
761
|
+
};
|
|
762
|
+
visit(sourceFile);
|
|
763
|
+
return best;
|
|
764
|
+
}
|
|
765
|
+
function firstExpressionOnLine(sourceFile, line) {
|
|
766
|
+
let best;
|
|
767
|
+
const visit = (node) => {
|
|
768
|
+
const nodeLine = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
769
|
+
if (nodeLine === line && isPreferredTarget(node) && !best) {
|
|
770
|
+
best = node;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
node.forEachChild(visit);
|
|
774
|
+
};
|
|
775
|
+
visit(sourceFile);
|
|
776
|
+
return best;
|
|
777
|
+
}
|
|
778
|
+
// ===========================================================================
|
|
779
|
+
// #438/#439 symbol-anchor guards: value-only demotion + const->type re-aim.
|
|
780
|
+
// ===========================================================================
|
|
781
|
+
/** Follow re-export aliases to the symbol they ultimately denote. */
|
|
782
|
+
function resolveSymbolAliases(checker, symbol) {
|
|
783
|
+
let current = symbol;
|
|
784
|
+
// Bounded: alias chains are short; the guard is against a pathological cycle.
|
|
785
|
+
for (let i = 0; i < 16 && (current.flags & ts.SymbolFlags.Alias) !== 0; i++) {
|
|
786
|
+
let next;
|
|
787
|
+
try {
|
|
788
|
+
next = checker.getAliasedSymbol(current);
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
if (!next || next === current)
|
|
794
|
+
break;
|
|
795
|
+
current = next;
|
|
796
|
+
}
|
|
797
|
+
return current;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* True when the symbol can stand in TYPE position (class, interface, enum,
|
|
801
|
+
* type alias, type parameter, ...). A pure value export (const / let / var /
|
|
802
|
+
* function) has no type-space meaning, so referencing it as a type is TS2694.
|
|
803
|
+
* Structural — never a name check. `ts.SymbolFlags.Type` is the compiler's own
|
|
804
|
+
* composite of the type-space meanings.
|
|
805
|
+
*/
|
|
806
|
+
function symbolHasTypeMeaning(symbol) {
|
|
807
|
+
return (symbol.flags & ts.SymbolFlags.Type) !== 0;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* #439 part 2: when a symbol anchor names a value-only export, look in the
|
|
811
|
+
* SAME module for a single exported type alias whose type node contains a
|
|
812
|
+
* `typeof <const>` type-query (covers `z.infer<typeof C>`, `Infer<typeof C>`,
|
|
813
|
+
* and any equivalent — matched structurally, no library names). Exactly one
|
|
814
|
+
* match re-aims; zero or several fall back to the value-only demotion (never
|
|
815
|
+
* guess which of an input/output pair is the request).
|
|
816
|
+
*/
|
|
817
|
+
function reaimValueSymbolToTypeSibling(checker, sourceFile, ctx) {
|
|
818
|
+
const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
|
|
819
|
+
if (!moduleSymbol)
|
|
820
|
+
return undefined;
|
|
821
|
+
const matches = [];
|
|
822
|
+
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
|
|
823
|
+
const resolved = resolveSymbolAliases(checker, exported);
|
|
824
|
+
if ((resolved.flags & ts.SymbolFlags.TypeAlias) === 0)
|
|
825
|
+
continue;
|
|
826
|
+
const decl = resolved.declarations?.find(ts.isTypeAliasDeclaration);
|
|
827
|
+
if (!decl)
|
|
828
|
+
continue;
|
|
829
|
+
if (typeAliasRhsInfersConst(checker, decl.type, ctx.request.symbol_name, ctx.valueSymbol)) {
|
|
830
|
+
matches.push(exported.getName());
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (matches.length !== 1)
|
|
834
|
+
return undefined;
|
|
835
|
+
return {
|
|
836
|
+
request: ctx.request,
|
|
837
|
+
aliasText: `import('${ctx.spec}').${matches[0]}${ctx.arraySuffix}`,
|
|
838
|
+
serialization: 'emitted',
|
|
839
|
+
reaimNote: `symbol anchor '${ctx.request.symbol_name}' is a value; re-aimed at sibling ` +
|
|
840
|
+
`type alias '${matches[0]}' (derived via \`typeof ${ctx.request.symbol_name}\`)`,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* True iff the type alias RHS IS the inferred type derived from a
|
|
845
|
+
* `typeof <const>` query — the ordinary `export type T = z.infer<typeof C>`
|
|
846
|
+
* shape — and NOT a wrapper that merely embeds it. Structurally: the whole
|
|
847
|
+
* type node is a generic type reference (`Infer<typeof C>` /
|
|
848
|
+
* `SomeGeneric<typeof C>`) whose DIRECT type arguments include a `typeof C`
|
|
849
|
+
* query.
|
|
850
|
+
*
|
|
851
|
+
* An object / union / intersection / array that embeds the query as a member
|
|
852
|
+
* or element (`{ data: Infer<typeof C>; meta: string }`) is a WRAPPER: re-aiming
|
|
853
|
+
* at it would capture the wrapper shape, not the payload, and (since the
|
|
854
|
+
* wrapper self-checks concretely) manufacture a false incompatible. Those fall
|
|
855
|
+
* back to the value-only demotion — a guaranteed abstain, never a wrong
|
|
856
|
+
* concrete verdict. A bare `typeof C` alias is also rejected: it denotes the
|
|
857
|
+
* schema wrapper value's type, not the inferred payload. Still no library-name
|
|
858
|
+
* checks: matched purely on shape.
|
|
859
|
+
*/
|
|
860
|
+
function typeAliasRhsInfersConst(checker, node, symbolName, valueSymbol) {
|
|
861
|
+
if (!ts.isTypeReferenceNode(node))
|
|
862
|
+
return false;
|
|
863
|
+
for (const arg of node.typeArguments ?? []) {
|
|
864
|
+
let inner = arg;
|
|
865
|
+
while (ts.isParenthesizedTypeNode(inner))
|
|
866
|
+
inner = inner.type;
|
|
867
|
+
if (ts.isTypeQueryNode(inner) &&
|
|
868
|
+
typeQueryMatchesSymbol(checker, inner, symbolName, valueSymbol)) {
|
|
869
|
+
return true;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Does a `typeof X` query denote `valueSymbol`? Symbol identity is primary
|
|
876
|
+
* (resolves through re-exports and works on a bare checkout: the value symbol
|
|
877
|
+
* resolves without the schema library present); a textual identifier match on
|
|
878
|
+
* `symbolName` is the fallback.
|
|
879
|
+
*/
|
|
880
|
+
function typeQueryMatchesSymbol(checker, query, symbolName, valueSymbol) {
|
|
881
|
+
const entity = query.exprName;
|
|
882
|
+
const idNode = ts.isQualifiedName(entity) ? entity.right : entity;
|
|
883
|
+
const queried = checker.getSymbolAtLocation(entity) ?? checker.getSymbolAtLocation(idNode);
|
|
884
|
+
if (queried && resolveSymbolAliases(checker, queried) === valueSymbol)
|
|
885
|
+
return true;
|
|
886
|
+
return ts.isIdentifier(idNode) && idNode.text === symbolName;
|
|
887
|
+
}
|
|
888
|
+
function reaimBuilderChainPayload(checker, located) {
|
|
889
|
+
const descriptor = enclosingChainDescriptorArgument(checker, located);
|
|
890
|
+
if (!descriptor)
|
|
891
|
+
return { kind: 'none' };
|
|
892
|
+
const chain = fluentChainCalls(descriptor.call);
|
|
893
|
+
if (chain.length < 2)
|
|
894
|
+
return { kind: 'none' };
|
|
895
|
+
const candidates = payloadCandidates(checker, chain, descriptor.argument);
|
|
896
|
+
if (candidates.length === 1) {
|
|
897
|
+
return {
|
|
898
|
+
kind: 'reaim',
|
|
899
|
+
node: candidates[0],
|
|
900
|
+
note: 'producer anchor landed on a builder-chain config descriptor; ' +
|
|
901
|
+
're-aimed at the chain schema argument',
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
// Zero (a descriptor with no schema argument) or several (ambiguous
|
|
905
|
+
// input/output schemas): never keep the descriptor captured.
|
|
906
|
+
return {
|
|
907
|
+
kind: 'demote',
|
|
908
|
+
reason: candidates.length === 0
|
|
909
|
+
? 'producer anchor landed on a builder-chain config descriptor with no ' +
|
|
910
|
+
'identifiable schema argument; demoted to keep the request type honest'
|
|
911
|
+
: `producer anchor landed on a builder-chain config descriptor; ` +
|
|
912
|
+
`${candidates.length} schema arguments are present and cannot be ` +
|
|
913
|
+
'disambiguated structurally; demoted to keep the request type honest',
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Walk up from the located node to the enclosing call argument that carries the
|
|
918
|
+
* descriptor signature: all-literal metadata, written inline or referenced by
|
|
919
|
+
* name (#497). Returns the call and the descriptor argument itself, or
|
|
920
|
+
* undefined when the located node is not inside such a descriptor. The argument
|
|
921
|
+
* comes back so the caller can exclude exactly that node from the payload
|
|
922
|
+
* candidates by identity.
|
|
923
|
+
*/
|
|
924
|
+
function enclosingChainDescriptorArgument(checker, located) {
|
|
925
|
+
let node = located;
|
|
926
|
+
while (node) {
|
|
927
|
+
const parent = node.parent;
|
|
928
|
+
if (parent &&
|
|
929
|
+
ts.isCallExpression(parent) &&
|
|
930
|
+
parent.arguments.includes(node) &&
|
|
931
|
+
isMetadataDescriptor(checker, node)) {
|
|
932
|
+
return { call: parent, argument: node };
|
|
933
|
+
}
|
|
934
|
+
node = parent;
|
|
935
|
+
}
|
|
936
|
+
return undefined;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* A config-descriptor argument: a non-empty, all-literal object literal, either
|
|
940
|
+
* written inline or named by a reference whose declaration initialises it to
|
|
941
|
+
* one (#497 — `.meta(createWidgetMeta)` with the const in a sibling module).
|
|
942
|
+
*
|
|
943
|
+
* The REFERENCE spelling is deliberately STRICTER than the inline one, and the
|
|
944
|
+
* asymmetry is load-bearing. In this file's model an inline all-literal object
|
|
945
|
+
* argument is never a payload, so the inline test can stay loose. A reference is
|
|
946
|
+
* an ordinary identifier that `payloadCandidates` otherwise treats as a payload
|
|
947
|
+
* candidate, so classifying one as a descriptor takes it out of the running: a
|
|
948
|
+
* flat hand-rolled const (`const s = { id: 0, name: "" }` passed to `.input`)
|
|
949
|
+
* would be misread as the descriptor and the capture would re-aim onto the
|
|
950
|
+
* metadata beside it — manufacturing the exact defect #497 fixes. Requiring a
|
|
951
|
+
* nested object-literal property (the `{ openapi: { ... } }` shape both observed
|
|
952
|
+
* variants carry) separates a descriptor from a flat payload const without
|
|
953
|
+
* consulting a single method or library name.
|
|
954
|
+
*/
|
|
955
|
+
function isMetadataDescriptor(checker, arg) {
|
|
956
|
+
if (ts.isObjectLiteralExpression(arg))
|
|
957
|
+
return isAllLiteralMetadata(arg);
|
|
958
|
+
const literal = referencedObjectLiteral(checker, arg);
|
|
959
|
+
return (literal !== undefined &&
|
|
960
|
+
isAllLiteralMetadata(literal) &&
|
|
961
|
+
literal.properties.some((p) => ts.isPropertyAssignment(p) && ts.isObjectLiteralExpression(p.initializer)));
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* The object literal a reference ultimately initialises to, or undefined. Only
|
|
965
|
+
* plain name references are followed (an identifier or a property access);
|
|
966
|
+
* re-export aliases are resolved so an imported const reaches its declaration,
|
|
967
|
+
* and `as const` / `satisfies` / parenthesised initialisers are unwrapped.
|
|
968
|
+
*/
|
|
969
|
+
function referencedObjectLiteral(checker, arg) {
|
|
970
|
+
if (!ts.isIdentifier(arg) && !ts.isPropertyAccessExpression(arg))
|
|
971
|
+
return undefined;
|
|
972
|
+
const symbol = checker.getSymbolAtLocation(arg);
|
|
973
|
+
if (!symbol)
|
|
974
|
+
return undefined;
|
|
975
|
+
const declaration = resolveSymbolAliases(checker, symbol).valueDeclaration;
|
|
976
|
+
if (!declaration)
|
|
977
|
+
return undefined;
|
|
978
|
+
let initializer;
|
|
979
|
+
if (ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration)) {
|
|
980
|
+
initializer = declaration.initializer;
|
|
981
|
+
}
|
|
982
|
+
while (initializer &&
|
|
983
|
+
(ts.isAsExpression(initializer) ||
|
|
984
|
+
ts.isSatisfiesExpression(initializer) ||
|
|
985
|
+
ts.isParenthesizedExpression(initializer) ||
|
|
986
|
+
ts.isTypeAssertionExpression(initializer))) {
|
|
987
|
+
initializer = initializer.expression;
|
|
988
|
+
}
|
|
989
|
+
return initializer && ts.isObjectLiteralExpression(initializer)
|
|
990
|
+
? initializer
|
|
991
|
+
: undefined;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* The full set of calls in the fluent chain containing `anyCall`
|
|
995
|
+
* (`x.a(...).b(...).c(...)` -> the three calls). Climbs to the outermost call,
|
|
996
|
+
* then descends the callee chain.
|
|
997
|
+
*/
|
|
998
|
+
function fluentChainCalls(anyCall) {
|
|
999
|
+
let outer = anyCall;
|
|
1000
|
+
for (;;) {
|
|
1001
|
+
const access = outer.parent;
|
|
1002
|
+
if (access &&
|
|
1003
|
+
(ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
|
|
1004
|
+
access.expression === outer &&
|
|
1005
|
+
access.parent &&
|
|
1006
|
+
ts.isCallExpression(access.parent) &&
|
|
1007
|
+
access.parent.expression === access) {
|
|
1008
|
+
outer = access.parent;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
const calls = [];
|
|
1014
|
+
let current = outer;
|
|
1015
|
+
while (ts.isCallExpression(current)) {
|
|
1016
|
+
calls.push(current);
|
|
1017
|
+
const callee = current.expression;
|
|
1018
|
+
if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
|
|
1019
|
+
current = callee.expression;
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return calls;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Chain arguments that carry type-space PAYLOAD meaning: a named or constructed
|
|
1029
|
+
* schema (identifier / member access / call) whose RESOLVED type is an object
|
|
1030
|
+
* shape. Two filters, both load-bearing:
|
|
1031
|
+
* - syntactic: inline object/array literals and inline functions (handlers)
|
|
1032
|
+
* are never candidates;
|
|
1033
|
+
* - semantic: a candidate whose resolved type is a primitive or literal
|
|
1034
|
+
* (`.tag(routeName)` with `routeName: string`), a bare callable (a named
|
|
1035
|
+
* handler), or a top type is REJECTED — capturing a string-literal type as a
|
|
1036
|
+
* request payload manufactures a false incompatible. Only an object-typed
|
|
1037
|
+
* argument is a real schema.
|
|
1038
|
+
* With this filter the single-candidate re-aim fires only on a genuine schema,
|
|
1039
|
+
* and a non-object, ambiguous (>1), or absent candidate abstains via demote.
|
|
1040
|
+
*
|
|
1041
|
+
* `descriptor` is the argument the anchor landed in, excluded by NODE IDENTITY
|
|
1042
|
+
* (#497). Identity, not shape: an inline descriptor was already excluded by the
|
|
1043
|
+
* syntactic filter, but a descriptor named by reference is an identifier like
|
|
1044
|
+
* any other, and excluding it by shape would also strike out a payload schema
|
|
1045
|
+
* that happens to be an all-literal const.
|
|
1046
|
+
*/
|
|
1047
|
+
function payloadCandidates(checker, chain, descriptor) {
|
|
1048
|
+
const out = [];
|
|
1049
|
+
for (const call of chain) {
|
|
1050
|
+
for (const arg of call.arguments) {
|
|
1051
|
+
if (arg === descriptor)
|
|
1052
|
+
continue;
|
|
1053
|
+
if ((ts.isIdentifier(arg) ||
|
|
1054
|
+
ts.isPropertyAccessExpression(arg) ||
|
|
1055
|
+
ts.isElementAccessExpression(arg) ||
|
|
1056
|
+
ts.isCallExpression(arg)) &&
|
|
1057
|
+
typeIsObjectPayload(checker.getTypeAtLocation(arg))) {
|
|
1058
|
+
out.push(arg);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return out;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* A type with object/payload meaning: a real object shape (or a union /
|
|
1066
|
+
* intersection of them), never a primitive, string/number/boolean/bigint/enum
|
|
1067
|
+
* literal, null/undefined/void, a top type (`any`/`unknown`/`never` — cannot be
|
|
1068
|
+
* confirmed a payload), or a bare callable value (a handler function). This is
|
|
1069
|
+
* the gate that keeps the builder-chain re-aim from capturing a non-schema.
|
|
1070
|
+
*/
|
|
1071
|
+
function typeIsObjectPayload(type) {
|
|
1072
|
+
const nonObject = ts.TypeFlags.Any |
|
|
1073
|
+
ts.TypeFlags.Unknown |
|
|
1074
|
+
ts.TypeFlags.Never |
|
|
1075
|
+
ts.TypeFlags.Null |
|
|
1076
|
+
ts.TypeFlags.Undefined |
|
|
1077
|
+
ts.TypeFlags.Void |
|
|
1078
|
+
ts.TypeFlags.StringLike |
|
|
1079
|
+
ts.TypeFlags.NumberLike |
|
|
1080
|
+
ts.TypeFlags.BooleanLike |
|
|
1081
|
+
ts.TypeFlags.BigIntLike |
|
|
1082
|
+
ts.TypeFlags.ESSymbolLike |
|
|
1083
|
+
ts.TypeFlags.EnumLike;
|
|
1084
|
+
if (type.flags & nonObject)
|
|
1085
|
+
return false;
|
|
1086
|
+
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) {
|
|
1087
|
+
// Every constituent must be an object shape: `string | { a }` is not a
|
|
1088
|
+
// clean payload, while a union of objects stays eligible.
|
|
1089
|
+
return type.types.every(typeIsObjectPayload);
|
|
1090
|
+
}
|
|
1091
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
1092
|
+
// A bare callable (a named handler passed as a value) is not a payload; a
|
|
1093
|
+
// schema object carries no top-level call/construct signature.
|
|
1094
|
+
if (type.getCallSignatures().length > 0 ||
|
|
1095
|
+
type.getConstructSignatures().length > 0) {
|
|
1096
|
+
return false;
|
|
1097
|
+
}
|
|
1098
|
+
return true;
|
|
1099
|
+
}
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
/** A non-empty object literal whose every property is a literal-typed value. */
|
|
1103
|
+
function isAllLiteralMetadata(obj) {
|
|
1104
|
+
if (obj.properties.length === 0)
|
|
1105
|
+
return false;
|
|
1106
|
+
for (const prop of obj.properties) {
|
|
1107
|
+
if (!ts.isPropertyAssignment(prop))
|
|
1108
|
+
return false;
|
|
1109
|
+
if (!isLiteralValueExpression(prop.initializer))
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
function isLiteralValueExpression(expr) {
|
|
1115
|
+
switch (expr.kind) {
|
|
1116
|
+
case ts.SyntaxKind.StringLiteral:
|
|
1117
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
1118
|
+
case ts.SyntaxKind.NumericLiteral:
|
|
1119
|
+
case ts.SyntaxKind.TrueKeyword:
|
|
1120
|
+
case ts.SyntaxKind.FalseKeyword:
|
|
1121
|
+
case ts.SyntaxKind.NullKeyword:
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
if (ts.isPrefixUnaryExpression(expr))
|
|
1125
|
+
return isLiteralValueExpression(expr.operand);
|
|
1126
|
+
if (ts.isArrayLiteralExpression(expr)) {
|
|
1127
|
+
return expr.elements.every(isLiteralValueExpression);
|
|
1128
|
+
}
|
|
1129
|
+
if (ts.isObjectLiteralExpression(expr))
|
|
1130
|
+
return isAllLiteralMetadata(expr);
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|