@telorun/analyzer 0.22.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/README.md +2 -2
- package/dist/alias-resolver.d.ts +17 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +28 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +61 -20
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +4 -2
- package/dist/cel-environment.d.ts +8 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +48 -0
- package/dist/flatten-for-analyzer.d.ts +52 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +192 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- 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 +33 -0
- package/src/analyzer.ts +72 -19
- package/src/builtins.ts +4 -2
- package/src/cel-environment.ts +57 -0
- package/src/flatten-for-analyzer.ts +217 -4
- package/src/index.ts +7 -0
- package/src/resolve-throws-union.ts +56 -18
- package/src/validate-throws-coverage.ts +22 -6
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import type {
|
|
2
|
+
import type { LoadedGraph, LoadedModule } from "./loaded-types.js";
|
|
3
|
+
import type { LoadedFile } from "./loaded-types.js";
|
|
3
4
|
import { isModuleKind } from "./module-kinds.js";
|
|
4
5
|
|
|
6
|
+
/** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
|
|
7
|
+
* instance name or kind suffix (the part after the dot, or the whole entry); `alias`
|
|
8
|
+
* (when set) is this library's own import the entry RE-EXPORTS from. */
|
|
9
|
+
export interface ParsedExportEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
alias?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parse a single dotted export entry: `Alias.Name` → `{name: "Name", alias: "Alias"}`,
|
|
15
|
+
* bare `Name` → `{name: "Name"}`. The single grammar for `exports.resources` and
|
|
16
|
+
* `exports.kinds`, shared by the kernel's import controller and the analyzer/editor so
|
|
17
|
+
* the dotted-name split can't drift. A leading dot (`.Name`) has no alias by design —
|
|
18
|
+
* the empty prefix isn't a valid alias. */
|
|
19
|
+
export function parseExportEntry(entry: string): ParsedExportEntry {
|
|
20
|
+
const dot = entry.indexOf(".");
|
|
21
|
+
return dot > 0 ? { name: entry.slice(dot + 1), alias: entry.slice(0, dot) } : { name: entry };
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
/** The import-boundary forwarding rule, shared by `flattenForAnalyzer` (the
|
|
6
25
|
* CLI / kernel loader path) and the telo-editor's workspace projection so the
|
|
7
26
|
* two cannot drift. Given one module's stamped manifests and whether that
|
|
@@ -29,9 +48,14 @@ export function selectModuleManifestsForAnalysis(
|
|
|
29
48
|
if (isRoot) return moduleManifests;
|
|
30
49
|
|
|
31
50
|
const libDoc = moduleManifests.find((m) => isModuleKind(m.kind));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
51
|
+
// An `exports.resources` entry is a bare name or a dotted `Alias.Name` (re-export). Only the
|
|
52
|
+
// export NAME matches a local instance below; re-exports are forwarded by `forwardReExports`.
|
|
53
|
+
const exportedResources = new Set<string>();
|
|
54
|
+
for (const entry of (libDoc as { exports?: { resources?: unknown[] } } | undefined)?.exports
|
|
55
|
+
?.resources ?? []) {
|
|
56
|
+
if (typeof entry !== "string") continue;
|
|
57
|
+
exportedResources.add(parseExportEntry(entry).name);
|
|
58
|
+
}
|
|
35
59
|
|
|
36
60
|
const out: ResourceManifest[] = [];
|
|
37
61
|
for (const m of moduleManifests) {
|
|
@@ -96,6 +120,8 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
|
|
|
96
120
|
}
|
|
97
121
|
}
|
|
98
122
|
|
|
123
|
+
forwardReExports(graph, result);
|
|
124
|
+
|
|
99
125
|
// Stamp resolved import identity on every Telo.Import in the result by
|
|
100
126
|
// reading the edge's pre-resolved name/namespace — no re-derivation from
|
|
101
127
|
// manifest metadata. The edge is keyed by (owner-file, alias) which is
|
|
@@ -120,6 +146,193 @@ export function flattenForAnalyzer(graph: LoadedGraph): ResourceManifest[] {
|
|
|
120
146
|
return result;
|
|
121
147
|
}
|
|
122
148
|
|
|
149
|
+
/** A re-export declared in a library's `exports.resources` as a dotted `Alias.Name`:
|
|
150
|
+
* module `module` re-exports the instance `name` reached through its own import
|
|
151
|
+
* aliased `alias`. */
|
|
152
|
+
export interface ReExportSpec {
|
|
153
|
+
module: string;
|
|
154
|
+
alias: string;
|
|
155
|
+
name: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Extract re-export specs from a library doc's `exports.resources` — the dotted `Alias.Name`
|
|
159
|
+
* entries (bare-name locals are forwarded by the BFS instead). Shared by the CLI graph path
|
|
160
|
+
* and the editor's workspace projection so the two cannot drift. */
|
|
161
|
+
export function reExportSpecsFromExports(
|
|
162
|
+
moduleName: string,
|
|
163
|
+
exportsResources: readonly unknown[] | undefined,
|
|
164
|
+
): ReExportSpec[] {
|
|
165
|
+
const specs: ReExportSpec[] = [];
|
|
166
|
+
for (const entry of exportsResources ?? []) {
|
|
167
|
+
if (typeof entry !== "string") continue;
|
|
168
|
+
const { name, alias } = parseExportEntry(entry);
|
|
169
|
+
if (!alias || alias === "Self") continue;
|
|
170
|
+
specs.push({ module: moduleName, alias, name });
|
|
171
|
+
}
|
|
172
|
+
return specs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Forward re-exported instances (`exports.resources: [!ref Alias.name]`) transitively so a
|
|
176
|
+
* consumer's `!ref Consumer.name` resolves in `resolveRefSentinels` (keyed by the RE-EXPORTING
|
|
177
|
+
* module). The owning instance is already forwarded under its own module; here we emit an
|
|
178
|
+
* additional copy stamped under each re-exporting module, with an already-canonical kind. A
|
|
179
|
+
* fixpoint loop forwards chains of arbitrary depth (`app → api → domain → …`): each pass can
|
|
180
|
+
* resolve a re-export whose source was emitted in a prior pass. Graph-agnostic: `aliasToModule`
|
|
181
|
+
* maps `(module, alias)` to the imported module's name. Mutates `result` in place. */
|
|
182
|
+
export function forwardReExportManifests(
|
|
183
|
+
result: ResourceManifest[],
|
|
184
|
+
specs: readonly ReExportSpec[],
|
|
185
|
+
aliasToModule: (module: string, alias: string) => string | undefined,
|
|
186
|
+
): void {
|
|
187
|
+
// Index forwarded instances by `module\0name` (only re-export TARGETS are forwarded).
|
|
188
|
+
const forwarded = new Map<string, ResourceManifest>();
|
|
189
|
+
for (const m of result) {
|
|
190
|
+
const meta = m.metadata as { module?: string; name?: string; forwardedExport?: boolean };
|
|
191
|
+
if (meta?.forwardedExport && meta.module && meta.name) {
|
|
192
|
+
forwarded.set(`${meta.module}\0${meta.name}`, m);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Canonicalize an authored/forwarded kind to a scope-independent `<module>.<Kind>` using the
|
|
197
|
+
// owning module's own import aliases. Idempotent: an already-canonical kind whose prefix isn't
|
|
198
|
+
// an alias of `ownerModule` is returned unchanged, so re-exports of re-exports stay stable.
|
|
199
|
+
const canonicalKind = (kind: string, ownerModule: string): string => {
|
|
200
|
+
if (kind.startsWith("Self.")) return `${ownerModule}.${kind.slice("Self.".length)}`;
|
|
201
|
+
const dot = kind.indexOf(".");
|
|
202
|
+
if (dot <= 0) return kind;
|
|
203
|
+
const target = aliasToModule(ownerModule, kind.slice(0, dot));
|
|
204
|
+
return target ? `${target}.${kind.slice(dot + 1)}` : kind;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Fixpoint — bounded by the number of specs (each can be satisfied at most once).
|
|
208
|
+
for (let pass = 0; pass <= specs.length; pass++) {
|
|
209
|
+
let added = false;
|
|
210
|
+
for (const spec of specs) {
|
|
211
|
+
const key = `${spec.module}\0${spec.name}`;
|
|
212
|
+
if (forwarded.has(key)) continue;
|
|
213
|
+
const sourceModule = aliasToModule(spec.module, spec.alias);
|
|
214
|
+
if (!sourceModule) continue;
|
|
215
|
+
const src = forwarded.get(`${sourceModule}\0${spec.name}`);
|
|
216
|
+
if (!src) continue; // source not forwarded yet — a later pass may satisfy it
|
|
217
|
+
const kind = canonicalKind(src.kind as string, sourceModule);
|
|
218
|
+
const manifest: ResourceManifest = {
|
|
219
|
+
...src,
|
|
220
|
+
kind,
|
|
221
|
+
metadata: {
|
|
222
|
+
...src.metadata,
|
|
223
|
+
name: spec.name,
|
|
224
|
+
module: spec.module,
|
|
225
|
+
forwardedExport: true,
|
|
226
|
+
} as ResourceManifest["metadata"],
|
|
227
|
+
};
|
|
228
|
+
result.push(manifest);
|
|
229
|
+
forwarded.set(key, manifest);
|
|
230
|
+
added = true;
|
|
231
|
+
}
|
|
232
|
+
if (!added) break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Resolve every library's `exports.kinds` to a per-module map `suffix → canonical
|
|
237
|
+
* <owningModule>.<Kind>`, following re-exports (`Alias.Kind`) transitively via a fixpoint.
|
|
238
|
+
* `modules` lists each library's name + its raw `exports.kinds`; `aliasToModule(module, alias)`
|
|
239
|
+
* maps one of that module's import aliases to the imported module's name. Graph-agnostic —
|
|
240
|
+
* shared by the CLI graph path and the editor's workspace projection. */
|
|
241
|
+
export function resolveExportedKinds(
|
|
242
|
+
modules: ReadonlyArray<{ module: string; exportsKinds: readonly string[] }>,
|
|
243
|
+
aliasToModule: (module: string, alias: string) => string | undefined,
|
|
244
|
+
): Map<string, Map<string, string>> {
|
|
245
|
+
const out = new Map<string, Map<string, string>>();
|
|
246
|
+
const tableFor = (m: string): Map<string, string> => {
|
|
247
|
+
let t = out.get(m);
|
|
248
|
+
if (!t) out.set(m, (t = new Map()));
|
|
249
|
+
return t;
|
|
250
|
+
};
|
|
251
|
+
for (let pass = 0; pass <= modules.length; pass++) {
|
|
252
|
+
let changed = false;
|
|
253
|
+
for (const { module, exportsKinds } of modules) {
|
|
254
|
+
const table = tableFor(module);
|
|
255
|
+
for (const entry of exportsKinds) {
|
|
256
|
+
const { name: suffix, alias } = parseExportEntry(entry);
|
|
257
|
+
if (table.has(suffix)) continue;
|
|
258
|
+
if (!alias) {
|
|
259
|
+
table.set(suffix, `${module}.${suffix}`);
|
|
260
|
+
changed = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const source = aliasToModule(module, alias);
|
|
264
|
+
const canonical = source ? out.get(source)?.get(suffix) : undefined;
|
|
265
|
+
if (canonical) {
|
|
266
|
+
table.set(suffix, canonical);
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!changed) break;
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Stamp `metadata.reExportedKinds` (suffix → canonical kind) onto every `Telo.Import` whose
|
|
277
|
+
* target re-exports kinds, so the analyzer can register the re-export mappings. Only entries
|
|
278
|
+
* that point at a module OTHER than the import's own target are stamped (genuine re-exports;
|
|
279
|
+
* a locally-defined kind resolves through the normal alias path). Stamped on `metadata` (which
|
|
280
|
+
* permits additional properties, like `resolvedModuleName`) since the `Telo.Import` schema
|
|
281
|
+
* forbids extra top-level fields. Shared by both paths. */
|
|
282
|
+
export function stampReExportedKinds(
|
|
283
|
+
imports: ReadonlyArray<{ manifest: ResourceManifest; targetModule: string }>,
|
|
284
|
+
exportedKinds: Map<string, Map<string, string>>,
|
|
285
|
+
): void {
|
|
286
|
+
for (const { manifest, targetModule } of imports) {
|
|
287
|
+
const table = exportedKinds.get(targetModule);
|
|
288
|
+
if (!table) continue;
|
|
289
|
+
const reExported: Record<string, string> = {};
|
|
290
|
+
for (const [suffix, canonical] of table) {
|
|
291
|
+
if (canonical !== `${targetModule}.${suffix}`) reExported[suffix] = canonical;
|
|
292
|
+
}
|
|
293
|
+
if (Object.keys(reExported).length === 0) continue;
|
|
294
|
+
(manifest.metadata as Record<string, unknown>).reExportedKinds = reExported;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** CLI/kernel adapter: collect re-export specs + alias map from a LoadedGraph. */
|
|
299
|
+
function forwardReExports(graph: LoadedGraph, result: ResourceManifest[]): void {
|
|
300
|
+
const ownerSourceOf = new Map<string, string>();
|
|
301
|
+
const specs: ReExportSpec[] = [];
|
|
302
|
+
const kindModules: Array<{ module: string; exportsKinds: string[] }> = [];
|
|
303
|
+
for (const [source, mod] of graph.modules) {
|
|
304
|
+
if (source === graph.rootSource) continue; // root is an Application — no exports
|
|
305
|
+
const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind)) as
|
|
306
|
+
| (ResourceManifest & { exports?: { resources?: unknown[]; kinds?: string[] } })
|
|
307
|
+
| undefined;
|
|
308
|
+
const moduleName = libDoc?.metadata?.name as string | undefined;
|
|
309
|
+
if (!libDoc || !moduleName) continue;
|
|
310
|
+
ownerSourceOf.set(moduleName, mod.owner.source);
|
|
311
|
+
specs.push(...reExportSpecsFromExports(moduleName, libDoc.exports?.resources));
|
|
312
|
+
kindModules.push({ module: moduleName, exportsKinds: libDoc.exports?.kinds ?? [] });
|
|
313
|
+
}
|
|
314
|
+
const aliasToModule = (module: string, alias: string): string | undefined => {
|
|
315
|
+
const ownerSource = ownerSourceOf.get(module);
|
|
316
|
+
return ownerSource
|
|
317
|
+
? (graph.importEdges.get(ownerSource)?.get(alias)?.targetModuleName ?? undefined)
|
|
318
|
+
: undefined;
|
|
319
|
+
};
|
|
320
|
+
forwardReExportManifests(result, specs, aliasToModule);
|
|
321
|
+
|
|
322
|
+
// Resolve every library's re-exported kinds and stamp them onto the consumer-facing
|
|
323
|
+
// Telo.Import manifests so the analyzer can register the re-export mappings.
|
|
324
|
+
const exportedKinds = resolveExportedKinds(kindModules, aliasToModule);
|
|
325
|
+
const imports: Array<{ manifest: ResourceManifest; targetModule: string }> = [];
|
|
326
|
+
for (const m of result) {
|
|
327
|
+
if (m.kind !== "Telo.Import") continue;
|
|
328
|
+
const owner = (m.metadata as { source?: string } | undefined)?.source;
|
|
329
|
+
const alias = m.metadata?.name as string | undefined;
|
|
330
|
+
const target = owner && alias ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName : undefined;
|
|
331
|
+
if (target) imports.push({ manifest: m, targetModule: target });
|
|
332
|
+
}
|
|
333
|
+
stampReExportedKinds(imports, exportedKinds);
|
|
334
|
+
}
|
|
335
|
+
|
|
123
336
|
/** Project a LoadedModule (owner + partials) to a flat ResourceManifest[]
|
|
124
337
|
* with `metadata.module` stamped on non-module docs. The kernel's runtime
|
|
125
338
|
* entry load uses this to convert a `Loader.loadModule` result into the
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,14 @@ export type {
|
|
|
12
12
|
export {
|
|
13
13
|
flattenForAnalyzer,
|
|
14
14
|
flattenLoadedModule,
|
|
15
|
+
forwardReExportManifests,
|
|
16
|
+
parseExportEntry,
|
|
17
|
+
reExportSpecsFromExports,
|
|
18
|
+
resolveExportedKinds,
|
|
15
19
|
selectModuleManifestsForAnalysis,
|
|
20
|
+
stampReExportedKinds,
|
|
21
|
+
type ParsedExportEntry,
|
|
22
|
+
type ReExportSpec,
|
|
16
23
|
} from "./flatten-for-analyzer.js";
|
|
17
24
|
export { visitManifest } from "./manifest-visitor.js";
|
|
18
25
|
export type {
|
|
@@ -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 };
|