@telorun/ide-support 0.13.3 → 0.14.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-chain.d.ts +36 -0
- package/dist/cel-chain.d.ts.map +1 -0
- package/dist/cel-chain.js +77 -0
- package/dist/completions/detect-context.d.ts +5 -5
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +72 -5
- package/dist/completions/prop-keys.d.ts +1 -1
- package/dist/completions/prop-keys.d.ts.map +1 -1
- package/dist/completions/prop-keys.js +18 -1
- package/dist/definition/resolve-cel-target.d.ts.map +1 -1
- package/dist/definition/resolve-cel-target.js +1 -60
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/rename/build-rename.d.ts +39 -0
- package/dist/rename/build-rename.d.ts.map +1 -0
- package/dist/rename/build-rename.js +407 -0
- package/dist/rename/find-sites.d.ts +49 -0
- package/dist/rename/find-sites.d.ts.map +1 -0
- package/dist/rename/find-sites.js +202 -0
- package/dist/rename/index.d.ts +3 -0
- package/dist/rename/index.d.ts.map +1 -0
- package/dist/rename/index.js +1 -0
- package/dist/rename/types.d.ts +55 -0
- package/dist/rename/types.d.ts.map +1 -0
- package/dist/rename/types.js +1 -0
- package/package.json +2 -2
- package/src/cel-chain.ts +86 -0
- package/src/completions/detect-context.ts +73 -6
- package/src/completions/prop-keys.ts +23 -2
- package/src/definition/resolve-cel-target.ts +1 -68
- package/src/index.ts +1 -0
- package/src/rename/build-rename.ts +513 -0
- package/src/rename/find-sites.ts +215 -0
- package/src/rename/index.ts +9 -0
- package/src/rename/types.ts +51 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CelParseError,
|
|
3
|
+
type AstDocument,
|
|
4
|
+
type AstNode,
|
|
5
|
+
type AstScalar,
|
|
6
|
+
type CelNode,
|
|
7
|
+
} from "@telorun/analyzer";
|
|
8
|
+
|
|
9
|
+
import { walkCel } from "../cel-chain.js";
|
|
10
|
+
import { scalarString } from "../completions/resolve-node.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every place a name is written, found over the read-only YAML AST plus the CEL
|
|
14
|
+
* AST inside each scalar. Document offsets in, document offsets out — the
|
|
15
|
+
* caller maps them to `Range`s once, with the line table it already built.
|
|
16
|
+
*
|
|
17
|
+
* **Offsets rather than ranges, and a flat list rather than a tree**, because
|
|
18
|
+
* the two things a rename must guarantee are that no site is missed and that no
|
|
19
|
+
* two edits overlap. Both are properties of a flat, sorted offset list, and
|
|
20
|
+
* neither is checkable once the sites have been shaped per-feature.
|
|
21
|
+
*
|
|
22
|
+
* A `!ref` scalar's own range is its VALUE, excluding the tag (`!ref other` →
|
|
23
|
+
* the span of `other`), so a local reference is a whole-node replacement. A CEL
|
|
24
|
+
* identifier is a sub-span of its scalar, taken from `propertyRange` — which the
|
|
25
|
+
* analyzer's `CelNode` has carried since it was written, for exactly this.
|
|
26
|
+
*/
|
|
27
|
+
export interface NameSite {
|
|
28
|
+
/** `[start, end]` in document offsets — the identifier alone, never the
|
|
29
|
+
* enclosing scalar or the `!ref`/`!cel` tag. */
|
|
30
|
+
range: [number, number];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Walk every scalar of a document, in source order. */
|
|
34
|
+
function eachScalar(node: AstNode, visit: (scalar: AstScalar) => void): void {
|
|
35
|
+
if (node.kind === "map") {
|
|
36
|
+
for (const pair of node.entries) {
|
|
37
|
+
eachScalar(pair.key, visit);
|
|
38
|
+
if (pair.value) eachScalar(pair.value, visit);
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (node.kind === "seq") {
|
|
43
|
+
for (const item of node.items) eachScalar(item, visit);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
visit(node);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Every CEL node of a scalar's every segment.
|
|
50
|
+
*
|
|
51
|
+
* A body that does not parse yields nothing: an author mid-edit is not a
|
|
52
|
+
* reason to fail a rename, and the analyzer reports the syntax error itself.
|
|
53
|
+
* Only that failure is tolerated — a defect in the CEL wrapper propagates,
|
|
54
|
+
* the posture `resolveCelTarget` already takes. */
|
|
55
|
+
function eachCelNode(scalar: AstScalar, visit: (node: CelNode) => void): void {
|
|
56
|
+
for (const segment of scalar.celSegments()) {
|
|
57
|
+
let ast: CelNode;
|
|
58
|
+
try {
|
|
59
|
+
ast = segment.ast();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!(error instanceof CelParseError)) throw error;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
walkCel(ast, visit);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** `<scope>.<name>` read as a member access — `resources.db`, `steps.build`,
|
|
69
|
+
* `variables.apiUrl`. Returns the span of `name` alone. */
|
|
70
|
+
function scopeMemberSite(node: CelNode, scope: string, name: string): NameSite | undefined {
|
|
71
|
+
if (node.kind !== "member" || node.property !== name) return undefined;
|
|
72
|
+
if (node.target.kind !== "ident" || node.target.name !== scope) return undefined;
|
|
73
|
+
return { range: node.propertyRange };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SELF_PREFIX = "Self.";
|
|
77
|
+
|
|
78
|
+
/** The span of `name` inside a `!ref` scalar, or undefined when the scalar names
|
|
79
|
+
* something else. Accepts the bare form and the `Self.`-qualified one, which
|
|
80
|
+
* also resolves locally; `<Alias>.<name>` is a different module's export and is
|
|
81
|
+
* deliberately not matched. */
|
|
82
|
+
function refSite(scalar: AstScalar, name: string): NameSite | undefined {
|
|
83
|
+
if (scalar.tag !== "!ref") return undefined;
|
|
84
|
+
const [start, end] = scalar.range;
|
|
85
|
+
const raw = refText(scalar);
|
|
86
|
+
if (raw === name) return { range: [start, end] };
|
|
87
|
+
if (raw === `${SELF_PREFIX}${name}`) return { range: [start + SELF_PREFIX.length, end] };
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A `!ref` scalar's resolved value is a `TaggedSentinel`, so the written text
|
|
92
|
+
* is read off the sentinel rather than assumed to be a plain string. */
|
|
93
|
+
function refText(scalar: AstScalar): string | undefined {
|
|
94
|
+
const value = scalar.value as { source?: unknown } | string | undefined;
|
|
95
|
+
if (typeof value === "string") return value;
|
|
96
|
+
if (value && typeof value === "object" && typeof value.source === "string") return value.source;
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A resource's references within one document: `!ref <name>` (and its `Self.`
|
|
101
|
+
* form) plus `resources.<name>` in CEL. */
|
|
102
|
+
export function resourceSites(doc: AstDocument, name: string): NameSite[] {
|
|
103
|
+
const sites: NameSite[] = [];
|
|
104
|
+
if (!doc.root) return sites;
|
|
105
|
+
eachScalar(doc.root, (scalar) => {
|
|
106
|
+
const ref = refSite(scalar, name);
|
|
107
|
+
if (ref) sites.push(ref);
|
|
108
|
+
eachCelNode(scalar, (node) => {
|
|
109
|
+
const site = scopeMemberSite(node, "resources", name);
|
|
110
|
+
if (site) sites.push(site);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return sites;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A step's references within its declaring document: `steps.<name>` in CEL.
|
|
117
|
+
* Deliberately document-scoped — `steps.<name>.result` is readable only inside
|
|
118
|
+
* the resource whose body declares the step, and a resource is one document. */
|
|
119
|
+
export function stepSites(doc: AstDocument, name: string): NameSite[] {
|
|
120
|
+
const sites: NameSite[] = [];
|
|
121
|
+
if (!doc.root) return sites;
|
|
122
|
+
eachScalar(doc.root, (scalar) => {
|
|
123
|
+
eachCelNode(scalar, (node) => {
|
|
124
|
+
const site = scopeMemberSite(node, "steps", name);
|
|
125
|
+
if (site) sites.push(site);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
return sites;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A `variables:` / `secrets:` / `ports:` entry's reads: `<block>.<name>`. */
|
|
132
|
+
export function declarationSites(doc: AstDocument, block: string, name: string): NameSite[] {
|
|
133
|
+
const sites: NameSite[] = [];
|
|
134
|
+
if (!doc.root) return sites;
|
|
135
|
+
eachScalar(doc.root, (scalar) => {
|
|
136
|
+
eachCelNode(scalar, (node) => {
|
|
137
|
+
const site = scopeMemberSite(node, block, name);
|
|
138
|
+
if (site) sites.push(site);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
return sites;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Every map in a document that declares a resource named `name` — a `kind:`
|
|
146
|
+
* beside a `metadata.name`.
|
|
147
|
+
*
|
|
148
|
+
* Used to detect a **shadowing scope declaration**: a resource declared inside
|
|
149
|
+
* another's `x-telo-scope` array shadows a module-level name of the same
|
|
150
|
+
* spelling within that scope's regions, so renaming the module-level one must
|
|
151
|
+
* not rewrite references that resolve to the scoped one. Detected structurally
|
|
152
|
+
* rather than by reading `x-telo-scope` off the kind's schema, because the
|
|
153
|
+
* question a rename needs answered is "is this spelling declared more than once
|
|
154
|
+
* in reach", which is true of any nested declaration whether or not the slot
|
|
155
|
+
* carrying it is annotated.
|
|
156
|
+
*/
|
|
157
|
+
export function resourceDeclarations(doc: AstDocument, name: string): Array<[number, number]> {
|
|
158
|
+
const found: Array<[number, number]> = [];
|
|
159
|
+
const visit = (node: AstNode): void => {
|
|
160
|
+
if (node.kind === "map") {
|
|
161
|
+
let hasKind = false;
|
|
162
|
+
let nameNode: AstScalar | undefined;
|
|
163
|
+
for (const pair of node.entries) {
|
|
164
|
+
const key = scalarString(pair.key);
|
|
165
|
+
if (key === "kind") hasKind = true;
|
|
166
|
+
if (key === "metadata" && pair.value?.kind === "map") {
|
|
167
|
+
for (const inner of pair.value.entries) {
|
|
168
|
+
if (scalarString(inner.key) === "name" && inner.value?.kind === "scalar") {
|
|
169
|
+
nameNode = inner.value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (hasKind && nameNode && scalarString(nameNode) === name) found.push(nameNode.range);
|
|
175
|
+
for (const pair of node.entries) if (pair.value) visit(pair.value);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (node.kind === "seq") {
|
|
179
|
+
for (const item of node.items) visit(item);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
if (doc.root) visit(doc.root);
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Every step in a document declaring `name:` — the span of the name scalar.
|
|
187
|
+
* More than one means the spelling is ambiguous within the resource, which is
|
|
188
|
+
* a refusal rather than a guess. */
|
|
189
|
+
export function stepDeclarations(doc: AstDocument, name: string): Array<[number, number]> {
|
|
190
|
+
const found: Array<[number, number]> = [];
|
|
191
|
+
const visit = (node: AstNode, inStepArray: boolean): void => {
|
|
192
|
+
if (node.kind === "map") {
|
|
193
|
+
// A step is a map in a sequence carrying a `name:`; a resource's own
|
|
194
|
+
// `metadata.name` is nested under `metadata:` and so never matches here.
|
|
195
|
+
if (inStepArray) {
|
|
196
|
+
for (const pair of node.entries) {
|
|
197
|
+
if (
|
|
198
|
+
scalarString(pair.key) === "name" &&
|
|
199
|
+
pair.value?.kind === "scalar" &&
|
|
200
|
+
scalarString(pair.value) === name
|
|
201
|
+
) {
|
|
202
|
+
found.push(pair.value.range);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const pair of node.entries) if (pair.value) visit(pair.value, false);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (node.kind === "seq") {
|
|
210
|
+
for (const item of node.items) visit(item, true);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
if (doc.root) visit(doc.root, false);
|
|
214
|
+
return found;
|
|
215
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Range } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which naming surface a rename targets. Only the value-level, module-local
|
|
5
|
+
* ones are supported — see `build-rename.ts` for why each of the others is a
|
|
6
|
+
* refusal rather than an omission.
|
|
7
|
+
*/
|
|
8
|
+
export type RenameSymbolKind =
|
|
9
|
+
/** A resource instance's `metadata.name`. */
|
|
10
|
+
| "resource"
|
|
11
|
+
/** A `name:` on a step inside a step array. */
|
|
12
|
+
| "step"
|
|
13
|
+
/** A key of the module doc's `variables:` / `secrets:` / `ports:` block. */
|
|
14
|
+
| "declaration";
|
|
15
|
+
|
|
16
|
+
/** What the cursor resolved to, and the span the host pre-fills in its rename
|
|
17
|
+
* box. `block` is set only for a `declaration`. */
|
|
18
|
+
export interface RenameSymbol {
|
|
19
|
+
kind: RenameSymbolKind;
|
|
20
|
+
name: string;
|
|
21
|
+
range: Range;
|
|
22
|
+
block?: "variables" | "secrets" | "ports";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One replacement. `range` is a source span in `uri`; `newText` replaces it
|
|
26
|
+
* wholesale. Spans never overlap and never cross a line, because every site is
|
|
27
|
+
* either a bare scalar value or an identifier inside one. */
|
|
28
|
+
export interface RenameEdit {
|
|
29
|
+
range: Range;
|
|
30
|
+
newText: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RenameFileEdits {
|
|
34
|
+
uri: string;
|
|
35
|
+
edits: RenameEdit[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A refusal carries the reason, always. A rename the tool declines is a
|
|
40
|
+
* decision the author has to act on — silently returning "nothing to rename"
|
|
41
|
+
* would read as "this name has no references", which is the opposite of what a
|
|
42
|
+
* refusal usually means here (an exported name has too many, in files this
|
|
43
|
+
* workspace cannot see).
|
|
44
|
+
*/
|
|
45
|
+
export type RenamePreparation =
|
|
46
|
+
| { ok: true; symbol: RenameSymbol }
|
|
47
|
+
| { ok: false; reason: string };
|
|
48
|
+
|
|
49
|
+
export type RenameResult =
|
|
50
|
+
| { ok: true; symbol: RenameSymbol; files: RenameFileEdits[] }
|
|
51
|
+
| { ok: false; reason: string };
|