@telorun/analyzer 0.23.0 → 0.23.1
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/alias-resolver.d.ts +10 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +14 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +13 -7
- package/dist/resolve-throws-union.d.ts +9 -2
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +47 -23
- package/dist/validate-throws-coverage.d.ts +2 -2
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +15 -7
- package/package.json +2 -2
- package/src/alias-resolver.ts +19 -0
- package/src/analyzer.ts +15 -8
- package/src/resolve-throws-union.ts +56 -18
- package/src/validate-throws-coverage.ts +22 -6
package/dist/alias-resolver.d.ts
CHANGED
|
@@ -26,4 +26,14 @@ export declare class AliasResolver {
|
|
|
26
26
|
* back into its user-facing alias form (e.g. "Http.Server"). */
|
|
27
27
|
aliasesFor(targetModule: string): string[];
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The alias resolver for a resource's own lexical scope. A resource that
|
|
31
|
+
* originated in an imported library (its `ownModule` names a non-root module —
|
|
32
|
+
* e.g. an inline handler extracted from an imported Http.Api) resolves its kind
|
|
33
|
+
* aliases against THAT library's import map, so an anonymous child inherits the
|
|
34
|
+
* lexical scope of the document that declares it. Returns undefined for
|
|
35
|
+
* root/consumer-owned resources (and unknown modules), so callers fall back to
|
|
36
|
+
* the root `aliases`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function scopeResolverForModule(ownModule: string | undefined, rootModules: Set<string>, aliasesByModule: Map<string, AliasResolver>): AliasResolver | undefined;
|
|
29
39
|
//# sourceMappingURL=alias-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE;;4FAEwF;IACxF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;IAE7D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF;sFACkF;IAClF,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAIhF;;;;qDAIiD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIjD,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAmB7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;IAIxB;;qEAEiE;IACjE,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE;CAO3C"}
|
|
1
|
+
{"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAChE;;4FAEwF;IACxF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;IAE7D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF;sFACkF;IAClF,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAIhF;;;;qDAIiD;IACjD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIjD,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAmB7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;IAIxB;;qEAEiE;IACjE,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE;CAO3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,aAAa,GAAG,SAAS,CAI3B"}
|
package/dist/alias-resolver.js
CHANGED
|
@@ -67,3 +67,17 @@ export class AliasResolver {
|
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* The alias resolver for a resource's own lexical scope. A resource that
|
|
72
|
+
* originated in an imported library (its `ownModule` names a non-root module —
|
|
73
|
+
* e.g. an inline handler extracted from an imported Http.Api) resolves its kind
|
|
74
|
+
* aliases against THAT library's import map, so an anonymous child inherits the
|
|
75
|
+
* lexical scope of the document that declares it. Returns undefined for
|
|
76
|
+
* root/consumer-owned resources (and unknown modules), so callers fall back to
|
|
77
|
+
* the root `aliases`.
|
|
78
|
+
*/
|
|
79
|
+
export function scopeResolverForModule(ownModule, rootModules, aliasesByModule) {
|
|
80
|
+
return ownModule && !rootModules.has(ownModule)
|
|
81
|
+
? aliasesByModule.get(ownModule)
|
|
82
|
+
: undefined;
|
|
83
|
+
}
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAstBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAerB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
2
|
-
import { AliasResolver } from "./alias-resolver.js";
|
|
2
|
+
import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
|
|
3
3
|
import { buildCelEnvironment, buildImportInputCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
|
|
4
4
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
@@ -796,8 +796,15 @@ export class StaticAnalyzer {
|
|
|
796
796
|
const resource = { kind: m.kind, name: m.metadata?.name };
|
|
797
797
|
// Resolve kind through alias if needed; direct lookup takes priority so that
|
|
798
798
|
// aliases whose name matches the module name (the common case) work without
|
|
799
|
-
// path-derived name mangling.
|
|
800
|
-
|
|
799
|
+
// path-derived name mangling. A resource that originated in an imported library
|
|
800
|
+
// (its `metadata.module` names a non-root module — e.g. an inline route handler
|
|
801
|
+
// extracted from an imported Http.Api) must resolve its kind alias against THAT
|
|
802
|
+
// library's import map, not the consumer's; an anonymous child inherits the
|
|
803
|
+
// lexical scope of the document that declares it. Mirrors the nested-inline and
|
|
804
|
+
// reference-resolution paths: own-module scope first, root/consumer aliases last.
|
|
805
|
+
const ownModule = m.metadata?.module;
|
|
806
|
+
const scopeResolver = scopeResolverForModule(ownModule, rootModules, aliasesByModule);
|
|
807
|
+
const resolvedKind = scopeResolver?.resolveKind(m.kind) ?? aliases.resolveKind(m.kind);
|
|
801
808
|
const definition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
802
809
|
if (!definition) {
|
|
803
810
|
const suggestedKind = computeSuggestKind(m.kind, aliases, defs);
|
|
@@ -865,9 +872,8 @@ export class StaticAnalyzer {
|
|
|
865
872
|
// first, then the parent module's own aliases (for resources declared
|
|
866
873
|
// inside an imported module), then the root aliases. Mirrors how the
|
|
867
874
|
// analyzer resolves kinds elsewhere so module-scoped aliases don't
|
|
868
|
-
// produce false UNDEFINED_KIND diagnostics.
|
|
869
|
-
|
|
870
|
-
const scopeResolver = ownModule && !rootModules.has(ownModule) ? aliasesByModule.get(ownModule) : undefined;
|
|
875
|
+
// produce false UNDEFINED_KIND diagnostics. `scopeResolver` is the
|
|
876
|
+
// owning module's resolver computed above.
|
|
871
877
|
diagnostics.push(...validateNestedInlineResources(m, definition.schema, (kind) => {
|
|
872
878
|
const direct = defs.resolve(kind);
|
|
873
879
|
if (direct)
|
|
@@ -1082,7 +1088,7 @@ export class StaticAnalyzer {
|
|
|
1082
1088
|
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
1083
1089
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
1084
1090
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
1085
|
-
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
1091
|
+
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv, aliasesByModule, rootModules));
|
|
1086
1092
|
// Warn about declared variables / secrets / ports that no CEL references.
|
|
1087
1093
|
diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
|
|
1088
1094
|
// Reroute diagnostics on synthetic (inline-extracted) resources back to
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import type
|
|
2
|
+
import { type AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
export interface ThrowsCodeMeta {
|
|
5
5
|
data?: Record<string, any>;
|
|
@@ -27,10 +27,17 @@ export interface ResolveCtx {
|
|
|
27
27
|
allManifests: ResourceManifest[];
|
|
28
28
|
defs: DefinitionRegistry;
|
|
29
29
|
aliases: AliasResolver;
|
|
30
|
+
/** Per-imported-library alias resolvers, keyed by module name. A manifest that
|
|
31
|
+
* originated in an imported library resolves its kind aliases against its own
|
|
32
|
+
* module's resolver, not the consumer's — an inline handler extracted from an
|
|
33
|
+
* imported Http.Api inherits the lexical scope of the library that declares it. */
|
|
34
|
+
aliasesByModule: Map<string, AliasResolver>;
|
|
35
|
+
/** The consumer/root module names; resources owned by these resolve against `aliases`. */
|
|
36
|
+
rootModules: Set<string>;
|
|
30
37
|
memo: Map<string, ThrowsUnion>;
|
|
31
38
|
inProgress: Set<string>;
|
|
32
39
|
}
|
|
33
|
-
export declare function createResolveCtx(allManifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver): ResolveCtx;
|
|
40
|
+
export declare function createResolveCtx(allManifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, aliasesByModule?: Map<string, AliasResolver>, rootModules?: Set<string>): ResolveCtx;
|
|
34
41
|
/** Resolve the effective throw union for a named manifest. The result combines
|
|
35
42
|
* explicit `throws.codes`, `throws.inherit: true` dataflow (step-context
|
|
36
43
|
* traversal with try/catch subtraction), and unbounded markers for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-throws-union.d.ts","sourceRoot":"","sources":["../src/resolve-throws-union.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"resolve-throws-union.d.ts","sourceRoot":"","sources":["../src/resolve-throws-union.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B;AAED;;gFAEgF;AAChF,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AAEjD,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACnC;;;8EAG0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB;;;;gDAI4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,aAAa,CAAC;IACvB;;;wFAGoF;IACpF,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC5C,0FAA0F;IAC1F,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,gBAAgB,EAAE,EAChC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,UAAU,CAUZ;AA6CD;;;;8CAI8C;AAC9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,gBAAgB,EAC1B,GAAG,EAAE,UAAU,GACd,WAAW,CAiDb"}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { scopeResolverForModule } from "./alias-resolver.js";
|
|
2
3
|
/** Code a non-`InvokeError` failure surfaces as inside a `catch` block. Mirrors
|
|
3
4
|
* `PLAIN_ERROR_CODE` in `@telorun/run`'s `toSequenceError`: any invoke can throw
|
|
4
5
|
* a plain error, which the catch sees as `error.code === "INTERNAL_ERROR"`. */
|
|
5
6
|
export const PLAIN_ERROR_CODE = "INTERNAL_ERROR";
|
|
6
|
-
export function createResolveCtx(allManifests, defs, aliases) {
|
|
7
|
+
export function createResolveCtx(allManifests, defs, aliases, aliasesByModule = new Map(), rootModules = new Set()) {
|
|
7
8
|
return {
|
|
8
9
|
allManifests,
|
|
9
10
|
defs,
|
|
10
11
|
aliases,
|
|
12
|
+
aliasesByModule,
|
|
13
|
+
rootModules,
|
|
11
14
|
memo: new Map(),
|
|
12
15
|
inProgress: new Set(),
|
|
13
16
|
};
|
|
@@ -15,6 +18,10 @@ export function createResolveCtx(allManifests, defs, aliases) {
|
|
|
15
18
|
function emptyUnion() {
|
|
16
19
|
return { codes: new Map(), unbounded: false };
|
|
17
20
|
}
|
|
21
|
+
/** The owning module's alias resolver for a manifest in this resolve context. */
|
|
22
|
+
function scopeResolverFor(ctx, ownModule) {
|
|
23
|
+
return scopeResolverForModule(ownModule, ctx.rootModules, ctx.aliasesByModule);
|
|
24
|
+
}
|
|
18
25
|
function unionInto(target, src) {
|
|
19
26
|
for (const [code, meta] of src.codes) {
|
|
20
27
|
if (!target.codes.has(code))
|
|
@@ -25,9 +32,18 @@ function unionInto(target, src) {
|
|
|
25
32
|
if (src.canThrowPlain)
|
|
26
33
|
target.canThrowPlain = true;
|
|
27
34
|
}
|
|
28
|
-
function definitionFor(kind, defs, aliases) {
|
|
35
|
+
function definitionFor(kind, defs, aliases, scopeResolver) {
|
|
36
|
+
const direct = defs.resolve(kind);
|
|
37
|
+
if (direct)
|
|
38
|
+
return direct;
|
|
39
|
+
const scoped = scopeResolver?.resolveKind(kind);
|
|
40
|
+
if (scoped) {
|
|
41
|
+
const d = defs.resolve(scoped);
|
|
42
|
+
if (d)
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
29
45
|
const resolved = aliases.resolveKind(kind);
|
|
30
|
-
return
|
|
46
|
+
return resolved ? defs.resolve(resolved) : undefined;
|
|
31
47
|
}
|
|
32
48
|
function codesFromDefinition(definition) {
|
|
33
49
|
const out = new Map();
|
|
@@ -51,7 +67,9 @@ export function resolveThrowsUnion(manifest, ctx) {
|
|
|
51
67
|
if (ctx.inProgress.has(name))
|
|
52
68
|
return emptyUnion();
|
|
53
69
|
}
|
|
54
|
-
const
|
|
70
|
+
const ownModule = manifest.metadata?.module;
|
|
71
|
+
const scopeResolver = scopeResolverFor(ctx, ownModule);
|
|
72
|
+
const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases, scopeResolver);
|
|
55
73
|
if (!definition) {
|
|
56
74
|
const u = { codes: new Map(), unbounded: true };
|
|
57
75
|
if (name)
|
|
@@ -78,7 +96,7 @@ export function resolveThrowsUnion(manifest, ctx) {
|
|
|
78
96
|
result.unbounded = true;
|
|
79
97
|
}
|
|
80
98
|
if (throws.inherit) {
|
|
81
|
-
const inherited = resolveInherited(manifest, definition, ctx);
|
|
99
|
+
const inherited = resolveInherited(manifest, definition, ctx, ownModule);
|
|
82
100
|
unionInto(result, inherited);
|
|
83
101
|
}
|
|
84
102
|
if (name)
|
|
@@ -90,7 +108,7 @@ export function resolveThrowsUnion(manifest, ctx) {
|
|
|
90
108
|
ctx.inProgress.delete(name);
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
|
-
function resolveInherited(manifest, definition, ctx) {
|
|
111
|
+
function resolveInherited(manifest, definition, ctx, ownerModule) {
|
|
94
112
|
const result = { codes: new Map(), unbounded: false };
|
|
95
113
|
const props = definition.schema?.properties;
|
|
96
114
|
if (!props)
|
|
@@ -102,16 +120,16 @@ function resolveInherited(manifest, definition, ctx) {
|
|
|
102
120
|
const steps = manifest[fieldName];
|
|
103
121
|
if (!Array.isArray(steps))
|
|
104
122
|
continue;
|
|
105
|
-
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx));
|
|
123
|
+
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx, ownerModule));
|
|
106
124
|
}
|
|
107
125
|
return result;
|
|
108
126
|
}
|
|
109
|
-
function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx) {
|
|
127
|
+
function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx, ownerModule) {
|
|
110
128
|
const result = emptyUnion();
|
|
111
129
|
for (const step of steps) {
|
|
112
130
|
if (!step || typeof step !== "object")
|
|
113
131
|
continue;
|
|
114
|
-
unionInto(result, collectStepThrows(step, invokeField, enclosingTryCodes, ctx));
|
|
132
|
+
unionInto(result, collectStepThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
115
133
|
}
|
|
116
134
|
return result;
|
|
117
135
|
}
|
|
@@ -120,11 +138,11 @@ function collectStepArrayThrows(steps, invokeField, enclosingTryCodes, ctx) {
|
|
|
120
138
|
* / `else` / `elseif` / `do` / `cases` / `default`) are the same set already
|
|
121
139
|
* traversed by the analyzer's `x-telo-step-context` schema builder, so future
|
|
122
140
|
* composers that reuse those shape conventions work without changes here. */
|
|
123
|
-
function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
141
|
+
function collectStepThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule) {
|
|
124
142
|
if (step[invokeField]) {
|
|
125
143
|
// Any invoked resource can throw a non-InvokeError at runtime, which an
|
|
126
144
|
// enclosing catch surfaces as PLAIN_ERROR_CODE — record that possibility.
|
|
127
|
-
const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx));
|
|
145
|
+
const u = cloneUnion(resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
128
146
|
u.canThrowPlain = true;
|
|
129
147
|
return u;
|
|
130
148
|
}
|
|
@@ -132,7 +150,7 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
132
150
|
return resolveThrowStepCode(step.throw, enclosingTryCodes);
|
|
133
151
|
}
|
|
134
152
|
if (Array.isArray(step.try)) {
|
|
135
|
-
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx);
|
|
153
|
+
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx, ownerModule);
|
|
136
154
|
let propagated;
|
|
137
155
|
if (Array.isArray(step.catch)) {
|
|
138
156
|
// Catch absorbs the try block's codes; the catch's own throws propagate
|
|
@@ -144,7 +162,7 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
144
162
|
// rethrow can propagate it — seed the set the catch resolves against.
|
|
145
163
|
if (tryUnion.canThrowPlain)
|
|
146
164
|
tryCodes.add(PLAIN_ERROR_CODE);
|
|
147
|
-
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
|
|
165
|
+
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx, ownerModule);
|
|
148
166
|
// Unbounded in the try block still signals the caller to expect
|
|
149
167
|
// arbitrary codes to flow through the catch (e.g. via passthrough).
|
|
150
168
|
if (tryUnion.unbounded)
|
|
@@ -154,37 +172,37 @@ function collectStepThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
154
172
|
propagated = cloneUnion(tryUnion);
|
|
155
173
|
}
|
|
156
174
|
if (Array.isArray(step.finally)) {
|
|
157
|
-
unionInto(propagated, collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx));
|
|
175
|
+
unionInto(propagated, collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
158
176
|
}
|
|
159
177
|
return propagated;
|
|
160
178
|
}
|
|
161
179
|
if (Array.isArray(step.then)) {
|
|
162
180
|
const result = emptyUnion();
|
|
163
|
-
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx));
|
|
181
|
+
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
164
182
|
if (Array.isArray(step.else)) {
|
|
165
|
-
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx));
|
|
183
|
+
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
166
184
|
}
|
|
167
185
|
if (Array.isArray(step.elseif)) {
|
|
168
186
|
for (const branch of step.elseif) {
|
|
169
187
|
if (Array.isArray(branch?.then)) {
|
|
170
|
-
unionInto(result, collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx));
|
|
188
|
+
unionInto(result, collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
return result;
|
|
175
193
|
}
|
|
176
194
|
if (Array.isArray(step.do)) {
|
|
177
|
-
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx);
|
|
195
|
+
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx, ownerModule);
|
|
178
196
|
}
|
|
179
197
|
if (step.cases && typeof step.cases === "object") {
|
|
180
198
|
const result = emptyUnion();
|
|
181
199
|
for (const arr of Object.values(step.cases)) {
|
|
182
200
|
if (Array.isArray(arr)) {
|
|
183
|
-
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx));
|
|
201
|
+
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
204
|
if (Array.isArray(step.default)) {
|
|
187
|
-
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx));
|
|
205
|
+
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
188
206
|
}
|
|
189
207
|
return result;
|
|
190
208
|
}
|
|
@@ -199,14 +217,18 @@ function cloneUnion(u) {
|
|
|
199
217
|
out.canThrowPlain = true;
|
|
200
218
|
return out;
|
|
201
219
|
}
|
|
202
|
-
function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
220
|
+
function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule) {
|
|
203
221
|
const invokeRef = step[invokeField];
|
|
204
222
|
if (!invokeRef || typeof invokeRef !== "object")
|
|
205
223
|
return emptyUnion();
|
|
206
224
|
const invokedKind = invokeRef.kind;
|
|
207
225
|
if (!invokedKind)
|
|
208
226
|
return emptyUnion();
|
|
209
|
-
|
|
227
|
+
// The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
|
|
228
|
+
// composer that declares the step), so a library's step referencing its own
|
|
229
|
+
// import resolves against that library, not the consumer.
|
|
230
|
+
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
231
|
+
const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases, scopeResolver);
|
|
210
232
|
if (!definition)
|
|
211
233
|
return { codes: new Map(), unbounded: true };
|
|
212
234
|
if (definition.throws?.passthrough) {
|
|
@@ -215,10 +237,12 @@ function resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx) {
|
|
|
215
237
|
// Named manifest: resolve the full chain (covers transitive inherit).
|
|
216
238
|
const invokeName = invokeRef.name;
|
|
217
239
|
if (invokeName) {
|
|
240
|
+
const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
|
|
218
241
|
const target = ctx.allManifests.find((m) => m.metadata?.name === invokeName &&
|
|
219
242
|
(m.kind === invokedKind ||
|
|
220
243
|
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
221
|
-
m.kind === ctx.aliases.resolveKind(invokedKind)
|
|
244
|
+
m.kind === ctx.aliases.resolveKind(invokedKind) ||
|
|
245
|
+
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind)));
|
|
222
246
|
if (target)
|
|
223
247
|
return resolveThrowsUnion(target, ctx);
|
|
224
248
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
-
import type
|
|
3
|
+
import { type AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { type AnalysisDiagnostic } from "./types.js";
|
|
6
6
|
/** Entry point — invoked once per analyze() run. */
|
|
7
|
-
export declare function validateThrowsCoverage(manifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, env: Environment): AnalysisDiagnostic[];
|
|
7
|
+
export declare function validateThrowsCoverage(manifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, env: Environment, aliasesByModule?: Map<string, AliasResolver>, rootModules?: Set<string>): AnalysisDiagnostic[];
|
|
8
8
|
//# sourceMappingURL=validate-throws-coverage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA4dzE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,EAChB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,kBAAkB,EAAE,CAkDtB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scopeResolverForModule } from "./alias-resolver.js";
|
|
1
2
|
import { createResolveCtx, resolveThrowsUnion, } from "./resolve-throws-union.js";
|
|
2
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
3
4
|
import { extractAccessChains, validateChainAgainstSchema } from "./validate-cel-context.js";
|
|
@@ -406,16 +407,20 @@ function schemaHasStepContext(schema) {
|
|
|
406
407
|
return false;
|
|
407
408
|
}
|
|
408
409
|
/** Entry point — invoked once per analyze() run. */
|
|
409
|
-
export function validateThrowsCoverage(manifests, defs, aliases, env) {
|
|
410
|
+
export function validateThrowsCoverage(manifests, defs, aliases, env, aliasesByModule = new Map(), rootModules = new Set()) {
|
|
410
411
|
const diagnostics = [];
|
|
411
412
|
diagnostics.push(...validateThrowsDeclarations(manifests));
|
|
412
|
-
const resolveCtx = createResolveCtx(manifests, defs, aliases);
|
|
413
|
+
const resolveCtx = createResolveCtx(manifests, defs, aliases, aliasesByModule, rootModules);
|
|
414
|
+
// The alias resolver for a manifest's own lexical scope — an imported library's
|
|
415
|
+
// resolver when it owns the manifest, else undefined (fall back to root aliases).
|
|
416
|
+
const scopeResolverFor = (m) => scopeResolverForModule(m.metadata?.module, rootModules, aliasesByModule);
|
|
413
417
|
for (const manifest of manifests) {
|
|
414
418
|
if (!manifest.kind || !manifest.metadata?.name)
|
|
415
419
|
continue;
|
|
416
420
|
if (manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract")
|
|
417
421
|
continue;
|
|
418
|
-
const
|
|
422
|
+
const scopeResolver = scopeResolverFor(manifest);
|
|
423
|
+
const resolvedKind = scopeResolver?.resolveKind(manifest.kind) ?? aliases.resolveKind(manifest.kind);
|
|
419
424
|
const definition = defs.resolve(manifest.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
420
425
|
if (!definition?.schema)
|
|
421
426
|
continue;
|
|
@@ -426,7 +431,7 @@ export function validateThrowsCoverage(manifests, defs, aliases, env) {
|
|
|
426
431
|
}, (entries, arrayPath, siblingData, catchesFor) => {
|
|
427
432
|
diagnostics.push(...checkCatchAllPlacement(entries, resource, "catches", filePath, arrayPath));
|
|
428
433
|
const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
|
|
429
|
-
const union = handlerRefUnion(handlerRef, manifests, resolveCtx);
|
|
434
|
+
const union = handlerRefUnion(handlerRef, manifests, resolveCtx, scopeResolver);
|
|
430
435
|
diagnostics.push(...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env));
|
|
431
436
|
diagnostics.push(...checkTypedErrorData(entries, union, resource, filePath, arrayPath, env));
|
|
432
437
|
});
|
|
@@ -436,19 +441,22 @@ export function validateThrowsCoverage(manifests, defs, aliases, env) {
|
|
|
436
441
|
/** Resolve a handler ref's effective throw union. Prefers the named manifest
|
|
437
442
|
* (so `inherit: true` handlers expose their transitive union); falls back to
|
|
438
443
|
* the definition's own codes when no name is given. */
|
|
439
|
-
function handlerRefUnion(handlerRef, manifests, ctx) {
|
|
444
|
+
function handlerRefUnion(handlerRef, manifests, ctx, scopeResolver) {
|
|
440
445
|
if (!handlerRef)
|
|
441
446
|
return { codes: new Map(), unbounded: false };
|
|
442
447
|
if (handlerRef.name) {
|
|
443
|
-
const resolvedKind = ctx.aliases.resolveKind(handlerRef.kind);
|
|
448
|
+
const resolvedKind = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
|
|
444
449
|
const targetManifest = manifests.find((m) => m.metadata?.name === handlerRef.name &&
|
|
445
450
|
(m.kind === handlerRef.kind ||
|
|
446
451
|
m.kind === resolvedKind ||
|
|
452
|
+
scopeResolver?.resolveKind(m.kind) === handlerRef.kind ||
|
|
447
453
|
ctx.aliases.resolveKind(m.kind) === handlerRef.kind));
|
|
448
454
|
if (targetManifest)
|
|
449
455
|
return resolveThrowsUnion(targetManifest, ctx);
|
|
450
456
|
}
|
|
451
|
-
|
|
457
|
+
// No named target — fall back to the handler kind's own declared codes,
|
|
458
|
+
// resolving the kind in the owner's lexical scope first, then root aliases.
|
|
459
|
+
const resolved = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
|
|
452
460
|
const def = ctx.defs.resolve(handlerRef.kind) ?? (resolved ? ctx.defs.resolve(resolved) : undefined);
|
|
453
461
|
if (!def?.throws)
|
|
454
462
|
return { codes: new Map(), unbounded: false };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.26.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/alias-resolver.ts
CHANGED
|
@@ -69,3 +69,22 @@ export class AliasResolver {
|
|
|
69
69
|
return result;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The alias resolver for a resource's own lexical scope. A resource that
|
|
75
|
+
* originated in an imported library (its `ownModule` names a non-root module —
|
|
76
|
+
* e.g. an inline handler extracted from an imported Http.Api) resolves its kind
|
|
77
|
+
* aliases against THAT library's import map, so an anonymous child inherits the
|
|
78
|
+
* lexical scope of the document that declares it. Returns undefined for
|
|
79
|
+
* root/consumer-owned resources (and unknown modules), so callers fall back to
|
|
80
|
+
* the root `aliases`.
|
|
81
|
+
*/
|
|
82
|
+
export function scopeResolverForModule(
|
|
83
|
+
ownModule: string | undefined,
|
|
84
|
+
rootModules: Set<string>,
|
|
85
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
86
|
+
): AliasResolver | undefined {
|
|
87
|
+
return ownModule && !rootModules.has(ownModule)
|
|
88
|
+
? aliasesByModule.get(ownModule)
|
|
89
|
+
: undefined;
|
|
90
|
+
}
|
package/src/analyzer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
3
|
import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
4
|
-
import { AliasResolver } from "./alias-resolver.js";
|
|
4
|
+
import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
|
|
5
5
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
6
6
|
import {
|
|
7
7
|
buildCelEnvironment,
|
|
@@ -938,8 +938,15 @@ export class StaticAnalyzer {
|
|
|
938
938
|
|
|
939
939
|
// Resolve kind through alias if needed; direct lookup takes priority so that
|
|
940
940
|
// aliases whose name matches the module name (the common case) work without
|
|
941
|
-
// path-derived name mangling.
|
|
942
|
-
|
|
941
|
+
// path-derived name mangling. A resource that originated in an imported library
|
|
942
|
+
// (its `metadata.module` names a non-root module — e.g. an inline route handler
|
|
943
|
+
// extracted from an imported Http.Api) must resolve its kind alias against THAT
|
|
944
|
+
// library's import map, not the consumer's; an anonymous child inherits the
|
|
945
|
+
// lexical scope of the document that declares it. Mirrors the nested-inline and
|
|
946
|
+
// reference-resolution paths: own-module scope first, root/consumer aliases last.
|
|
947
|
+
const ownModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
948
|
+
const scopeResolver = scopeResolverForModule(ownModule, rootModules, aliasesByModule);
|
|
949
|
+
const resolvedKind = scopeResolver?.resolveKind(m.kind) ?? aliases.resolveKind(m.kind);
|
|
943
950
|
const definition =
|
|
944
951
|
defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
945
952
|
if (!definition) {
|
|
@@ -1025,10 +1032,8 @@ export class StaticAnalyzer {
|
|
|
1025
1032
|
// first, then the parent module's own aliases (for resources declared
|
|
1026
1033
|
// inside an imported module), then the root aliases. Mirrors how the
|
|
1027
1034
|
// analyzer resolves kinds elsewhere so module-scoped aliases don't
|
|
1028
|
-
// produce false UNDEFINED_KIND diagnostics.
|
|
1029
|
-
|
|
1030
|
-
const scopeResolver =
|
|
1031
|
-
ownModule && !rootModules.has(ownModule) ? aliasesByModule.get(ownModule) : undefined;
|
|
1035
|
+
// produce false UNDEFINED_KIND diagnostics. `scopeResolver` is the
|
|
1036
|
+
// owning module's resolver computed above.
|
|
1032
1037
|
diagnostics.push(
|
|
1033
1038
|
...validateNestedInlineResources(
|
|
1034
1039
|
m,
|
|
@@ -1304,7 +1309,9 @@ export class StaticAnalyzer {
|
|
|
1304
1309
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
1305
1310
|
|
|
1306
1311
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
1307
|
-
diagnostics.push(
|
|
1312
|
+
diagnostics.push(
|
|
1313
|
+
...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv, aliasesByModule, rootModules),
|
|
1314
|
+
);
|
|
1308
1315
|
|
|
1309
1316
|
// Warn about declared variables / secrets / ports that no CEL references.
|
|
1310
1317
|
diagnostics.push(...validateUnusedDeclarations(allManifests, this.celEnv));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
|
-
import type
|
|
3
|
+
import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
|
|
6
6
|
export interface ThrowsCodeMeta {
|
|
@@ -32,6 +32,13 @@ export interface ResolveCtx {
|
|
|
32
32
|
allManifests: ResourceManifest[];
|
|
33
33
|
defs: DefinitionRegistry;
|
|
34
34
|
aliases: AliasResolver;
|
|
35
|
+
/** Per-imported-library alias resolvers, keyed by module name. A manifest that
|
|
36
|
+
* originated in an imported library resolves its kind aliases against its own
|
|
37
|
+
* module's resolver, not the consumer's — an inline handler extracted from an
|
|
38
|
+
* imported Http.Api inherits the lexical scope of the library that declares it. */
|
|
39
|
+
aliasesByModule: Map<string, AliasResolver>;
|
|
40
|
+
/** The consumer/root module names; resources owned by these resolve against `aliases`. */
|
|
41
|
+
rootModules: Set<string>;
|
|
35
42
|
memo: Map<string, ThrowsUnion>;
|
|
36
43
|
inProgress: Set<string>;
|
|
37
44
|
}
|
|
@@ -40,11 +47,15 @@ export function createResolveCtx(
|
|
|
40
47
|
allManifests: ResourceManifest[],
|
|
41
48
|
defs: DefinitionRegistry,
|
|
42
49
|
aliases: AliasResolver,
|
|
50
|
+
aliasesByModule: Map<string, AliasResolver> = new Map(),
|
|
51
|
+
rootModules: Set<string> = new Set(),
|
|
43
52
|
): ResolveCtx {
|
|
44
53
|
return {
|
|
45
54
|
allManifests,
|
|
46
55
|
defs,
|
|
47
56
|
aliases,
|
|
57
|
+
aliasesByModule,
|
|
58
|
+
rootModules,
|
|
48
59
|
memo: new Map(),
|
|
49
60
|
inProgress: new Set(),
|
|
50
61
|
};
|
|
@@ -54,6 +65,11 @@ function emptyUnion(): ThrowsUnion {
|
|
|
54
65
|
return { codes: new Map(), unbounded: false };
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
/** The owning module's alias resolver for a manifest in this resolve context. */
|
|
69
|
+
function scopeResolverFor(ctx: ResolveCtx, ownModule: string | undefined): AliasResolver | undefined {
|
|
70
|
+
return scopeResolverForModule(ownModule, ctx.rootModules, ctx.aliasesByModule);
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
function unionInto(target: ThrowsUnion, src: ThrowsUnion): void {
|
|
58
74
|
for (const [code, meta] of src.codes) {
|
|
59
75
|
if (!target.codes.has(code)) target.codes.set(code, meta);
|
|
@@ -66,9 +82,17 @@ function definitionFor(
|
|
|
66
82
|
kind: string,
|
|
67
83
|
defs: DefinitionRegistry,
|
|
68
84
|
aliases: AliasResolver,
|
|
85
|
+
scopeResolver?: AliasResolver,
|
|
69
86
|
): ResourceDefinition | undefined {
|
|
87
|
+
const direct = defs.resolve(kind);
|
|
88
|
+
if (direct) return direct;
|
|
89
|
+
const scoped = scopeResolver?.resolveKind(kind);
|
|
90
|
+
if (scoped) {
|
|
91
|
+
const d = defs.resolve(scoped);
|
|
92
|
+
if (d) return d;
|
|
93
|
+
}
|
|
70
94
|
const resolved = aliases.resolveKind(kind);
|
|
71
|
-
return
|
|
95
|
+
return resolved ? defs.resolve(resolved) : undefined;
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
function codesFromDefinition(definition: ResourceDefinition): Map<string, ThrowsCodeMeta> {
|
|
@@ -97,7 +121,9 @@ export function resolveThrowsUnion(
|
|
|
97
121
|
if (ctx.inProgress.has(name)) return emptyUnion();
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
const
|
|
124
|
+
const ownModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
125
|
+
const scopeResolver = scopeResolverFor(ctx, ownModule);
|
|
126
|
+
const definition = definitionFor(manifest.kind, ctx.defs, ctx.aliases, scopeResolver);
|
|
101
127
|
if (!definition) {
|
|
102
128
|
const u: ThrowsUnion = { codes: new Map(), unbounded: true };
|
|
103
129
|
if (name) ctx.memo.set(name, u);
|
|
@@ -126,7 +152,7 @@ export function resolveThrowsUnion(
|
|
|
126
152
|
}
|
|
127
153
|
|
|
128
154
|
if (throws.inherit) {
|
|
129
|
-
const inherited = resolveInherited(manifest, definition, ctx);
|
|
155
|
+
const inherited = resolveInherited(manifest, definition, ctx, ownModule);
|
|
130
156
|
unionInto(result, inherited);
|
|
131
157
|
}
|
|
132
158
|
|
|
@@ -141,6 +167,7 @@ function resolveInherited(
|
|
|
141
167
|
manifest: ResourceManifest,
|
|
142
168
|
definition: ResourceDefinition,
|
|
143
169
|
ctx: ResolveCtx,
|
|
170
|
+
ownerModule: string | undefined,
|
|
144
171
|
): ThrowsUnion {
|
|
145
172
|
const result: ThrowsUnion = { codes: new Map(), unbounded: false };
|
|
146
173
|
const props = definition.schema?.properties as Record<string, any> | undefined;
|
|
@@ -151,7 +178,7 @@ function resolveInherited(
|
|
|
151
178
|
if (!stepCtx?.invoke) continue;
|
|
152
179
|
const steps = (manifest as Record<string, any>)[fieldName];
|
|
153
180
|
if (!Array.isArray(steps)) continue;
|
|
154
|
-
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx));
|
|
181
|
+
unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx, ownerModule));
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
return result;
|
|
@@ -162,13 +189,14 @@ function collectStepArrayThrows(
|
|
|
162
189
|
invokeField: string,
|
|
163
190
|
enclosingTryCodes: Set<string> | undefined,
|
|
164
191
|
ctx: ResolveCtx,
|
|
192
|
+
ownerModule: string | undefined,
|
|
165
193
|
): ThrowsUnion {
|
|
166
194
|
const result = emptyUnion();
|
|
167
195
|
for (const step of steps) {
|
|
168
196
|
if (!step || typeof step !== "object") continue;
|
|
169
197
|
unionInto(
|
|
170
198
|
result,
|
|
171
|
-
collectStepThrows(step as Record<string, any>, invokeField, enclosingTryCodes, ctx),
|
|
199
|
+
collectStepThrows(step as Record<string, any>, invokeField, enclosingTryCodes, ctx, ownerModule),
|
|
172
200
|
);
|
|
173
201
|
}
|
|
174
202
|
return result;
|
|
@@ -184,11 +212,14 @@ function collectStepThrows(
|
|
|
184
212
|
invokeField: string,
|
|
185
213
|
enclosingTryCodes: Set<string> | undefined,
|
|
186
214
|
ctx: ResolveCtx,
|
|
215
|
+
ownerModule: string | undefined,
|
|
187
216
|
): ThrowsUnion {
|
|
188
217
|
if (step[invokeField]) {
|
|
189
218
|
// Any invoked resource can throw a non-InvokeError at runtime, which an
|
|
190
219
|
// enclosing catch surfaces as PLAIN_ERROR_CODE — record that possibility.
|
|
191
|
-
const u = cloneUnion(
|
|
220
|
+
const u = cloneUnion(
|
|
221
|
+
resolveStepInvokeThrows(step, invokeField, enclosingTryCodes, ctx, ownerModule),
|
|
222
|
+
);
|
|
192
223
|
u.canThrowPlain = true;
|
|
193
224
|
return u;
|
|
194
225
|
}
|
|
@@ -198,7 +229,7 @@ function collectStepThrows(
|
|
|
198
229
|
}
|
|
199
230
|
|
|
200
231
|
if (Array.isArray(step.try)) {
|
|
201
|
-
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx);
|
|
232
|
+
const tryUnion = collectStepArrayThrows(step.try, invokeField, enclosingTryCodes, ctx, ownerModule);
|
|
202
233
|
let propagated: ThrowsUnion;
|
|
203
234
|
if (Array.isArray(step.catch)) {
|
|
204
235
|
// Catch absorbs the try block's codes; the catch's own throws propagate
|
|
@@ -209,7 +240,7 @@ function collectStepThrows(
|
|
|
209
240
|
// `error.code === PLAIN_ERROR_CODE`, so a `throw: { code: error.code }`
|
|
210
241
|
// rethrow can propagate it — seed the set the catch resolves against.
|
|
211
242
|
if (tryUnion.canThrowPlain) tryCodes.add(PLAIN_ERROR_CODE);
|
|
212
|
-
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx);
|
|
243
|
+
propagated = collectStepArrayThrows(step.catch, invokeField, tryCodes, ctx, ownerModule);
|
|
213
244
|
// Unbounded in the try block still signals the caller to expect
|
|
214
245
|
// arbitrary codes to flow through the catch (e.g. via passthrough).
|
|
215
246
|
if (tryUnion.unbounded) propagated.unbounded = true;
|
|
@@ -219,7 +250,7 @@ function collectStepThrows(
|
|
|
219
250
|
if (Array.isArray(step.finally)) {
|
|
220
251
|
unionInto(
|
|
221
252
|
propagated,
|
|
222
|
-
collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx),
|
|
253
|
+
collectStepArrayThrows(step.finally, invokeField, enclosingTryCodes, ctx, ownerModule),
|
|
223
254
|
);
|
|
224
255
|
}
|
|
225
256
|
return propagated;
|
|
@@ -227,16 +258,16 @@ function collectStepThrows(
|
|
|
227
258
|
|
|
228
259
|
if (Array.isArray(step.then)) {
|
|
229
260
|
const result = emptyUnion();
|
|
230
|
-
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx));
|
|
261
|
+
unionInto(result, collectStepArrayThrows(step.then, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
231
262
|
if (Array.isArray(step.else)) {
|
|
232
|
-
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx));
|
|
263
|
+
unionInto(result, collectStepArrayThrows(step.else, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
233
264
|
}
|
|
234
265
|
if (Array.isArray(step.elseif)) {
|
|
235
266
|
for (const branch of step.elseif) {
|
|
236
267
|
if (Array.isArray(branch?.then)) {
|
|
237
268
|
unionInto(
|
|
238
269
|
result,
|
|
239
|
-
collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx),
|
|
270
|
+
collectStepArrayThrows(branch.then, invokeField, enclosingTryCodes, ctx, ownerModule),
|
|
240
271
|
);
|
|
241
272
|
}
|
|
242
273
|
}
|
|
@@ -245,18 +276,18 @@ function collectStepThrows(
|
|
|
245
276
|
}
|
|
246
277
|
|
|
247
278
|
if (Array.isArray(step.do)) {
|
|
248
|
-
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx);
|
|
279
|
+
return collectStepArrayThrows(step.do, invokeField, enclosingTryCodes, ctx, ownerModule);
|
|
249
280
|
}
|
|
250
281
|
|
|
251
282
|
if (step.cases && typeof step.cases === "object") {
|
|
252
283
|
const result = emptyUnion();
|
|
253
284
|
for (const arr of Object.values(step.cases as Record<string, unknown>)) {
|
|
254
285
|
if (Array.isArray(arr)) {
|
|
255
|
-
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx));
|
|
286
|
+
unionInto(result, collectStepArrayThrows(arr, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
256
287
|
}
|
|
257
288
|
}
|
|
258
289
|
if (Array.isArray(step.default)) {
|
|
259
|
-
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx));
|
|
290
|
+
unionInto(result, collectStepArrayThrows(step.default, invokeField, enclosingTryCodes, ctx, ownerModule));
|
|
260
291
|
}
|
|
261
292
|
return result;
|
|
262
293
|
}
|
|
@@ -277,13 +308,18 @@ function resolveStepInvokeThrows(
|
|
|
277
308
|
invokeField: string,
|
|
278
309
|
enclosingTryCodes: Set<string> | undefined,
|
|
279
310
|
ctx: ResolveCtx,
|
|
311
|
+
ownerModule: string | undefined,
|
|
280
312
|
): ThrowsUnion {
|
|
281
313
|
const invokeRef = step[invokeField];
|
|
282
314
|
if (!invokeRef || typeof invokeRef !== "object") return emptyUnion();
|
|
283
315
|
const invokedKind = invokeRef.kind as string | undefined;
|
|
284
316
|
if (!invokedKind) return emptyUnion();
|
|
285
317
|
|
|
286
|
-
|
|
318
|
+
// The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
|
|
319
|
+
// composer that declares the step), so a library's step referencing its own
|
|
320
|
+
// import resolves against that library, not the consumer.
|
|
321
|
+
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
322
|
+
const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases, scopeResolver);
|
|
287
323
|
if (!definition) return { codes: new Map(), unbounded: true };
|
|
288
324
|
|
|
289
325
|
if (definition.throws?.passthrough) {
|
|
@@ -293,12 +329,14 @@ function resolveStepInvokeThrows(
|
|
|
293
329
|
// Named manifest: resolve the full chain (covers transitive inherit).
|
|
294
330
|
const invokeName = invokeRef.name as string | undefined;
|
|
295
331
|
if (invokeName) {
|
|
332
|
+
const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
|
|
296
333
|
const target = ctx.allManifests.find(
|
|
297
334
|
(m) =>
|
|
298
335
|
m.metadata?.name === invokeName &&
|
|
299
336
|
(m.kind === invokedKind ||
|
|
300
337
|
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
301
|
-
m.kind === ctx.aliases.resolveKind(invokedKind)
|
|
338
|
+
m.kind === ctx.aliases.resolveKind(invokedKind) ||
|
|
339
|
+
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind)),
|
|
302
340
|
);
|
|
303
341
|
if (target) return resolveThrowsUnion(target, ctx);
|
|
304
342
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ASTNode, Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
-
import type
|
|
3
|
+
import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import {
|
|
6
6
|
createResolveCtx,
|
|
@@ -490,16 +490,28 @@ export function validateThrowsCoverage(
|
|
|
490
490
|
defs: DefinitionRegistry,
|
|
491
491
|
aliases: AliasResolver,
|
|
492
492
|
env: Environment,
|
|
493
|
+
aliasesByModule: Map<string, AliasResolver> = new Map(),
|
|
494
|
+
rootModules: Set<string> = new Set(),
|
|
493
495
|
): AnalysisDiagnostic[] {
|
|
494
496
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
495
497
|
diagnostics.push(...validateThrowsDeclarations(manifests));
|
|
496
498
|
|
|
497
|
-
const resolveCtx = createResolveCtx(manifests, defs, aliases);
|
|
499
|
+
const resolveCtx = createResolveCtx(manifests, defs, aliases, aliasesByModule, rootModules);
|
|
500
|
+
|
|
501
|
+
// The alias resolver for a manifest's own lexical scope — an imported library's
|
|
502
|
+
// resolver when it owns the manifest, else undefined (fall back to root aliases).
|
|
503
|
+
const scopeResolverFor = (m: ResourceManifest): AliasResolver | undefined =>
|
|
504
|
+
scopeResolverForModule(
|
|
505
|
+
(m.metadata as { module?: string } | undefined)?.module,
|
|
506
|
+
rootModules,
|
|
507
|
+
aliasesByModule,
|
|
508
|
+
);
|
|
498
509
|
|
|
499
510
|
for (const manifest of manifests) {
|
|
500
511
|
if (!manifest.kind || !manifest.metadata?.name) continue;
|
|
501
512
|
if (manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract") continue;
|
|
502
|
-
const
|
|
513
|
+
const scopeResolver = scopeResolverFor(manifest);
|
|
514
|
+
const resolvedKind = scopeResolver?.resolveKind(manifest.kind) ?? aliases.resolveKind(manifest.kind);
|
|
503
515
|
const definition =
|
|
504
516
|
defs.resolve(manifest.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
505
517
|
if (!definition?.schema) continue;
|
|
@@ -519,7 +531,7 @@ export function validateThrowsCoverage(
|
|
|
519
531
|
...checkCatchAllPlacement(entries, resource, "catches", filePath, arrayPath),
|
|
520
532
|
);
|
|
521
533
|
const handlerRef = resolveHandlerRef(siblingData[catchesFor]);
|
|
522
|
-
const union = handlerRefUnion(handlerRef, manifests, resolveCtx);
|
|
534
|
+
const union = handlerRefUnion(handlerRef, manifests, resolveCtx, scopeResolver);
|
|
523
535
|
diagnostics.push(
|
|
524
536
|
...checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env),
|
|
525
537
|
);
|
|
@@ -539,20 +551,24 @@ function handlerRefUnion(
|
|
|
539
551
|
handlerRef: { kind: string; name?: string } | null,
|
|
540
552
|
manifests: ResourceManifest[],
|
|
541
553
|
ctx: ReturnType<typeof createResolveCtx>,
|
|
554
|
+
scopeResolver: AliasResolver | undefined,
|
|
542
555
|
): ThrowsUnion {
|
|
543
556
|
if (!handlerRef) return { codes: new Map(), unbounded: false };
|
|
544
557
|
if (handlerRef.name) {
|
|
545
|
-
const resolvedKind = ctx.aliases.resolveKind(handlerRef.kind);
|
|
558
|
+
const resolvedKind = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
|
|
546
559
|
const targetManifest = manifests.find(
|
|
547
560
|
(m) =>
|
|
548
561
|
m.metadata?.name === handlerRef.name &&
|
|
549
562
|
(m.kind === handlerRef.kind ||
|
|
550
563
|
m.kind === resolvedKind ||
|
|
564
|
+
scopeResolver?.resolveKind(m.kind) === handlerRef.kind ||
|
|
551
565
|
ctx.aliases.resolveKind(m.kind) === handlerRef.kind),
|
|
552
566
|
);
|
|
553
567
|
if (targetManifest) return resolveThrowsUnion(targetManifest, ctx);
|
|
554
568
|
}
|
|
555
|
-
|
|
569
|
+
// No named target — fall back to the handler kind's own declared codes,
|
|
570
|
+
// resolving the kind in the owner's lexical scope first, then root aliases.
|
|
571
|
+
const resolved = scopeResolver?.resolveKind(handlerRef.kind) ?? ctx.aliases.resolveKind(handlerRef.kind);
|
|
556
572
|
const def =
|
|
557
573
|
ctx.defs.resolve(handlerRef.kind) ?? (resolved ? ctx.defs.resolve(resolved) : undefined);
|
|
558
574
|
if (!def?.throws) return { codes: new Map(), unbounded: false };
|