@telorun/analyzer 0.40.0 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cel-ast.d.ts +98 -0
- package/dist/cel-ast.d.ts.map +1 -0
- package/dist/cel-ast.js +188 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/loaded-types.d.ts +7 -1
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +4 -1
- package/dist/position-metadata.d.ts +7 -6
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +11 -18
- package/dist/yaml-ast.d.ts +46 -0
- package/dist/yaml-ast.d.ts.map +1 -0
- package/dist/yaml-ast.js +58 -0
- package/package.json +2 -2
- package/src/cel-ast.ts +259 -0
- package/src/index.ts +5 -0
- package/src/loaded-types.ts +7 -1
- package/src/parse-loaded-file.ts +4 -1
- package/src/position-metadata.ts +17 -21
- package/src/yaml-ast.ts +108 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type ASTNode as CelJsNode } from "@marcbachmann/cel-js";
|
|
2
|
+
/** Read-only CEL expression tree owned by the analyzer. The third-party
|
|
3
|
+
* `@marcbachmann/cel-js` `ASTNode` stays an internal detail — `wrapCelAst`
|
|
4
|
+
* translates it into this union so no external AST type leaks through the
|
|
5
|
+
* public surface (full symmetry with the YAML `AstNode` decision). Every
|
|
6
|
+
* `range` is `[start, end]` in DOCUMENT offsets. */
|
|
7
|
+
export type CelNode = {
|
|
8
|
+
kind: "literal";
|
|
9
|
+
range: [number, number];
|
|
10
|
+
value: unknown;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "ident";
|
|
13
|
+
range: [number, number];
|
|
14
|
+
name: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "member";
|
|
17
|
+
range: [number, number];
|
|
18
|
+
target: CelNode;
|
|
19
|
+
property: string;
|
|
20
|
+
/** Span of just the `.prop` identifier, for a future rename. */
|
|
21
|
+
propertyRange: [number, number];
|
|
22
|
+
/** `.?` optional member access. */
|
|
23
|
+
optional: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "index";
|
|
26
|
+
range: [number, number];
|
|
27
|
+
target: CelNode;
|
|
28
|
+
index: CelNode;
|
|
29
|
+
/** `[?]` optional index. */
|
|
30
|
+
optional: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "call";
|
|
33
|
+
range: [number, number];
|
|
34
|
+
name: string;
|
|
35
|
+
args: CelNode[];
|
|
36
|
+
} | {
|
|
37
|
+
kind: "methodCall";
|
|
38
|
+
range: [number, number];
|
|
39
|
+
name: string;
|
|
40
|
+
receiver: CelNode;
|
|
41
|
+
args: CelNode[];
|
|
42
|
+
} | {
|
|
43
|
+
kind: "list";
|
|
44
|
+
range: [number, number];
|
|
45
|
+
items: CelNode[];
|
|
46
|
+
} | {
|
|
47
|
+
kind: "map";
|
|
48
|
+
range: [number, number];
|
|
49
|
+
entries: {
|
|
50
|
+
key: CelNode;
|
|
51
|
+
value: CelNode;
|
|
52
|
+
}[];
|
|
53
|
+
} | {
|
|
54
|
+
kind: "ternary";
|
|
55
|
+
range: [number, number];
|
|
56
|
+
cond: CelNode;
|
|
57
|
+
then: CelNode;
|
|
58
|
+
else: CelNode;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "unary";
|
|
61
|
+
range: [number, number];
|
|
62
|
+
op: string;
|
|
63
|
+
operand: CelNode;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "binary";
|
|
66
|
+
range: [number, number];
|
|
67
|
+
op: string;
|
|
68
|
+
left: CelNode;
|
|
69
|
+
right: CelNode;
|
|
70
|
+
};
|
|
71
|
+
/** A `${{ }}` / `!cel` region inside a YAML scalar. Ranges are DOCUMENT
|
|
72
|
+
* offsets; `source` is the CEL body (a longest-valid prefix when `open`).
|
|
73
|
+
* `ast()` parses lazily — nothing parses CEL during `parseToAst`, only the
|
|
74
|
+
* expression a caller actually inspects. */
|
|
75
|
+
export interface CelSegment {
|
|
76
|
+
/** Segment span in document offsets (includes the `${{ }}` for interpolation). */
|
|
77
|
+
range: [number, number];
|
|
78
|
+
/** The CEL body (a prefix when `open`). */
|
|
79
|
+
source: string;
|
|
80
|
+
/** True when a `${{` has no matching `}}` yet (the user is mid-typing). */
|
|
81
|
+
open: boolean;
|
|
82
|
+
/** Lazily parse + wrap; ranges are already absolute. */
|
|
83
|
+
ast(): CelNode;
|
|
84
|
+
}
|
|
85
|
+
/** Maps a `@marcbachmann/cel-js` node into the analyzer `CelNode`, translating
|
|
86
|
+
* each node's segment-relative `start`/`end` to absolute document offsets by
|
|
87
|
+
* adding `segmentStart`. */
|
|
88
|
+
export declare function wrapCelAst(node: CelJsNode, segmentStart: number): CelNode;
|
|
89
|
+
/** Build the CEL segments of a scalar from its raw source slice. `scalarText`
|
|
90
|
+
* is `text.slice(start, valueEnd)` and `scalarStart` its document offset.
|
|
91
|
+
*
|
|
92
|
+
* - `tag === "!cel"` → one closed segment spanning the tagged body.
|
|
93
|
+
* - otherwise → one closed segment per `${{ … }}` match, plus a trailing
|
|
94
|
+
* `open` segment for a dangling `${{` with no `}}` (bounded to its line, so
|
|
95
|
+
* an unterminated quote that swallowed following lines still recovers the
|
|
96
|
+
* region the user is typing in). */
|
|
97
|
+
export declare function buildCelSegments(scalarText: string, scalarStart: number, tag: string | undefined, taggedSource: string | undefined): CelSegment[];
|
|
98
|
+
//# sourceMappingURL=cel-ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cel-ast.d.ts","sourceRoot":"","sources":["../src/cel-ast.ts"],"names":[],"mappings":"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"}
|
package/dist/cel-ast.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { parse } from "@marcbachmann/cel-js";
|
|
2
|
+
const BINARY_OPS = new Set([
|
|
3
|
+
"!=",
|
|
4
|
+
"==",
|
|
5
|
+
"in",
|
|
6
|
+
"+",
|
|
7
|
+
"-",
|
|
8
|
+
"*",
|
|
9
|
+
"/",
|
|
10
|
+
"%",
|
|
11
|
+
"<",
|
|
12
|
+
"<=",
|
|
13
|
+
">",
|
|
14
|
+
">=",
|
|
15
|
+
"||",
|
|
16
|
+
"&&",
|
|
17
|
+
]);
|
|
18
|
+
/** Maps a `@marcbachmann/cel-js` node into the analyzer `CelNode`, translating
|
|
19
|
+
* each node's segment-relative `start`/`end` to absolute document offsets by
|
|
20
|
+
* adding `segmentStart`. */
|
|
21
|
+
export function wrapCelAst(node, segmentStart) {
|
|
22
|
+
const range = abs(node, segmentStart);
|
|
23
|
+
const op = node.op;
|
|
24
|
+
const args = node.args;
|
|
25
|
+
if (op === "value")
|
|
26
|
+
return { kind: "literal", range, value: args };
|
|
27
|
+
if (op === "id")
|
|
28
|
+
return { kind: "ident", range, name: String(args) };
|
|
29
|
+
if (op === "." || op === ".?") {
|
|
30
|
+
const [target, property] = args;
|
|
31
|
+
return {
|
|
32
|
+
kind: "member",
|
|
33
|
+
range,
|
|
34
|
+
target: wrapCelAst(target, segmentStart),
|
|
35
|
+
property,
|
|
36
|
+
propertyRange: [range[1] - property.length, range[1]],
|
|
37
|
+
optional: op === ".?",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (op === "[]" || op === "[?]") {
|
|
41
|
+
const [target, index] = args;
|
|
42
|
+
return {
|
|
43
|
+
kind: "index",
|
|
44
|
+
range,
|
|
45
|
+
target: wrapCelAst(target, segmentStart),
|
|
46
|
+
index: wrapCelAst(index, segmentStart),
|
|
47
|
+
optional: op === "[?]",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (op === "call") {
|
|
51
|
+
const [name, callArgs] = args;
|
|
52
|
+
return { kind: "call", range, name, args: callArgs.map((a) => wrapCelAst(a, segmentStart)) };
|
|
53
|
+
}
|
|
54
|
+
if (op === "rcall") {
|
|
55
|
+
const [name, receiver, callArgs] = args;
|
|
56
|
+
return {
|
|
57
|
+
kind: "methodCall",
|
|
58
|
+
range,
|
|
59
|
+
name,
|
|
60
|
+
receiver: wrapCelAst(receiver, segmentStart),
|
|
61
|
+
args: callArgs.map((a) => wrapCelAst(a, segmentStart)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (op === "list") {
|
|
65
|
+
return { kind: "list", range, items: args.map((a) => wrapCelAst(a, segmentStart)) };
|
|
66
|
+
}
|
|
67
|
+
if (op === "map") {
|
|
68
|
+
return {
|
|
69
|
+
kind: "map",
|
|
70
|
+
range,
|
|
71
|
+
entries: args.map(([k, v]) => ({
|
|
72
|
+
key: wrapCelAst(k, segmentStart),
|
|
73
|
+
value: wrapCelAst(v, segmentStart),
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (op === "?:") {
|
|
78
|
+
const [cond, then, els] = args;
|
|
79
|
+
return {
|
|
80
|
+
kind: "ternary",
|
|
81
|
+
range,
|
|
82
|
+
cond: wrapCelAst(cond, segmentStart),
|
|
83
|
+
then: wrapCelAst(then, segmentStart),
|
|
84
|
+
else: wrapCelAst(els, segmentStart),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (op === "!_" || op === "-_") {
|
|
88
|
+
return { kind: "unary", range, op, operand: wrapCelAst(args, segmentStart) };
|
|
89
|
+
}
|
|
90
|
+
if (BINARY_OPS.has(op)) {
|
|
91
|
+
const [left, right] = args;
|
|
92
|
+
return {
|
|
93
|
+
kind: "binary",
|
|
94
|
+
range,
|
|
95
|
+
op,
|
|
96
|
+
left: wrapCelAst(left, segmentStart),
|
|
97
|
+
right: wrapCelAst(right, segmentStart),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Unknown operator — surface it as a literal so consumers can still hit-test
|
|
101
|
+
// the range rather than crash on an unmapped node.
|
|
102
|
+
return { kind: "literal", range, value: undefined };
|
|
103
|
+
}
|
|
104
|
+
function abs(node, segmentStart) {
|
|
105
|
+
const r = node.range ?? { start: node.start, end: node.end };
|
|
106
|
+
return [r.start + segmentStart, r.end + segmentStart];
|
|
107
|
+
}
|
|
108
|
+
/** Parse `source` and wrap it, tolerating a trailing partial member/index
|
|
109
|
+
* access (`req.`, `req.fo`) by falling back to the longest parseable prefix.
|
|
110
|
+
* Used for `open` segments where completion fires mid-token. */
|
|
111
|
+
function parseLenient(source, segmentStart, range) {
|
|
112
|
+
const candidates = [source, source.replace(/[.?[]+\w*$/, ""), source.replace(/[.?[(]+.*$/, "")];
|
|
113
|
+
for (const candidate of candidates) {
|
|
114
|
+
const trimmed = candidate.trim();
|
|
115
|
+
if (!trimmed)
|
|
116
|
+
break;
|
|
117
|
+
try {
|
|
118
|
+
return wrapCelAst(parse(trimmed).ast, segmentStart);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// try the next-shorter prefix
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { kind: "ident", range, name: source.trim() };
|
|
125
|
+
}
|
|
126
|
+
const OPEN_MARKER = "${{";
|
|
127
|
+
/** Build the CEL segments of a scalar from its raw source slice. `scalarText`
|
|
128
|
+
* is `text.slice(start, valueEnd)` and `scalarStart` its document offset.
|
|
129
|
+
*
|
|
130
|
+
* - `tag === "!cel"` → one closed segment spanning the tagged body.
|
|
131
|
+
* - otherwise → one closed segment per `${{ … }}` match, plus a trailing
|
|
132
|
+
* `open` segment for a dangling `${{` with no `}}` (bounded to its line, so
|
|
133
|
+
* an unterminated quote that swallowed following lines still recovers the
|
|
134
|
+
* region the user is typing in). */
|
|
135
|
+
export function buildCelSegments(scalarText, scalarStart, tag, taggedSource) {
|
|
136
|
+
if (tag === "!cel" && taggedSource != null) {
|
|
137
|
+
const idx = scalarText.indexOf(taggedSource);
|
|
138
|
+
const bodyStart = scalarStart + (idx >= 0 ? idx : 0);
|
|
139
|
+
const range = [bodyStart, bodyStart + taggedSource.length];
|
|
140
|
+
return [
|
|
141
|
+
{
|
|
142
|
+
range,
|
|
143
|
+
source: taggedSource,
|
|
144
|
+
open: false,
|
|
145
|
+
ast: () => wrapCelAst(parse(taggedSource).ast, bodyStart),
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
const segments = [];
|
|
150
|
+
const re = /\$\{\{([\s\S]*?)\}\}/g;
|
|
151
|
+
let match;
|
|
152
|
+
let lastClosedEnd = 0;
|
|
153
|
+
while ((match = re.exec(scalarText)) !== null) {
|
|
154
|
+
const whole = match[0];
|
|
155
|
+
const inner = match[1];
|
|
156
|
+
const leadingWs = inner.match(/^\s*/)?.[0].length ?? 0;
|
|
157
|
+
const bodyStart = scalarStart + match.index + OPEN_MARKER.length + leadingWs;
|
|
158
|
+
const source = inner.trim();
|
|
159
|
+
segments.push({
|
|
160
|
+
range: [scalarStart + match.index, scalarStart + match.index + whole.length],
|
|
161
|
+
source,
|
|
162
|
+
open: false,
|
|
163
|
+
ast: () => wrapCelAst(parse(source).ast, bodyStart),
|
|
164
|
+
});
|
|
165
|
+
lastClosedEnd = match.index + whole.length;
|
|
166
|
+
}
|
|
167
|
+
const openIdx = scalarText.indexOf(OPEN_MARKER, lastClosedEnd);
|
|
168
|
+
if (openIdx >= 0 && scalarText.indexOf("}}", openIdx) < 0) {
|
|
169
|
+
let lineEnd = scalarText.indexOf("\n", openIdx);
|
|
170
|
+
if (lineEnd < 0)
|
|
171
|
+
lineEnd = scalarText.length;
|
|
172
|
+
const after = openIdx + OPEN_MARKER.length;
|
|
173
|
+
// Drop a trailing scalar-closing quote so `foo: "${{ req"` recovers `req`,
|
|
174
|
+
// not `req"` — the quote closes the YAML string, it isn't part of the CEL.
|
|
175
|
+
const rawBody = scalarText.slice(after, lineEnd).replace(/["']\s*$/, "");
|
|
176
|
+
const leadingWs = rawBody.match(/^\s*/)?.[0].length ?? 0;
|
|
177
|
+
const bodyStart = scalarStart + after + leadingWs;
|
|
178
|
+
const source = rawBody.trim();
|
|
179
|
+
const range = [scalarStart + openIdx, scalarStart + lineEnd];
|
|
180
|
+
segments.push({
|
|
181
|
+
range,
|
|
182
|
+
source,
|
|
183
|
+
open: true,
|
|
184
|
+
ast: () => parseLenient(source, bodyStart, range),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return segments;
|
|
188
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type { SyntheticImport } from "./inline-imports.js";
|
|
|
23
23
|
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
24
24
|
export type { VersionReconciliation } from "./reconcile-module-versions.js";
|
|
25
25
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
26
|
-
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
26
|
+
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, offsetToPosition, } from "./position-metadata.js";
|
|
27
27
|
export type { DocumentPosition } from "./position-metadata.js";
|
|
28
28
|
export { HttpSource } from "./sources/http-source.js";
|
|
29
29
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
@@ -37,6 +37,10 @@ export { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
|
37
37
|
export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifestCacheKey, manifestCacheUrl, ociManifestCacheCoords, urlManifestCacheCoords, } from "./sources/manifest-cache.js";
|
|
38
38
|
export type { ManifestCacheCoords } from "./sources/manifest-cache.js";
|
|
39
39
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
40
|
+
export { documentToAst, parseToAst } from "./yaml-ast.js";
|
|
41
|
+
export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
|
|
42
|
+
export { buildCelSegments, wrapCelAst } from "./cel-ast.js";
|
|
43
|
+
export type { CelNode, CelSegment } from "./cel-ast.js";
|
|
40
44
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
41
45
|
export type { AnalysisDiagnostic, AnalysisOptions, LoaderInitOptions, LoadOptions, ManifestSource, Position, PositionIndex, Range } from "./types.js";
|
|
42
46
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,oCAAoC,CAAC;AACjF,YAAY,EACR,cAAc,EACd,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,UAAU,GACb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACH,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,gCAAgC,EAChC,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GACpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EACL,aAAa,EACb,yBAAyB,EACzB,qBAAqB,EACrB,0BAA0B,EAC1B,mBAAmB,EACnB,qBAAqB,EACrB,aAAa,GACd,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC5F,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,YAAY,EACR,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,GACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,YAAY,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EACH,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
|
13
13
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
14
14
|
export { reconcileModuleVersions } from "./reconcile-module-versions.js";
|
|
15
15
|
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
16
|
-
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, } from "./position-metadata.js";
|
|
16
|
+
export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, offsetToPosition, } from "./position-metadata.js";
|
|
17
17
|
export { HttpSource } from "./sources/http-source.js";
|
|
18
18
|
export { RegistrySource } from "./sources/registry-source.js";
|
|
19
19
|
export { defaultSources } from "./sources/default-sources.js";
|
|
@@ -23,4 +23,6 @@ export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
|
|
|
23
23
|
export { isLocalPathSource } from "./sources/local-path-ref.js";
|
|
24
24
|
export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifestCacheKey, manifestCacheUrl, ociManifestCacheCoords, urlManifestCacheCoords, } from "./sources/manifest-cache.js";
|
|
25
25
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
26
|
+
export { documentToAst, parseToAst } from "./yaml-ast.js";
|
|
27
|
+
export { buildCelSegments, wrapCelAst } from "./cel-ast.js";
|
|
26
28
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
package/dist/loaded-types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { Document } from "yaml";
|
|
3
3
|
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
4
|
import type { AnalysisDiagnostic, Range } from "./types.js";
|
|
5
|
+
import type { AstDocument } from "./yaml-ast.js";
|
|
5
6
|
/** One physical file's parsed result. Returned for the owner manifest, for
|
|
6
7
|
* each `include:` partial, and for each external import target.
|
|
7
8
|
*
|
|
@@ -16,8 +17,13 @@ export interface LoadedFile {
|
|
|
16
17
|
requestedUrl: string;
|
|
17
18
|
/** Raw text exactly as `read()` returned it. */
|
|
18
19
|
text: string;
|
|
19
|
-
/** Per-document parsed AST, in source order.
|
|
20
|
+
/** Per-document parsed `yaml` AST, in source order. The editor's mutable
|
|
21
|
+
* round-trip model reads this handle; structure-only consumers use the
|
|
22
|
+
* read-only `astDocuments` instead so `yaml` stays an internal detail. */
|
|
20
23
|
documents: Document[];
|
|
24
|
+
/** Per-document read-only `AstNode` view (`yaml`-free), aligned to
|
|
25
|
+
* `documents`. The shared structural source of truth for IDE features. */
|
|
26
|
+
astDocuments: AstDocument[];
|
|
21
27
|
/** Per-document JSON projection (`doc.toJSON()`). Aligned to `documents`. */
|
|
22
28
|
manifests: Array<ResourceManifest | null>;
|
|
23
29
|
/** Per-document `{sourceLine, positionIndex}`. Aligned to `documents`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"loaded-types.d.ts","sourceRoot":"","sources":["../src/loaded-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;4EAI4E;AAC5E,MAAM,WAAW,UAAU;IACzB;+DAC2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf;gEAC4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb;;+EAE2E;IAC3E,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB;+EAC2E;IAC3E,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,6EAA6E;IAC7E,SAAS,EAAE,KAAK,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC1C,0EAA0E;IAC1E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED;mCACmC;AACnC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,UAAU,CAAC;IAClB;oEACgE;IAChE,QAAQ,EAAE,UAAU,EAAE,CAAC;CACxB;AAED;;;;;2EAK2E;AAC3E,MAAM,WAAW,UAAU;IACzB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB;6EACyE;IACzE,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,oEAAoE;IACpE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;0BAC0B;AAC1B,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;IACpB;;wBAEoB;IACpB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnC;;;;2BAIuB;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAClD;;;iFAG6E;IAC7E,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;iDAG6C;IAC7C,kBAAkB,EAAE,kBAAkB,EAAE,CAAC;IACzC;;;8EAG0E;IAC1E,gBAAgB,EAAE,kBAAkB,EAAE,CAAC;IACvC;yCACqC;IACrC,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B;8BAC0B;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ;;qEAEiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B;;gFAE4E;IAC5E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,KAAK,CAAC;CACd"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"parse-loaded-file.d.ts","sourceRoot":"","sources":["../src/parse-loaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAKxD,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,mBAAmB,CAAC;AAKhE,MAAM,WAAW,YAAY;IAC3B;4EACwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA4BD,oEAAoE;AACpE,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,YAAY,GACrB,UAAU,CAiDZ"}
|
|
@@ -3,6 +3,7 @@ import { parseAllDocuments } from "yaml";
|
|
|
3
3
|
import { buildCelEnvironment } from "./cel-environment.js";
|
|
4
4
|
import { buildDocumentPositions } from "./position-metadata.js";
|
|
5
5
|
import { precompileDoc } from "./precompile.js";
|
|
6
|
+
import { documentToAst } from "./yaml-ast.js";
|
|
6
7
|
/** Append an actionable hint to raw yaml-parser messages that are otherwise
|
|
7
8
|
* cryptic. The parser reports `BLOCK_AS_IMPLICIT_KEY` ("Nested mappings are
|
|
8
9
|
* not allowed in compact mappings") when a plain (unquoted) scalar contains
|
|
@@ -29,7 +30,8 @@ function rangeFromLinePos(linePos) {
|
|
|
29
30
|
/** Pure: text in, structured load result out. No I/O, no caches. */
|
|
30
31
|
export function parseLoadedFile(source, requestedUrl, text, options) {
|
|
31
32
|
const documents = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
32
|
-
const
|
|
33
|
+
const astDocuments = documents.map((doc) => documentToAst(doc, text));
|
|
34
|
+
const positions = buildDocumentPositions(text, astDocuments);
|
|
33
35
|
const parseErrors = [];
|
|
34
36
|
documents.forEach((doc, documentIndex) => {
|
|
35
37
|
for (const err of doc.errors) {
|
|
@@ -67,6 +69,7 @@ export function parseLoadedFile(source, requestedUrl, text, options) {
|
|
|
67
69
|
requestedUrl,
|
|
68
70
|
text,
|
|
69
71
|
documents,
|
|
72
|
+
astDocuments,
|
|
70
73
|
manifests,
|
|
71
74
|
positions,
|
|
72
75
|
parseErrors,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
1
|
+
import type { Position, PositionIndex } from "./types.js";
|
|
2
|
+
import type { AstDocument } from "./yaml-ast.js";
|
|
3
3
|
/** Single source of truth for "given the source text of a multi-document YAML
|
|
4
4
|
* file, where does each document start, and what is the byte→(line,char)
|
|
5
5
|
* table for the file." Both the analyzer's `Loader` and editor frontends
|
|
6
|
-
* feed the same
|
|
6
|
+
* feed the same adapted `AstDocument[]` through this so diagnostics
|
|
7
7
|
* resolved against `positionIndex` / `sourceLine` line up identically
|
|
8
8
|
* across hosts. */
|
|
9
9
|
/** Per-document position metadata used by `normalizeDiagnostic`'s fallback chain. */
|
|
@@ -12,7 +12,7 @@ export interface DocumentPosition {
|
|
|
12
12
|
positionIndex: PositionIndex;
|
|
13
13
|
}
|
|
14
14
|
/** Builds DocumentPosition entries aligned to `parsedDocs[i]`. */
|
|
15
|
-
export declare function buildDocumentPositions(text: string, parsedDocs:
|
|
15
|
+
export declare function buildDocumentPositions(text: string, parsedDocs: AstDocument[]): DocumentPosition[];
|
|
16
16
|
/** Line numbers (0-indexed) where each YAML document in a multi-doc file
|
|
17
17
|
* starts. The first document is always at line 0; subsequent entries point
|
|
18
18
|
* to the line after each `---` separator.
|
|
@@ -26,11 +26,12 @@ export declare function documentLineOffsets(text: string): number[];
|
|
|
26
26
|
* the first character on line `i`. Used with `offsetToPosition` to turn a
|
|
27
27
|
* yaml-AST node range into Range coordinates. */
|
|
28
28
|
export declare function buildLineOffsets(text: string): number[];
|
|
29
|
-
|
|
29
|
+
export declare function offsetToPosition(offset: number, lineOffsets: number[]): Position;
|
|
30
|
+
/** Walks the AST and records source ranges for every field value, keyed
|
|
30
31
|
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path").
|
|
31
32
|
* Map keys are also recorded under the `@key:<path>` namespace so diagnostic
|
|
32
33
|
* resolvers can squiggle just the key identifier instead of the full value
|
|
33
34
|
* block — used when a diagnostic targets a missing child property and the
|
|
34
35
|
* resolver has to fall back to the parent. */
|
|
35
|
-
export declare function buildPositionIndex(doc:
|
|
36
|
+
export declare function buildPositionIndex(doc: AstDocument, lineOffsets: number[]): PositionIndex;
|
|
36
37
|
//# sourceMappingURL=position-metadata.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"position-metadata.d.ts","sourceRoot":"","sources":["../src/position-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"position-metadata.d.ts","sourceRoot":"","sources":["../src/position-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,eAAe,CAAC;AAE1D;;;;;oBAKoB;AAEpB,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,kEAAkE;AAClE,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,WAAW,EAAE,GACxB,gBAAgB,EAAE,CAOpB;AAED;;;;;;;wCAOwC;AACxC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW1D;AAED;;kDAEkD;AAClD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMvD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,QAAQ,CAShF;AAED;;;;;+CAK+C;AAC/C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,aAAa,CAsCzF"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isMap, isPair, isScalar, isSeq } from "yaml";
|
|
2
1
|
/** Builds DocumentPosition entries aligned to `parsedDocs[i]`. */
|
|
3
2
|
export function buildDocumentPositions(text, parsedDocs) {
|
|
4
3
|
const docOffsets = documentLineOffsets(text);
|
|
@@ -40,7 +39,7 @@ export function buildLineOffsets(text) {
|
|
|
40
39
|
}
|
|
41
40
|
return offsets;
|
|
42
41
|
}
|
|
43
|
-
function offsetToPosition(offset, lineOffsets) {
|
|
42
|
+
export function offsetToPosition(offset, lineOffsets) {
|
|
44
43
|
let lo = 0;
|
|
45
44
|
let hi = lineOffsets.length - 1;
|
|
46
45
|
while (lo < hi) {
|
|
@@ -52,7 +51,7 @@ function offsetToPosition(offset, lineOffsets) {
|
|
|
52
51
|
}
|
|
53
52
|
return { line: lo, character: offset - lineOffsets[lo] };
|
|
54
53
|
}
|
|
55
|
-
/** Walks the
|
|
54
|
+
/** Walks the AST and records source ranges for every field value, keyed
|
|
56
55
|
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path").
|
|
57
56
|
* Map keys are also recorded under the `@key:<path>` namespace so diagnostic
|
|
58
57
|
* resolvers can squiggle just the key identifier instead of the full value
|
|
@@ -61,33 +60,27 @@ function offsetToPosition(offset, lineOffsets) {
|
|
|
61
60
|
export function buildPositionIndex(doc, lineOffsets) {
|
|
62
61
|
const index = new Map();
|
|
63
62
|
function recordNode(node, path) {
|
|
64
|
-
|
|
65
|
-
return;
|
|
66
|
-
const [start, , end] = node.range;
|
|
63
|
+
const [start, end] = node.range;
|
|
67
64
|
index.set(path, {
|
|
68
65
|
start: offsetToPosition(start, lineOffsets),
|
|
69
66
|
end: offsetToPosition(end, lineOffsets),
|
|
70
67
|
});
|
|
71
68
|
}
|
|
72
69
|
function walk(node, path) {
|
|
73
|
-
if (
|
|
74
|
-
for (const pair of node.
|
|
75
|
-
|
|
76
|
-
continue;
|
|
77
|
-
const key = isScalar(pair.key) ? String(pair.key.value) : null;
|
|
70
|
+
if (node.kind === "map") {
|
|
71
|
+
for (const pair of node.entries) {
|
|
72
|
+
const key = pair.key.kind === "scalar" ? String(pair.key.value) : null;
|
|
78
73
|
if (key == null)
|
|
79
74
|
continue;
|
|
80
75
|
const childPath = path ? `${path}.${key}` : key;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
if (pair.value != null) {
|
|
76
|
+
recordNode(pair.key, `@key:${childPath}`);
|
|
77
|
+
if (pair.value) {
|
|
85
78
|
recordNode(pair.value, childPath);
|
|
86
79
|
walk(pair.value, childPath);
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
82
|
}
|
|
90
|
-
else if (
|
|
83
|
+
else if (node.kind === "seq") {
|
|
91
84
|
for (let i = 0; i < node.items.length; i++) {
|
|
92
85
|
const item = node.items[i];
|
|
93
86
|
const childPath = `${path}[${i}]`;
|
|
@@ -96,8 +89,8 @@ export function buildPositionIndex(doc, lineOffsets) {
|
|
|
96
89
|
}
|
|
97
90
|
}
|
|
98
91
|
}
|
|
99
|
-
if (doc.
|
|
100
|
-
walk(doc.
|
|
92
|
+
if (doc.root) {
|
|
93
|
+
walk(doc.root, "");
|
|
101
94
|
}
|
|
102
95
|
return index;
|
|
103
96
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Document } from "yaml";
|
|
2
|
+
import { type CelSegment } from "./cel-ast.js";
|
|
3
|
+
/** Read-only YAML node model owned by the analyzer — the shared, browser-safe
|
|
4
|
+
* structural source of truth for every IDE feature. Ranges are `[start, end]`
|
|
5
|
+
* in byte offsets (yaml's value-end, so a range spans exactly the node's own
|
|
6
|
+
* text, not the trailing newline). `yaml` is an internal implementation
|
|
7
|
+
* detail behind `parseToAst`; no consumer imports it to read structure. */
|
|
8
|
+
export type AstNode = AstMap | AstSeq | AstScalar;
|
|
9
|
+
export interface AstMap {
|
|
10
|
+
kind: "map";
|
|
11
|
+
range: [number, number];
|
|
12
|
+
entries: AstPair[];
|
|
13
|
+
}
|
|
14
|
+
export interface AstSeq {
|
|
15
|
+
kind: "seq";
|
|
16
|
+
range: [number, number];
|
|
17
|
+
items: AstNode[];
|
|
18
|
+
}
|
|
19
|
+
export interface AstScalar {
|
|
20
|
+
kind: "scalar";
|
|
21
|
+
range: [number, number];
|
|
22
|
+
/** Resolved scalar value — a `TaggedSentinel` for `!cel` / `!ref` scalars. */
|
|
23
|
+
value: unknown;
|
|
24
|
+
/** The scalar's tag when present (`!cel`, `!ref`, …). */
|
|
25
|
+
tag?: string;
|
|
26
|
+
/** The embedded CEL regions (lazy — nothing parses CEL until called). */
|
|
27
|
+
celSegments(): CelSegment[];
|
|
28
|
+
}
|
|
29
|
+
export interface AstPair {
|
|
30
|
+
key: AstNode;
|
|
31
|
+
value?: AstNode;
|
|
32
|
+
}
|
|
33
|
+
export interface AstDocument {
|
|
34
|
+
root?: AstNode;
|
|
35
|
+
/** Full document span `[start, end]` — used to select the `---` document a
|
|
36
|
+
* cursor offset falls in. */
|
|
37
|
+
range: [number, number];
|
|
38
|
+
}
|
|
39
|
+
/** Parse `text` into the read-only AST. Wraps `parseAllDocuments` with the
|
|
40
|
+
* repo's custom tags (`!cel` / `!ref`) and adapts each `yaml` tree into a
|
|
41
|
+
* thin `AstNode` view — CEL parsing stays deferred to `celSegments().ast()`. */
|
|
42
|
+
export declare function parseToAst(text: string): AstDocument[];
|
|
43
|
+
/** Adapt one already-parsed `yaml.Document` into an `AstDocument`. Lets a host
|
|
44
|
+
* that already parsed for analysis reuse that parse instead of re-parsing. */
|
|
45
|
+
export declare function documentToAst(doc: Document, text: string): AstDocument;
|
|
46
|
+
//# sourceMappingURL=yaml-ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml-ast.d.ts","sourceRoot":"","sources":["../src/yaml-ast.ts"],"names":[],"mappings":"AACA,OAAO,EAA6C,KAAK,QAAQ,EAAa,MAAM,MAAM,CAAC;AAC3F,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAEjE;;;;4EAI4E;AAC5E,MAAM,MAAM,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAElD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,OAAO,EAAE,OAAO,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,KAAK,CAAC;IACZ,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,KAAK,EAAE,OAAO,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxB,8EAA8E;IAC9E,KAAK,EAAE,OAAO,CAAC;IACf,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,IAAI,UAAU,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;kCAC8B;IAC9B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzB;AAED;;iFAEiF;AACjF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAGtD;AAED;+EAC+E;AAC/E,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAMtE"}
|
package/dist/yaml-ast.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defaultCustomTags, isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { isMap, isScalar, isSeq, parseAllDocuments } from "yaml";
|
|
3
|
+
import { buildCelSegments } from "./cel-ast.js";
|
|
4
|
+
/** Parse `text` into the read-only AST. Wraps `parseAllDocuments` with the
|
|
5
|
+
* repo's custom tags (`!cel` / `!ref`) and adapts each `yaml` tree into a
|
|
6
|
+
* thin `AstNode` view — CEL parsing stays deferred to `celSegments().ast()`. */
|
|
7
|
+
export function parseToAst(text) {
|
|
8
|
+
const documents = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
9
|
+
return documents.map((doc) => documentToAst(doc, text));
|
|
10
|
+
}
|
|
11
|
+
/** Adapt one already-parsed `yaml.Document` into an `AstDocument`. Lets a host
|
|
12
|
+
* that already parsed for analysis reuse that parse instead of re-parsing. */
|
|
13
|
+
export function documentToAst(doc, text) {
|
|
14
|
+
const r = doc.range;
|
|
15
|
+
return {
|
|
16
|
+
root: doc.contents ? adaptNode(doc.contents, text) : undefined,
|
|
17
|
+
range: r ? [r[0], r[2]] : [0, text.length],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function nodeRange(node) {
|
|
21
|
+
const r = node.range;
|
|
22
|
+
return r ? [r[0], r[1]] : [0, 0];
|
|
23
|
+
}
|
|
24
|
+
function adaptNode(node, text) {
|
|
25
|
+
if (isMap(node)) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
for (const item of node.items) {
|
|
28
|
+
const key = adaptNode(item.key, text);
|
|
29
|
+
if (!key)
|
|
30
|
+
continue;
|
|
31
|
+
const value = item.value != null ? adaptNode(item.value, text) : undefined;
|
|
32
|
+
entries.push({ key, value });
|
|
33
|
+
}
|
|
34
|
+
return { kind: "map", range: nodeRange(node), entries };
|
|
35
|
+
}
|
|
36
|
+
if (isSeq(node)) {
|
|
37
|
+
const items = [];
|
|
38
|
+
for (const item of node.items) {
|
|
39
|
+
const adapted = adaptNode(item, text);
|
|
40
|
+
if (adapted)
|
|
41
|
+
items.push(adapted);
|
|
42
|
+
}
|
|
43
|
+
return { kind: "seq", range: nodeRange(node), items };
|
|
44
|
+
}
|
|
45
|
+
if (isScalar(node)) {
|
|
46
|
+
const range = nodeRange(node);
|
|
47
|
+
const value = node.value;
|
|
48
|
+
const tag = typeof node.tag === "string" ? node.tag : undefined;
|
|
49
|
+
return {
|
|
50
|
+
kind: "scalar",
|
|
51
|
+
range,
|
|
52
|
+
value,
|
|
53
|
+
tag,
|
|
54
|
+
celSegments: () => buildCelSegments(text.slice(range[0], range[1]), range[0], tag, isTaggedSentinel(value) ? value.source : undefined),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"ajv-formats": "^3.0.1",
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"yaml": "^2.8.3",
|
|
45
|
-
"@telorun/templating": "0.
|
|
45
|
+
"@telorun/templating": "0.11.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
package/src/cel-ast.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { parse, type ASTNode as CelJsNode } from "@marcbachmann/cel-js";
|
|
2
|
+
|
|
3
|
+
/** Read-only CEL expression tree owned by the analyzer. The third-party
|
|
4
|
+
* `@marcbachmann/cel-js` `ASTNode` stays an internal detail — `wrapCelAst`
|
|
5
|
+
* translates it into this union so no external AST type leaks through the
|
|
6
|
+
* public surface (full symmetry with the YAML `AstNode` decision). Every
|
|
7
|
+
* `range` is `[start, end]` in DOCUMENT offsets. */
|
|
8
|
+
export type CelNode =
|
|
9
|
+
| { kind: "literal"; range: [number, number]; value: unknown }
|
|
10
|
+
| { kind: "ident"; range: [number, number]; name: string }
|
|
11
|
+
| {
|
|
12
|
+
kind: "member";
|
|
13
|
+
range: [number, number];
|
|
14
|
+
target: CelNode;
|
|
15
|
+
property: string;
|
|
16
|
+
/** Span of just the `.prop` identifier, for a future rename. */
|
|
17
|
+
propertyRange: [number, number];
|
|
18
|
+
/** `.?` optional member access. */
|
|
19
|
+
optional: boolean;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
kind: "index";
|
|
23
|
+
range: [number, number];
|
|
24
|
+
target: CelNode;
|
|
25
|
+
index: CelNode;
|
|
26
|
+
/** `[?]` optional index. */
|
|
27
|
+
optional: boolean;
|
|
28
|
+
}
|
|
29
|
+
| { kind: "call"; range: [number, number]; name: string; args: CelNode[] }
|
|
30
|
+
| {
|
|
31
|
+
kind: "methodCall";
|
|
32
|
+
range: [number, number];
|
|
33
|
+
name: string;
|
|
34
|
+
receiver: CelNode;
|
|
35
|
+
args: CelNode[];
|
|
36
|
+
}
|
|
37
|
+
| { kind: "list"; range: [number, number]; items: CelNode[] }
|
|
38
|
+
| { kind: "map"; range: [number, number]; entries: { key: CelNode; value: CelNode }[] }
|
|
39
|
+
| {
|
|
40
|
+
kind: "ternary";
|
|
41
|
+
range: [number, number];
|
|
42
|
+
cond: CelNode;
|
|
43
|
+
then: CelNode;
|
|
44
|
+
else: CelNode;
|
|
45
|
+
}
|
|
46
|
+
| { kind: "unary"; range: [number, number]; op: string; operand: CelNode }
|
|
47
|
+
| { kind: "binary"; range: [number, number]; op: string; left: CelNode; right: CelNode };
|
|
48
|
+
|
|
49
|
+
/** A `${{ }}` / `!cel` region inside a YAML scalar. Ranges are DOCUMENT
|
|
50
|
+
* offsets; `source` is the CEL body (a longest-valid prefix when `open`).
|
|
51
|
+
* `ast()` parses lazily — nothing parses CEL during `parseToAst`, only the
|
|
52
|
+
* expression a caller actually inspects. */
|
|
53
|
+
export interface CelSegment {
|
|
54
|
+
/** Segment span in document offsets (includes the `${{ }}` for interpolation). */
|
|
55
|
+
range: [number, number];
|
|
56
|
+
/** The CEL body (a prefix when `open`). */
|
|
57
|
+
source: string;
|
|
58
|
+
/** True when a `${{` has no matching `}}` yet (the user is mid-typing). */
|
|
59
|
+
open: boolean;
|
|
60
|
+
/** Lazily parse + wrap; ranges are already absolute. */
|
|
61
|
+
ast(): CelNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const BINARY_OPS = new Set([
|
|
65
|
+
"!=",
|
|
66
|
+
"==",
|
|
67
|
+
"in",
|
|
68
|
+
"+",
|
|
69
|
+
"-",
|
|
70
|
+
"*",
|
|
71
|
+
"/",
|
|
72
|
+
"%",
|
|
73
|
+
"<",
|
|
74
|
+
"<=",
|
|
75
|
+
">",
|
|
76
|
+
">=",
|
|
77
|
+
"||",
|
|
78
|
+
"&&",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/** Maps a `@marcbachmann/cel-js` node into the analyzer `CelNode`, translating
|
|
82
|
+
* each node's segment-relative `start`/`end` to absolute document offsets by
|
|
83
|
+
* adding `segmentStart`. */
|
|
84
|
+
export function wrapCelAst(node: CelJsNode, segmentStart: number): CelNode {
|
|
85
|
+
const range = abs(node, segmentStart);
|
|
86
|
+
const op = node.op;
|
|
87
|
+
const args = node.args as unknown;
|
|
88
|
+
|
|
89
|
+
if (op === "value") return { kind: "literal", range, value: args };
|
|
90
|
+
if (op === "id") return { kind: "ident", range, name: String(args) };
|
|
91
|
+
if (op === "." || op === ".?") {
|
|
92
|
+
const [target, property] = args as [CelJsNode, string];
|
|
93
|
+
return {
|
|
94
|
+
kind: "member",
|
|
95
|
+
range,
|
|
96
|
+
target: wrapCelAst(target, segmentStart),
|
|
97
|
+
property,
|
|
98
|
+
propertyRange: [range[1] - property.length, range[1]],
|
|
99
|
+
optional: op === ".?",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (op === "[]" || op === "[?]") {
|
|
103
|
+
const [target, index] = args as [CelJsNode, CelJsNode];
|
|
104
|
+
return {
|
|
105
|
+
kind: "index",
|
|
106
|
+
range,
|
|
107
|
+
target: wrapCelAst(target, segmentStart),
|
|
108
|
+
index: wrapCelAst(index, segmentStart),
|
|
109
|
+
optional: op === "[?]",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (op === "call") {
|
|
113
|
+
const [name, callArgs] = args as [string, CelJsNode[]];
|
|
114
|
+
return { kind: "call", range, name, args: callArgs.map((a) => wrapCelAst(a, segmentStart)) };
|
|
115
|
+
}
|
|
116
|
+
if (op === "rcall") {
|
|
117
|
+
const [name, receiver, callArgs] = args as [string, CelJsNode, CelJsNode[]];
|
|
118
|
+
return {
|
|
119
|
+
kind: "methodCall",
|
|
120
|
+
range,
|
|
121
|
+
name,
|
|
122
|
+
receiver: wrapCelAst(receiver, segmentStart),
|
|
123
|
+
args: callArgs.map((a) => wrapCelAst(a, segmentStart)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (op === "list") {
|
|
127
|
+
return { kind: "list", range, items: (args as CelJsNode[]).map((a) => wrapCelAst(a, segmentStart)) };
|
|
128
|
+
}
|
|
129
|
+
if (op === "map") {
|
|
130
|
+
return {
|
|
131
|
+
kind: "map",
|
|
132
|
+
range,
|
|
133
|
+
entries: (args as [CelJsNode, CelJsNode][]).map(([k, v]) => ({
|
|
134
|
+
key: wrapCelAst(k, segmentStart),
|
|
135
|
+
value: wrapCelAst(v, segmentStart),
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (op === "?:") {
|
|
140
|
+
const [cond, then, els] = args as [CelJsNode, CelJsNode, CelJsNode];
|
|
141
|
+
return {
|
|
142
|
+
kind: "ternary",
|
|
143
|
+
range,
|
|
144
|
+
cond: wrapCelAst(cond, segmentStart),
|
|
145
|
+
then: wrapCelAst(then, segmentStart),
|
|
146
|
+
else: wrapCelAst(els, segmentStart),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (op === "!_" || op === "-_") {
|
|
150
|
+
return { kind: "unary", range, op, operand: wrapCelAst(args as CelJsNode, segmentStart) };
|
|
151
|
+
}
|
|
152
|
+
if (BINARY_OPS.has(op)) {
|
|
153
|
+
const [left, right] = args as [CelJsNode, CelJsNode];
|
|
154
|
+
return {
|
|
155
|
+
kind: "binary",
|
|
156
|
+
range,
|
|
157
|
+
op,
|
|
158
|
+
left: wrapCelAst(left, segmentStart),
|
|
159
|
+
right: wrapCelAst(right, segmentStart),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Unknown operator — surface it as a literal so consumers can still hit-test
|
|
163
|
+
// the range rather than crash on an unmapped node.
|
|
164
|
+
return { kind: "literal", range, value: undefined };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function abs(node: CelJsNode, segmentStart: number): [number, number] {
|
|
168
|
+
const r = node.range ?? { start: node.start, end: node.end };
|
|
169
|
+
return [r.start + segmentStart, r.end + segmentStart];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Parse `source` and wrap it, tolerating a trailing partial member/index
|
|
173
|
+
* access (`req.`, `req.fo`) by falling back to the longest parseable prefix.
|
|
174
|
+
* Used for `open` segments where completion fires mid-token. */
|
|
175
|
+
function parseLenient(source: string, segmentStart: number, range: [number, number]): CelNode {
|
|
176
|
+
const candidates = [source, source.replace(/[.?[]+\w*$/, ""), source.replace(/[.?[(]+.*$/, "")];
|
|
177
|
+
for (const candidate of candidates) {
|
|
178
|
+
const trimmed = candidate.trim();
|
|
179
|
+
if (!trimmed) break;
|
|
180
|
+
try {
|
|
181
|
+
return wrapCelAst(parse(trimmed).ast, segmentStart);
|
|
182
|
+
} catch {
|
|
183
|
+
// try the next-shorter prefix
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { kind: "ident", range, name: source.trim() };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const OPEN_MARKER = "${{";
|
|
190
|
+
|
|
191
|
+
/** Build the CEL segments of a scalar from its raw source slice. `scalarText`
|
|
192
|
+
* is `text.slice(start, valueEnd)` and `scalarStart` its document offset.
|
|
193
|
+
*
|
|
194
|
+
* - `tag === "!cel"` → one closed segment spanning the tagged body.
|
|
195
|
+
* - otherwise → one closed segment per `${{ … }}` match, plus a trailing
|
|
196
|
+
* `open` segment for a dangling `${{` with no `}}` (bounded to its line, so
|
|
197
|
+
* an unterminated quote that swallowed following lines still recovers the
|
|
198
|
+
* region the user is typing in). */
|
|
199
|
+
export function buildCelSegments(
|
|
200
|
+
scalarText: string,
|
|
201
|
+
scalarStart: number,
|
|
202
|
+
tag: string | undefined,
|
|
203
|
+
taggedSource: string | undefined,
|
|
204
|
+
): CelSegment[] {
|
|
205
|
+
if (tag === "!cel" && taggedSource != null) {
|
|
206
|
+
const idx = scalarText.indexOf(taggedSource);
|
|
207
|
+
const bodyStart = scalarStart + (idx >= 0 ? idx : 0);
|
|
208
|
+
const range: [number, number] = [bodyStart, bodyStart + taggedSource.length];
|
|
209
|
+
return [
|
|
210
|
+
{
|
|
211
|
+
range,
|
|
212
|
+
source: taggedSource,
|
|
213
|
+
open: false,
|
|
214
|
+
ast: () => wrapCelAst(parse(taggedSource).ast, bodyStart),
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const segments: CelSegment[] = [];
|
|
220
|
+
const re = /\$\{\{([\s\S]*?)\}\}/g;
|
|
221
|
+
let match: RegExpExecArray | null;
|
|
222
|
+
let lastClosedEnd = 0;
|
|
223
|
+
while ((match = re.exec(scalarText)) !== null) {
|
|
224
|
+
const whole = match[0];
|
|
225
|
+
const inner = match[1];
|
|
226
|
+
const leadingWs = inner.match(/^\s*/)?.[0].length ?? 0;
|
|
227
|
+
const bodyStart = scalarStart + match.index + OPEN_MARKER.length + leadingWs;
|
|
228
|
+
const source = inner.trim();
|
|
229
|
+
segments.push({
|
|
230
|
+
range: [scalarStart + match.index, scalarStart + match.index + whole.length],
|
|
231
|
+
source,
|
|
232
|
+
open: false,
|
|
233
|
+
ast: () => wrapCelAst(parse(source).ast, bodyStart),
|
|
234
|
+
});
|
|
235
|
+
lastClosedEnd = match.index + whole.length;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const openIdx = scalarText.indexOf(OPEN_MARKER, lastClosedEnd);
|
|
239
|
+
if (openIdx >= 0 && scalarText.indexOf("}}", openIdx) < 0) {
|
|
240
|
+
let lineEnd = scalarText.indexOf("\n", openIdx);
|
|
241
|
+
if (lineEnd < 0) lineEnd = scalarText.length;
|
|
242
|
+
const after = openIdx + OPEN_MARKER.length;
|
|
243
|
+
// Drop a trailing scalar-closing quote so `foo: "${{ req"` recovers `req`,
|
|
244
|
+
// not `req"` — the quote closes the YAML string, it isn't part of the CEL.
|
|
245
|
+
const rawBody = scalarText.slice(after, lineEnd).replace(/["']\s*$/, "");
|
|
246
|
+
const leadingWs = rawBody.match(/^\s*/)?.[0].length ?? 0;
|
|
247
|
+
const bodyStart = scalarStart + after + leadingWs;
|
|
248
|
+
const source = rawBody.trim();
|
|
249
|
+
const range: [number, number] = [scalarStart + openIdx, scalarStart + lineEnd];
|
|
250
|
+
segments.push({
|
|
251
|
+
range,
|
|
252
|
+
source,
|
|
253
|
+
open: true,
|
|
254
|
+
ast: () => parseLenient(source, bodyStart, range),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return segments;
|
|
259
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ export {
|
|
|
68
68
|
buildLineOffsets,
|
|
69
69
|
buildPositionIndex,
|
|
70
70
|
documentLineOffsets,
|
|
71
|
+
offsetToPosition,
|
|
71
72
|
} from "./position-metadata.js";
|
|
72
73
|
export type { DocumentPosition } from "./position-metadata.js";
|
|
73
74
|
export { HttpSource } from "./sources/http-source.js";
|
|
@@ -97,6 +98,10 @@ export {
|
|
|
97
98
|
} from "./sources/manifest-cache.js";
|
|
98
99
|
export type { ManifestCacheCoords } from "./sources/manifest-cache.js";
|
|
99
100
|
export { withSyntheticPositions } from "./with-synthetic-positions.js";
|
|
101
|
+
export { documentToAst, parseToAst } from "./yaml-ast.js";
|
|
102
|
+
export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
|
|
103
|
+
export { buildCelSegments, wrapCelAst } from "./cel-ast.js";
|
|
104
|
+
export type { CelNode, CelSegment } from "./cel-ast.js";
|
|
100
105
|
export { DEFAULT_MANIFEST_FILENAME, DiagnosticSeverity } from "./types.js";
|
|
101
106
|
export type {
|
|
102
107
|
AnalysisDiagnostic,
|
package/src/loaded-types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { Document } from "yaml";
|
|
3
3
|
import type { DocumentPosition } from "./position-metadata.js";
|
|
4
4
|
import type { AnalysisDiagnostic, Range } from "./types.js";
|
|
5
|
+
import type { AstDocument } from "./yaml-ast.js";
|
|
5
6
|
|
|
6
7
|
/** One physical file's parsed result. Returned for the owner manifest, for
|
|
7
8
|
* each `include:` partial, and for each external import target.
|
|
@@ -17,8 +18,13 @@ export interface LoadedFile {
|
|
|
17
18
|
requestedUrl: string;
|
|
18
19
|
/** Raw text exactly as `read()` returned it. */
|
|
19
20
|
text: string;
|
|
20
|
-
/** Per-document parsed AST, in source order.
|
|
21
|
+
/** Per-document parsed `yaml` AST, in source order. The editor's mutable
|
|
22
|
+
* round-trip model reads this handle; structure-only consumers use the
|
|
23
|
+
* read-only `astDocuments` instead so `yaml` stays an internal detail. */
|
|
21
24
|
documents: Document[];
|
|
25
|
+
/** Per-document read-only `AstNode` view (`yaml`-free), aligned to
|
|
26
|
+
* `documents`. The shared structural source of truth for IDE features. */
|
|
27
|
+
astDocuments: AstDocument[];
|
|
22
28
|
/** Per-document JSON projection (`doc.toJSON()`). Aligned to `documents`. */
|
|
23
29
|
manifests: Array<ResourceManifest | null>;
|
|
24
30
|
/** Per-document `{sourceLine, positionIndex}`. Aligned to `documents`. */
|
package/src/parse-loaded-file.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { buildCelEnvironment } from "./cel-environment.js";
|
|
|
6
6
|
import type { LoadedFile, ParseError } from "./loaded-types.js";
|
|
7
7
|
import { buildDocumentPositions } from "./position-metadata.js";
|
|
8
8
|
import { precompileDoc } from "./precompile.js";
|
|
9
|
+
import { documentToAst } from "./yaml-ast.js";
|
|
9
10
|
|
|
10
11
|
export interface ParseOptions {
|
|
11
12
|
/** When true, runs `precompileDoc` per document and stamps compiled CEL
|
|
@@ -49,7 +50,8 @@ export function parseLoadedFile(
|
|
|
49
50
|
options?: ParseOptions,
|
|
50
51
|
): LoadedFile {
|
|
51
52
|
const documents = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
52
|
-
const
|
|
53
|
+
const astDocuments = documents.map((doc) => documentToAst(doc, text));
|
|
54
|
+
const positions = buildDocumentPositions(text, astDocuments);
|
|
53
55
|
|
|
54
56
|
const parseErrors: ParseError[] = [];
|
|
55
57
|
documents.forEach((doc, documentIndex) => {
|
|
@@ -90,6 +92,7 @@ export function parseLoadedFile(
|
|
|
90
92
|
requestedUrl,
|
|
91
93
|
text,
|
|
92
94
|
documents,
|
|
95
|
+
astDocuments,
|
|
93
96
|
manifests,
|
|
94
97
|
positions,
|
|
95
98
|
parseErrors,
|
package/src/position-metadata.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { isMap, isPair, isScalar, isSeq, type Document } from "yaml";
|
|
2
1
|
import type { Position, PositionIndex } from "./types.js";
|
|
2
|
+
import type { AstDocument, AstNode } from "./yaml-ast.js";
|
|
3
3
|
|
|
4
4
|
/** Single source of truth for "given the source text of a multi-document YAML
|
|
5
5
|
* file, where does each document start, and what is the byte→(line,char)
|
|
6
6
|
* table for the file." Both the analyzer's `Loader` and editor frontends
|
|
7
|
-
* feed the same
|
|
7
|
+
* feed the same adapted `AstDocument[]` through this so diagnostics
|
|
8
8
|
* resolved against `positionIndex` / `sourceLine` line up identically
|
|
9
9
|
* across hosts. */
|
|
10
10
|
|
|
@@ -17,7 +17,7 @@ export interface DocumentPosition {
|
|
|
17
17
|
/** Builds DocumentPosition entries aligned to `parsedDocs[i]`. */
|
|
18
18
|
export function buildDocumentPositions(
|
|
19
19
|
text: string,
|
|
20
|
-
parsedDocs:
|
|
20
|
+
parsedDocs: AstDocument[],
|
|
21
21
|
): DocumentPosition[] {
|
|
22
22
|
const docOffsets = documentLineOffsets(text);
|
|
23
23
|
const lineOffsets = buildLineOffsets(text);
|
|
@@ -59,7 +59,7 @@ export function buildLineOffsets(text: string): number[] {
|
|
|
59
59
|
return offsets;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function offsetToPosition(offset: number, lineOffsets: number[]): Position {
|
|
62
|
+
export function offsetToPosition(offset: number, lineOffsets: number[]): Position {
|
|
63
63
|
let lo = 0;
|
|
64
64
|
let hi = lineOffsets.length - 1;
|
|
65
65
|
while (lo < hi) {
|
|
@@ -70,40 +70,36 @@ function offsetToPosition(offset: number, lineOffsets: number[]): Position {
|
|
|
70
70
|
return { line: lo, character: offset - lineOffsets[lo] };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** Walks the
|
|
73
|
+
/** Walks the AST and records source ranges for every field value, keyed
|
|
74
74
|
* by dotted path (e.g. "kind", "config.handler", "config.routes[0].path").
|
|
75
75
|
* Map keys are also recorded under the `@key:<path>` namespace so diagnostic
|
|
76
76
|
* resolvers can squiggle just the key identifier instead of the full value
|
|
77
77
|
* block — used when a diagnostic targets a missing child property and the
|
|
78
78
|
* resolver has to fall back to the parent. */
|
|
79
|
-
export function buildPositionIndex(doc:
|
|
79
|
+
export function buildPositionIndex(doc: AstDocument, lineOffsets: number[]): PositionIndex {
|
|
80
80
|
const index: PositionIndex = new Map();
|
|
81
81
|
|
|
82
|
-
function recordNode(node:
|
|
83
|
-
|
|
84
|
-
const [start, , end] = node.range as [number, number, number];
|
|
82
|
+
function recordNode(node: AstNode, path: string): void {
|
|
83
|
+
const [start, end] = node.range;
|
|
85
84
|
index.set(path, {
|
|
86
85
|
start: offsetToPosition(start, lineOffsets),
|
|
87
86
|
end: offsetToPosition(end, lineOffsets),
|
|
88
87
|
});
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
function walk(node:
|
|
92
|
-
if (
|
|
93
|
-
for (const pair of node.
|
|
94
|
-
|
|
95
|
-
const key = isScalar(pair.key) ? String(pair.key.value) : null;
|
|
90
|
+
function walk(node: AstNode, path: string): void {
|
|
91
|
+
if (node.kind === "map") {
|
|
92
|
+
for (const pair of node.entries) {
|
|
93
|
+
const key = pair.key.kind === "scalar" ? String(pair.key.value) : null;
|
|
96
94
|
if (key == null) continue;
|
|
97
95
|
const childPath = path ? `${path}.${key}` : key;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (pair.value != null) {
|
|
96
|
+
recordNode(pair.key, `@key:${childPath}`);
|
|
97
|
+
if (pair.value) {
|
|
102
98
|
recordNode(pair.value, childPath);
|
|
103
99
|
walk(pair.value, childPath);
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
|
-
} else if (
|
|
102
|
+
} else if (node.kind === "seq") {
|
|
107
103
|
for (let i = 0; i < node.items.length; i++) {
|
|
108
104
|
const item = node.items[i];
|
|
109
105
|
const childPath = `${path}[${i}]`;
|
|
@@ -113,8 +109,8 @@ export function buildPositionIndex(doc: Document, lineOffsets: number[]): Positi
|
|
|
113
109
|
}
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
if (doc.
|
|
117
|
-
walk(doc.
|
|
112
|
+
if (doc.root) {
|
|
113
|
+
walk(doc.root, "");
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
return index;
|
package/src/yaml-ast.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { defaultCustomTags, isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { isMap, isScalar, isSeq, parseAllDocuments, type Document, type Node } from "yaml";
|
|
3
|
+
import { buildCelSegments, type CelSegment } from "./cel-ast.js";
|
|
4
|
+
|
|
5
|
+
/** Read-only YAML node model owned by the analyzer — the shared, browser-safe
|
|
6
|
+
* structural source of truth for every IDE feature. Ranges are `[start, end]`
|
|
7
|
+
* in byte offsets (yaml's value-end, so a range spans exactly the node's own
|
|
8
|
+
* text, not the trailing newline). `yaml` is an internal implementation
|
|
9
|
+
* detail behind `parseToAst`; no consumer imports it to read structure. */
|
|
10
|
+
export type AstNode = AstMap | AstSeq | AstScalar;
|
|
11
|
+
|
|
12
|
+
export interface AstMap {
|
|
13
|
+
kind: "map";
|
|
14
|
+
range: [number, number];
|
|
15
|
+
entries: AstPair[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AstSeq {
|
|
19
|
+
kind: "seq";
|
|
20
|
+
range: [number, number];
|
|
21
|
+
items: AstNode[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AstScalar {
|
|
25
|
+
kind: "scalar";
|
|
26
|
+
range: [number, number];
|
|
27
|
+
/** Resolved scalar value — a `TaggedSentinel` for `!cel` / `!ref` scalars. */
|
|
28
|
+
value: unknown;
|
|
29
|
+
/** The scalar's tag when present (`!cel`, `!ref`, …). */
|
|
30
|
+
tag?: string;
|
|
31
|
+
/** The embedded CEL regions (lazy — nothing parses CEL until called). */
|
|
32
|
+
celSegments(): CelSegment[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AstPair {
|
|
36
|
+
key: AstNode;
|
|
37
|
+
value?: AstNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AstDocument {
|
|
41
|
+
root?: AstNode;
|
|
42
|
+
/** Full document span `[start, end]` — used to select the `---` document a
|
|
43
|
+
* cursor offset falls in. */
|
|
44
|
+
range: [number, number];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse `text` into the read-only AST. Wraps `parseAllDocuments` with the
|
|
48
|
+
* repo's custom tags (`!cel` / `!ref`) and adapts each `yaml` tree into a
|
|
49
|
+
* thin `AstNode` view — CEL parsing stays deferred to `celSegments().ast()`. */
|
|
50
|
+
export function parseToAst(text: string): AstDocument[] {
|
|
51
|
+
const documents = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
52
|
+
return documents.map((doc) => documentToAst(doc, text));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Adapt one already-parsed `yaml.Document` into an `AstDocument`. Lets a host
|
|
56
|
+
* that already parsed for analysis reuse that parse instead of re-parsing. */
|
|
57
|
+
export function documentToAst(doc: Document, text: string): AstDocument {
|
|
58
|
+
const r = doc.range as [number, number, number] | null | undefined;
|
|
59
|
+
return {
|
|
60
|
+
root: doc.contents ? adaptNode(doc.contents, text) : undefined,
|
|
61
|
+
range: r ? [r[0], r[2]] : [0, text.length],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nodeRange(node: Node): [number, number] {
|
|
66
|
+
const r = node.range as [number, number, number] | null | undefined;
|
|
67
|
+
return r ? [r[0], r[1]] : [0, 0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function adaptNode(node: Node, text: string): AstNode | undefined {
|
|
71
|
+
if (isMap(node)) {
|
|
72
|
+
const entries: AstPair[] = [];
|
|
73
|
+
for (const item of node.items) {
|
|
74
|
+
const key = adaptNode(item.key as Node, text);
|
|
75
|
+
if (!key) continue;
|
|
76
|
+
const value = item.value != null ? adaptNode(item.value as Node, text) : undefined;
|
|
77
|
+
entries.push({ key, value });
|
|
78
|
+
}
|
|
79
|
+
return { kind: "map", range: nodeRange(node), entries };
|
|
80
|
+
}
|
|
81
|
+
if (isSeq(node)) {
|
|
82
|
+
const items: AstNode[] = [];
|
|
83
|
+
for (const item of node.items) {
|
|
84
|
+
const adapted = adaptNode(item as Node, text);
|
|
85
|
+
if (adapted) items.push(adapted);
|
|
86
|
+
}
|
|
87
|
+
return { kind: "seq", range: nodeRange(node), items };
|
|
88
|
+
}
|
|
89
|
+
if (isScalar(node)) {
|
|
90
|
+
const range = nodeRange(node);
|
|
91
|
+
const value = node.value;
|
|
92
|
+
const tag = typeof node.tag === "string" ? node.tag : undefined;
|
|
93
|
+
return {
|
|
94
|
+
kind: "scalar",
|
|
95
|
+
range,
|
|
96
|
+
value,
|
|
97
|
+
tag,
|
|
98
|
+
celSegments: () =>
|
|
99
|
+
buildCelSegments(
|
|
100
|
+
text.slice(range[0], range[1]),
|
|
101
|
+
range[0],
|
|
102
|
+
tag,
|
|
103
|
+
isTaggedSentinel(value) ? value.source : undefined,
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|