@telorun/analyzer 0.71.0 → 0.72.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 +42 -2
- package/dist/catch-scope.d.ts +72 -0
- package/dist/catch-scope.d.ts.map +1 -0
- package/dist/catch-scope.js +102 -0
- package/dist/deprecation.d.ts +21 -0
- package/dist/deprecation.d.ts.map +1 -0
- package/dist/deprecation.js +26 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/manifest-visitor.d.ts +17 -1
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +5 -1
- package/dist/migrations/report.d.ts +1 -1
- package/dist/migrations/report.d.ts.map +1 -1
- package/dist/migrations/report.js +5 -0
- package/dist/ref-slot.d.ts +15 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +7 -0
- package/dist/resolve-throws-union.d.ts +29 -1
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +111 -16
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +13 -1
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +48 -4
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +3 -1
- package/dist/schema-walk.d.ts +27 -0
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +44 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/validate-identifier-names.d.ts +2 -2
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +22 -7
- package/dist/validate-ref-slots.d.ts +1 -1
- package/dist/validate-ref-slots.d.ts.map +1 -1
- package/dist/validate-ref-slots.js +34 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +27 -3
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +236 -85
- package/package.json +2 -2
- package/src/analyzer.ts +54 -1
- package/src/catch-scope.ts +157 -0
- package/src/deprecation.ts +36 -0
- package/src/index.ts +8 -1
- package/src/manifest-visitor.ts +19 -2
- package/src/migrations/report.ts +5 -1
- package/src/ref-slot.ts +19 -0
- package/src/resolve-throws-union.ts +139 -21
- package/src/schema-compat.ts +13 -0
- package/src/schema-error-report.ts +50 -6
- package/src/schema-keywords.ts +4 -1
- package/src/schema-walk.ts +56 -0
- package/src/telo-version.ts +1 -1
- package/src/types.ts +18 -0
- package/src/validate-identifier-names.ts +28 -9
- package/src/validate-ref-slots.ts +41 -1
- package/src/validate-references.ts +33 -3
- package/src/validate-throws-coverage.ts +333 -92
|
@@ -53,10 +53,42 @@ const UNION_KEYWORDS = new Set(["anyOf", "oneOf"]);
|
|
|
53
53
|
* further in. These are what make a branch implausible. */
|
|
54
54
|
const SHAPE_KEYWORDS = new Set(["required", "type", "additionalProperties", "enum", "const"]);
|
|
55
55
|
|
|
56
|
+
/** Keywords that exist to DISCRIMINATE, so a mismatch is positive evidence that
|
|
57
|
+
* the value is not this branch — at any depth, not only at the union node.
|
|
58
|
+
*
|
|
59
|
+
* Depth is otherwise the tiebreak, and it inverts exactly here: a branch that
|
|
60
|
+
* agreed on the discriminator and failed one constraint reported at the union
|
|
61
|
+
* node loses to every branch that disagreed about the discriminator one level
|
|
62
|
+
* in. That is how a `capability: Telo.Service` document declaring a forbidden
|
|
63
|
+
* key was reported as `/capability must be equal to constant` — naming neither
|
|
64
|
+
* the key at fault nor a branch the value could ever have been. */
|
|
65
|
+
const DISCRIMINATOR_KEYWORDS = new Set(["const", "enum"]);
|
|
66
|
+
|
|
67
|
+
/** RFC 6901 escapes: `~1` is a literal `/` in the key, `~0` a literal `~`.
|
|
68
|
+
* Decoded wherever a segment is shown or matched, because encoded it is not the
|
|
69
|
+
* key the manifest holds — a content map (`application/json`) anchors nowhere
|
|
70
|
+
* and reads wrong in the sentence. `~0` is expanded LAST, or `~01` would decode
|
|
71
|
+
* to `/` instead of the literal `~1` it encodes. */
|
|
72
|
+
function unescapeSegment(part: string): string {
|
|
73
|
+
return part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The whole pointer, segment by segment, for prose that quotes it verbatim. */
|
|
77
|
+
function unescapePointer(pointer: string): string {
|
|
78
|
+
return pointer
|
|
79
|
+
.split("/")
|
|
80
|
+
.map((segment, i) => (i === 0 ? segment : unescapeSegment(segment)))
|
|
81
|
+
.join("/");
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
/* ------------------------------------------------------------------ prose */
|
|
57
85
|
|
|
58
86
|
export function formatSingleError(err: AjvErrorLike): string {
|
|
59
|
-
|
|
87
|
+
// Unescaped for the same reason the path is: a reader acting on this sentence
|
|
88
|
+
// needs the key the manifest holds (`application/json`), not its RFC 6901
|
|
89
|
+
// encoding. Reporting one form in `path` and the other in `message` made one
|
|
90
|
+
// diagnostic disagree with itself.
|
|
91
|
+
const p = unescapePointer(err.instancePath || "") || "/";
|
|
60
92
|
const params = err.params ?? {};
|
|
61
93
|
switch (err.keyword) {
|
|
62
94
|
case "additionalProperties":
|
|
@@ -67,6 +99,16 @@ export function formatSingleError(err: AjvErrorLike): string {
|
|
|
67
99
|
return `${p} ${err.message ?? "is invalid"} (${(params.allowedValues as unknown[])?.join(" | ")})`;
|
|
68
100
|
case "type":
|
|
69
101
|
return `${p} must be ${params.type}${describeActual(err)}`;
|
|
102
|
+
// A `false` schema at a property is how a branch forbids a key it otherwise
|
|
103
|
+
// declares. AJV's own text ("boolean schema is false") describes the schema
|
|
104
|
+
// rather than the value, and the path is the only part a reader can act on.
|
|
105
|
+
case "false schema":
|
|
106
|
+
return `${p} is not allowed here`;
|
|
107
|
+
// `not:` says a forbidden shape matched, and AJV reports nothing about the
|
|
108
|
+
// inner schema — so the honest message says where, and no more. A branch
|
|
109
|
+
// that wants to name the key writes `properties: { <key>: false }` instead.
|
|
110
|
+
case "not":
|
|
111
|
+
return `${p} matches a shape that is not allowed here`;
|
|
70
112
|
default:
|
|
71
113
|
return `${p} ${err.message ?? "is invalid"}`;
|
|
72
114
|
}
|
|
@@ -130,11 +172,13 @@ function describeAlternatives(errors: AjvErrorLike[], unionInstancePath: string)
|
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
/** Is this branch a plausible reading of the value — does it accept the value's
|
|
133
|
-
* shape at the union node itself, and
|
|
175
|
+
* shape at the union node itself, and agree with every discriminator it pins? */
|
|
134
176
|
function isPlausible(errors: AjvErrorLike[], unionInstancePath: string): boolean {
|
|
135
|
-
return !errors.some(
|
|
136
|
-
|
|
137
|
-
|
|
177
|
+
return !errors.some((e) => {
|
|
178
|
+
const keyword = e.keyword ?? "";
|
|
179
|
+
if (DISCRIMINATOR_KEYWORDS.has(keyword)) return true;
|
|
180
|
+
return (e.instancePath || "") === unionInstancePath && SHAPE_KEYWORDS.has(keyword);
|
|
181
|
+
});
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
/**
|
|
@@ -392,7 +436,7 @@ export function ajvErrorToPath(err: AjvErrorLike): string {
|
|
|
392
436
|
let result = "";
|
|
393
437
|
for (const part of parts) {
|
|
394
438
|
if (/^\d+$/.test(part)) result += `[${part}]`;
|
|
395
|
-
else result += result ? `.${part}` : part;
|
|
439
|
+
else result += result ? `.${unescapeSegment(part)}` : unescapeSegment(part);
|
|
396
440
|
}
|
|
397
441
|
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
398
442
|
const missing = err.params.missingProperty as string;
|
package/src/schema-keywords.ts
CHANGED
|
@@ -386,7 +386,10 @@ export const TELO_SCHEMA_ANNOTATIONS: Record<
|
|
|
386
386
|
},
|
|
387
387
|
"x-telo-catches-for": {
|
|
388
388
|
title: "Catches for",
|
|
389
|
-
description:
|
|
389
|
+
description:
|
|
390
|
+
"Whose failures this catch list renders: a sibling field naming the handler, " +
|
|
391
|
+
"or the EMPTY string for everything this resource drives — a scope-level list, " +
|
|
392
|
+
"which owes coverage of nothing itself and answers for every site it encloses.",
|
|
390
393
|
type: "string",
|
|
391
394
|
},
|
|
392
395
|
};
|
package/src/schema-walk.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* scope rule is consumed by the IDE, which must not pull the pass in behind it.
|
|
10
10
|
*/
|
|
11
11
|
import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
|
|
12
|
+
import { readRefSlot, type RefSlot } from "./ref-slot.js";
|
|
13
|
+
import { readStepSlot, type StepSlot } from "./step-slot.js";
|
|
12
14
|
|
|
13
15
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
14
16
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
@@ -142,3 +144,57 @@ export function walkStepArray(
|
|
|
142
144
|
}
|
|
143
145
|
});
|
|
144
146
|
}
|
|
147
|
+
/** A slot through which a resource drives another. */
|
|
148
|
+
export type DrivenSlot =
|
|
149
|
+
| { kind: "step"; slot: StepSlot; data: unknown[]; path: string }
|
|
150
|
+
| { kind: "ref"; slot: RefSlot; data: unknown; path: string };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Every slot of one resource through which it drives another, schema and data in
|
|
154
|
+
* tandem.
|
|
155
|
+
*
|
|
156
|
+
* One traversal with two consumers — the inherited union here, and the catch
|
|
157
|
+
* scope enclosure in `validate-throws-coverage.ts` — because both ask the same
|
|
158
|
+
* structural question and two copies would eventually disagree about where the
|
|
159
|
+
* walk stops. It terminates on the manifest's own depth, and it stops AT a step
|
|
160
|
+
* slot (that traversal owns everything below it, `try`/`catch` subtraction
|
|
161
|
+
* included) and AT a reference slot (a resolved ref is a leaf, `{kind, name}`,
|
|
162
|
+
* with nothing beneath it to visit).
|
|
163
|
+
*/
|
|
164
|
+
export function forEachDrivenSlot(
|
|
165
|
+
schema: unknown,
|
|
166
|
+
data: unknown,
|
|
167
|
+
visit: (slot: DrivenSlot) => void,
|
|
168
|
+
path = "",
|
|
169
|
+
): void {
|
|
170
|
+
if (!schema || typeof schema !== "object" || data === undefined || data === null) return;
|
|
171
|
+
const node = schema as Record<string, any>;
|
|
172
|
+
|
|
173
|
+
const stepSlot = readStepSlot(node);
|
|
174
|
+
if (stepSlot) {
|
|
175
|
+
if (Array.isArray(data)) visit({ kind: "step", slot: stepSlot, data, path });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const refSlot = readRefSlot(node);
|
|
180
|
+
if (refSlot) {
|
|
181
|
+
visit({ kind: "ref", slot: refSlot, data, path });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const props = node.properties as Record<string, any> | undefined;
|
|
186
|
+
if (props && typeof data === "object" && !Array.isArray(data)) {
|
|
187
|
+
const obj = data as Record<string, unknown>;
|
|
188
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
189
|
+
if (obj[key] === undefined) continue;
|
|
190
|
+
forEachDrivenSlot(propSchema, obj[key], visit, path ? `${path}.${key}` : key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (node.items && Array.isArray(data)) {
|
|
195
|
+
for (const [i, item] of data.entries()) {
|
|
196
|
+
forEachDrivenSlot(node.items, item, visit, `${path}[${i}]`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
package/src/telo-version.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -11,6 +11,19 @@ export const DiagnosticSeverity = {
|
|
|
11
11
|
} as const;
|
|
12
12
|
export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
|
|
13
13
|
|
|
14
|
+
/** Matches LSP DiagnosticTag values exactly.
|
|
15
|
+
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag
|
|
16
|
+
*
|
|
17
|
+
* Declared whole rather than trimmed to what Telo emits today, for the reason
|
|
18
|
+
* the severity ladder above is: it is someone else's closed vocabulary, and a
|
|
19
|
+
* partial copy of one is what drifts. `Unnecessary` (rendered faded) has an
|
|
20
|
+
* obvious future consumer in the unused-declaration checks. */
|
|
21
|
+
export const DiagnosticTag = {
|
|
22
|
+
Unnecessary: 1,
|
|
23
|
+
Deprecated: 2,
|
|
24
|
+
} as const;
|
|
25
|
+
export type DiagnosticTag = (typeof DiagnosticTag)[keyof typeof DiagnosticTag];
|
|
26
|
+
|
|
14
27
|
/** Default entry-point filename when a directory is given instead of a file. */
|
|
15
28
|
export const DEFAULT_MANIFEST_FILENAME = "telo.yaml";
|
|
16
29
|
|
|
@@ -70,6 +83,11 @@ export interface AnalysisDiagnostic {
|
|
|
70
83
|
/** e.g. "telo-analyzer" */
|
|
71
84
|
source?: string;
|
|
72
85
|
message: string;
|
|
86
|
+
/** What KIND of thing this is, orthogonal to how loudly it asks to be dealt
|
|
87
|
+
* with. A deprecation is warning-grade *and* a deprecation; severity alone
|
|
88
|
+
* can only say the first, which is why an editor renders a deprecated symbol
|
|
89
|
+
* struck through rather than merely yellow. */
|
|
90
|
+
tags?: DiagnosticTag[];
|
|
73
91
|
/** Telo-specific extras such as { resource: { kind, name }, path } */
|
|
74
92
|
data?: unknown;
|
|
75
93
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { ResourceManifest } from "@telorun/sdk";
|
|
1
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
moduleScopedDefResolver,
|
|
5
|
+
type AliasResolver,
|
|
6
|
+
type ModuleScopes,
|
|
7
|
+
} from "./alias-resolver.js";
|
|
4
8
|
import type { CallGraph } from "./call-graph.js";
|
|
5
9
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
10
|
+
import { inheritedCapability, type DefResolver } from "./extends-resolution.js";
|
|
6
11
|
import {
|
|
7
12
|
TYPE_LEVEL_DOC_KINDS,
|
|
8
13
|
checkName,
|
|
@@ -44,8 +49,10 @@ export function validateIdentifierNames(
|
|
|
44
49
|
aliases: AliasResolver,
|
|
45
50
|
rootModules: Set<string>,
|
|
46
51
|
graph: CallGraph,
|
|
52
|
+
scopes: ModuleScopes,
|
|
47
53
|
): AnalysisDiagnostic[] {
|
|
48
54
|
const out: AnalysisDiagnostic[] = [];
|
|
55
|
+
const resolveDef = moduleScopedDefResolver<ResourceDefinition>(registry, aliases, scopes);
|
|
49
56
|
|
|
50
57
|
for (const manifest of manifests) {
|
|
51
58
|
const metadata = manifest.metadata as Record<string, unknown> | undefined;
|
|
@@ -65,7 +72,7 @@ export function validateIdentifierNames(
|
|
|
65
72
|
const ownModule = metadata?.module as string | undefined;
|
|
66
73
|
if (ownModule && !rootModules.has(ownModule)) continue;
|
|
67
74
|
|
|
68
|
-
const level = levelFor(manifest,
|
|
75
|
+
const level = levelFor(manifest, resolveDef, ownModule);
|
|
69
76
|
push(out, checkName(name, level, surfaceFor(manifest.kind)), {
|
|
70
77
|
kind: manifest.kind,
|
|
71
78
|
name,
|
|
@@ -126,20 +133,32 @@ export function validateIdentifierNames(
|
|
|
126
133
|
* a resource. Capability-driven rather than by kind name, so no resource kind
|
|
127
134
|
* is hardcoded here.
|
|
128
135
|
*
|
|
136
|
+
* **The capability is the INHERITED one**, not the leaf declaration. Capability
|
|
137
|
+
* is inherited and immutable along `extends` — a kind omits it to take its
|
|
138
|
+
* ancestor's — so reading the leaf answers `undefined` for every kind that does,
|
|
139
|
+
* and the fallback then calls a type a value. That is not hypothetical: it is
|
|
140
|
+
* exactly `Type.JsonSchema`, a pure alias of the built-in (`extends:
|
|
141
|
+
* Telo.JsonSchema`, no `capability:` of its own), so a shape declared through
|
|
142
|
+
* the deprecated spelling was reported as miscased while the identical
|
|
143
|
+
* declaration written as `kind: Telo.JsonSchema` was not. Renaming on that
|
|
144
|
+
* report is worse than the warning: `extends:` between two named shapes is not a
|
|
145
|
+
* reference slot, so the inheritance edge does not move with the name.
|
|
146
|
+
*
|
|
129
147
|
* An unresolvable kind falls back to value level — the honest default, since
|
|
130
148
|
* `UNDEFINED_KIND` already reports the real problem and guessing type level
|
|
131
149
|
* would stack a case error on top of it.
|
|
132
150
|
*/
|
|
133
151
|
function levelFor(
|
|
134
152
|
manifest: ResourceManifest,
|
|
135
|
-
|
|
136
|
-
|
|
153
|
+
resolveDef: DefResolver & { in(kind: string, module?: string): ResourceDefinition | undefined },
|
|
154
|
+
ownModule: string | undefined,
|
|
137
155
|
): NameLevel {
|
|
138
156
|
if (TYPE_LEVEL_DOC_KINDS.has(manifest.kind as string)) return "type";
|
|
139
|
-
// The
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
// The top-level lookup is in the module that WROTE the `kind:` — a root one,
|
|
158
|
+
// the others having been skipped above — while the chain walk re-scopes at
|
|
159
|
+
// every hop, since an `extends` alias belongs to the file declaring it.
|
|
160
|
+
const def = resolveDef.in(manifest.kind as string, ownModule);
|
|
161
|
+
return inheritedCapability(def, resolveDef) === "Telo.Type" ? "type" : "value";
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
/** The noun phrase a diagnostic uses as its subject. */
|
|
@@ -38,7 +38,9 @@ export interface RefSlotIssue {
|
|
|
38
38
|
| "X_TELO_REF_MISSING_USE"
|
|
39
39
|
| "X_TELO_REF_MISSING_KIND"
|
|
40
40
|
| "X_TELO_REF_USE_CONFLICT"
|
|
41
|
-
| "X_TELO_REF_DYNAMIC_SELECTOR"
|
|
41
|
+
| "X_TELO_REF_DYNAMIC_SELECTOR"
|
|
42
|
+
| "X_TELO_REF_UNKNOWN_KEY"
|
|
43
|
+
| "X_TELO_REF_INVALID_THROWS_THROUGH";
|
|
42
44
|
/** The definition (schema issues) or resource (selector issues) at fault. */
|
|
43
45
|
manifest: ResourceManifest;
|
|
44
46
|
/** Schema path of the slot (schema issues) or concrete value path of the
|
|
@@ -140,9 +142,47 @@ function checkAnnotation(
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
// `throwsThrough` is read as `=== true`, so anything else is silently absent —
|
|
146
|
+
// and absent means the declaring resource's catch scope stops enclosing what
|
|
147
|
+
// it holds, so every route under it starts reporting UNCOVERED_THROW_CODE with
|
|
148
|
+
// nothing naming the cause. The same failure `X_TELO_REF_INVALID_USE` exists
|
|
149
|
+
// to prevent, one key over.
|
|
150
|
+
if (obj.throwsThrough !== undefined && typeof obj.throwsThrough !== "boolean") {
|
|
151
|
+
issues.push({
|
|
152
|
+
code: "X_TELO_REF_INVALID_THROWS_THROUGH",
|
|
153
|
+
manifest,
|
|
154
|
+
path,
|
|
155
|
+
message:
|
|
156
|
+
`x-telo-ref at '${path}' declares 'throwsThrough: ${JSON.stringify(obj.throwsThrough)}', ` +
|
|
157
|
+
`which is not a boolean. Only 'true' declares that throws from this slot's target ` +
|
|
158
|
+
`surface through the declaring resource; anything else reads as absent, which ` +
|
|
159
|
+
`silently stops its catch list from enclosing what it holds.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Closed, for the reason the token sets are: a misspelled key is indexed by
|
|
164
|
+
// nothing and read by nothing, so it validates, ships, and does exactly what
|
|
165
|
+
// omitting it would.
|
|
166
|
+
for (const key of Object.keys(obj)) {
|
|
167
|
+
if (REF_ANNOTATION_KEYS.has(key)) continue;
|
|
168
|
+
issues.push({
|
|
169
|
+
code: "X_TELO_REF_UNKNOWN_KEY",
|
|
170
|
+
manifest,
|
|
171
|
+
path,
|
|
172
|
+
message:
|
|
173
|
+
`x-telo-ref at '${path}' declares unrecognized key '${key}'. Known keys: ` +
|
|
174
|
+
`${[...REF_ANNOTATION_KEYS].sort().join(", ")}. An unrecognized key is read by nothing, ` +
|
|
175
|
+
`so it has exactly the effect of leaving it out.`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
143
179
|
return declaredUses(use);
|
|
144
180
|
}
|
|
145
181
|
|
|
182
|
+
/** Every key the structured `x-telo-ref` form accepts — the write side of
|
|
183
|
+
* `readRefSlot`'s read side. Adding one belongs in both. */
|
|
184
|
+
const REF_ANNOTATION_KEYS = new Set(["kind", "use", "inputs", "throwsThrough"]);
|
|
185
|
+
|
|
146
186
|
/** True when a node is a reference slot: it carries `x-telo-ref` directly or on
|
|
147
187
|
* an `anyOf`/`oneOf` branch. */
|
|
148
188
|
function carriesRefAnnotation(obj: Record<string, unknown>): boolean {
|
|
@@ -562,14 +562,44 @@ export function validateReferences(
|
|
|
562
562
|
|
|
563
563
|
for (const { value: fieldValue, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
564
564
|
if (fieldValue == null) continue;
|
|
565
|
-
|
|
565
|
+
// CEL leaves become schema-shaped placeholders first, exactly as the
|
|
566
|
+
// sibling-ref branch below does and for the same reason: a slot
|
|
567
|
+
// anchored at a shared value-shape is overwhelmingly written as
|
|
568
|
+
// expressions, so validating it raw reports every one of them as a
|
|
569
|
+
// type error and the check fires only on the literal case nobody
|
|
570
|
+
// writes. Omitting it here made one annotation mean two different
|
|
571
|
+
// things depending on which branch resolved it — a `when:` typed
|
|
572
|
+
// `boolean` accepted a `!cel` at a route's inline slot and rejected
|
|
573
|
+
// the identical expression at a slot anchored on the carrier that
|
|
574
|
+
// declares that very shape.
|
|
575
|
+
const substituted = substituteCelFields(
|
|
576
|
+
fieldValue,
|
|
577
|
+
subSchema as Record<string, any>,
|
|
578
|
+
);
|
|
579
|
+
// Anchored at the offending node INSIDE the value, not at the slot:
|
|
580
|
+
// a `returns:` list is an array of entries, and reporting every one
|
|
581
|
+
// of its issues on the `returns:` line puts three diagnostics on one
|
|
582
|
+
// line and none on the entry that is wrong.
|
|
583
|
+
const issues = registry.validateResourceConfig(
|
|
584
|
+
substituted,
|
|
585
|
+
subSchema as Record<string, any>,
|
|
586
|
+
);
|
|
566
587
|
for (const issue of issues) {
|
|
567
588
|
diagnostics.push({
|
|
568
589
|
severity: DiagnosticSeverity.Error,
|
|
569
590
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
570
591
|
source: SOURCE,
|
|
571
|
-
message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
|
|
572
|
-
data: {
|
|
592
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue.message}`,
|
|
593
|
+
data: {
|
|
594
|
+
resource: resourceData,
|
|
595
|
+
filePath,
|
|
596
|
+
// An index-first sub-path (`[0].content`) joins with no dot.
|
|
597
|
+
path: !issue.path
|
|
598
|
+
? concretePath
|
|
599
|
+
: issue.path.startsWith("[")
|
|
600
|
+
? `${concretePath}${issue.path}`
|
|
601
|
+
: `${concretePath}.${issue.path}`,
|
|
602
|
+
},
|
|
573
603
|
});
|
|
574
604
|
}
|
|
575
605
|
}
|