@telorun/analyzer 0.70.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +4 -5
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +92 -10
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +10 -4
- package/dist/catch-scope.d.ts +72 -0
- package/dist/catch-scope.d.ts.map +1 -0
- package/dist/catch-scope.js +102 -0
- package/dist/cel-scope-query.d.ts +14 -0
- package/dist/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +36 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +3 -4
- package/dist/deprecation.d.ts +21 -0
- package/dist/deprecation.d.ts.map +1 -0
- package/dist/deprecation.js +26 -0
- package/dist/flatten-for-analyzer.d.ts +2 -2
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/manifest-visitor.d.ts +17 -1
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +16 -4
- package/dist/migrations/report.d.ts +1 -1
- package/dist/migrations/report.d.ts.map +1 -1
- package/dist/migrations/report.js +5 -0
- package/dist/module-alias-scope.d.ts +65 -0
- package/dist/module-alias-scope.d.ts.map +1 -0
- package/dist/module-alias-scope.js +25 -0
- package/dist/{zone-module-documents.d.ts → module-documents.d.ts} +11 -7
- package/dist/module-documents.d.ts.map +1 -0
- package/dist/ref-sentinel-target.d.ts +38 -0
- package/dist/ref-sentinel-target.d.ts.map +1 -0
- package/dist/ref-sentinel-target.js +13 -0
- package/dist/ref-slot.d.ts +15 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +7 -0
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +2 -1
- package/dist/resolve-throws-union.d.ts +47 -2
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +199 -20
- package/dist/resolve-zone-requirements.d.ts +3 -3
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +12 -12
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +13 -1
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +48 -4
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +3 -1
- package/dist/schema-walk.d.ts +27 -0
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +44 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +2 -4
- package/dist/types.d.ts +20 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/validate-identifier-names.d.ts +2 -2
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +22 -7
- package/dist/validate-invocation-contract.d.ts +5 -0
- package/dist/validate-invocation-contract.d.ts.map +1 -1
- package/dist/validate-invocation-contract.js +114 -3
- package/dist/validate-logging.d.ts.map +1 -1
- package/dist/validate-logging.js +2 -2
- package/dist/validate-ref-slots.d.ts +1 -1
- package/dist/validate-ref-slots.d.ts.map +1 -1
- package/dist/validate-ref-slots.js +34 -0
- package/dist/validate-references.d.ts +16 -5
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +57 -15
- package/dist/validate-resource-inputs.d.ts +1 -26
- package/dist/validate-resource-inputs.d.ts.map +1 -1
- package/dist/validate-resource-inputs.js +12 -2
- package/dist/validate-schema-type-refs.d.ts.map +1 -1
- package/dist/validate-schema-type-refs.js +2 -1
- package/dist/validate-throws-coverage.d.ts +5 -1
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +241 -86
- package/package.json +3 -3
- package/src/analysis-registry.ts +4 -5
- package/src/analyzer.ts +115 -11
- package/src/call-graph.ts +9 -3
- package/src/catch-scope.ts +157 -0
- package/src/cel-scope-query.ts +45 -8
- package/src/definition-registry.ts +3 -6
- package/src/deprecation.ts +36 -0
- package/src/flatten-for-analyzer.ts +3 -3
- package/src/index.ts +10 -3
- package/src/manifest-visitor.ts +30 -5
- package/src/migrations/report.ts +5 -1
- package/src/module-alias-scope.ts +94 -0
- package/src/{zone-module-documents.ts → module-documents.ts} +10 -6
- package/src/ref-sentinel-target.ts +46 -0
- package/src/ref-slot.ts +19 -0
- package/src/resolve-schema-type-refs.ts +2 -1
- package/src/resolve-throws-union.ts +253 -20
- package/src/resolve-zone-requirements.ts +14 -14
- package/src/schema-compat.ts +13 -0
- package/src/schema-error-report.ts +50 -6
- package/src/schema-keywords.ts +4 -1
- package/src/schema-walk.ts +56 -0
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +2 -5
- package/src/types.ts +21 -3
- package/src/validate-identifier-names.ts +28 -9
- package/src/validate-invocation-contract.ts +128 -2
- package/src/validate-logging.ts +2 -3
- package/src/validate-ref-slots.ts +41 -1
- package/src/validate-references.ts +74 -14
- package/src/validate-resource-inputs.ts +26 -3
- package/src/validate-schema-type-refs.ts +2 -1
- package/src/validate-throws-coverage.ts +344 -92
- package/dist/zone-module-documents.d.ts.map +0 -1
- /package/dist/{zone-module-documents.js → module-documents.js} +0 -0
|
@@ -3,16 +3,25 @@ import { isTaggedSentinel } from "@telorun/templating";
|
|
|
3
3
|
import {
|
|
4
4
|
AMBIENT_CONTRACT_ERROR_CODES,
|
|
5
5
|
isAmbientContractErrorCode,
|
|
6
|
+
type ResourceDefinition,
|
|
6
7
|
type ResourceManifest,
|
|
7
8
|
} from "@telorun/sdk";
|
|
8
9
|
import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
|
|
9
10
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
10
11
|
import {
|
|
11
12
|
createResolveCtx,
|
|
13
|
+
resolveScopeUnion,
|
|
12
14
|
resolveThrowsUnion,
|
|
13
15
|
type ThrowsCodeMeta,
|
|
14
16
|
type ThrowsUnion,
|
|
15
17
|
} from "./resolve-throws-union.js";
|
|
18
|
+
import {
|
|
19
|
+
buildEnclosers,
|
|
20
|
+
collectScopedManifests,
|
|
21
|
+
enclosingCoverage,
|
|
22
|
+
type ProvenCoverage,
|
|
23
|
+
type ScopedManifest,
|
|
24
|
+
} from "./catch-scope.js";
|
|
16
25
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
17
26
|
import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
|
|
18
27
|
import { isStepSlot } from "./step-slot.js";
|
|
@@ -103,7 +112,11 @@ function walkSchemaData(
|
|
|
103
112
|
}
|
|
104
113
|
} else {
|
|
105
114
|
const catchesFor = propSchema["x-telo-catches-for"] as string | undefined;
|
|
106
|
-
|
|
115
|
+
// The EMPTY pointer names the resource the list is written on, the
|
|
116
|
+
// spelling `x-telo-schema-projection-from` already uses for the same
|
|
117
|
+
// "this declaration, not one it references" meaning — so the test is
|
|
118
|
+
// presence, never truthiness.
|
|
119
|
+
if (catchesFor !== undefined) {
|
|
107
120
|
// Fire even when absent so the coverage check can flag handlers
|
|
108
121
|
// whose declared union is non-empty but the list is missing.
|
|
109
122
|
ctx.onCatches(entries, nextPath, dataObj, catchesFor);
|
|
@@ -211,6 +224,7 @@ function checkCatchAllPlacement(
|
|
|
211
224
|
channel: "returns" | "catches",
|
|
212
225
|
filePath: string | undefined,
|
|
213
226
|
arrayPath: string,
|
|
227
|
+
routing: { kind: string; name: string } = resource,
|
|
214
228
|
): AnalysisDiagnostic[] {
|
|
215
229
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
216
230
|
for (let i = 0; i < entries.length - 1; i++) {
|
|
@@ -221,90 +235,120 @@ function checkCatchAllPlacement(
|
|
|
221
235
|
code: "CATCHALL_NOT_LAST",
|
|
222
236
|
source: SOURCE,
|
|
223
237
|
message: `${channel}: catch-all entry (no \`when:\`) at index ${i} must be last — entries after it are unreachable.`,
|
|
224
|
-
data: { resource, filePath, path: `${arrayPath}[${i}]` },
|
|
238
|
+
data: { resource: routing, filePath, path: `${arrayPath}[${i}]` },
|
|
225
239
|
});
|
|
226
240
|
}
|
|
227
241
|
}
|
|
228
242
|
return diagnostics;
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
/**
|
|
232
|
-
*
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
/** Read one list's {@link ProvenCoverage} — the codes its coverage-proving
|
|
246
|
+
* `when:` clauses name, and whether it ends in a catch-all. */
|
|
247
|
+
function provenCoverage(entries: OutcomeEntry[], env: Environment): ProvenCoverage {
|
|
248
|
+
const codes = new Set<string>();
|
|
249
|
+
let hasCatchAll = false;
|
|
250
|
+
for (const e of entries) {
|
|
251
|
+
if (!e) continue;
|
|
252
|
+
if (!e.when) {
|
|
253
|
+
hasCatchAll = true;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const { proven, codes: entryCodes } = extractCoveredCodes(e.when, env);
|
|
257
|
+
if (!proven) continue;
|
|
258
|
+
for (const c of entryCodes) codes.add(c);
|
|
259
|
+
}
|
|
260
|
+
return { codes, hasCatchAll };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Rule 4: a coverage-proving `when:` may only name a code the list's own
|
|
264
|
+
* denominator can produce. Runs for EVERY list, scope lists included — the
|
|
265
|
+
* denominator differs (a handler's union, or the enclosing resource's own), the
|
|
266
|
+
* typo check does not. */
|
|
267
|
+
function checkUndeclaredCodes(
|
|
236
268
|
entries: OutcomeEntry[],
|
|
237
269
|
union: ThrowsUnion,
|
|
238
270
|
resource: { kind: string; name: string },
|
|
239
271
|
filePath: string | undefined,
|
|
240
272
|
arrayPath: string,
|
|
241
273
|
env: Environment,
|
|
242
|
-
|
|
274
|
+
denominator: string,
|
|
275
|
+
routing: { kind: string; name: string } = resource,
|
|
243
276
|
): AnalysisDiagnostic[] {
|
|
244
277
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
245
278
|
const declaredCodes = new Set(union.codes.keys());
|
|
246
|
-
const covered = new Set<string>();
|
|
247
|
-
let hasCatchAll = false;
|
|
248
279
|
|
|
249
280
|
for (let i = 0; i < entries.length; i++) {
|
|
250
281
|
const e = entries[i];
|
|
251
|
-
if (!e) continue;
|
|
252
|
-
if (!e.when) {
|
|
253
|
-
hasCatchAll = true;
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
282
|
+
if (!e?.when) continue;
|
|
256
283
|
const { proven, codes } = extractCoveredCodes(e.when, env);
|
|
257
|
-
if (proven)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
} else {
|
|
274
|
-
covered.add(c);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
284
|
+
if (!proven) continue;
|
|
285
|
+
for (const c of codes) {
|
|
286
|
+
// An ambient kernel code (contract violations) is raised by the kernel,
|
|
287
|
+
// not declared by the kind, so naming it is legal and still typo-checked
|
|
288
|
+
// — but it is NOT part of the declared union, so it never counts toward
|
|
289
|
+
// coverage. Folding these into every union would make every bounded
|
|
290
|
+
// catches: block in the standard library incomplete overnight.
|
|
291
|
+
if (isAmbientContractErrorCode(c)) continue;
|
|
292
|
+
if (declaredCodes.has(c)) continue;
|
|
293
|
+
diagnostics.push({
|
|
294
|
+
severity: DiagnosticSeverity.Error,
|
|
295
|
+
code: "UNDECLARED_THROW_CODE",
|
|
296
|
+
source: SOURCE,
|
|
297
|
+
message: `catches[${i}] references code '${c}' which is not in ${denominator} {${[...declaredCodes].sort().join(", ") || "∅"}} (ambient kernel codes ${AMBIENT_CONTRACT_ERROR_CODES.join(", ")} may also be named)${union.unbounded ? "; the union is unbounded, so a catch-all is required" : ""}.`,
|
|
298
|
+
data: { resource: routing, filePath, path: `${arrayPath}[${i}].when` },
|
|
299
|
+
});
|
|
277
300
|
}
|
|
278
301
|
}
|
|
279
302
|
|
|
303
|
+
return diagnostics;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Rule 1 + the unbounded-union rule, asked ONCE per dispatch site over every
|
|
307
|
+
* list that can render its throws — the site's own, its resource's scope list,
|
|
308
|
+
* and every scope enclosing that resource.
|
|
309
|
+
*
|
|
310
|
+
* Asking it per list is what would make this a false check rather than a
|
|
311
|
+
* missing one: a route that declares no `catches:` under a router that renders
|
|
312
|
+
* everything is completely covered, and reporting it fires on precisely the
|
|
313
|
+
* manifests scope lists exist to enable. */
|
|
314
|
+
function checkCoverage(
|
|
315
|
+
union: ThrowsUnion,
|
|
316
|
+
resource: { kind: string; name: string },
|
|
317
|
+
filePath: string | undefined,
|
|
318
|
+
arrayPath: string,
|
|
319
|
+
handler: { kind: string; name?: string } | null,
|
|
320
|
+
covered: ProvenCoverage,
|
|
321
|
+
routing: { kind: string; name: string } = resource,
|
|
322
|
+
): AnalysisDiagnostic[] {
|
|
323
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
324
|
+
if (covered.hasCatchAll) return diagnostics;
|
|
325
|
+
|
|
280
326
|
// Unbounded union (passthrough or transitive): authors can't enumerate the
|
|
281
|
-
// codes, so a catch-all is mandatory.
|
|
282
|
-
if (union.unbounded
|
|
327
|
+
// codes, so a catch-all is mandatory — at this list or any enclosing scope.
|
|
328
|
+
if (union.unbounded) {
|
|
283
329
|
diagnostics.push({
|
|
284
330
|
severity: DiagnosticSeverity.Error,
|
|
285
331
|
code: "UNBOUNDED_UNION_NEEDS_CATCHALL",
|
|
286
332
|
source: SOURCE,
|
|
287
|
-
message: `The handler's throw union is unbounded (inherit/passthrough resolution couldn't enumerate all codes).
|
|
288
|
-
data: { resource, filePath, path: arrayPath },
|
|
333
|
+
message: `The handler's throw union is unbounded (inherit/passthrough resolution couldn't enumerate all codes). A catch-all entry (no \`when:\`) is required — on this catches: list or on an enclosing one.`,
|
|
334
|
+
data: { resource: routing, filePath, path: arrayPath },
|
|
289
335
|
});
|
|
290
336
|
}
|
|
291
337
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
});
|
|
307
|
-
}
|
|
338
|
+
// One diagnostic per site, not per code: every uncovered code sits at the
|
|
339
|
+
// same dispatch site, and one catch-all answers all of them at once. A
|
|
340
|
+
// diagnostic each repeated the same location and the same fix N times.
|
|
341
|
+
const uncovered = [...union.codes.keys()].filter((c) => !covered.codes.has(c)).sort();
|
|
342
|
+
if (uncovered.length > 0) {
|
|
343
|
+
diagnostics.push({
|
|
344
|
+
severity: DiagnosticSeverity.Error,
|
|
345
|
+
code: "UNCOVERED_THROW_CODE",
|
|
346
|
+
source: SOURCE,
|
|
347
|
+
message:
|
|
348
|
+
`handler ${handler?.name ? `\`!ref ${handler.name}\`` : `\`${handler?.kind ?? "?"}\``} can throw ${uncovered.length} code${uncovered.length === 1 ? "" : "s"} that no catches: entry handles — at this list or any enclosing scope: ${uncovered.map((c) => `'${c}'`).join(", ")}. ` +
|
|
349
|
+
`Give each a matching \`when:\` (e.g. \`when: !cel "error.code == '${uncovered[0]}'"\`), or add a catch-all entry — one with no \`when:\`, placed last.`,
|
|
350
|
+
data: { resource: routing, filePath, path: arrayPath, uncovered },
|
|
351
|
+
});
|
|
308
352
|
}
|
|
309
353
|
|
|
310
354
|
return diagnostics;
|
|
@@ -321,6 +365,7 @@ function checkTypedErrorData(
|
|
|
321
365
|
filePath: string | undefined,
|
|
322
366
|
arrayPath: string,
|
|
323
367
|
env: Environment,
|
|
368
|
+
routing: { kind: string; name: string } = resource,
|
|
324
369
|
): AnalysisDiagnostic[] {
|
|
325
370
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
326
371
|
// If the union is unbounded we can't narrow data schemas reliably — skip
|
|
@@ -350,20 +395,16 @@ function checkTypedErrorData(
|
|
|
350
395
|
const schemas = applicable.map((c) => dataByCode[c]).filter(Boolean) as Record<string, any>[];
|
|
351
396
|
if (schemas.length === 0) continue;
|
|
352
397
|
const dataSchema = intersectDataSchemas(schemas);
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
398
|
+
// The WHOLE entry, not an enumerated `body` / `headers` pair. An HTTP catch
|
|
399
|
+
// entry keeps its body at `content[<mime>].body`, so reading `e.body` walked
|
|
400
|
+
// a field that shape never has and this check was inert for every catch list
|
|
401
|
+
// in the standard library. Walking the entry also covers `when:` and the
|
|
402
|
+
// per-MIME header overrides, which are equally places `error.data` is read.
|
|
403
|
+
collectCelStrings(e, `${arrayPath}[${i}]`).forEach((entry) => {
|
|
356
404
|
diagnostics.push(
|
|
357
|
-
...checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, env),
|
|
405
|
+
...checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, env, routing),
|
|
358
406
|
);
|
|
359
407
|
});
|
|
360
|
-
if (e.headers) {
|
|
361
|
-
collectCelStrings(e.headers, `${arrayPath}[${i}].headers`).forEach((entry) => {
|
|
362
|
-
diagnostics.push(
|
|
363
|
-
...checkCelChainAgainstDataSchema(entry, dataSchema, resource, filePath, env),
|
|
364
|
-
);
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
408
|
}
|
|
368
409
|
return diagnostics;
|
|
369
410
|
}
|
|
@@ -404,6 +445,14 @@ interface CelString {
|
|
|
404
445
|
|
|
405
446
|
function collectCelStrings(value: unknown, path: string): CelString[] {
|
|
406
447
|
const out: CelString[] = [];
|
|
448
|
+
// A `!cel` sentinel and a `${{ … }}` string are load-equivalent, and the
|
|
449
|
+
// formatter normalizes to the tag — so recognising only the string form left
|
|
450
|
+
// this check answering about a spelling no manifest in the repository uses,
|
|
451
|
+
// while walking the sentinel as a plain object found nothing.
|
|
452
|
+
if (isTaggedSentinel(value)) {
|
|
453
|
+
if (value.engine === "cel") out.push({ expr: value.source.trim(), path });
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
407
456
|
if (typeof value === "string") {
|
|
408
457
|
for (const m of value.matchAll(TEMPLATE_REGEX)) {
|
|
409
458
|
out.push({ expr: m[1].trim(), path });
|
|
@@ -430,6 +479,7 @@ function checkCelChainAgainstDataSchema(
|
|
|
430
479
|
resource: { kind: string; name: string },
|
|
431
480
|
filePath: string | undefined,
|
|
432
481
|
env: Environment,
|
|
482
|
+
routing: { kind: string; name: string } = resource,
|
|
433
483
|
): AnalysisDiagnostic[] {
|
|
434
484
|
let ast: ASTNode;
|
|
435
485
|
try {
|
|
@@ -450,7 +500,7 @@ function checkCelChainAgainstDataSchema(
|
|
|
450
500
|
code: "CEL_UNKNOWN_FIELD",
|
|
451
501
|
source: SOURCE,
|
|
452
502
|
message: `${resource.kind}/${resource.name}: CEL at '${entry.path}': error.data.${err}`,
|
|
453
|
-
data: { resource, filePath, path: entry.path },
|
|
503
|
+
data: { resource: routing, filePath, path: entry.path },
|
|
454
504
|
});
|
|
455
505
|
}
|
|
456
506
|
}
|
|
@@ -463,6 +513,31 @@ function checkCelChainAgainstDataSchema(
|
|
|
463
513
|
* carrying the legacy `x-telo-step-context` annotation. That is what drives the
|
|
464
514
|
* resolver's generic step traversal; a definition with `inherit: true` and no
|
|
465
515
|
* such array has no invocables to inherit from. */
|
|
516
|
+
/** The capabilities whose lifecycle includes a dispatch a caller can catch. On
|
|
517
|
+
* every other one a thrown error is a boot-time failure, not a structured
|
|
518
|
+
* runtime error for a downstream caller — a provider resolves configuration, a
|
|
519
|
+
* type has no instance, a sink is written to directly, and a service or mount
|
|
520
|
+
* is STARTED rather than called (what a router renders is not what a router
|
|
521
|
+
* throws).
|
|
522
|
+
*
|
|
523
|
+
* The strict half of the kernel's `ResourceDefinitionSchema` rule 8, and it has
|
|
524
|
+
* to exist here for the reason every `x-telo-*` accessor has a strict half: the
|
|
525
|
+
* kernel refuses at `create()`, which is a boot failure on a manifest that
|
|
526
|
+
* passed `telo check` — the static/runtime disagreement this repository treats
|
|
527
|
+
* as a defect. The two must agree; a change to either belongs in both.
|
|
528
|
+
*
|
|
529
|
+
* **Reported for a DEPENDENCY's definition too**, unlike `X_TELO_REF_UNRESOLVED`
|
|
530
|
+
* and `DEPRECATED_KIND`, which are entry-module-scoped. Those report something a
|
|
531
|
+
* consumer can live with — a slot that cannot be checked, a kind that still
|
|
532
|
+
* works — so silence costs them nothing and the noise is not theirs to fix.
|
|
533
|
+
* This one reports a definition the kernel REFUSES, so the manifest importing it
|
|
534
|
+
* cannot start at all: withholding that would replace a `telo check` error with
|
|
535
|
+
* an identical boot failure and no earlier warning. The action is a consumer's
|
|
536
|
+
* to take (pin another version, report upstream) even though the edit is not.
|
|
537
|
+
* Matches the neighbouring `INHERIT_WITHOUT_STEP_CONTEXT`, which is fatal the
|
|
538
|
+
* same way. */
|
|
539
|
+
const THROWS_CAPABLE_CAPABILITIES = new Set(["Telo.Invocable", "Telo.Runnable"]);
|
|
540
|
+
|
|
466
541
|
function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiagnostic[] {
|
|
467
542
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
468
543
|
for (const m of manifests) {
|
|
@@ -471,9 +546,29 @@ function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiag
|
|
|
471
546
|
if (!throws) continue;
|
|
472
547
|
const name = (m.metadata?.name as string | undefined) ?? "<unnamed>";
|
|
473
548
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
549
|
+
|
|
550
|
+
// Only a DECLARED capability is judged. One inherited through `extends` is
|
|
551
|
+
// resolved elsewhere, and an unknown one is third-party extensibility the
|
|
552
|
+
// kernel's schema deliberately leaves open.
|
|
553
|
+
const capability = (m as Record<string, any>).capability as string | undefined;
|
|
554
|
+
if (typeof capability === "string" && !THROWS_CAPABLE_CAPABILITIES.has(capability)) {
|
|
555
|
+
diagnostics.push({
|
|
556
|
+
severity: DiagnosticSeverity.Error,
|
|
557
|
+
code: "THROWS_ON_NON_DISPATCH_CAPABILITY",
|
|
558
|
+
source: SOURCE,
|
|
559
|
+
message:
|
|
560
|
+
`Telo.Definition '${name}' declares throws: but its capability is '${capability}'. ` +
|
|
561
|
+
`A throw union describes what a CALLER can catch, so it is only meaningful on ` +
|
|
562
|
+
`${[...THROWS_CAPABLE_CAPABILITIES].join(" or ")}; on '${capability}' a thrown error is a ` +
|
|
563
|
+
`boot-time failure with no caller to render it. The kernel refuses this definition at ` +
|
|
564
|
+
`create(), so a manifest carrying it cannot start.`,
|
|
565
|
+
data: { resource: { kind: m.kind, name }, filePath, path: "throws" },
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
474
569
|
if (throws.inherit === true) {
|
|
475
570
|
const schema = (m as Record<string, any>).schema as Record<string, any> | undefined;
|
|
476
|
-
if (!
|
|
571
|
+
if (!schemaDrivesInvocables(schema)) {
|
|
477
572
|
diagnostics.push({
|
|
478
573
|
severity: DiagnosticSeverity.Error,
|
|
479
574
|
code: "INHERIT_WITHOUT_STEP_CONTEXT",
|
|
@@ -491,25 +586,25 @@ function validateThrowsDeclarations(manifests: ResourceManifest[]): AnalysisDiag
|
|
|
491
586
|
return diagnostics;
|
|
492
587
|
}
|
|
493
588
|
|
|
494
|
-
function
|
|
589
|
+
function schemaDrivesInvocables(schema: Record<string, any> | undefined): boolean {
|
|
495
590
|
if (!schema || typeof schema !== "object") return false;
|
|
496
591
|
if (isStepSlot(schema)) return true;
|
|
497
592
|
const props = schema.properties;
|
|
498
593
|
if (props && typeof props === "object") {
|
|
499
594
|
for (const v of Object.values(props as Record<string, any>)) {
|
|
500
|
-
if (
|
|
595
|
+
if (schemaDrivesInvocables(v)) return true;
|
|
501
596
|
}
|
|
502
597
|
}
|
|
503
|
-
if (schema.items &&
|
|
598
|
+
if (schema.items && schemaDrivesInvocables(schema.items)) return true;
|
|
504
599
|
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
505
600
|
const arr = schema[key];
|
|
506
601
|
if (Array.isArray(arr)) {
|
|
507
|
-
for (const sub of arr) if (
|
|
602
|
+
for (const sub of arr) if (schemaDrivesInvocables(sub)) return true;
|
|
508
603
|
}
|
|
509
604
|
}
|
|
510
605
|
if (schema.$defs && typeof schema.$defs === "object") {
|
|
511
606
|
for (const v of Object.values(schema.$defs as Record<string, any>)) {
|
|
512
|
-
if (
|
|
607
|
+
if (schemaDrivesInvocables(v)) return true;
|
|
513
608
|
}
|
|
514
609
|
}
|
|
515
610
|
return false;
|
|
@@ -523,11 +618,35 @@ export function validateThrowsCoverage(
|
|
|
523
618
|
env: Environment,
|
|
524
619
|
aliasesByModule: Map<string, AliasResolver> = new Map(),
|
|
525
620
|
rootModules: Set<string> = new Set(),
|
|
621
|
+
/** Each imported library's full document set, so the walk can follow an
|
|
622
|
+
* exported entry point into the siblings it invokes — which a consumer's
|
|
623
|
+
* flat set does not carry. */
|
|
624
|
+
moduleManifests: Map<string, ResourceManifest[]> = new Map(),
|
|
526
625
|
): AnalysisDiagnostic[] {
|
|
527
626
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
528
627
|
diagnostics.push(...validateThrowsDeclarations(manifests));
|
|
529
628
|
|
|
530
|
-
|
|
629
|
+
// A `with:`-scoped declaration is a resource like any other — it has a kind, a
|
|
630
|
+
// name, and, for a scoped `Http.Server`, a catch list that renders what its
|
|
631
|
+
// mounts throw. It is simply not in the flat set, so every check here used to
|
|
632
|
+
// skip it: its own entries went unchecked AND its coverage reached nothing it
|
|
633
|
+
// encloses. Standing a server up around a test is exactly that shape, so the
|
|
634
|
+
// sanctioned pattern was the one the pass could not see.
|
|
635
|
+
//
|
|
636
|
+
// Discovered through the shared visitor rather than a second scope walk, and
|
|
637
|
+
// folded into the pool every name is resolved against — a scoped mount whose
|
|
638
|
+
// target the resolver cannot find reads as an empty union, which reports every
|
|
639
|
+
// entry of that server's list as naming a code nothing throws.
|
|
640
|
+
const scoped = collectScopedManifests(manifests, defs, aliases, aliasesByModule, rootModules);
|
|
641
|
+
const allManifests = [...manifests, ...scoped.map((s) => s.manifest)];
|
|
642
|
+
const resolveCtx = createResolveCtx(
|
|
643
|
+
allManifests,
|
|
644
|
+
defs,
|
|
645
|
+
aliases,
|
|
646
|
+
aliasesByModule,
|
|
647
|
+
rootModules,
|
|
648
|
+
moduleManifests,
|
|
649
|
+
);
|
|
531
650
|
|
|
532
651
|
// The alias resolver for a manifest's own lexical scope — an imported library's
|
|
533
652
|
// resolver when it owns the manifest, else undefined (fall back to root aliases).
|
|
@@ -538,43 +657,176 @@ export function validateThrowsCoverage(
|
|
|
538
657
|
aliasesByModule,
|
|
539
658
|
);
|
|
540
659
|
|
|
541
|
-
|
|
660
|
+
// Pass 1 — read every outcome list, and record which resources each scope
|
|
661
|
+
// list encloses. A scope list has to be known before any site it covers is
|
|
662
|
+
// judged, so collection and judgement cannot be one loop.
|
|
663
|
+
const sites: CatchSite[] = [];
|
|
664
|
+
const scopeOf = new Map<ResourceManifest, ProvenCoverage>();
|
|
665
|
+
const scopedBy = new Map<ResourceManifest, ScopedManifest>();
|
|
666
|
+
for (const s of scoped) scopedBy.set(s.manifest, s);
|
|
667
|
+
|
|
668
|
+
const definitionFor = (manifest: ResourceManifest) => {
|
|
669
|
+
// A scoped declaration's kind is written in the alias scope of the module
|
|
670
|
+
// that declared the ENCLOSING resource; it carries no `metadata.module` of
|
|
671
|
+
// its own to find one by.
|
|
672
|
+
const anchor = scopedBy.get(manifest)?.owner ?? manifest;
|
|
673
|
+
const scopeResolver = scopeResolverFor(anchor);
|
|
674
|
+
const resolvedKind =
|
|
675
|
+
scopeResolver?.resolveKind(manifest.kind) ?? aliases.resolveKind(manifest.kind);
|
|
676
|
+
return defs.resolve(manifest.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const enclosers = buildEnclosers(
|
|
680
|
+
allManifests,
|
|
681
|
+
definitionFor,
|
|
682
|
+
(m) => ((scopedBy.get(m)?.owner ?? m).metadata as { module?: string } | undefined)?.module,
|
|
683
|
+
resolveCtx,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
for (const manifest of allManifests) {
|
|
542
687
|
if (!manifest.kind || !manifest.metadata?.name) continue;
|
|
543
688
|
if (manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract") continue;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
689
|
+
// A scoped declaration is reported against the document it is WRITTEN in —
|
|
690
|
+
// its owner's — at its own position inside that owner's scope array, because
|
|
691
|
+
// position lookup finds a TOP-LEVEL doc by (kind, name) and a scoped
|
|
692
|
+
// resource is not one. The message still names the scoped resource, so the
|
|
693
|
+
// reader is not sent to a resource that has no `catches:` at all.
|
|
694
|
+
const enclosing = scopedBy.get(manifest);
|
|
695
|
+
const anchor = enclosing?.owner ?? manifest;
|
|
696
|
+
const pathPrefix = enclosing ? `${enclosing.path}.` : "";
|
|
697
|
+
const scopeResolver = scopeResolverFor(anchor);
|
|
698
|
+
const definition = definitionFor(manifest);
|
|
548
699
|
if (!definition?.schema) continue;
|
|
549
700
|
const resource = { kind: manifest.kind, name: manifest.metadata.name as string };
|
|
550
|
-
const
|
|
701
|
+
const routing = enclosing
|
|
702
|
+
? { kind: anchor.kind, name: anchor.metadata!.name as string }
|
|
703
|
+
: resource;
|
|
704
|
+
const filePath = (anchor.metadata as { source?: string } | undefined)?.source;
|
|
551
705
|
|
|
552
706
|
collectOutcomeLists(
|
|
553
707
|
manifest,
|
|
554
708
|
definition.schema,
|
|
555
709
|
(ret) => {
|
|
556
710
|
diagnostics.push(
|
|
557
|
-
...checkCatchAllPlacement(
|
|
711
|
+
...checkCatchAllPlacement(
|
|
712
|
+
ret.entries,
|
|
713
|
+
resource,
|
|
714
|
+
"returns",
|
|
715
|
+
filePath,
|
|
716
|
+
`${pathPrefix}${ret.arrayPath}`,
|
|
717
|
+
routing,
|
|
718
|
+
),
|
|
558
719
|
);
|
|
559
720
|
},
|
|
560
721
|
(entries, arrayPath, siblingData, catchesFor) => {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
722
|
+
const site: CatchSite = {
|
|
723
|
+
manifest,
|
|
724
|
+
definition,
|
|
725
|
+
resource,
|
|
726
|
+
routing,
|
|
727
|
+
filePath,
|
|
728
|
+
entries,
|
|
729
|
+
arrayPath: `${pathPrefix}${arrayPath}`,
|
|
730
|
+
scopeResolver,
|
|
731
|
+
handlerRef: catchesFor === "" ? null : resolveHandlerRef(siblingData[catchesFor]),
|
|
732
|
+
isScope: catchesFor === "",
|
|
733
|
+
};
|
|
734
|
+
sites.push(site);
|
|
735
|
+
if (site.isScope) scopeOf.set(manifest, provenCoverage(entries, env));
|
|
572
736
|
},
|
|
573
737
|
);
|
|
574
738
|
}
|
|
739
|
+
|
|
740
|
+
const coverageMemo = new Map<ResourceManifest, ProvenCoverage>();
|
|
741
|
+
const scopeCoverageFor = (manifest: ResourceManifest): ProvenCoverage =>
|
|
742
|
+
enclosingCoverage(manifest, scopeOf, enclosers, coverageMemo);
|
|
743
|
+
|
|
744
|
+
// Pass 2 — judge each list against its own denominator, and each dispatch site
|
|
745
|
+
// against everything that can render its throws.
|
|
746
|
+
for (const site of sites) {
|
|
747
|
+
diagnostics.push(
|
|
748
|
+
...checkCatchAllPlacement(
|
|
749
|
+
site.entries,
|
|
750
|
+
site.resource,
|
|
751
|
+
"catches",
|
|
752
|
+
site.filePath,
|
|
753
|
+
site.arrayPath,
|
|
754
|
+
site.routing,
|
|
755
|
+
),
|
|
756
|
+
);
|
|
757
|
+
const union = site.isScope
|
|
758
|
+
? resolveScopeUnion(site.manifest, site.definition, resolveCtx)
|
|
759
|
+
: handlerRefUnion(site.handlerRef, allManifests, resolveCtx, site.scopeResolver);
|
|
760
|
+
diagnostics.push(
|
|
761
|
+
...checkUndeclaredCodes(
|
|
762
|
+
site.entries,
|
|
763
|
+
union,
|
|
764
|
+
site.resource,
|
|
765
|
+
site.filePath,
|
|
766
|
+
site.arrayPath,
|
|
767
|
+
env,
|
|
768
|
+
site.isScope
|
|
769
|
+
? "the throw union of everything this resource drives"
|
|
770
|
+
: "the handler's declared throw union",
|
|
771
|
+
site.routing,
|
|
772
|
+
),
|
|
773
|
+
);
|
|
774
|
+
diagnostics.push(
|
|
775
|
+
...checkTypedErrorData(
|
|
776
|
+
site.entries,
|
|
777
|
+
union,
|
|
778
|
+
site.resource,
|
|
779
|
+
site.filePath,
|
|
780
|
+
site.arrayPath,
|
|
781
|
+
env,
|
|
782
|
+
site.routing,
|
|
783
|
+
),
|
|
784
|
+
);
|
|
785
|
+
if (site.isScope) continue;
|
|
786
|
+
|
|
787
|
+
const own = provenCoverage(site.entries, env);
|
|
788
|
+
const scope = scopeCoverageFor(site.manifest);
|
|
789
|
+
const covered: ProvenCoverage = {
|
|
790
|
+
codes: new Set([...own.codes, ...scope.codes]),
|
|
791
|
+
hasCatchAll: own.hasCatchAll || scope.hasCatchAll,
|
|
792
|
+
};
|
|
793
|
+
diagnostics.push(
|
|
794
|
+
...checkCoverage(
|
|
795
|
+
union,
|
|
796
|
+
site.resource,
|
|
797
|
+
site.filePath,
|
|
798
|
+
site.arrayPath,
|
|
799
|
+
site.handlerRef,
|
|
800
|
+
covered,
|
|
801
|
+
site.routing,
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
575
806
|
return diagnostics;
|
|
576
807
|
}
|
|
577
808
|
|
|
809
|
+
/** One `catches:` list in one manifest, with everything needed to judge it. */
|
|
810
|
+
interface CatchSite {
|
|
811
|
+
manifest: ResourceManifest;
|
|
812
|
+
definition: ResourceDefinition;
|
|
813
|
+
/** Named in the MESSAGE — the resource whose list this is. */
|
|
814
|
+
resource: { kind: string; name: string };
|
|
815
|
+
/** Named in `data.resource`, which is how position lookup finds a document:
|
|
816
|
+
* it searches TOP-LEVEL docs by (kind, name), so a scoped resource routes
|
|
817
|
+
* through its owner while the message still names the scoped one. */
|
|
818
|
+
routing: { kind: string; name: string };
|
|
819
|
+
filePath: string | undefined;
|
|
820
|
+
entries: OutcomeEntry[];
|
|
821
|
+
arrayPath: string;
|
|
822
|
+
scopeResolver: AliasResolver | undefined;
|
|
823
|
+
/** The handler this list renders throws for; null for a scope list. */
|
|
824
|
+
handlerRef: { kind: string; name?: string } | null;
|
|
825
|
+
/** `x-telo-catches-for: ""` — the list covers everything its resource drives
|
|
826
|
+
* and owes coverage of nothing on its own. */
|
|
827
|
+
isScope: boolean;
|
|
828
|
+
}
|
|
829
|
+
|
|
578
830
|
/** Resolve a handler ref's effective throw union. Prefers the named manifest
|
|
579
831
|
* (so `inherit: true` handlers expose their transitive union); falls back to
|
|
580
832
|
* the definition's own codes when no name is given. */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"zone-module-documents.d.ts","sourceRoot":"","sources":["../src/zone-module-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC"}
|
|
File without changes
|