@telorun/analyzer 0.73.0 → 0.74.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +26 -7
- package/dist/artifact-axes.d.ts +14 -0
- package/dist/artifact-axes.d.ts.map +1 -0
- package/dist/artifact-axes.js +12 -0
- package/dist/artifact-layer-index.d.ts +2 -2
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +9 -4
- package/dist/artifact-selector.d.ts +47 -21
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +85 -16
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +73 -16
- package/dist/call-graph.d.ts +12 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +22 -0
- package/dist/catch-scope.js +1 -1
- package/dist/forwarded-declaration.d.ts +26 -0
- package/dist/forwarded-declaration.d.ts.map +1 -0
- package/dist/forwarded-declaration.js +31 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +21 -5
- package/dist/module-alias-scope.d.ts +11 -0
- package/dist/module-alias-scope.d.ts.map +1 -1
- package/dist/module-alias-scope.js +10 -0
- package/dist/module-file-claims.d.ts +17 -1
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +38 -3
- package/dist/module-named-files.d.ts +72 -0
- package/dist/module-named-files.d.ts.map +1 -0
- package/dist/module-named-files.js +115 -0
- package/dist/native-entries.d.ts +83 -0
- package/dist/native-entries.d.ts.map +1 -0
- package/dist/native-entries.js +169 -0
- package/dist/normalize-inline-resources.d.ts +41 -13
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +236 -54
- package/dist/reference-field-map.d.ts +59 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +200 -34
- package/dist/release/payload-digest.d.ts +2 -1
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +2 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +8 -5
- package/dist/resolve-throws-union.d.ts +25 -4
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +62 -32
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +2 -1
- package/dist/rewrite-synthetic-origins.d.ts +6 -1
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
- package/dist/rewrite-synthetic-origins.js +107 -13
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +18 -5
- package/dist/schema-walk.d.ts +47 -13
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +150 -30
- package/dist/scope-declarations.d.ts +59 -0
- package/dist/scope-declarations.d.ts.map +1 -0
- package/dist/scope-declarations.js +45 -0
- package/dist/source-entries.d.ts +119 -0
- package/dist/source-entries.d.ts.map +1 -0
- package/dist/source-entries.js +349 -0
- package/dist/step-bodies.d.ts +24 -0
- package/dist/step-bodies.d.ts.map +1 -0
- package/dist/step-bodies.js +28 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +3 -2
- package/dist/validate-module-artifact.d.ts +4 -2
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +43 -4
- package/dist/validate-native-entries.d.ts +15 -0
- package/dist/validate-native-entries.d.ts.map +1 -0
- package/dist/validate-native-entries.js +103 -0
- package/dist/validate-observed-state.d.ts +7 -1
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +24 -6
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +2 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +11 -5
- package/dist/validate-scope-reach.d.ts +31 -0
- package/dist/validate-scope-reach.d.ts.map +1 -0
- package/dist/validate-scope-reach.js +180 -0
- package/dist/validate-source-entries.d.ts +16 -0
- package/dist/validate-source-entries.d.ts.map +1 -0
- package/dist/validate-source-entries.js +90 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +21 -39
- package/dist/value-type-keyword.d.ts +22 -5
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +16 -7
- package/package.json +3 -3
- package/src/analyzer.ts +33 -9
- package/src/artifact-axes.ts +24 -0
- package/src/artifact-layer-index.ts +9 -6
- package/src/artifact-selector.ts +110 -35
- package/src/builtins.ts +75 -16
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +36 -1
- package/src/kernel-globals.ts +28 -5
- package/src/migrations/CLAUDE.md +35 -0
- package/src/module-alias-scope.ts +17 -0
- package/src/module-file-claims.ts +63 -4
- package/src/module-named-files.ts +162 -0
- package/src/native-entries.ts +227 -0
- package/src/normalize-inline-resources.ts +326 -70
- package/src/reference-field-map.ts +257 -52
- package/src/release/CLAUDE.md +32 -0
- package/src/release/payload-digest.ts +2 -1
- package/src/resolve-ref-sentinels.ts +7 -6
- package/src/resolve-throws-union.ts +87 -32
- package/src/resolve-zone-requirements.ts +2 -1
- package/src/rewrite-synthetic-origins.ts +107 -16
- package/src/schema-compat.ts +18 -4
- package/src/schema-walk.ts +162 -34
- package/src/scope-declarations.ts +98 -0
- package/src/source-entries.ts +502 -0
- package/src/step-bodies.ts +55 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +3 -2
- package/src/validate-module-artifact.ts +46 -3
- package/src/validate-native-entries.ts +147 -0
- package/src/validate-observed-state.ts +36 -7
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +10 -6
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
|
@@ -4,6 +4,7 @@ import { parseLayerIndex, LayerIndexError } from "./artifact-layer-index.js";
|
|
|
4
4
|
import {
|
|
5
5
|
ArtifactSelectorError,
|
|
6
6
|
PLATFORM_AXES,
|
|
7
|
+
selectorContradictions,
|
|
7
8
|
selectorFromQualifiers,
|
|
8
9
|
selectorKey,
|
|
9
10
|
} from "./artifact-selector.js";
|
|
@@ -26,10 +27,12 @@ const SOURCE = "telo-analyzer";
|
|
|
26
27
|
* selector (spec §1), which is what every module with two `js` controllers relies
|
|
27
28
|
* on.
|
|
28
29
|
*
|
|
29
|
-
* 1. **Controller selector qualifiers.**
|
|
30
|
+
* 1. **Controller selector qualifiers.** The platform axes and `siblings` are
|
|
30
31
|
* authored surface. An unknown qualifier is reported rather than ignored, since
|
|
31
32
|
* ignoring is what makes a typo invisible; an invalid value is reported here
|
|
32
|
-
* instead of throwing from the loader later
|
|
33
|
+
* instead of throwing from the loader later, and so is a combination no host
|
|
34
|
+
* can match (`selectorContradictions`, shared with `native:` and
|
|
35
|
+
* `exports.code:`).
|
|
33
36
|
* 2. **The published layer index.** The owner doc's JSON Schema covers shape; the
|
|
34
37
|
* semantic rules — controller-requires-selector, singletons carry none, no
|
|
35
38
|
* duplicate selector, the token grammar (`os: Linux` passes the schema and
|
|
@@ -79,6 +82,16 @@ function validateLibraryCandidates(manifest: ResourceManifest, out: AnalysisDiag
|
|
|
79
82
|
// mean a consumer's import resolves by whichever candidate is read first.
|
|
80
83
|
const seen = new Map<string, LibraryCandidate>();
|
|
81
84
|
for (const candidate of candidates) {
|
|
85
|
+
for (const contradiction of selectorContradictions(candidate.selector)) {
|
|
86
|
+
out.push({
|
|
87
|
+
severity: DiagnosticSeverity.Error,
|
|
88
|
+
code: `LIBRARY_${contradiction.rule}`,
|
|
89
|
+
source: SOURCE,
|
|
90
|
+
message:
|
|
91
|
+
`Telo.Library/${metadata?.name ?? "(unnamed)"}: ${candidate.origin}: ${contradiction.detail}`,
|
|
92
|
+
data: { resource, filePath: metadata?.source, path: "exports/code" },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
82
95
|
const key = selectorKey(candidate.selector);
|
|
83
96
|
const first = seen.get(key);
|
|
84
97
|
if (first) {
|
|
@@ -162,7 +175,37 @@ function validateControllerSelectors(
|
|
|
162
175
|
// Validate the selector; the value is not otherwise needed here, since
|
|
163
176
|
// candidates sharing a selector legitimately share a layer.
|
|
164
177
|
try {
|
|
165
|
-
selectorFromQualifiers(parsed.format, parsed.qualifiers, candidate);
|
|
178
|
+
const selector = selectorFromQualifiers(parsed.format, parsed.qualifiers, candidate);
|
|
179
|
+
for (const contradiction of selectorContradictions(selector)) {
|
|
180
|
+
out.push({
|
|
181
|
+
severity: DiagnosticSeverity.Error,
|
|
182
|
+
code: `CONTROLLER_${contradiction.rule}`,
|
|
183
|
+
source: SOURCE,
|
|
184
|
+
message:
|
|
185
|
+
`${manifest.kind}/${name ?? "(unnamed)"}: controller candidate "${candidate}": ` +
|
|
186
|
+
contradiction.detail,
|
|
187
|
+
data: { resource, filePath, path: `${at}?${contradiction.axis}` },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// A `dylib` opens over the Rust controller ABI, and a library built
|
|
191
|
+
// against another version of it must never match — so the candidate states
|
|
192
|
+
// which, in the `telo` family.
|
|
193
|
+
if (parsed.format === "dylib" && !selector.abi?.startsWith("telo-")) {
|
|
194
|
+
out.push({
|
|
195
|
+
severity: DiagnosticSeverity.Error,
|
|
196
|
+
code: "CONTROLLER_DYLIB_ABI_MISSING",
|
|
197
|
+
source: SOURCE,
|
|
198
|
+
message:
|
|
199
|
+
`${manifest.kind}/${name ?? "(unnamed)"}: dylib candidate "${candidate}" ` +
|
|
200
|
+
(selector.abi === undefined
|
|
201
|
+
? `states no abi. `
|
|
202
|
+
: `states abi=${selector.abi}, which is not a Rust controller ABI. `) +
|
|
203
|
+
`A dylib is built against one version of the Rust controller ABI and must say which — ` +
|
|
204
|
+
`add abi=telo-<version>, e.g. abi=telo-2. Without it the Rust kernel downloads the ` +
|
|
205
|
+
`library on every host and refuses it only when it opens it.`,
|
|
206
|
+
data: { resource, filePath, path: at },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
166
209
|
} catch (err) {
|
|
167
210
|
if (!(err instanceof ArtifactSelectorError)) throw err;
|
|
168
211
|
out.push({
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
import { describeSelector, selectorContradictions, selectorKey } from "./artifact-selector.js";
|
|
4
|
+
import { moduleDocumentClaims } from "./module-file-claims.js";
|
|
5
|
+
import { describeClaim, nativeClaimConflicts } from "./module-named-files.js";
|
|
6
|
+
import { readNativeEntries, type NativeEntry } from "./native-entries.js";
|
|
7
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const SOURCE = "telo-analyzer";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The strict half of `native-entries.ts`: what `telo check` reports about a
|
|
13
|
+
* module doc's `native:` block.
|
|
14
|
+
*
|
|
15
|
+
* Every rule is decidable from the manifest and would otherwise surface only on
|
|
16
|
+
* a consumer's host, as a native file that never matches or a layer that
|
|
17
|
+
* overwrites another's file. Shape errors (a missing key, an unknown one) are
|
|
18
|
+
* left to the owner doc's JSON Schema, which reports them in the same pass.
|
|
19
|
+
*
|
|
20
|
+
* Entry-module-scoped: a dependency's block is not the consumer's to fix.
|
|
21
|
+
*/
|
|
22
|
+
export function validateNativeEntries(
|
|
23
|
+
manifests: ResourceManifest[],
|
|
24
|
+
entryModules?: ReadonlySet<string>,
|
|
25
|
+
): AnalysisDiagnostic[] {
|
|
26
|
+
const out: AnalysisDiagnostic[] = [];
|
|
27
|
+
for (const manifest of manifests) {
|
|
28
|
+
if (manifest.kind !== "Telo.Application" && manifest.kind !== "Telo.Library") continue;
|
|
29
|
+
const metadata = (manifest.metadata ?? {}) as Record<string, unknown>;
|
|
30
|
+
const name = typeof metadata.name === "string" ? metadata.name : undefined;
|
|
31
|
+
const owned =
|
|
32
|
+
entryModules === undefined ||
|
|
33
|
+
(typeof metadata.module === "string"
|
|
34
|
+
? entryModules.has(metadata.module)
|
|
35
|
+
: name === undefined || entryModules.has(name));
|
|
36
|
+
if (!owned) continue;
|
|
37
|
+
|
|
38
|
+
const label = `${manifest.kind}/${name ?? "(unnamed)"}`;
|
|
39
|
+
const report = (code: string, path: string, message: string) =>
|
|
40
|
+
out.push({
|
|
41
|
+
severity: DiagnosticSeverity.Error,
|
|
42
|
+
code,
|
|
43
|
+
source: SOURCE,
|
|
44
|
+
message: `${label}: ${message}`,
|
|
45
|
+
data: {
|
|
46
|
+
resource: { kind: manifest.kind, name },
|
|
47
|
+
filePath: typeof metadata.source === "string" ? metadata.source : undefined,
|
|
48
|
+
path,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const { entries, problems } = readNativeEntries(manifest);
|
|
53
|
+
for (const problem of problems) {
|
|
54
|
+
if (problem.kind === "shape") continue;
|
|
55
|
+
report(
|
|
56
|
+
problem.kind === "escape" ? "NATIVE_PATH_ESCAPES_MODULE" : "NATIVE_ENTRY_INVALID",
|
|
57
|
+
problem.path,
|
|
58
|
+
problem.message,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (entries.length === 0) continue;
|
|
62
|
+
|
|
63
|
+
const moduleName = typeof metadata.module === "string" ? metadata.module : name;
|
|
64
|
+
for (const { entry, claim } of nativeClaimConflicts(
|
|
65
|
+
entries,
|
|
66
|
+
moduleDocumentClaims(manifests, moduleName),
|
|
67
|
+
)) {
|
|
68
|
+
report(
|
|
69
|
+
"NATIVE_PATH_CLAIMED",
|
|
70
|
+
`native[${entry.index}].path`,
|
|
71
|
+
`${entry.origin}: path '${entry.path}' is also named by ${describeClaim(claim)}. A native ` +
|
|
72
|
+
`file ships only in its platform's native layer and a file extracts from exactly one ` +
|
|
73
|
+
`layer, so publish refuses it — give the native entry its own file, or drop the other ` +
|
|
74
|
+
`declaration.`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bySelector = new Map<string, NativeEntry>();
|
|
79
|
+
const byPath = new Map<string, NativeEntry>();
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
const at = `native[${entry.index}]`;
|
|
82
|
+
if (entry.selector.format === "node" && entry.selector.abi === undefined) {
|
|
83
|
+
report(
|
|
84
|
+
"NATIVE_NODE_ABI_MISSING",
|
|
85
|
+
at,
|
|
86
|
+
`${entry.origin}: a 'node' addon is built against one Node ABI and must state it — ` +
|
|
87
|
+
`add abi: node-<NODE_MODULE_VERSION>, e.g. 'node-137'. Without it the file matches ` +
|
|
88
|
+
`every Node release and fails to load on all but one.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (const contradiction of selectorContradictions(entry.selector)) {
|
|
92
|
+
report(
|
|
93
|
+
`NATIVE_${contradiction.rule}`,
|
|
94
|
+
`${at}.${contradiction.axis}`,
|
|
95
|
+
`${entry.origin}: ${contradiction.detail}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const key = `${entry.name}\0${selectorKey(entry.selector)}`;
|
|
100
|
+
const twin = bySelector.get(key);
|
|
101
|
+
if (twin) {
|
|
102
|
+
report(
|
|
103
|
+
"NATIVE_ENTRY_DUPLICATE",
|
|
104
|
+
at,
|
|
105
|
+
`${entry.origin}: ${twin.origin} already declares '${entry.name}' for ` +
|
|
106
|
+
`${describeSelector(entry.selector)}. A name has one entry per platform tuple, or ` +
|
|
107
|
+
`a lookup by name could resolve to either file.`,
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
bySelector.set(key, entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const sharer = byPath.get(entry.path);
|
|
114
|
+
if (sharer && selectorKey(sharer.selector) !== selectorKey(entry.selector)) {
|
|
115
|
+
report(
|
|
116
|
+
"NATIVE_PATH_SHARED",
|
|
117
|
+
`${at}.path`,
|
|
118
|
+
`${entry.origin}: path '${entry.path}' is also declared by ${sharer.origin} for ` +
|
|
119
|
+
`${describeSelector(sharer.selector)}. Every layer of a module extracts into one ` +
|
|
120
|
+
`directory, so each platform tuple needs its own path — e.g. ` +
|
|
121
|
+
`'native/<os>-<arch>/…'.`,
|
|
122
|
+
);
|
|
123
|
+
} else if (!sharer) {
|
|
124
|
+
byPath.set(entry.path, entry);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const through = [...byPath.values()].find(
|
|
128
|
+
(other) =>
|
|
129
|
+
other !== entry &&
|
|
130
|
+
(entry.path.startsWith(`${other.path}/`) || other.path.startsWith(`${entry.path}/`)),
|
|
131
|
+
);
|
|
132
|
+
if (through) {
|
|
133
|
+
const relation = entry.path.startsWith(`${through.path}/`)
|
|
134
|
+
? `runs through '${through.path}' as a directory`
|
|
135
|
+
: `is a directory that '${through.path}' runs through`;
|
|
136
|
+
report(
|
|
137
|
+
"NATIVE_PATH_NESTED",
|
|
138
|
+
`${at}.path`,
|
|
139
|
+
`${entry.origin}: path '${entry.path}' ${relation}, and ${through.origin} declares ` +
|
|
140
|
+
`'${through.path}'. Every layer of a module extracts into one directory, so a path ` +
|
|
141
|
+
`cannot be both a file and a directory — give each entry its own path.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
3
3
|
import { effectiveStatusSchema } from "./extends-resolution.js";
|
|
4
|
+
import { isForwardedDeclaration } from "./forwarded-declaration.js";
|
|
4
5
|
import { parseExportEntry } from "./flatten-for-analyzer.js";
|
|
5
6
|
import {
|
|
6
7
|
moduleScopedDefResolver,
|
|
@@ -129,11 +130,20 @@ export function collectRunReachableNames(graph: CallGraph): Set<string> {
|
|
|
129
130
|
|
|
130
131
|
/** What a resource name resolves to for CEL purposes. `status` is present only
|
|
131
132
|
* when the kind declares one; `scoped` marks a resource declared inside an
|
|
132
|
-
* `x-telo-scope` slot, which resolves only within that scope's regions.
|
|
133
|
+
* `x-telo-scope` slot, which resolves only within that scope's regions.
|
|
134
|
+
* `forwardedFrom` marks a dependency's declaration, keyed by
|
|
135
|
+
* `forwardedResourceKey`: it is in no consumer's `resources`, but a read inside
|
|
136
|
+
* that dependency's own manifests still resolves to it. */
|
|
133
137
|
export interface AnalyzedResource {
|
|
134
138
|
kind: string;
|
|
135
139
|
status?: Record<string, any>;
|
|
136
140
|
scoped?: boolean;
|
|
141
|
+
forwardedFrom?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The index key of a name a dependency declares, as read from inside it. */
|
|
145
|
+
export function forwardedResourceKey(module: string, name: string): string {
|
|
146
|
+
return `${module}\0${name}`;
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
/**
|
|
@@ -157,16 +167,35 @@ export function buildObservedStateIndex(
|
|
|
157
167
|
/** `module` is the resource's DECLARING module: an exported instance is
|
|
158
168
|
* written with that library's aliases (`kind: Self.Listener`), which the
|
|
159
169
|
* consumer's table cannot resolve. */
|
|
160
|
-
const record = (
|
|
170
|
+
const record = (
|
|
171
|
+
kind: string,
|
|
172
|
+
key: string,
|
|
173
|
+
scoped: boolean,
|
|
174
|
+
module?: string,
|
|
175
|
+
forwardedFrom?: string,
|
|
176
|
+
): void => {
|
|
161
177
|
const status = effectiveStatusSchema(resolve.in(kind, module), resolve);
|
|
162
|
-
out.set(key, {
|
|
178
|
+
out.set(key, {
|
|
179
|
+
kind,
|
|
180
|
+
...(status ? { status } : {}),
|
|
181
|
+
...(scoped ? { scoped } : {}),
|
|
182
|
+
...(forwardedFrom !== undefined ? { forwardedFrom } : {}),
|
|
183
|
+
});
|
|
163
184
|
};
|
|
164
185
|
|
|
165
186
|
for (const manifest of manifests) {
|
|
166
187
|
const kind = manifest.kind as string | undefined;
|
|
167
188
|
const name = manifest.metadata?.name as string | undefined;
|
|
168
189
|
if (!kind || SYSTEM_KINDS.has(kind)) continue;
|
|
169
|
-
|
|
190
|
+
const module = manifest.metadata?.module as string | undefined;
|
|
191
|
+
// A dependency's declarations publish into ITS module's `resources`, not this
|
|
192
|
+
// one's: they are keyed by that module, and an export is also indexed under
|
|
193
|
+
// `<Alias>.<name>` below.
|
|
194
|
+
const forwardedFrom = isForwardedDeclaration(manifest) ? module : undefined;
|
|
195
|
+
if (forwardedFrom === undefined && isForwardedDeclaration(manifest)) continue;
|
|
196
|
+
const keyOf = (n: string) =>
|
|
197
|
+
forwardedFrom === undefined ? n : forwardedResourceKey(forwardedFrom, n);
|
|
198
|
+
if (name) record(kind, keyOf(name), false, module, forwardedFrom);
|
|
170
199
|
|
|
171
200
|
const schema = resolve(kind)?.schema as Record<string, any> | undefined;
|
|
172
201
|
if (!schema) continue;
|
|
@@ -177,7 +206,7 @@ export function buildObservedStateIndex(
|
|
|
177
206
|
const scopedKind = (scopedEntry as ResourceManifest)?.kind;
|
|
178
207
|
const scopedName = (scopedEntry as ResourceManifest)?.metadata?.name;
|
|
179
208
|
if (typeof scopedKind === "string" && typeof scopedName === "string") {
|
|
180
|
-
record(scopedKind, scopedName, true);
|
|
209
|
+
record(scopedKind, keyOf(scopedName), true, forwardedFrom, forwardedFrom);
|
|
181
210
|
}
|
|
182
211
|
}
|
|
183
212
|
}
|
|
@@ -254,8 +283,8 @@ export function buildObservedStateResourcesSchema(
|
|
|
254
283
|
open: boolean,
|
|
255
284
|
): Record<string, any> {
|
|
256
285
|
const properties: Record<string, any> = {};
|
|
257
|
-
for (const [key, { status }] of index) {
|
|
258
|
-
if (!status) continue;
|
|
286
|
+
for (const [key, { status, forwardedFrom }] of index) {
|
|
287
|
+
if (!status || forwardedFrom !== undefined) continue;
|
|
259
288
|
applyObservedStateNode(properties, key, status);
|
|
260
289
|
}
|
|
261
290
|
return open
|
|
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
+
import { isForwardedDeclaration } from "./forwarded-declaration.js";
|
|
5
6
|
import { visitManifest } from "./manifest-visitor.js";
|
|
6
7
|
import { satisfiesValueBranch } from "./reference-field-map.js";
|
|
7
8
|
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
@@ -50,9 +51,7 @@ export function validateReferenceForms(
|
|
|
50
51
|
if (!aliases) return [];
|
|
51
52
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
-
(r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
|
|
55
|
-
const localResources = resources.filter((r) => !isForeign(r));
|
|
54
|
+
const localResources = resources.filter((r) => !isForwardedDeclaration(r));
|
|
56
55
|
|
|
57
56
|
visitManifest(
|
|
58
57
|
localResources,
|
|
@@ -17,6 +17,8 @@ import type { DefinitionRegistry } from "./definition-registry.js";
|
|
|
17
17
|
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
18
18
|
import { isModuleKind } from "./module-kinds.js";
|
|
19
19
|
import { isInjectedDeclaration } from "./resource-input.js";
|
|
20
|
+
import { outsideScopesOf } from "./scope-declarations.js";
|
|
21
|
+
import { isForwardedDeclaration, isForwardedExport } from "./forwarded-declaration.js";
|
|
20
22
|
|
|
21
23
|
const SOURCE = "telo-analyzer";
|
|
22
24
|
|
|
@@ -308,11 +310,10 @@ export function validateReferences(
|
|
|
308
310
|
// duplicate detection and local name resolution, and never walked as ref sources.
|
|
309
311
|
const moduleOf = (r: ResourceManifest): string | undefined =>
|
|
310
312
|
(r.metadata as { module?: string } | undefined)?.module;
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
const isForeign =
|
|
315
|
-
(r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
|
|
313
|
+
// A dependency's code — a forwarded export, or what extraction pulled out of one — is
|
|
314
|
+
// excluded from duplicate detection and local name resolution, and never walked as a
|
|
315
|
+
// ref source. Only an EXPORT is a cross-module resolution target.
|
|
316
|
+
const isForeign = isForwardedDeclaration;
|
|
316
317
|
// Forwarded exported instances keyed `${module}\0${name}` — the lookup that resolves
|
|
317
318
|
// whether a cross-module `!ref Alias.name` names a real exported instance.
|
|
318
319
|
const byModuleName = new Map<string, ResourceManifest>();
|
|
@@ -329,7 +330,7 @@ export function validateReferences(
|
|
|
329
330
|
if (typeof m === "string") loadedModules.add(m);
|
|
330
331
|
continue;
|
|
331
332
|
}
|
|
332
|
-
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !
|
|
333
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForwardedExport(r)) continue;
|
|
333
334
|
const m = moduleOf(r);
|
|
334
335
|
if (!m) continue;
|
|
335
336
|
byModuleName.set(`${m}\0${r.metadata.name as string}`, r);
|
|
@@ -422,6 +423,9 @@ export function validateReferences(
|
|
|
422
423
|
visibleScopeManifests.find((m) => m.metadata?.name === localName) ??
|
|
423
424
|
byName.get(localName);
|
|
424
425
|
if (!target) {
|
|
426
|
+
// A scoped name out of this declaration's reach has its own
|
|
427
|
+
// diagnostic, which says why; this one would say only "not found".
|
|
428
|
+
if (outsideScopesOf(r).some((scope) => scope.names.includes(localName))) return;
|
|
425
429
|
diagnostics.push({
|
|
426
430
|
severity: DiagnosticSeverity.Error,
|
|
427
431
|
code: "UNRESOLVED_REFERENCE",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { walkCelExpressions } from "@telorun/templating";
|
|
3
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
+
import { isForwardedDeclaration } from "./forwarded-declaration.js";
|
|
6
|
+
import { definitionInScope } from "./module-alias-scope.js";
|
|
7
|
+
import { isRefEntry, resolveFieldEntries } from "./reference-field-map.js";
|
|
8
|
+
import {
|
|
9
|
+
declaredScopes,
|
|
10
|
+
isScopeMember,
|
|
11
|
+
outsideScopesOf,
|
|
12
|
+
scopeEncloses,
|
|
13
|
+
type DeclaredScope,
|
|
14
|
+
type OutsideScope,
|
|
15
|
+
} from "./scope-declarations.js";
|
|
16
|
+
import { forEachStep, stepBodiesOf } from "./step-bodies.js";
|
|
17
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
18
|
+
|
|
19
|
+
const SOURCE = "telo-analyzer";
|
|
20
|
+
|
|
21
|
+
const DECLARATION_KINDS = new Set([
|
|
22
|
+
"Telo.Definition",
|
|
23
|
+
"Telo.Abstract",
|
|
24
|
+
"Telo.Application",
|
|
25
|
+
"Telo.Library",
|
|
26
|
+
"Telo.Import",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `SCOPED_NAME_OUT_OF_REACH` — a declaration written inside a scope's region but
|
|
31
|
+
* created outside the scope (see {@link OutsideScope}) naming one of the scope's
|
|
32
|
+
* resources.
|
|
33
|
+
*
|
|
34
|
+
* Written there, the name reads as reachable — the scope's names shadow the
|
|
35
|
+
* module's in its regions — while at run time it resolves to nothing, or to a
|
|
36
|
+
* module-level resource that merely shares it. Reported at the reference, in
|
|
37
|
+
* each spelling a name can be read in:
|
|
38
|
+
*
|
|
39
|
+
* - a `!ref` sentinel, anywhere — the tag is unambiguous;
|
|
40
|
+
* - a resolved `{kind, name}`, but ONLY where the declaring kind says a
|
|
41
|
+
* reference is: a reference slot of its field map, or a step's dispatch slot.
|
|
42
|
+
* An object that merely has those keys is data;
|
|
43
|
+
* - `resources.<name>` in a CEL expression.
|
|
44
|
+
*
|
|
45
|
+
* Lexical: a scope declared on the way from the out-of-reach declaration down to
|
|
46
|
+
* the reference — found through the kinds' own scope slots — shadows the name
|
|
47
|
+
* again, since that one IS created around it. Entry-module-scoped: a
|
|
48
|
+
* dependency's code is its own analysis's to report.
|
|
49
|
+
*
|
|
50
|
+
* Browser-safe.
|
|
51
|
+
*/
|
|
52
|
+
export function validateScopedNameReach(
|
|
53
|
+
manifests: ResourceManifest[],
|
|
54
|
+
registry: DefinitionRegistry,
|
|
55
|
+
aliases: AliasResolver,
|
|
56
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
57
|
+
rootModules: ReadonlySet<string>,
|
|
58
|
+
/** Member-access chains of a CEL expression — the analyzer's own parse. */
|
|
59
|
+
accessChains: (expression: string) => string[][],
|
|
60
|
+
): AnalysisDiagnostic[] {
|
|
61
|
+
const out: AnalysisDiagnostic[] = [];
|
|
62
|
+
|
|
63
|
+
for (const root of manifests) {
|
|
64
|
+
if (!root.kind || DECLARATION_KINDS.has(root.kind) || isForwardedDeclaration(root)) continue;
|
|
65
|
+
const meta = root.metadata as { name?: unknown; module?: unknown; source?: unknown } | undefined;
|
|
66
|
+
if (typeof meta?.name !== "string") continue;
|
|
67
|
+
const module = typeof meta.module === "string" ? meta.module : undefined;
|
|
68
|
+
if (module && !rootModules.has(module)) continue;
|
|
69
|
+
|
|
70
|
+
const resource = { kind: root.kind, name: meta.name };
|
|
71
|
+
const filePath = typeof meta.source === "string" ? meta.source : undefined;
|
|
72
|
+
const report = (path: string, name: string, scope: OutsideScope) =>
|
|
73
|
+
out.push({
|
|
74
|
+
severity: DiagnosticSeverity.Error,
|
|
75
|
+
code: "SCOPED_NAME_OUT_OF_REACH",
|
|
76
|
+
source: SOURCE,
|
|
77
|
+
message:
|
|
78
|
+
`${scope.ownerKind}/${scope.ownerName}: '${name}' is declared in '${scope.field}:', but ` +
|
|
79
|
+
`the inline declaration referencing it is created where '${scope.ownerName}' is ` +
|
|
80
|
+
`declared, outside that scope, so the reference cannot reach it. Declare the ` +
|
|
81
|
+
`referencing resource under '${scope.field}:' with a name of its own and point at it ` +
|
|
82
|
+
`with '!ref'.`,
|
|
83
|
+
data: { resource, filePath, path },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** A declaration's kind, field map and scopes, read in its module's aliases. */
|
|
87
|
+
const shape = (declaration: ResourceManifest) => {
|
|
88
|
+
const view =
|
|
89
|
+
module !== undefined && typeof declaration.metadata?.module !== "string"
|
|
90
|
+
? ({ ...declaration, metadata: { ...declaration.metadata, module } } as ResourceManifest)
|
|
91
|
+
: declaration;
|
|
92
|
+
const fieldMap = registry.expandedFieldMapForResource(view, aliases, aliasesByModule);
|
|
93
|
+
const definition = definitionInScope<ResourceDefinition>(
|
|
94
|
+
registry,
|
|
95
|
+
view.kind,
|
|
96
|
+
view.metadata,
|
|
97
|
+
aliases,
|
|
98
|
+
aliasesByModule,
|
|
99
|
+
);
|
|
100
|
+
return {
|
|
101
|
+
fieldMap,
|
|
102
|
+
scopes: declaredScopes(declaration as Record<string, unknown>, fieldMap),
|
|
103
|
+
schema: registry.effectiveSchemaOf(definition) as Record<string, any> | undefined,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const check = (
|
|
108
|
+
declaration: ResourceManifest,
|
|
109
|
+
prefix: string,
|
|
110
|
+
reach: ReadonlyMap<string, OutsideScope>,
|
|
111
|
+
shadowed: ReadonlySet<string>,
|
|
112
|
+
): void => {
|
|
113
|
+
const stamped = outsideScopesOf(declaration);
|
|
114
|
+
const nextReach = new Map(reach);
|
|
115
|
+
const nextShadowed = new Set(shadowed);
|
|
116
|
+
for (const scope of stamped) {
|
|
117
|
+
for (const name of scope.names) {
|
|
118
|
+
nextReach.set(name, scope);
|
|
119
|
+
nextShadowed.delete(name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const { fieldMap, scopes, schema } = shape(declaration);
|
|
123
|
+
for (const scope of scopes) {
|
|
124
|
+
for (const member of scope.declarations) {
|
|
125
|
+
if (isScopeMember(member)) nextShadowed.add(member.metadata.name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const at = (path: string) => (prefix ? (path ? `${prefix}.${path}` : prefix) : path);
|
|
130
|
+
const test = (name: string | undefined, path: string) => {
|
|
131
|
+
if (name === undefined || nextShadowed.has(name)) return;
|
|
132
|
+
const scope = nextReach.get(name);
|
|
133
|
+
if (scope) report(at(path), name, scope);
|
|
134
|
+
};
|
|
135
|
+
const inScopeArray = (path: string) =>
|
|
136
|
+
scopes.some((scope: DeclaredScope) => scopeEncloses({ ...scope, regions: [scope.path] }, path));
|
|
137
|
+
|
|
138
|
+
// Tagged `!ref`s and CEL, anywhere but the declaration's own metadata and
|
|
139
|
+
// scope arrays (their members are checked below, with their own shadowing).
|
|
140
|
+
walkCelExpressions(declaration, "", (source, path, engine) => {
|
|
141
|
+
if (path === "metadata" || path.startsWith("metadata.") || inScopeArray(path)) return;
|
|
142
|
+
if (engine === "ref") {
|
|
143
|
+
test(localName(source), path);
|
|
144
|
+
} else if (engine === "cel") {
|
|
145
|
+
for (const chain of accessChains(source)) {
|
|
146
|
+
if (chain[0] === "resources" && chain.length > 1) test(chain[1], path);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// A resolved reference, where the kind declares one.
|
|
152
|
+
if (fieldMap) {
|
|
153
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
154
|
+
if (!isRefEntry(entry)) continue;
|
|
155
|
+
for (const { value, path } of resolveFieldEntries(declaration, fieldPath)) {
|
|
156
|
+
if (inScopeArray(path)) continue;
|
|
157
|
+
test(resolvedLocalName(value), path);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (schema) {
|
|
162
|
+
for (const body of stepBodiesOf(declaration as Record<string, unknown>, schema)) {
|
|
163
|
+
forEachStep(body, schema, (step, stepPath) =>
|
|
164
|
+
test(resolvedLocalName(step[body.invokeField]), `${stepPath}.${body.invokeField}`),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const scope of scopes) {
|
|
170
|
+
scope.declarations.forEach((member, i) => {
|
|
171
|
+
if (isScopeMember(member)) {
|
|
172
|
+
check(member, at(`${scope.path}[${i}]`), nextReach, nextShadowed);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Only a stamped declaration can hold an out-of-reach reference; the rest
|
|
179
|
+
* is searched through the scope arrays that can hold one. */
|
|
180
|
+
const visit = (declaration: ResourceManifest, prefix: string): void => {
|
|
181
|
+
if (outsideScopesOf(declaration).length > 0) {
|
|
182
|
+
check(declaration, prefix, new Map(), new Set());
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
for (const scope of shape(declaration).scopes) {
|
|
186
|
+
scope.declarations.forEach((member, i) => {
|
|
187
|
+
if (!isScopeMember(member)) return;
|
|
188
|
+
visit(member, `${prefix ? `${prefix}.` : ""}${scope.path}[${i}]`);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
visit(root, "");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The local name a `!ref` source names, or undefined for a cross-module one. */
|
|
200
|
+
function localName(source: string): string | undefined {
|
|
201
|
+
const dot = source.indexOf(".");
|
|
202
|
+
if (dot === -1) return source;
|
|
203
|
+
return source.slice(0, dot) === "Self" ? source.slice(dot + 1) : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The local name a resolved `{kind, name, alias?}` reference names. */
|
|
207
|
+
function resolvedLocalName(value: unknown): string | undefined {
|
|
208
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
209
|
+
const ref = value as { kind?: unknown; name?: unknown; alias?: unknown };
|
|
210
|
+
if (typeof ref.kind !== "string" || typeof ref.name !== "string") return undefined;
|
|
211
|
+
if (typeof ref.alias === "string" && ref.alias !== "Self") return undefined;
|
|
212
|
+
return ref.name;
|
|
213
|
+
}
|