@telorun/analyzer 0.69.0 → 0.71.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 +50 -8
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +10 -4
- package/dist/canonical-json.d.ts +18 -0
- package/dist/canonical-json.d.ts.map +1 -0
- package/dist/canonical-json.js +26 -0
- package/dist/cel-access-chains.d.ts +14 -0
- package/dist/cel-access-chains.d.ts.map +1 -0
- package/dist/cel-access-chains.js +45 -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/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 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/manifest-diff.d.ts +111 -0
- package/dist/manifest-diff.d.ts.map +1 -0
- package/dist/manifest-diff.js +130 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +11 -3
- 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/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +3 -25
- 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/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 +18 -1
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +93 -9
- 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/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 +3 -3
- package/dist/types.d.ts.map +1 -1
- 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-references.d.ts +16 -5
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +30 -12
- 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 +6 -2
- package/package.json +2 -2
- package/src/analysis-registry.ts +4 -5
- package/src/analyzer.ts +61 -10
- package/src/call-graph.ts +9 -3
- package/src/canonical-json.ts +24 -0
- package/src/cel-access-chains.ts +47 -0
- package/src/cel-scope-query.ts +45 -8
- package/src/definition-registry.ts +3 -6
- package/src/flatten-for-analyzer.ts +3 -3
- package/src/index.ts +10 -2
- package/src/manifest-diff.ts +219 -0
- package/src/manifest-visitor.ts +11 -3
- package/src/module-alias-scope.ts +94 -0
- package/src/{zone-module-documents.ts → module-documents.ts} +10 -6
- package/src/module-graph.ts +2 -24
- package/src/ref-sentinel-target.ts +46 -0
- package/src/resolve-schema-type-refs.ts +2 -1
- package/src/resolve-throws-union.ts +123 -8
- package/src/resolve-zone-requirements.ts +14 -14
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +2 -5
- package/src/types.ts +3 -3
- package/src/validate-invocation-contract.ts +128 -2
- package/src/validate-logging.ts +2 -3
- package/src/validate-references.ts +41 -11
- package/src/validate-resource-inputs.ts +26 -3
- package/src/validate-schema-type-refs.ts +2 -1
- package/src/validate-throws-coverage.ts +12 -1
- package/dist/zone-module-documents.d.ts.map +0 -1
- /package/dist/{zone-module-documents.js → module-documents.js} +0 -0
|
@@ -5,6 +5,8 @@ import { navigateJsonPointer, substituteCelFields } from "./schema-compat.js";
|
|
|
5
5
|
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
6
6
|
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
7
7
|
import { DiagnosticSeverity } from "./types.js";
|
|
8
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
9
|
+
import { isInjectedDeclaration } from "./resource-input.js";
|
|
8
10
|
const SOURCE = "telo-analyzer";
|
|
9
11
|
/**
|
|
10
12
|
* Liskov substitutability at a kind constraint: is `resolved` (a canonical
|
|
@@ -16,17 +18,28 @@ const SOURCE = "telo-analyzer";
|
|
|
16
18
|
*
|
|
17
19
|
* A value satisfies the slot when it transitively extends the target kind, or —
|
|
18
20
|
* for a CONCRETE target — IS that kind; `getByExtends` is the same transitive
|
|
19
|
-
* subtype index for both
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* subtype index for both. Accepts a constraint that resolves to nothing, and an
|
|
22
|
+
* abstract with no loaded implementations: partial context, where a rejection
|
|
23
|
+
* would be a guess.
|
|
24
|
+
*
|
|
25
|
+
* An abstract is satisfied only by an implementer, never by the abstract itself,
|
|
26
|
+
* because a resource declared `kind: <some abstract>` is refused at `create()`
|
|
27
|
+
* and reported as `ABSTRACT_KIND_INSTANTIATED` at its declaration — so accepting
|
|
28
|
+
* it here would leave the reference sites silent about a manifest that cannot
|
|
29
|
+
* run. `isDeclaration` is the ONE exception and it is not an instance: a
|
|
30
|
+
* library's `resources:` entry is constrained by kind alone, so its kind-only
|
|
31
|
+
* stand-in routinely IS an abstract (`connection: {kind: Sql.Connection}`), and
|
|
32
|
+
* every use of that name inside the library is a reference to it.
|
|
23
33
|
*/
|
|
24
|
-
export function kindSatisfies(resolved, targetKind, registry
|
|
34
|
+
export function kindSatisfies(resolved, targetKind, registry,
|
|
35
|
+
/** The referent is a kind-only stand-in for a `resources:` entry rather than a
|
|
36
|
+
* resource anything instantiates, so identity satisfies an abstract slot. */
|
|
37
|
+
isDeclaration = false) {
|
|
25
38
|
const canonical = registry.resolveRef(targetKind) ?? targetKind;
|
|
26
39
|
const targetDef = registry.resolve(canonical);
|
|
27
40
|
if (!targetDef)
|
|
28
41
|
return true;
|
|
29
|
-
if (targetDef.kind !== "Telo.Abstract" && resolved === canonical)
|
|
42
|
+
if ((targetDef.kind !== "Telo.Abstract" || isDeclaration) && resolved === canonical)
|
|
30
43
|
return true;
|
|
31
44
|
const subtypes = registry.getByExtends(canonical);
|
|
32
45
|
if (subtypes.some((d) => `${d.metadata.module}.${d.metadata.name}` === resolved))
|
|
@@ -53,7 +66,9 @@ export function kindSatisfies(resolved, targetKind, registry) {
|
|
|
53
66
|
/** {@link kindSatisfies} at every kind a slot accepts, rendered as messages.
|
|
54
67
|
* The acceptance rule is not restated here — only what to say when it fails,
|
|
55
68
|
* which needs the slot's alternatives and the target's flavour. */
|
|
56
|
-
function checkKind(kind, entry, registry, aliases
|
|
69
|
+
function checkKind(kind, entry, registry, aliases,
|
|
70
|
+
/** See {@link kindSatisfies}: the referent is a `resources:` stand-in. */
|
|
71
|
+
isDeclaration = false) {
|
|
57
72
|
const resolved = aliases.resolveKind(kind) ?? kind;
|
|
58
73
|
// A qualified kind whose prefix names no import in this scope is a bad NAME
|
|
59
74
|
// rather than a bad type — `{kind: NotAnAlias.Script}` where only `Lib` is
|
|
@@ -76,7 +91,7 @@ function checkKind(kind, entry, registry, aliases) {
|
|
|
76
91
|
const targetDef = registry.resolve(targetKind);
|
|
77
92
|
if (!targetDef)
|
|
78
93
|
return [];
|
|
79
|
-
if (!unknownAlias && kindSatisfies(resolved, targetKind, registry))
|
|
94
|
+
if (!unknownAlias && kindSatisfies(resolved, targetKind, registry, isDeclaration))
|
|
80
95
|
return [];
|
|
81
96
|
const subtypes = registry.getByExtends(targetKind);
|
|
82
97
|
const subtypeKinds = new Set(subtypes.map((d) => `${d.metadata.module}.${d.metadata.name}`));
|
|
@@ -311,7 +326,7 @@ export function validateReferences(resources, context) {
|
|
|
311
326
|
});
|
|
312
327
|
return;
|
|
313
328
|
}
|
|
314
|
-
const kindErrors = checkKind(target.kind, entry, registry, aliases);
|
|
329
|
+
const kindErrors = checkKind(target.kind, entry, registry, aliases, isInjectedDeclaration(target));
|
|
315
330
|
if (kindErrors.length > 0) {
|
|
316
331
|
diagnostics.push({
|
|
317
332
|
severity: DiagnosticSeverity.Error,
|
|
@@ -359,7 +374,11 @@ export function validateReferences(resources, context) {
|
|
|
359
374
|
return;
|
|
360
375
|
}
|
|
361
376
|
// 2. Kind check
|
|
362
|
-
const
|
|
377
|
+
const objectTarget = typeof refVal.name === "string"
|
|
378
|
+
? (visibleScopeManifests.find((m) => m.metadata?.name === refVal.name) ??
|
|
379
|
+
byName.get(refVal.name))
|
|
380
|
+
: undefined;
|
|
381
|
+
const kindErrors = checkKind(refVal.kind, entry, registry, aliases, isInjectedDeclaration(objectTarget));
|
|
363
382
|
if (kindErrors.length > 0) {
|
|
364
383
|
diagnostics.push({
|
|
365
384
|
severity: DiagnosticSeverity.Error,
|
|
@@ -448,8 +467,7 @@ export function validateReferences(resources, context) {
|
|
|
448
467
|
if (!isAbsolute && anchorName.includes(".")) {
|
|
449
468
|
const resolvedResourceKind = aliases.resolveKind(r.kind) ?? r.kind;
|
|
450
469
|
const resourceDef = registry.resolve(r.kind) ?? registry.resolve(resolvedResourceKind);
|
|
451
|
-
const
|
|
452
|
-
const ownerScope = (owningModule ? aliasesByModule?.get(owningModule) : undefined) ?? aliases;
|
|
470
|
+
const ownerScope = moduleAliasScope(resourceDef?.metadata, aliases, aliasesByModule);
|
|
453
471
|
const targetKind = ownerScope.resolveKind(anchorName);
|
|
454
472
|
if (!targetKind) {
|
|
455
473
|
// Names the ALIAS, not the whole kind path: the alias is what an
|
|
@@ -2,34 +2,9 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
import { type AnalysisDiagnostic } from "./types.js";
|
|
5
|
-
/**
|
|
6
|
-
* The strict half of {@link readResourceInputs} — the `validate-ref-slots.ts`
|
|
7
|
-
* split applied to the resource-input boundary, and not optional for the same
|
|
8
|
-
* reason: the two sides fail in OPPOSITE directions. An unreadable DECLARATION
|
|
9
|
-
* leaves the library referencing a name nothing stands behind; an unchecked
|
|
10
|
-
* INJECTION hands a library an instance of a kind it never asked for, which
|
|
11
|
-
* surfaces as a method-missing failure inside someone else's module.
|
|
12
|
-
*
|
|
13
|
-
* Two surfaces, scoped the same way every other declaration check is — to the
|
|
14
|
-
* entry's own modules, since a published dependency's block is not the
|
|
15
|
-
* consumer's to fix:
|
|
16
|
-
*
|
|
17
|
-
* - **The library's own declaration.** Each entry's alias-qualified `kind:` must
|
|
18
|
-
* resolve in the DECLARING module's scope (`RESOURCE_INPUT_KIND_UNRESOLVED`).
|
|
19
|
-
* A constraint that resolves to nothing accepts anything, which is the same
|
|
20
|
-
* hole `X_TELO_REF_UNRESOLVED` exists to close one level down.
|
|
21
|
-
* - **The injection site.** Every declared entry must be supplied
|
|
22
|
-
* (`RESOURCE_INPUT_MISSING` — the same failure as a missing required
|
|
23
|
-
* variable), nothing beyond them may be (`RESOURCE_INPUT_UNKNOWN`), the value
|
|
24
|
-
* must name a resource that exists (`RESOURCE_INPUT_UNRESOLVED`), and its
|
|
25
|
-
* kind must satisfy the declared constraint transitively
|
|
26
|
-
* (`RESOURCE_INPUT_KIND_MISMATCH`).
|
|
27
|
-
*
|
|
28
|
-
* Browser-safe.
|
|
29
|
-
*/
|
|
30
5
|
export declare function validateResourceInputs(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver, rootModules: Set<string>,
|
|
31
6
|
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
32
7
|
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
33
8
|
* two cannot disagree about what satisfies a constraint. */
|
|
34
|
-
acceptsKind: (suppliedKind: string, requiredKind: string) => boolean): AnalysisDiagnostic[];
|
|
9
|
+
acceptsKind: (suppliedKind: string, requiredKind: string, suppliedIsDeclaration: boolean) => boolean): AnalysisDiagnostic[];
|
|
35
10
|
//# sourceMappingURL=validate-resource-inputs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-resource-inputs.d.ts","sourceRoot":"","sources":["../src/validate-resource-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-resource-inputs.d.ts","sourceRoot":"","sources":["../src/validate-resource-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,YAAY,CAAC;AAqCzE,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC;AACxB;;6DAE6D;AAC7D,WAAW,EAAE,CACX,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,EACpB,qBAAqB,EAAE,OAAO,KAC3B,OAAO,GACX,kBAAkB,EAAE,CAyMtB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isRefSentinel } from "@telorun/templating";
|
|
2
2
|
import { findDynamicLeaf } from "./resource-rule.js";
|
|
3
|
-
import { readResourceInputs, readSuppliedResources } from "./resource-input.js";
|
|
3
|
+
import { isInjectedDeclaration, readResourceInputs, readSuppliedResources, } from "./resource-input.js";
|
|
4
4
|
import { DiagnosticSeverity } from "./types.js";
|
|
5
5
|
const SOURCE = "telo-analyzer";
|
|
6
6
|
/**
|
|
@@ -28,6 +28,7 @@ const SOURCE = "telo-analyzer";
|
|
|
28
28
|
*
|
|
29
29
|
* Browser-safe.
|
|
30
30
|
*/
|
|
31
|
+
const declaringModule = (m) => m.metadata?.module;
|
|
31
32
|
export function validateResourceInputs(manifests, registry, aliases, rootModules,
|
|
32
33
|
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
33
34
|
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
@@ -186,7 +187,16 @@ acceptsKind) {
|
|
|
186
187
|
if (typeof suppliedKind !== "string")
|
|
187
188
|
continue;
|
|
188
189
|
const canonical = aliases.resolveKind(suppliedKind) ?? suppliedKind;
|
|
189
|
-
|
|
190
|
+
// A library forwarding an input it was itself handed supplies a kind-only
|
|
191
|
+
// stand-in, whose kind is routinely the abstract the target declares. That
|
|
192
|
+
// is identity against an abstract, which an ordinary reference is refused
|
|
193
|
+
// — the referent there would be a resource nothing can instantiate, while
|
|
194
|
+
// here it is another declaration.
|
|
195
|
+
const suppliedName = value.name;
|
|
196
|
+
const supplied = typeof suppliedName === "string"
|
|
197
|
+
? manifests.find((c) => c.metadata?.name === suppliedName && isOwn(declaringModule(c)))
|
|
198
|
+
: undefined;
|
|
199
|
+
if (acceptsKind(canonical, entry, isInjectedDeclaration(supplied)))
|
|
190
200
|
continue;
|
|
191
201
|
out.push({
|
|
192
202
|
severity: DiagnosticSeverity.Error,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-schema-type-refs.d.ts","sourceRoot":"","sources":["../src/validate-schema-type-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-schema-type-refs.d.ts","sourceRoot":"","sources":["../src/validate-schema-type-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAMzE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAC3C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GACvB,kBAAkB,EAAE,CAuDtB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
|
|
2
2
|
import { DiagnosticSeverity } from "./types.js";
|
|
3
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
3
4
|
const SOURCE = "telo-analyzer";
|
|
4
5
|
const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
|
|
5
6
|
/**
|
|
@@ -28,7 +29,7 @@ export function validateSchemaTypeRefs(manifests, registry, aliases, aliasesByMo
|
|
|
28
29
|
// validated against their own library when it's analyzed as a root.
|
|
29
30
|
if (ownModule && !rootModules.has(ownModule))
|
|
30
31
|
continue;
|
|
31
|
-
const resolver = (
|
|
32
|
+
const resolver = moduleAliasScope(m.metadata, aliases, aliasesByModule);
|
|
32
33
|
const filePath = m.metadata?.source;
|
|
33
34
|
const label = `${m.kind}/${name}`;
|
|
34
35
|
const walk = (value, path) => {
|
|
@@ -4,5 +4,9 @@ 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, aliasesByModule?: Map<string, AliasResolver>, rootModules?: Set<string
|
|
7
|
+
export declare function validateThrowsCoverage(manifests: ResourceManifest[], defs: DefinitionRegistry, aliases: AliasResolver, env: Environment, aliasesByModule?: Map<string, AliasResolver>, rootModules?: Set<string>,
|
|
8
|
+
/** Each imported library's full document set, so the walk can follow an
|
|
9
|
+
* exported entry point into the siblings it invokes — which a consumer's
|
|
10
|
+
* flat set does not carry. */
|
|
11
|
+
moduleManifests?: Map<string, ResourceManifest[]>): AnalysisDiagnostic[];
|
|
8
12
|
//# 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;AAEjE,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,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;AAsfzE,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,
|
|
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;AAEjE,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,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;AAsfzE,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;AACpC;;+BAE+B;AAC/B,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAa,GAC3D,kBAAkB,EAAE,CAyDtB"}
|
|
@@ -431,10 +431,14 @@ function schemaHasStepContext(schema) {
|
|
|
431
431
|
return false;
|
|
432
432
|
}
|
|
433
433
|
/** Entry point — invoked once per analyze() run. */
|
|
434
|
-
export function validateThrowsCoverage(manifests, defs, aliases, env, aliasesByModule = new Map(), rootModules = new Set()
|
|
434
|
+
export function validateThrowsCoverage(manifests, defs, aliases, env, aliasesByModule = new Map(), rootModules = new Set(),
|
|
435
|
+
/** Each imported library's full document set, so the walk can follow an
|
|
436
|
+
* exported entry point into the siblings it invokes — which a consumer's
|
|
437
|
+
* flat set does not carry. */
|
|
438
|
+
moduleManifests = new Map()) {
|
|
435
439
|
const diagnostics = [];
|
|
436
440
|
diagnostics.push(...validateThrowsDeclarations(manifests));
|
|
437
|
-
const resolveCtx = createResolveCtx(manifests, defs, aliases, aliasesByModule, rootModules);
|
|
441
|
+
const resolveCtx = createResolveCtx(manifests, defs, aliases, aliasesByModule, rootModules, moduleManifests);
|
|
438
442
|
// The alias resolver for a manifest's own lexical scope — an imported library's
|
|
439
443
|
// resolver when it owns the manifest, else undefined (fall back to root aliases).
|
|
440
444
|
const scopeResolverFor = (m) => scopeResolverForModule(m.metadata?.module, rootModules, aliasesByModule);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@types/node": "^20.0.0",
|
|
50
50
|
"typescript": "^5.0.0",
|
|
51
51
|
"vitest": "^2.1.8",
|
|
52
|
-
"@telorun/sdk": "0.
|
|
52
|
+
"@telorun/sdk": "0.87.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@telorun/sdk": "*"
|
package/src/analysis-registry.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { visitManifest as runVisitManifest, type ManifestVisitor } from "./manif
|
|
|
9
9
|
import type { ContractDirection, DefResolver } from "./extends-resolution.js";
|
|
10
10
|
import { resolveContract } from "./invocation-contract.js";
|
|
11
11
|
import { createResolveCtx, resolveThrowsUnion } from "./resolve-throws-union.js";
|
|
12
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
12
13
|
import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
13
14
|
import { resolveSchemaTypeRefs as resolveSchemaTypeRefsIn } from "./resolve-schema-type-refs.js";
|
|
14
15
|
import type { AnalysisContext } from "./types.js";
|
|
@@ -258,8 +259,7 @@ export class AnalysisRegistry {
|
|
|
258
259
|
*/
|
|
259
260
|
resolveSchemaFrom(schemaFrom: string, declaringKind: string): Record<string, any> | undefined {
|
|
260
261
|
const def = this.resolveDefinition(declaringKind);
|
|
261
|
-
const
|
|
262
|
-
const scope = (ownerModule ? this.aliasesByModule.get(ownerModule) : undefined) ?? this.aliases;
|
|
262
|
+
const scope = moduleAliasScope(def?.metadata, this.aliases, this.aliasesByModule);
|
|
263
263
|
return this.defs.resolveSchemaFromNode(schemaFrom, scope);
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -337,7 +337,7 @@ export class AnalysisRegistry {
|
|
|
337
337
|
* the kind was read off. Falls back to the global table when the module is
|
|
338
338
|
* unknown or is a root. */
|
|
339
339
|
resolveDefinitionIn(kind: string, module?: string): ResourceDefinition | undefined {
|
|
340
|
-
const scope = (module
|
|
340
|
+
const scope = moduleAliasScope({ module }, this.aliases, this.aliasesByModule);
|
|
341
341
|
const canonical = scope.resolveKind(kind);
|
|
342
342
|
return this.defs.resolve(kind) ?? (canonical ? this.defs.resolve(canonical) : undefined);
|
|
343
343
|
}
|
|
@@ -354,8 +354,7 @@ export class AnalysisRegistry {
|
|
|
354
354
|
resolverForDefinition(def: {
|
|
355
355
|
metadata?: { module?: string };
|
|
356
356
|
}): (kind: string) => ResourceDefinition | undefined {
|
|
357
|
-
const
|
|
358
|
-
const scope = (ownModule ? this.aliasesByModule.get(ownModule) : undefined) ?? this.aliases;
|
|
357
|
+
const scope = moduleAliasScope(def?.metadata, this.aliases, this.aliasesByModule);
|
|
359
358
|
return (kind) => {
|
|
360
359
|
const canonical = scope.resolveKind(kind);
|
|
361
360
|
return this.defs.resolve(kind) ?? (canonical ? this.defs.resolve(canonical) : undefined);
|
package/src/analyzer.ts
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from "./validate-observed-state.js";
|
|
48
48
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
49
49
|
import { visitManifest } from "./manifest-visitor.js";
|
|
50
|
+
import { declaringModuleScope, moduleAliasScope } from "./module-alias-scope.js";
|
|
50
51
|
import { isModuleKind } from "./module-kinds.js";
|
|
51
52
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
52
53
|
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
@@ -999,10 +1000,10 @@ export class StaticAnalyzer {
|
|
|
999
1000
|
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
1000
1001
|
const def = m as unknown as ResourceDefinition;
|
|
1001
1002
|
const ownModule = (def.metadata as { module?: string } | undefined)?.module;
|
|
1002
|
-
const scopeResolver =
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1003
|
+
const scopeResolver = declaringModuleScope(ownModule, aliases, {
|
|
1004
|
+
aliasesByModule,
|
|
1005
|
+
rootModules,
|
|
1006
|
+
});
|
|
1006
1007
|
// Canonicalize alias-form `x-telo-ref` constraints in the DECLARING module's
|
|
1007
1008
|
// scope, before the schema reaches `register()` and the lazily-built field
|
|
1008
1009
|
// maps. Same pre-resolution `capability` / `extends` get below.
|
|
@@ -1425,7 +1426,7 @@ export class StaticAnalyzer {
|
|
|
1425
1426
|
// analyzer knowing it exists: going native costs a module, not a change
|
|
1426
1427
|
// here.
|
|
1427
1428
|
const resolveRegionDef = (kind: string, module?: string) => {
|
|
1428
|
-
const scope = (module
|
|
1429
|
+
const scope = moduleAliasScope({ module }, aliases, aliasesByModule);
|
|
1429
1430
|
const canonical = scope.resolveKind(kind);
|
|
1430
1431
|
return defs.resolve(kind) ?? (canonical ? defs.resolve(canonical) : undefined);
|
|
1431
1432
|
};
|
|
@@ -1456,8 +1457,10 @@ export class StaticAnalyzer {
|
|
|
1456
1457
|
if (!ownModule || !m.metadata?.name || typeof m.schema !== "object" || m.schema === null) {
|
|
1457
1458
|
continue;
|
|
1458
1459
|
}
|
|
1459
|
-
const scopeResolver =
|
|
1460
|
-
|
|
1460
|
+
const scopeResolver = declaringModuleScope(ownModule, aliases, {
|
|
1461
|
+
aliasesByModule,
|
|
1462
|
+
rootModules,
|
|
1463
|
+
});
|
|
1461
1464
|
const canonicalKind = scopeResolver.resolveKind(m.kind as string) ?? (m.kind as string);
|
|
1462
1465
|
if (defs.resolve(canonicalKind)?.capability !== "Telo.Type") continue;
|
|
1463
1466
|
const typeName = m.metadata.name as string;
|
|
@@ -1877,6 +1880,37 @@ export class StaticAnalyzer {
|
|
|
1877
1880
|
continue;
|
|
1878
1881
|
}
|
|
1879
1882
|
|
|
1883
|
+
// An abstract names a CONTRACT for reference slots, not something to
|
|
1884
|
+
// instantiate — the kernel refuses it outright at `create()`. Reported
|
|
1885
|
+
// here, at the declaration, because that is the line the author has to
|
|
1886
|
+
// change and because a runtime-only refusal is exactly the shape static
|
|
1887
|
+
// analysis exists to remove. The hint mirrors the kernel's: the concrete
|
|
1888
|
+
// implementations this analysis holds, or the reason there are none.
|
|
1889
|
+
//
|
|
1890
|
+
// Injected declarations never reach this loop (skipped above), which is
|
|
1891
|
+
// what keeps a `resources:` entry — kind-only and routinely abstract by
|
|
1892
|
+
// design — out of it.
|
|
1893
|
+
if (definition.kind === "Telo.Abstract") {
|
|
1894
|
+
const canonical = resolvedKind ?? m.kind;
|
|
1895
|
+
const impls = defs
|
|
1896
|
+
.getByExtends(canonical)
|
|
1897
|
+
.filter((d) => d.kind !== "Telo.Abstract")
|
|
1898
|
+
.map((d) => `${d.metadata.module}.${d.metadata.name}`);
|
|
1899
|
+
const kindInfo =
|
|
1900
|
+
canonical !== m.kind ? `'${m.kind}' (resolved to '${canonical}')` : `'${m.kind}'`;
|
|
1901
|
+
const hint = impls.length
|
|
1902
|
+
? `instantiate a concrete implementation: ${impls.join(", ")}`
|
|
1903
|
+
: "no concrete implementations are registered — import a module that provides one";
|
|
1904
|
+
diagnostics.push({
|
|
1905
|
+
severity: DiagnosticSeverity.Error,
|
|
1906
|
+
code: "ABSTRACT_KIND_INSTANTIATED",
|
|
1907
|
+
source: SOURCE,
|
|
1908
|
+
message: `Kind ${kindInfo} is abstract and cannot be instantiated directly; ${hint}.`,
|
|
1909
|
+
data: { resource, filePath, path: "kind" },
|
|
1910
|
+
});
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1880
1914
|
// Validate resource config against the definition's AUTHOR-FACING schema —
|
|
1881
1915
|
// inheritance-resolved, with `kind` / `metadata` injected. See
|
|
1882
1916
|
// `validationSchemaFor`, which is where both derivations and the reason
|
|
@@ -2690,9 +2724,25 @@ export class StaticAnalyzer {
|
|
|
2690
2724
|
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
2691
2725
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
2692
2726
|
|
|
2693
|
-
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
2727
|
+
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7).
|
|
2728
|
+
// The library document sets collected for the zone stage serve here too: a
|
|
2729
|
+
// library's exported entry point is forwarded into the flat set while the
|
|
2730
|
+
// siblings it invokes are not, so without them the walk stops at the
|
|
2731
|
+
// boundary and reports an empty union for a body that plainly throws.
|
|
2732
|
+
const libraryManifests = new Map<string, ResourceManifest[]>();
|
|
2733
|
+
for (const doc of options?.moduleDocuments ?? []) {
|
|
2734
|
+
libraryManifests.set(doc.module, doc.manifests);
|
|
2735
|
+
}
|
|
2694
2736
|
diagnostics.push(
|
|
2695
|
-
...validateThrowsCoverage(
|
|
2737
|
+
...validateThrowsCoverage(
|
|
2738
|
+
allManifests,
|
|
2739
|
+
defs,
|
|
2740
|
+
aliases,
|
|
2741
|
+
this.celEnv,
|
|
2742
|
+
aliasesByModule,
|
|
2743
|
+
rootModules,
|
|
2744
|
+
libraryManifests,
|
|
2745
|
+
),
|
|
2696
2746
|
);
|
|
2697
2747
|
|
|
2698
2748
|
// Warn about declared variables / secrets / ports that no CEL references.
|
|
@@ -2710,7 +2760,8 @@ export class StaticAnalyzer {
|
|
|
2710
2760
|
defs,
|
|
2711
2761
|
aliases,
|
|
2712
2762
|
rootModules,
|
|
2713
|
-
(supplied, required) =>
|
|
2763
|
+
(supplied, required, isDeclaration) =>
|
|
2764
|
+
kindSatisfies(supplied, required, defs, isDeclaration),
|
|
2714
2765
|
),
|
|
2715
2766
|
);
|
|
2716
2767
|
|
package/src/call-graph.ts
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
42
42
|
import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
43
|
+
import { refSentinelTarget } from "./ref-sentinel-target.js";
|
|
43
44
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
44
45
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
45
46
|
import {
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
type RefFieldEntry,
|
|
64
65
|
} from "./reference-field-map.js";
|
|
65
66
|
import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
67
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
66
68
|
|
|
67
69
|
export interface ResourceGraphNode {
|
|
68
70
|
type: "resource";
|
|
@@ -693,8 +695,7 @@ export function buildCallGraph(
|
|
|
693
695
|
const definitionFor = (manifest: ResourceManifest): ResourceDefinition | undefined => {
|
|
694
696
|
const direct = registry.resolve(manifest.kind as string);
|
|
695
697
|
if (direct) return direct;
|
|
696
|
-
const
|
|
697
|
-
const scope = (module ? options.aliasesByModule?.get(module) : undefined) ?? options.aliases;
|
|
698
|
+
const scope = moduleAliasScope(manifest.metadata, options.aliases, options.aliasesByModule);
|
|
698
699
|
const canonical = scope?.resolveKind(manifest.kind as string);
|
|
699
700
|
return canonical ? registry.resolve(canonical) : undefined;
|
|
700
701
|
};
|
|
@@ -962,7 +963,12 @@ export function buildCallGraph(
|
|
|
962
963
|
* here: an unresolved `!ref <name>` sentinel and the `{kind, name}` object
|
|
963
964
|
* `resolveRefSentinels` rewrites it into. */
|
|
964
965
|
function refTargetName(value: unknown): string | undefined {
|
|
965
|
-
|
|
966
|
+
// An edge's target is taken VERBATIM — `resolveScopedName` and the node index
|
|
967
|
+
// are what resolve it, and a cross-module `Alias.name` is a target this graph
|
|
968
|
+
// legitimately carries unresolved. That is this pass's reduction over the
|
|
969
|
+
// shared parse, not a second reading of the tag.
|
|
970
|
+
const sentinel = refSentinelTarget(value);
|
|
971
|
+
if (sentinel) return sentinel.source;
|
|
966
972
|
if (!value || typeof value !== "object") return undefined;
|
|
967
973
|
const name = (value as Record<string, unknown>).name;
|
|
968
974
|
return typeof name === "string" ? name : undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical JSON: a rendering under which two structurally equal values produce
|
|
3
|
+
* the same string — object keys sorted, `undefined`-valued entries dropped.
|
|
4
|
+
*
|
|
5
|
+
* Shared rather than duplicated because both readers compare the RESULT for
|
|
6
|
+
* equality: `contentKey` anchors a module-graph row's identity to its content so
|
|
7
|
+
* an inserted sibling does not move it, and `declarationSignature` decides
|
|
8
|
+
* whether a resource has to be rebuilt. Two implementations would eventually
|
|
9
|
+
* disagree about key order or about `undefined`, and the disagreement surfaces
|
|
10
|
+
* as a row that silently changes identity, or as a resource that silently fails
|
|
11
|
+
* to restart.
|
|
12
|
+
*
|
|
13
|
+
* Browser-safe: no hashing, no `node:crypto`. Callers that only need equality
|
|
14
|
+
* compare the strings, which cannot collide; a caller that needs a short key
|
|
15
|
+
* hashes it itself.
|
|
16
|
+
*/
|
|
17
|
+
export function canonicalJson(value: unknown): string {
|
|
18
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
19
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
20
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
21
|
+
.filter(([, v]) => v !== undefined)
|
|
22
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
23
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a manifest's CEL READS, as opposed to what its slots reference.
|
|
3
|
+
*
|
|
4
|
+
* A dependency a manifest states without a slot: `!cel "resources.db.dsn"` makes
|
|
5
|
+
* the reader depend on `db` as surely as a `!ref` would, and no reference walk
|
|
6
|
+
* can see it. Two consumers ask for it — the module graph draws it as a `data`
|
|
7
|
+
* edge, and the kernel needs it to know who becomes invalid when a resource is
|
|
8
|
+
* rebuilt — so the extraction is here rather than in either of them.
|
|
9
|
+
*
|
|
10
|
+
* The expensive half is the CEL environment used to parse, which is built once
|
|
11
|
+
* per process and evaluates nothing.
|
|
12
|
+
*/
|
|
13
|
+
import { extractAccessChains } from "@telorun/templating";
|
|
14
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
15
|
+
import { walkCelExpressions } from "@telorun/templating";
|
|
16
|
+
import { buildCelEnvironment } from "./cel-environment.js";
|
|
17
|
+
|
|
18
|
+
let parseEnv: ReturnType<typeof buildCelEnvironment> | undefined;
|
|
19
|
+
|
|
20
|
+
/** Access chains an expression reads, or none when it does not parse — a syntax
|
|
21
|
+
* error is the engine pass's to report, not this one's. */
|
|
22
|
+
export function accessChains(source: string): string[][] {
|
|
23
|
+
try {
|
|
24
|
+
parseEnv ??= buildCelEnvironment();
|
|
25
|
+
return extractAccessChains(parseEnv.parse(source).ast);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The bare resource names a manifest's expressions read through
|
|
33
|
+
* `resources.<name>`, deduplicated.
|
|
34
|
+
*
|
|
35
|
+
* Read from the DECLARATION: a compile-eval field is expanded before a
|
|
36
|
+
* controller sees it, so by then `resources.db.dsn` is a literal with nothing
|
|
37
|
+
* left to say where it came from.
|
|
38
|
+
*/
|
|
39
|
+
export function celResourceReads(manifest: ResourceManifest): string[] {
|
|
40
|
+
const names = new Set<string>();
|
|
41
|
+
walkCelExpressions(manifest as Record<string, unknown>, "", (source: string) => {
|
|
42
|
+
for (const chain of accessChains(source)) {
|
|
43
|
+
if (chain[0] === "resources" && chain.length >= 2 && chain[1]) names.add(chain[1]);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return [...names];
|
|
47
|
+
}
|
package/src/cel-scope-query.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { buildCelEnvironment } from "./cel-environment.js";
|
|
|
18
18
|
import { CelScopeResolver, type CelScope } from "./cel-scope.js";
|
|
19
19
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
20
20
|
import { buildKernelGlobalsIndex } from "./kernel-globals.js";
|
|
21
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
21
22
|
import { isModuleKind } from "./module-kinds.js";
|
|
22
23
|
import { navigateConcretePath } from "./manifest-path.js";
|
|
23
24
|
import { findManifest } from "./find-manifest.js";
|
|
@@ -41,6 +42,12 @@ export interface ContextDeclarationSite {
|
|
|
41
42
|
kind: string;
|
|
42
43
|
name: string;
|
|
43
44
|
path: string;
|
|
45
|
+
/** The declaring module, when the manifest carries one. Part of the identity,
|
|
46
|
+
* not decoration: a resource name is unique inside its module and not across a
|
|
47
|
+
* flattened set, so `(kind, name)` alone lets a host locating this site land on
|
|
48
|
+
* another library's same-named document — which for go-to-declaration means
|
|
49
|
+
* jumping to the wrong file with nothing to indicate it. */
|
|
50
|
+
module?: string;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/** Join a concrete path segment, tolerating an empty base (the manifest root). */
|
|
@@ -210,9 +217,14 @@ export class CelScopeQuery {
|
|
|
210
217
|
base = hit;
|
|
211
218
|
}
|
|
212
219
|
|
|
213
|
-
const metadata = origin.manifest.metadata as { name?: string } | undefined;
|
|
220
|
+
const metadata = origin.manifest.metadata as { name?: string; module?: string } | undefined;
|
|
214
221
|
if (!origin.manifest.kind || !metadata?.name) return undefined;
|
|
215
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
kind: origin.manifest.kind,
|
|
224
|
+
name: metadata.name,
|
|
225
|
+
path: base,
|
|
226
|
+
...(metadata.module === undefined ? {} : { module: metadata.module }),
|
|
227
|
+
};
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
/** The manifest and path an annotated context property is derived from, and
|
|
@@ -265,13 +277,29 @@ export class CelScopeQuery {
|
|
|
265
277
|
if (hash <= 0) return undefined;
|
|
266
278
|
const kindValue = navigateConcretePath(root, first.slice(0, hash).split("/").join("."));
|
|
267
279
|
if (typeof kindValue !== "string") return undefined;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
280
|
+
// The kind was read off THIS resource, so it is spelled in the alias scope
|
|
281
|
+
// of the module that declared it — not the entry's.
|
|
282
|
+
const scope = moduleAliasScope(resource.metadata, this.ctx.aliases, this.ctx.aliasesByModule);
|
|
283
|
+
const canonical = scope.resolveKind(kindValue) ?? kindValue;
|
|
284
|
+
const dot = canonical.indexOf(".");
|
|
285
|
+
const owningModule = dot === -1 ? undefined : canonical.slice(0, dot);
|
|
286
|
+
const suffix = canonical.slice(dot + 1);
|
|
287
|
+
// Matched by MODULE AND NAME once the canonical form carries a module: a
|
|
288
|
+
// definition name is unique inside its module and not across a flattened
|
|
289
|
+
// set, so two libraries each declaring an `Api` would otherwise resolve to
|
|
290
|
+
// whichever came first — and the editor would type a context region off the
|
|
291
|
+
// wrong kind's annotations, silently. Falls back to name alone only when the
|
|
292
|
+
// kind did not resolve to a canonical form, where there is nothing to narrow
|
|
293
|
+
// by and a best-effort match is still better than none.
|
|
294
|
+
const named = this.manifests.filter(
|
|
271
295
|
(m) =>
|
|
272
296
|
(m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") &&
|
|
273
297
|
(m.metadata as { name?: string } | undefined)?.name === suffix,
|
|
274
|
-
) as Record<string, any>
|
|
298
|
+
) as Record<string, any>[];
|
|
299
|
+
const target =
|
|
300
|
+
(owningModule
|
|
301
|
+
? named.find((m) => (m.metadata as { module?: string } | undefined)?.module === owningModule)
|
|
302
|
+
: undefined) ?? (owningModule && named.length > 1 ? undefined : named[0]);
|
|
275
303
|
if (!target) return undefined;
|
|
276
304
|
return { manifest: target, path: first.slice(hash + 1), propertyMap: false };
|
|
277
305
|
}
|
|
@@ -313,9 +341,18 @@ export class CelScopeQuery {
|
|
|
313
341
|
return undefined;
|
|
314
342
|
}
|
|
315
343
|
|
|
344
|
+
/** The kind's declaration, resolved in the scope of the module that DECLARED
|
|
345
|
+
* this resource — the same rule the manifest visitor applies, and the reason
|
|
346
|
+
* it has to be the same one: everything this query offers is read off the
|
|
347
|
+
* definition, so resolving a forwarded library resource's `kind: Http.Api`
|
|
348
|
+
* through the entry's aliases alone found nothing and the editor silently
|
|
349
|
+
* offered no `request` / `result` at a site `telo check` accepts. A
|
|
350
|
+
* completion list is a claim that the name it offers will pass the checker,
|
|
351
|
+
* so the two must resolve a kind identically. */
|
|
316
352
|
private definitionFor(resource: ResourceManifest): ResourceDefinition | undefined {
|
|
317
|
-
const { defs, aliases } = this.ctx;
|
|
318
|
-
const
|
|
353
|
+
const { defs, aliases, aliasesByModule } = this.ctx;
|
|
354
|
+
const scope = moduleAliasScope(resource.metadata, aliases, aliasesByModule);
|
|
355
|
+
const canonical = scope.resolveKind(resource.kind);
|
|
319
356
|
return defs.resolve(resource.kind) ?? (canonical ? defs.resolve(canonical) : undefined);
|
|
320
357
|
}
|
|
321
358
|
|