@telorun/analyzer 0.72.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 +76 -20
- 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 +144 -113
- 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/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +32 -11
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +1 -0
- package/dist/eval-paths.d.ts +36 -5
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +47 -6
- package/dist/extends-resolution.d.ts +12 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +23 -0
- 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 +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- 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/nearest-name.d.ts +22 -0
- package/dist/nearest-name.d.ts.map +1 -0
- package/dist/nearest-name.js +43 -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/destinations.d.ts +59 -0
- package/dist/release/destinations.d.ts.map +1 -0
- package/dist/release/destinations.js +75 -0
- package/dist/release/index.d.ts +6 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -1
- package/dist/release/ledger.d.ts +20 -11
- package/dist/release/ledger.d.ts.map +1 -1
- package/dist/release/ledger.js +43 -14
- 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/release/release-plan.d.ts +3 -2
- package/dist/release/release-plan.d.ts.map +1 -1
- package/dist/release/release-plan.js +12 -19
- package/dist/release/workspace-config.d.ts +114 -34
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +322 -38
- package/dist/release/workspace-schema.d.ts +41 -0
- package/dist/release/workspace-schema.d.ts.map +1 -0
- package/dist/release/workspace-schema.js +77 -0
- 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-error-report.d.ts +7 -0
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +9 -4
- 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/template-body.d.ts +10 -0
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +25 -0
- package/dist/validate-cel-context.d.ts +23 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +51 -18
- package/dist/validate-exports.d.ts +28 -0
- package/dist/validate-exports.d.ts.map +1 -0
- package/dist/validate-exports.js +146 -0
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +37 -1
- 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-provider-coherence.d.ts +4 -8
- package/dist/validate-provider-coherence.d.ts.map +1 -1
- package/dist/validate-provider-coherence.js +12 -123
- 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 +152 -65
- 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-template-body.d.ts +36 -0
- package/dist/validate-template-body.d.ts.map +1 -0
- package/dist/validate-template-body.js +246 -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 +4 -3
- package/src/analyzer.ts +97 -20
- 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 +160 -113
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/cel-scope-query.ts +28 -9
- package/src/cel-scope.ts +1 -0
- package/src/eval-paths.ts +53 -6
- package/src/extends-resolution.ts +24 -0
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +37 -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/nearest-name.ts +47 -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/destinations.ts +105 -0
- package/src/release/index.ts +33 -2
- package/src/release/ledger.ts +51 -21
- package/src/release/payload-digest.ts +2 -1
- package/src/release/release-plan.ts +16 -26
- package/src/release/workspace-config.ts +483 -47
- package/src/release/workspace-schema.ts +99 -0
- 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-error-report.ts +16 -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/template-body.ts +25 -0
- package/src/validate-cel-context.ts +60 -18
- package/src/validate-exports.ts +185 -0
- package/src/validate-extends.ts +43 -0
- 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-provider-coherence.ts +17 -127
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +151 -66
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-template-body.ts +274 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
- package/dist/validate-template-dispatch.d.ts +0 -27
- package/dist/validate-template-dispatch.d.ts.map +0 -1
- package/dist/validate-template-dispatch.js +0 -95
- package/src/validate-template-dispatch.ts +0 -99
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
import { moduleDocumentClaims } from "./module-file-claims.js";
|
|
4
|
+
import {
|
|
5
|
+
crossLayerSourceLinks,
|
|
6
|
+
stageableFiles,
|
|
7
|
+
unclaimedSourceEntries,
|
|
8
|
+
} from "./module-named-files.js";
|
|
9
|
+
import { normalizeNativePath, readNativeEntries } from "./native-entries.js";
|
|
10
|
+
import { readModuleSources } from "./source-entries.js";
|
|
11
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
12
|
+
|
|
13
|
+
const SOURCE = "telo-analyzer";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The strict half of `source-entries.ts`: what `telo check` reports about a
|
|
17
|
+
* module doc's `sources:` block.
|
|
18
|
+
*
|
|
19
|
+
* Every rule the block alone decides is a reader problem, reported here under
|
|
20
|
+
* its own code. This adds the rules that need the rest of the module or that
|
|
21
|
+
* only publish enforces: an entry must stage a file the manifest names, a link
|
|
22
|
+
* ships in its target's layer, and every file entry and every build carries its
|
|
23
|
+
* pin. Shape errors are left to the owner doc's JSON Schema.
|
|
24
|
+
*
|
|
25
|
+
* Entry-module-scoped: a dependency's block is not the consumer's to fix.
|
|
26
|
+
*/
|
|
27
|
+
export function validateSourceEntries(
|
|
28
|
+
manifests: ResourceManifest[],
|
|
29
|
+
entryModules?: ReadonlySet<string>,
|
|
30
|
+
): AnalysisDiagnostic[] {
|
|
31
|
+
const out: AnalysisDiagnostic[] = [];
|
|
32
|
+
for (const manifest of manifests) {
|
|
33
|
+
if (manifest.kind !== "Telo.Application" && manifest.kind !== "Telo.Library") continue;
|
|
34
|
+
const metadata = (manifest.metadata ?? {}) as Record<string, unknown>;
|
|
35
|
+
const name = typeof metadata.name === "string" ? metadata.name : undefined;
|
|
36
|
+
const owned =
|
|
37
|
+
entryModules === undefined ||
|
|
38
|
+
(typeof metadata.module === "string"
|
|
39
|
+
? entryModules.has(metadata.module)
|
|
40
|
+
: name === undefined || entryModules.has(name));
|
|
41
|
+
if (!owned) continue;
|
|
42
|
+
|
|
43
|
+
const label = `${manifest.kind}/${name ?? "(unnamed)"}`;
|
|
44
|
+
const report = (code: string, path: string, message: string) =>
|
|
45
|
+
out.push({
|
|
46
|
+
severity: DiagnosticSeverity.Error,
|
|
47
|
+
code,
|
|
48
|
+
source: SOURCE,
|
|
49
|
+
message: `${label}: ${message}`,
|
|
50
|
+
data: {
|
|
51
|
+
resource: { kind: manifest.kind, name },
|
|
52
|
+
filePath: typeof metadata.source === "string" ? metadata.source : undefined,
|
|
53
|
+
path,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { sources, problems } = readModuleSources(manifest);
|
|
58
|
+
for (const problem of problems) {
|
|
59
|
+
if (problem.code === "SHAPE") continue;
|
|
60
|
+
report(problem.code, problem.path, problem.message);
|
|
61
|
+
}
|
|
62
|
+
if (sources.length === 0) continue;
|
|
63
|
+
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
if (source.build && source.build.inputs === undefined) {
|
|
66
|
+
report(
|
|
67
|
+
"SOURCE_BUILD_UNPINNED",
|
|
68
|
+
`sources.${source.name}.build`,
|
|
69
|
+
`source '${source.name}' names the crate its files are built from but records no build ` +
|
|
70
|
+
`inputs, so a later edit to that crate cannot be caught — run \`telo release stage ` +
|
|
71
|
+
`--pin\` after building its files. \`telo release check\` and publish refuse it.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
for (const entry of source.entries) {
|
|
75
|
+
if (entry.kind !== "file" || entry.pin) continue;
|
|
76
|
+
report(
|
|
77
|
+
"SOURCE_ENTRY_UNPINNED",
|
|
78
|
+
`sources.${source.name}.entries.${entry.key}`,
|
|
79
|
+
`source '${source.name}' entry '${entry.key}' carries no pin (sha256 and executable), so ` +
|
|
80
|
+
`nothing can verify the file it stages: a kernel refuses to read it and publish refuses ` +
|
|
81
|
+
`to ship it — run \`telo release stage --pin\`.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const moduleName = typeof metadata.module === "string" ? metadata.module : name;
|
|
87
|
+
const stageable = stageableFiles(
|
|
88
|
+
readNativeEntries(manifest).entries,
|
|
89
|
+
moduleDocumentClaims(manifests, moduleName),
|
|
90
|
+
);
|
|
91
|
+
for (const { source, entry, layer, targetLayer } of crossLayerSourceLinks(sources, stageable)) {
|
|
92
|
+
report(
|
|
93
|
+
"SOURCE_LINK_TARGET_UNRESOLVED",
|
|
94
|
+
`sources.${source.name}.entries.${entry.key}.target`,
|
|
95
|
+
`source '${source.name}' entry '${entry.key}': the link ships in the ${layer} layer, but ` +
|
|
96
|
+
`its target '${entry.resolved}' ships in the ${targetLayer} layer. A runtime extracts ` +
|
|
97
|
+
`only the layers it needs, so the link would dangle — point it at a file of its own layer.`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
// A `native:` entry with an unrelated problem still names its path; reported
|
|
101
|
+
// once, as that problem, rather than again as an unclaimed source entry.
|
|
102
|
+
const unreadNative = new Set<string>();
|
|
103
|
+
const native = (manifest as { native?: unknown }).native;
|
|
104
|
+
for (const entry of Array.isArray(native) ? native : []) {
|
|
105
|
+
const path = (entry as { path?: unknown } | null)?.path;
|
|
106
|
+
const verdict = typeof path === "string" ? normalizeNativePath(path.trim()) : undefined;
|
|
107
|
+
if (verdict && "path" in verdict) unreadNative.add(verdict.path);
|
|
108
|
+
}
|
|
109
|
+
for (const { source, entry } of unclaimedSourceEntries(sources, stageable, unreadNative)) {
|
|
110
|
+
report(
|
|
111
|
+
"SOURCE_ENTRY_UNCLAIMED",
|
|
112
|
+
`sources.${source.name}.entries.${entry.key}`,
|
|
113
|
+
`source '${source.name}' entry '${entry.key}': nothing in the module names '${entry.path}' ` +
|
|
114
|
+
`— no native: entry's path, no platform-qualified controller candidate's path=, and ` +
|
|
115
|
+
`none of the source's notices. Name the file where it is used, or remove the entry.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { nearestName } from "./nearest-name.js";
|
|
2
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
+
import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
4
|
+
import type { AliasResolver, ModuleScopes } from "./alias-resolver.js";
|
|
5
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
6
|
+
import { refSentinelTarget } from "./ref-sentinel-target.js";
|
|
7
|
+
import { isRefEntry, resolveFieldEntries, satisfiesValueBranch } from "./reference-field-map.js";
|
|
8
|
+
import { templateBodies } from "./template-body.js";
|
|
9
|
+
import { DiagnosticSeverity, DiagnosticTag, type AnalysisDiagnostic } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const SOURCE = "telo-analyzer";
|
|
12
|
+
|
|
13
|
+
/** The four slots a `Telo.Definition` names its dispatch target in.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THE TARGET'S CAPABILITY IS IS DELIBERATELY NOT CHECKED. The kernel tests
|
|
16
|
+
* METHOD PRESENCE at dispatch (`entry.instance?.invoke`), so a declared
|
|
17
|
+
* capability says nothing about whether a dispatch works — the same structural
|
|
18
|
+
* -versus-nominal split that keeps `use` uncross-constrained against a slot's
|
|
19
|
+
* target. A check here read `Run.Sequence`'s `capability: Telo.Runnable` and
|
|
20
|
+
* rejected `Ai.Buffered`, a shipping module whose sequence implements `invoke()`
|
|
21
|
+
* over an inputs/outputs contract and dispatches correctly. Trimming the refused
|
|
22
|
+
* set would only move that: any controller may expose a method its capability
|
|
23
|
+
* does not name, and a template body is exactly where that is done on purpose. */
|
|
24
|
+
const DISPATCH_SLOTS = ["invoke", "run", "provide", "mount"] as const;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A TEMPLATE BODY IS WRITTEN LIKE EVERY OTHER MANIFEST, AND CHECKED LIKE ONE.
|
|
28
|
+
*
|
|
29
|
+
* A `Telo.Definition` is in both reference skip sets — its body holds
|
|
30
|
+
* declarations of OTHER kinds, and its dispatch slots carry no `x-telo-ref` — so
|
|
31
|
+
* no reference pass reached it, and each rule about it was added per construct:
|
|
32
|
+
* `provide:` and `mount:` had a target check for the `{ kind, name }` object
|
|
33
|
+
* form, `!ref` had one that switched itself off for any definition with a
|
|
34
|
+
* CEL-named entry, and `invoke:` / `run:` had none. What was tagged was caught
|
|
35
|
+
* and what was a plain name was resolved by the kernel alone. This pass is the
|
|
36
|
+
* one reader of a body's reference surface, and it rests on one spelling:
|
|
37
|
+
*
|
|
38
|
+
* - every `resources:` entry is named by a LITERAL (`TEMPLATE_ENTRY_NAME_DYNAMIC`).
|
|
39
|
+
* Each instance of a template owns its children in a child context of its
|
|
40
|
+
* own, so a per-instance suffix (`self.name + '-query'`) buys nothing — and a
|
|
41
|
+
* `!ref` is looked up verbatim, so a CEL-named sibling is one nothing can name;
|
|
42
|
+
* - a dispatch slot is `!ref <entry>` naming an entry
|
|
43
|
+
* (`TEMPLATE_DISPATCH_UNKNOWN`); the legacy spellings are deprecated rather
|
|
44
|
+
* than refused, since published artifacts carry them. What the target's
|
|
45
|
+
* CAPABILITY is stays unchecked — see `DISPATCH_SLOTS`;
|
|
46
|
+
* - a reference slot INSIDE an entry follows the rule every other slot does —
|
|
47
|
+
* `!ref` or an inline declaration, never `{ kind, name }` or a bare string
|
|
48
|
+
* (`INVALID_REFERENCE_FORM`) — and a bare `!ref` names a sibling or a
|
|
49
|
+
* resource of the defining module (`TEMPLATE_REF_UNKNOWN`). Slots are found
|
|
50
|
+
* through the nested kind's own field map, resolved in the DEFINING module's
|
|
51
|
+
* alias scope, since that is where the body's kinds are written.
|
|
52
|
+
*
|
|
53
|
+
* Entry-module-scoped, like every other declaration check: a published
|
|
54
|
+
* dependency's template body is not the consumer's to fix. Browser-safe.
|
|
55
|
+
*/
|
|
56
|
+
export function validateTemplateBody(
|
|
57
|
+
manifests: ResourceManifest[],
|
|
58
|
+
registry: DefinitionRegistry,
|
|
59
|
+
aliases: AliasResolver,
|
|
60
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
61
|
+
rootModules: ReadonlySet<string>,
|
|
62
|
+
): AnalysisDiagnostic[] {
|
|
63
|
+
const out: AnalysisDiagnostic[] = [];
|
|
64
|
+
const scopes: ModuleScopes = { aliasesByModule, rootModules };
|
|
65
|
+
|
|
66
|
+
// A definition forwarded from an imported library is that library's to fix;
|
|
67
|
+
// the imports name them (the `validate-extends` rule).
|
|
68
|
+
const importedModules = new Set<string>();
|
|
69
|
+
// Every named instance, by declaring module — what a body's bare `!ref` may
|
|
70
|
+
// reach beside its siblings. A manifest with no stamp belongs to the entry.
|
|
71
|
+
const instancesByModule = new Map<string | undefined, Set<string>>();
|
|
72
|
+
for (const m of manifests) {
|
|
73
|
+
const name = m.metadata?.name;
|
|
74
|
+
if (typeof name !== "string") continue;
|
|
75
|
+
if (m.kind === "Telo.Import") {
|
|
76
|
+
const resolved = (m.metadata as { resolvedModuleName?: string }).resolvedModuleName;
|
|
77
|
+
if (resolved) importedModules.add(resolved);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") continue;
|
|
81
|
+
if (m.kind === "Telo.Library" || m.kind === "Telo.Application") continue;
|
|
82
|
+
const mod = (m.metadata as { module?: string } | undefined)?.module;
|
|
83
|
+
let set = instancesByModule.get(mod);
|
|
84
|
+
if (!set) instancesByModule.set(mod, (set = new Set()));
|
|
85
|
+
set.add(name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const m of manifests) {
|
|
89
|
+
if (m.kind !== "Telo.Definition") continue;
|
|
90
|
+
const meta = m.metadata as { name?: string; module?: string; source?: string } | undefined;
|
|
91
|
+
const name = meta?.name;
|
|
92
|
+
if (!name) continue;
|
|
93
|
+
if (meta?.module && importedModules.has(meta.module)) continue;
|
|
94
|
+
|
|
95
|
+
const bodies = (m as Record<string, unknown>).resources;
|
|
96
|
+
const resourceRef = { kind: m.kind, name };
|
|
97
|
+
const filePath = meta?.source;
|
|
98
|
+
const label = `${m.kind}/${name}`;
|
|
99
|
+
const report = (code: string, path: string, message: string, fix?: string) =>
|
|
100
|
+
out.push({
|
|
101
|
+
severity: DiagnosticSeverity.Error,
|
|
102
|
+
code,
|
|
103
|
+
source: SOURCE,
|
|
104
|
+
message: `${label}: ${message}`,
|
|
105
|
+
data: {
|
|
106
|
+
resource: resourceRef,
|
|
107
|
+
filePath,
|
|
108
|
+
path,
|
|
109
|
+
...(fix ? { fix: { replacement: fix } } : {}),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/** A spelling the kernel still READS. Warning-grade because refusing it
|
|
114
|
+
* would break every app pinning a version that carries it, and tagged so an
|
|
115
|
+
* editor strikes it through rather than merely colouring it — the
|
|
116
|
+
* `DEPRECATED_KIND` shape, for the same reason. */
|
|
117
|
+
const deprecate = (code: string, path: string, message: string) =>
|
|
118
|
+
out.push({
|
|
119
|
+
severity: DiagnosticSeverity.Warning,
|
|
120
|
+
code,
|
|
121
|
+
source: SOURCE,
|
|
122
|
+
message: `${label}: ${message}`,
|
|
123
|
+
tags: [DiagnosticTag.Deprecated],
|
|
124
|
+
data: { resource: resourceRef, filePath, path },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const entries = Array.isArray(bodies) ? bodies : [];
|
|
128
|
+
const siblings: string[] = [];
|
|
129
|
+
let anyDynamic = false;
|
|
130
|
+
entries.forEach((entry, i) => {
|
|
131
|
+
const entryName = (entry as { metadata?: { name?: unknown } } | undefined)?.metadata?.name;
|
|
132
|
+
if (typeof entryName === "string" && !entryName.includes("${{")) {
|
|
133
|
+
siblings.push(entryName);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (entryName === undefined) return;
|
|
137
|
+
anyDynamic = true;
|
|
138
|
+
deprecate(
|
|
139
|
+
"DEPRECATED_TEMPLATE_ENTRY_NAME",
|
|
140
|
+
`resources[${i}].metadata.name`,
|
|
141
|
+
`a 'resources:' entry named by ${describeDynamic(entryName)} is deprecated — name it ` +
|
|
142
|
+
`with a literal and dispatch to it with '!ref'. Each instance of a template owns its ` +
|
|
143
|
+
`children in a child context of its own, so a per-instance suffix buys nothing, and a ` +
|
|
144
|
+
`'!ref' is looked up verbatim, so nothing can name a computed entry. While one is ` +
|
|
145
|
+
`present the target checks below are switched off for this kind.`,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Siblings PLUS the declaring module's own resources, because that is what
|
|
150
|
+
// the runtime resolves at a reference slot: a body's `!ref` is stamped
|
|
151
|
+
// `{kind, name}` with an empty kind for a non-sibling, and Phase-5 injection
|
|
152
|
+
// dispatches by NAME and recovers the kind from the instance it finds — so
|
|
153
|
+
// `client: !ref moduleClient` in a body reaches the module-level client and
|
|
154
|
+
// works. (A step's `invoke:` is a different path that does NOT: see the
|
|
155
|
+
// `resources` note in `validate-template-body`'s own tests.)
|
|
156
|
+
const reachable = new Set([
|
|
157
|
+
...siblings,
|
|
158
|
+
...(instancesByModule.get(meta?.module) ?? []),
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
// --- dispatch slots ---
|
|
162
|
+
for (const slot of DISPATCH_SLOTS) {
|
|
163
|
+
const value = (m as Record<string, unknown>)[slot];
|
|
164
|
+
if (value == null) continue;
|
|
165
|
+
if (!isRefSentinel(value)) {
|
|
166
|
+
deprecate(
|
|
167
|
+
"DEPRECATED_TEMPLATE_DISPATCH_FORM",
|
|
168
|
+
slot,
|
|
169
|
+
`'${slot}:' written as ${describeDispatchValue(value)} is deprecated — ` +
|
|
170
|
+
`write '!ref <entry>', the spelling every other reference uses. ` +
|
|
171
|
+
`Available: ${siblings.join(", ") || "(none)"}.`,
|
|
172
|
+
);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const source = value.source;
|
|
176
|
+
const target = source.startsWith("Self.") ? source.slice("Self.".length) : source;
|
|
177
|
+
if (siblings.includes(target)) continue;
|
|
178
|
+
// A dynamic sibling has already been reported, and might be the one
|
|
179
|
+
// meant — a second diagnostic about the same line would be noise.
|
|
180
|
+
if (anyDynamic) continue;
|
|
181
|
+
const suggestion = nearestName(target, siblings);
|
|
182
|
+
report(
|
|
183
|
+
"TEMPLATE_DISPATCH_UNKNOWN",
|
|
184
|
+
slot,
|
|
185
|
+
`'${slot}: !ref ${source}' names no entry in 'resources:'. ` +
|
|
186
|
+
`Available: ${siblings.join(", ") || "(none)"}.` +
|
|
187
|
+
(suggestion ? ` Did you mean '${suggestion}'?` : ""),
|
|
188
|
+
suggestion,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- reference slots inside each entry ---
|
|
193
|
+
for (const body of templateBodies(m, registry, aliases, scopes)) {
|
|
194
|
+
const kind = body.manifest.kind;
|
|
195
|
+
const fieldMap = registry.expandedFieldMapForResource(
|
|
196
|
+
{ ...body.manifest, metadata: { ...(body.manifest.metadata ?? {}), module: meta?.module } },
|
|
197
|
+
aliases,
|
|
198
|
+
aliasesByModule,
|
|
199
|
+
);
|
|
200
|
+
if (!fieldMap) continue;
|
|
201
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
202
|
+
if (!isRefEntry(entry)) continue;
|
|
203
|
+
for (const site of resolveFieldEntries(body.manifest, fieldPath)) {
|
|
204
|
+
const value = site.value;
|
|
205
|
+
if (value == null) continue;
|
|
206
|
+
const path = `${body.prefix}.${site.path}`;
|
|
207
|
+
if (isTaggedSentinel(value)) {
|
|
208
|
+
const target = refSentinelTarget(value);
|
|
209
|
+
if (!target) continue;
|
|
210
|
+
const { alias, name: refName } = target;
|
|
211
|
+
if (alias && alias !== "Self") continue;
|
|
212
|
+
if (reachable.has(refName)) continue;
|
|
213
|
+
const suggestion = nearestName(refName, [...reachable]);
|
|
214
|
+
report(
|
|
215
|
+
"TEMPLATE_REF_UNKNOWN",
|
|
216
|
+
path,
|
|
217
|
+
`'${site.path}: !ref ${value.source}' on the ${kind} entry names no sibling ` +
|
|
218
|
+
`'resources:' entry and no resource of this module. ` +
|
|
219
|
+
`Siblings: ${siblings.filter((s) => s !== body.manifest.metadata?.name).join(", ") || "(none)"}.` +
|
|
220
|
+
(suggestion ? ` Did you mean '${suggestion}'?` : ""),
|
|
221
|
+
suggestion,
|
|
222
|
+
);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const hasValueBranch = (entry.valueBranches?.length ?? 0) > 0;
|
|
226
|
+
if (hasValueBranch && typeof value !== "object") continue;
|
|
227
|
+
if (satisfiesValueBranch(value, entry.valueBranches, registry)) continue;
|
|
228
|
+
if (typeof value === "string") {
|
|
229
|
+
if (value.includes("${{")) continue;
|
|
230
|
+
report(
|
|
231
|
+
"INVALID_REFERENCE_FORM",
|
|
232
|
+
path,
|
|
233
|
+
`string reference at '${site.path}' on the ${kind} entry → '${value}' is not ` +
|
|
234
|
+
`supported; write it as '!ref ${value}'`,
|
|
235
|
+
);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
239
|
+
const obj = value as Record<string, unknown>;
|
|
240
|
+
if (typeof obj.kind === "string" && obj.name !== undefined) {
|
|
241
|
+
report(
|
|
242
|
+
"INVALID_REFERENCE_FORM",
|
|
243
|
+
path,
|
|
244
|
+
`object reference '{ kind, name }' at '${site.path}' on the ${kind} entry is ` +
|
|
245
|
+
`not supported; write it as '!ref ${describeRefName(obj.name)}'`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function describeDynamic(name: unknown): string {
|
|
258
|
+
if (isTaggedSentinel(name)) return `the expression '${name.source}'`;
|
|
259
|
+
if (typeof name === "string") return `the template '${name}'`;
|
|
260
|
+
return `a ${typeof name}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function describeDispatchValue(value: unknown): string {
|
|
264
|
+
if (isTaggedSentinel(value)) return `a '!${value.engine}' expression`;
|
|
265
|
+
if (typeof value === "string") return `the string '${value}'`;
|
|
266
|
+
if (value && typeof value === "object" && "name" in value) return "the '{ kind, name }' object form";
|
|
267
|
+
return `a ${typeof value}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function describeRefName(name: unknown): string {
|
|
271
|
+
if (typeof name === "string") return name;
|
|
272
|
+
if (isTaggedSentinel(name)) return "<entry>";
|
|
273
|
+
return String(name);
|
|
274
|
+
}
|