@telorun/analyzer 0.39.0 → 0.41.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/cel-ast.d.ts +98 -0
- package/dist/cel-ast.d.ts.map +1 -0
- package/dist/cel-ast.js +188 -0
- package/dist/import-resolution-diagnostics.d.ts +20 -0
- package/dist/import-resolution-diagnostics.d.ts.map +1 -0
- package/dist/import-resolution-diagnostics.js +59 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/loaded-types.d.ts +19 -2
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +11 -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/sources/local-path-ref.d.ts +4 -0
- package/dist/sources/local-path-ref.d.ts.map +1 -0
- package/dist/sources/local-path-ref.js +5 -0
- 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 +1 -1
- package/src/cel-ast.ts +259 -0
- package/src/import-resolution-diagnostics.ts +66 -0
- package/src/index.ts +7 -0
- package/src/loaded-types.ts +19 -2
- package/src/manifest-loader.ts +11 -1
- package/src/parse-loaded-file.ts +4 -1
- package/src/position-metadata.ts +17 -21
- package/src/sources/local-path-ref.ts +5 -0
- package/src/yaml-ast.ts +108 -0
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;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** True when `source` names an on-disk sibling manifest — a relative (`./`,
|
|
2
|
+
* `../`) or absolute (`/`) path — rather than a transport-owned remote ref. */
|
|
3
|
+
export function isLocalPathSource(source: string): boolean {
|
|
4
|
+
return source.startsWith(".") || source.startsWith("/");
|
|
5
|
+
}
|
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
|
+
}
|