@telorun/ide-support 0.13.3 → 0.14.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-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,407 @@
|
|
|
1
|
+
import { CelParseError, buildLineOffsets, checkName, offsetToPosition, parseToAst, } from "@telorun/analyzer";
|
|
2
|
+
import { chainAt } from "../cel-chain.js";
|
|
3
|
+
import { resolveNodeAtPosition, scalarString } from "../completions/resolve-node.js";
|
|
4
|
+
import { moduleDoc, moduleForFile, moduleFiles } from "../definition/manifest-navigation.js";
|
|
5
|
+
import { declarationSites, resourceDeclarations, resourceSites, stepDeclarations, stepSites, } from "./find-sites.js";
|
|
6
|
+
/**
|
|
7
|
+
* Rename a name and every reference to it.
|
|
8
|
+
*
|
|
9
|
+
* **This is a refactor, not a fix**, and the distinction is the reason it lives
|
|
10
|
+
* here rather than behind a `DiagnosticFix`. A fix is a whole-value replacement
|
|
11
|
+
* for ONE node, verified by the diagnostic that produced it; a rename is only
|
|
12
|
+
* correct when every reference moves with it, which means the operation's unit
|
|
13
|
+
* is the reference graph, not the node. Renaming `metadata.name` alone leaves
|
|
14
|
+
* every `!ref`, `resources.<name>` and `steps.<name>.result` pointing at a name
|
|
15
|
+
* that no longer exists — so a rename offered as a quick fix would break the
|
|
16
|
+
* file it claimed to repair.
|
|
17
|
+
*
|
|
18
|
+
* **Three renameable surfaces, and every other one is an explicit refusal.**
|
|
19
|
+
* What they have in common is that their reference set is *enumerable from this
|
|
20
|
+
* workspace*: a resource instance, a step, and a `variables:` / `secrets:` /
|
|
21
|
+
* `ports:` key are all module-local. A kind name, a module name and an import
|
|
22
|
+
* alias are not supported yet — their references reach schema annotations
|
|
23
|
+
* (`x-telo-ref`, `extends`, `exports.kinds`) as alias-qualified halves of a
|
|
24
|
+
* larger value, which is a materially bigger surface than a bare identifier and
|
|
25
|
+
* wants its own pass.
|
|
26
|
+
*
|
|
27
|
+
* **A name in `exports.resources` is refused outright**, and that is the load-
|
|
28
|
+
* bearing refusal. Such a name is the library's public ABI: a consumer writes
|
|
29
|
+
* `!ref Alias.name` and reads `resources.Alias.name`, in files this workspace
|
|
30
|
+
* may not contain and, for a published consumer, cannot. Renaming it is a
|
|
31
|
+
* breaking change to be *versioned*, not an edit to be applied — and a rename
|
|
32
|
+
* box that silently ships one would be the worst available framing.
|
|
33
|
+
*
|
|
34
|
+
* **Ambiguity is refused rather than guessed.** A name declared twice in reach
|
|
35
|
+
* (a `with:`-scoped resource shadowing a module-level one, two steps in one
|
|
36
|
+
* resource sharing a spelling) has references that resolve to different
|
|
37
|
+
* declarations, and no edit set is right for both.
|
|
38
|
+
*/
|
|
39
|
+
export function prepareRename(text, line, character, graph, currentFilePath, docs) {
|
|
40
|
+
const astDocs = docs ?? parseToAst(text);
|
|
41
|
+
const resolved = resolveNodeAtPosition(text, astDocs, line, character);
|
|
42
|
+
if (!resolved)
|
|
43
|
+
return { ok: false, reason: "Nothing renameable here." };
|
|
44
|
+
const mod = moduleForFile(graph, currentFilePath);
|
|
45
|
+
if (!mod) {
|
|
46
|
+
return { ok: false, reason: "This file is not part of a loaded Telo module." };
|
|
47
|
+
}
|
|
48
|
+
const lineOffsets = buildLineOffsets(text);
|
|
49
|
+
const toRange = (span) => ({
|
|
50
|
+
start: offsetToPosition(span[0], lineOffsets),
|
|
51
|
+
end: offsetToPosition(span[1], lineOffsets),
|
|
52
|
+
});
|
|
53
|
+
const symbol = symbolAt(resolved, astDocs, toRange);
|
|
54
|
+
if (!symbol)
|
|
55
|
+
return { ok: false, reason: "Nothing renameable here." };
|
|
56
|
+
if ("reason" in symbol)
|
|
57
|
+
return { ok: false, reason: symbol.reason };
|
|
58
|
+
const refusal = refuse(symbol, mod, currentFilePath, text, astDocs);
|
|
59
|
+
return refusal ? { ok: false, reason: refusal } : { ok: true, symbol };
|
|
60
|
+
}
|
|
61
|
+
/** Prepare, validate the new name, then collect every edit. */
|
|
62
|
+
export function buildRename(text, line, character, newName, graph, currentFilePath, docs) {
|
|
63
|
+
const prepared = prepareRename(text, line, character, graph, currentFilePath, docs);
|
|
64
|
+
if (!prepared.ok)
|
|
65
|
+
return prepared;
|
|
66
|
+
const { symbol } = prepared;
|
|
67
|
+
if (newName === symbol.name)
|
|
68
|
+
return { ok: true, symbol, files: [] };
|
|
69
|
+
// Every renameable surface is value-level, so the new name is checked against
|
|
70
|
+
// that half of the convention — the same rule `telo check` enforces. Renaming
|
|
71
|
+
// *into* a name the analyzer would reject is a mistake worth catching in the
|
|
72
|
+
// rename box rather than as a squiggle afterwards.
|
|
73
|
+
const violation = checkName(newName, "value", surfaceLabel(symbol));
|
|
74
|
+
if (violation)
|
|
75
|
+
return { ok: false, reason: violation.message };
|
|
76
|
+
const mod = moduleForFile(graph, currentFilePath);
|
|
77
|
+
const files = filesForRename(symbol, mod, currentFilePath, text, docs);
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const edits = editsIn(file, symbol, newName);
|
|
81
|
+
if (edits.length > 0)
|
|
82
|
+
out.push({ uri: file.uri, edits });
|
|
83
|
+
}
|
|
84
|
+
if (out.length === 0) {
|
|
85
|
+
return { ok: false, reason: `Found nothing to rename for '${symbol.name}'.` };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, symbol, files: out };
|
|
88
|
+
}
|
|
89
|
+
const DECLARATION_BLOCKS = new Set(["variables", "secrets", "ports"]);
|
|
90
|
+
const MODULE_DOC_KINDS = new Set(["Telo.Application", "Telo.Library"]);
|
|
91
|
+
const KIND_DOC_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
|
|
92
|
+
const SELF_PREFIX = "Self.";
|
|
93
|
+
/** Dispatch on what the cursor sits IN rather than on the field it is under —
|
|
94
|
+
* the posture `buildDefinition` takes, so the two features agree about what a
|
|
95
|
+
* given position means. */
|
|
96
|
+
function symbolAt(resolved, astDocs, toRange) {
|
|
97
|
+
if (resolved.cel)
|
|
98
|
+
return celSymbol(resolved.cel, toRange);
|
|
99
|
+
const path = resolved.path;
|
|
100
|
+
const node = resolved.node;
|
|
101
|
+
// A key inside `variables:` / `secrets:` / `ports:` on the module doc.
|
|
102
|
+
if (resolved.slot === "key" &&
|
|
103
|
+
node?.kind === "scalar" &&
|
|
104
|
+
path.length === 1 &&
|
|
105
|
+
DECLARATION_BLOCKS.has(path[0]) &&
|
|
106
|
+
MODULE_DOC_KINDS.has(resolved.docKind ?? "")) {
|
|
107
|
+
const name = scalarString(node);
|
|
108
|
+
if (!name)
|
|
109
|
+
return undefined;
|
|
110
|
+
return {
|
|
111
|
+
kind: "declaration",
|
|
112
|
+
name,
|
|
113
|
+
range: toRange(node.range),
|
|
114
|
+
block: path[0],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (resolved.slot !== "value" || node?.kind !== "scalar")
|
|
118
|
+
return undefined;
|
|
119
|
+
const scalar = node;
|
|
120
|
+
// A `!ref` target. `<Alias>.<name>` crosses an import boundary, where the
|
|
121
|
+
// declaration is another module's and the edit set is not this workspace's.
|
|
122
|
+
if (scalar.tag === "!ref") {
|
|
123
|
+
const raw = refText(scalar);
|
|
124
|
+
if (!raw)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (raw.startsWith(SELF_PREFIX)) {
|
|
127
|
+
const name = raw.slice(SELF_PREFIX.length);
|
|
128
|
+
const start = scalar.range[0] + SELF_PREFIX.length;
|
|
129
|
+
return { kind: "resource", name, range: toRange([start, scalar.range[1]]) };
|
|
130
|
+
}
|
|
131
|
+
if (raw.includes(".")) {
|
|
132
|
+
return {
|
|
133
|
+
reason: `'${raw}' names an instance exported by an imported module. Rename it where it is ` +
|
|
134
|
+
`declared — and only if it is not part of that module's exports.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return { kind: "resource", name: raw, range: toRange(scalar.range) };
|
|
138
|
+
}
|
|
139
|
+
const name = scalarString(scalar);
|
|
140
|
+
if (!name)
|
|
141
|
+
return undefined;
|
|
142
|
+
// `metadata.name` — a declaration site. Which surface it is depends on the
|
|
143
|
+
// document's kind, and two of the three are type-level.
|
|
144
|
+
if (path.length === 2 && path[0] === "metadata" && path[1] === "name") {
|
|
145
|
+
const docKind = resolved.docKind ?? "";
|
|
146
|
+
if (MODULE_DOC_KINDS.has(docKind)) {
|
|
147
|
+
return {
|
|
148
|
+
reason: "Renaming a module is not supported yet — its name is the canonical kind prefix, " +
|
|
149
|
+
"so every consumer's `kind:` and `extends:` values resolve through it.",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (KIND_DOC_KINDS.has(docKind)) {
|
|
153
|
+
return {
|
|
154
|
+
reason: "Renaming a kind is not supported yet — its references are alias-qualified halves " +
|
|
155
|
+
"of `kind:`, `extends:`, `x-telo-ref` and `exports.kinds` values.",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (docKind === "Telo.Import") {
|
|
159
|
+
return {
|
|
160
|
+
reason: "Renaming an import alias is not supported yet — the alias is the prefix of every " +
|
|
161
|
+
"`kind:`, `extends:` and `x-telo-ref` value that resolves through it.",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { kind: "resource", name, range: toRange(scalar.range) };
|
|
165
|
+
}
|
|
166
|
+
// A step's `name:` — the last path segment, with no enclosing `metadata`.
|
|
167
|
+
if (path.length >= 1 && path[path.length - 1] === "name") {
|
|
168
|
+
return { kind: "step", name, range: toRange(scalar.range) };
|
|
169
|
+
}
|
|
170
|
+
// A bare scalar under `exports.resources`, which is where the ABI refusal is
|
|
171
|
+
// most likely to be attempted from.
|
|
172
|
+
if (path.length === 2 && path[0] === "exports" && path[1] === "resources") {
|
|
173
|
+
return { kind: "resource", name, range: toRange(scalar.range) };
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/** A CEL chain root that names a renameable scope, with the cursor on its
|
|
178
|
+
* member. Anything deeper is a field of a resolved value, not a declaration. */
|
|
179
|
+
function celSymbol(cel, toRange) {
|
|
180
|
+
let ast;
|
|
181
|
+
try {
|
|
182
|
+
ast = cel.segment.ast();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (!(error instanceof CelParseError))
|
|
186
|
+
throw error;
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const hit = chainAt(ast, cel.offset);
|
|
190
|
+
if (!hit || hit.index !== 1)
|
|
191
|
+
return undefined;
|
|
192
|
+
const root = hit.parts[0].name;
|
|
193
|
+
const part = hit.parts[1];
|
|
194
|
+
if (root === "resources") {
|
|
195
|
+
return { kind: "resource", name: part.name, range: toRange(part.range) };
|
|
196
|
+
}
|
|
197
|
+
if (root === "steps") {
|
|
198
|
+
return { kind: "step", name: part.name, range: toRange(part.range) };
|
|
199
|
+
}
|
|
200
|
+
if (DECLARATION_BLOCKS.has(root)) {
|
|
201
|
+
return {
|
|
202
|
+
kind: "declaration",
|
|
203
|
+
name: part.name,
|
|
204
|
+
range: toRange(part.range),
|
|
205
|
+
block: root,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
function refText(scalar) {
|
|
211
|
+
const value = scalar.value;
|
|
212
|
+
if (typeof value === "string")
|
|
213
|
+
return value;
|
|
214
|
+
if (value && typeof value === "object" && typeof value.source === "string")
|
|
215
|
+
return value.source;
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function surfaceLabel(symbol) {
|
|
219
|
+
if (symbol.kind === "resource")
|
|
220
|
+
return "resource name";
|
|
221
|
+
if (symbol.kind === "step")
|
|
222
|
+
return "step name";
|
|
223
|
+
return `${symbol.block === "ports" ? "port" : symbol.block === "secrets" ? "secret" : "variable"} name`;
|
|
224
|
+
}
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Refusals
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
function refuse(symbol, mod, currentFilePath, text, astDocs) {
|
|
229
|
+
const doc = moduleDoc(mod);
|
|
230
|
+
if (symbol.kind === "resource") {
|
|
231
|
+
const exported = Array.isArray(doc?.exports?.resources)
|
|
232
|
+
? doc.exports.resources.filter((e) => typeof e === "string")
|
|
233
|
+
: [];
|
|
234
|
+
// An entry is either `<name>` (local) or `<Alias>.<name>` (a re-export,
|
|
235
|
+
// whose declaration is not this module's anyway).
|
|
236
|
+
if (exported.some((e) => e === symbol.name)) {
|
|
237
|
+
return (`'${symbol.name}' is listed in this module's 'exports.resources', so it is part of its ` +
|
|
238
|
+
`public surface — consumers reference it as '!ref <Alias>.${symbol.name}' in files this ` +
|
|
239
|
+
`workspace may not contain. Renaming it is a breaking change; version it instead.`);
|
|
240
|
+
}
|
|
241
|
+
const declarations = countResourceDeclarations(mod, symbol.name, currentFilePath, text, astDocs);
|
|
242
|
+
if (declarations > 1) {
|
|
243
|
+
return (`'${symbol.name}' is declared ${declarations} times in this module — a scoped ('with:') ` +
|
|
244
|
+
`declaration shadows a module-level one, so references resolve to different resources ` +
|
|
245
|
+
`depending on where they sit. Disambiguate them first.`);
|
|
246
|
+
}
|
|
247
|
+
if (declarations === 0) {
|
|
248
|
+
return `Could not find where '${symbol.name}' is declared in this module.`;
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
if (symbol.kind === "step") {
|
|
253
|
+
// Document-scoped, and the cursor is in this file, so the live docs are the
|
|
254
|
+
// only ones that can hold the declaration.
|
|
255
|
+
const target = astDocs.find((d) => stepDeclarations(d, symbol.name).length > 0);
|
|
256
|
+
if (!target)
|
|
257
|
+
return `Could not find a step named '${symbol.name}' in this document.`;
|
|
258
|
+
const count = stepDeclarations(target, symbol.name).length;
|
|
259
|
+
if (count > 1) {
|
|
260
|
+
return (`'${symbol.name}' names ${count} steps in this document, so 'steps.${symbol.name}.result' ` +
|
|
261
|
+
`is ambiguous. Disambiguate them first.`);
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
// A Library's declared config is its contract: an importer passes values
|
|
266
|
+
// keyed by these names, so renaming one breaks every consumer exactly as
|
|
267
|
+
// renaming an exported instance does.
|
|
268
|
+
if (doc?.kind === "Telo.Library") {
|
|
269
|
+
return (`'${symbol.block}.${symbol.name}' is part of this library's contract — importers pass ` +
|
|
270
|
+
`values keyed by that name. Renaming it is a breaking change; version it instead.`);
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
/** Declarations of a resource name across the module, counting nested (scoped)
|
|
275
|
+
* ones. The live buffer stands in for the current file's snapshot. */
|
|
276
|
+
function countResourceDeclarations(mod, name, currentFilePath, text, astDocs) {
|
|
277
|
+
let count = 0;
|
|
278
|
+
for (const file of moduleFiles(mod)) {
|
|
279
|
+
const docs = file.source === currentFilePath ? astDocs : file.astDocuments;
|
|
280
|
+
for (const doc of docs)
|
|
281
|
+
count += resourceDeclarations(doc, name).length;
|
|
282
|
+
}
|
|
283
|
+
return count;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Which files a rename may touch.
|
|
287
|
+
*
|
|
288
|
+
* A resource or a config declaration is module-scoped, so every file in the
|
|
289
|
+
* module's scope is in range. A **step is document-scoped**: `steps.<name>.result`
|
|
290
|
+
* is readable only inside the resource whose body declares the step, and a
|
|
291
|
+
* resource is one YAML document — so the edit set is that document alone, which
|
|
292
|
+
* is also what makes two same-named steps in one document the ambiguity to
|
|
293
|
+
* refuse rather than a cross-file hazard.
|
|
294
|
+
*
|
|
295
|
+
* The live buffer always stands in for the current file. The graph is a snapshot
|
|
296
|
+
* taken at the last analysis, so applying edits computed against it would write
|
|
297
|
+
* stale offsets into a file the author has since edited.
|
|
298
|
+
*/
|
|
299
|
+
function filesForRename(symbol, mod, currentFilePath, text, docs) {
|
|
300
|
+
const live = docs ?? parseToAst(text);
|
|
301
|
+
const asRenameFile = (file) => file.source === currentFilePath
|
|
302
|
+
? { uri: file.source, text, docs: live }
|
|
303
|
+
: { uri: file.source, text: file.text, docs: file.astDocuments };
|
|
304
|
+
const all = moduleFiles(mod).map(asRenameFile);
|
|
305
|
+
if (symbol.kind !== "step")
|
|
306
|
+
return all;
|
|
307
|
+
// Narrow to the one document declaring the step.
|
|
308
|
+
for (const file of all) {
|
|
309
|
+
const index = file.docs.findIndex((d) => stepDeclarations(d, symbol.name).length > 0);
|
|
310
|
+
if (index >= 0)
|
|
311
|
+
return [{ ...file, docs: [file.docs[index]] }];
|
|
312
|
+
}
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
function editsIn(file, symbol, newName) {
|
|
316
|
+
const lineOffsets = buildLineOffsets(file.text);
|
|
317
|
+
const spans = [];
|
|
318
|
+
for (const doc of file.docs) {
|
|
319
|
+
if (symbol.kind === "resource") {
|
|
320
|
+
for (const span of resourceDeclarations(doc, symbol.name))
|
|
321
|
+
spans.push(span);
|
|
322
|
+
for (const site of resourceSites(doc, symbol.name))
|
|
323
|
+
spans.push(site.range);
|
|
324
|
+
for (const span of exportEntrySpans(doc, symbol.name))
|
|
325
|
+
spans.push(span);
|
|
326
|
+
}
|
|
327
|
+
else if (symbol.kind === "step") {
|
|
328
|
+
for (const span of stepDeclarations(doc, symbol.name))
|
|
329
|
+
spans.push(span);
|
|
330
|
+
for (const site of stepSites(doc, symbol.name))
|
|
331
|
+
spans.push(site.range);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
for (const span of declarationKeySpans(doc, symbol.block, symbol.name))
|
|
335
|
+
spans.push(span);
|
|
336
|
+
for (const site of declarationSites(doc, symbol.block, symbol.name))
|
|
337
|
+
spans.push(site.range);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Sorted and de-duplicated: a host applies a set of edits, and two edits over
|
|
341
|
+
// one span — which a name reachable by two walks would produce — is an
|
|
342
|
+
// overlapping-edit error in every LSP client.
|
|
343
|
+
return dedupe(spans).map((span) => ({
|
|
344
|
+
range: {
|
|
345
|
+
start: offsetToPosition(span[0], lineOffsets),
|
|
346
|
+
end: offsetToPosition(span[1], lineOffsets),
|
|
347
|
+
},
|
|
348
|
+
newText: newName,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
function dedupe(spans) {
|
|
352
|
+
const seen = new Set();
|
|
353
|
+
const out = [];
|
|
354
|
+
for (const span of spans.sort((a, b) => a[0] - b[0] || a[1] - b[1])) {
|
|
355
|
+
const key = `${span[0]}:${span[1]}`;
|
|
356
|
+
if (seen.has(key))
|
|
357
|
+
continue;
|
|
358
|
+
seen.add(key);
|
|
359
|
+
out.push(span);
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
/** Spans of `exports.resources` entries naming the resource. Collected even
|
|
364
|
+
* though an exported name is refused, because the refusal is decided from the
|
|
365
|
+
* module doc's JSON while these come from the AST: a name reached here that the
|
|
366
|
+
* refusal did not catch (a re-export spelled `Self.<name>`) still has to move
|
|
367
|
+
* with its declaration rather than being left dangling. */
|
|
368
|
+
function exportEntrySpans(doc, name) {
|
|
369
|
+
const out = [];
|
|
370
|
+
if (doc.root?.kind !== "map")
|
|
371
|
+
return out;
|
|
372
|
+
for (const pair of doc.root.entries) {
|
|
373
|
+
if (scalarString(pair.key) !== "exports" || pair.value?.kind !== "map")
|
|
374
|
+
continue;
|
|
375
|
+
for (const inner of pair.value.entries) {
|
|
376
|
+
if (scalarString(inner.key) !== "resources" || inner.value?.kind !== "seq")
|
|
377
|
+
continue;
|
|
378
|
+
for (const item of inner.value.items) {
|
|
379
|
+
if (item.kind !== "scalar")
|
|
380
|
+
continue;
|
|
381
|
+
const value = scalarString(item);
|
|
382
|
+
if (value === name)
|
|
383
|
+
out.push(item.range);
|
|
384
|
+
else if (value === `${SELF_PREFIX}${name}`) {
|
|
385
|
+
out.push([item.range[0] + SELF_PREFIX.length, item.range[1]]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
/** The `variables:` / `secrets:` / `ports:` key itself. */
|
|
393
|
+
function declarationKeySpans(doc, block, name) {
|
|
394
|
+
const out = [];
|
|
395
|
+
if (doc.root?.kind !== "map")
|
|
396
|
+
return out;
|
|
397
|
+
for (const pair of doc.root.entries) {
|
|
398
|
+
if (scalarString(pair.key) !== block || pair.value?.kind !== "map")
|
|
399
|
+
continue;
|
|
400
|
+
for (const inner of pair.value.entries) {
|
|
401
|
+
if (inner.key.kind === "scalar" && scalarString(inner.key) === name) {
|
|
402
|
+
out.push(inner.key.range);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type AstDocument } from "@telorun/analyzer";
|
|
2
|
+
/**
|
|
3
|
+
* Every place a name is written, found over the read-only YAML AST plus the CEL
|
|
4
|
+
* AST inside each scalar. Document offsets in, document offsets out — the
|
|
5
|
+
* caller maps them to `Range`s once, with the line table it already built.
|
|
6
|
+
*
|
|
7
|
+
* **Offsets rather than ranges, and a flat list rather than a tree**, because
|
|
8
|
+
* the two things a rename must guarantee are that no site is missed and that no
|
|
9
|
+
* two edits overlap. Both are properties of a flat, sorted offset list, and
|
|
10
|
+
* neither is checkable once the sites have been shaped per-feature.
|
|
11
|
+
*
|
|
12
|
+
* A `!ref` scalar's own range is its VALUE, excluding the tag (`!ref other` →
|
|
13
|
+
* the span of `other`), so a local reference is a whole-node replacement. A CEL
|
|
14
|
+
* identifier is a sub-span of its scalar, taken from `propertyRange` — which the
|
|
15
|
+
* analyzer's `CelNode` has carried since it was written, for exactly this.
|
|
16
|
+
*/
|
|
17
|
+
export interface NameSite {
|
|
18
|
+
/** `[start, end]` in document offsets — the identifier alone, never the
|
|
19
|
+
* enclosing scalar or the `!ref`/`!cel` tag. */
|
|
20
|
+
range: [number, number];
|
|
21
|
+
}
|
|
22
|
+
/** A resource's references within one document: `!ref <name>` (and its `Self.`
|
|
23
|
+
* form) plus `resources.<name>` in CEL. */
|
|
24
|
+
export declare function resourceSites(doc: AstDocument, name: string): NameSite[];
|
|
25
|
+
/** A step's references within its declaring document: `steps.<name>` in CEL.
|
|
26
|
+
* Deliberately document-scoped — `steps.<name>.result` is readable only inside
|
|
27
|
+
* the resource whose body declares the step, and a resource is one document. */
|
|
28
|
+
export declare function stepSites(doc: AstDocument, name: string): NameSite[];
|
|
29
|
+
/** A `variables:` / `secrets:` / `ports:` entry's reads: `<block>.<name>`. */
|
|
30
|
+
export declare function declarationSites(doc: AstDocument, block: string, name: string): NameSite[];
|
|
31
|
+
/**
|
|
32
|
+
* Every map in a document that declares a resource named `name` — a `kind:`
|
|
33
|
+
* beside a `metadata.name`.
|
|
34
|
+
*
|
|
35
|
+
* Used to detect a **shadowing scope declaration**: a resource declared inside
|
|
36
|
+
* another's `x-telo-scope` array shadows a module-level name of the same
|
|
37
|
+
* spelling within that scope's regions, so renaming the module-level one must
|
|
38
|
+
* not rewrite references that resolve to the scoped one. Detected structurally
|
|
39
|
+
* rather than by reading `x-telo-scope` off the kind's schema, because the
|
|
40
|
+
* question a rename needs answered is "is this spelling declared more than once
|
|
41
|
+
* in reach", which is true of any nested declaration whether or not the slot
|
|
42
|
+
* carrying it is annotated.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resourceDeclarations(doc: AstDocument, name: string): Array<[number, number]>;
|
|
45
|
+
/** Every step in a document declaring `name:` — the span of the name scalar.
|
|
46
|
+
* More than one means the spelling is ambiguous within the resource, which is
|
|
47
|
+
* a refusal rather than a guess. */
|
|
48
|
+
export declare function stepDeclarations(doc: AstDocument, name: string): Array<[number, number]>;
|
|
49
|
+
//# sourceMappingURL=find-sites.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"find-sites.d.ts","sourceRoot":"","sources":["../../src/rename/find-sites.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAIjB,MAAM,mBAAmB,CAAC;AAK3B;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ;IACvB;qDACiD;IACjD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzB;AAqED;4CAC4C;AAC5C,wBAAgB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAYxE;AAED;;iFAEiF;AACjF,wBAAgB,SAAS,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAUpE;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAU1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA2B5F;AAED;;qCAEqC;AACrC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA0BxF"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { CelParseError, } from "@telorun/analyzer";
|
|
2
|
+
import { walkCel } from "../cel-chain.js";
|
|
3
|
+
import { scalarString } from "../completions/resolve-node.js";
|
|
4
|
+
/** Walk every scalar of a document, in source order. */
|
|
5
|
+
function eachScalar(node, visit) {
|
|
6
|
+
if (node.kind === "map") {
|
|
7
|
+
for (const pair of node.entries) {
|
|
8
|
+
eachScalar(pair.key, visit);
|
|
9
|
+
if (pair.value)
|
|
10
|
+
eachScalar(pair.value, visit);
|
|
11
|
+
}
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (node.kind === "seq") {
|
|
15
|
+
for (const item of node.items)
|
|
16
|
+
eachScalar(item, visit);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
visit(node);
|
|
20
|
+
}
|
|
21
|
+
/** Every CEL node of a scalar's every segment.
|
|
22
|
+
*
|
|
23
|
+
* A body that does not parse yields nothing: an author mid-edit is not a
|
|
24
|
+
* reason to fail a rename, and the analyzer reports the syntax error itself.
|
|
25
|
+
* Only that failure is tolerated — a defect in the CEL wrapper propagates,
|
|
26
|
+
* the posture `resolveCelTarget` already takes. */
|
|
27
|
+
function eachCelNode(scalar, visit) {
|
|
28
|
+
for (const segment of scalar.celSegments()) {
|
|
29
|
+
let ast;
|
|
30
|
+
try {
|
|
31
|
+
ast = segment.ast();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!(error instanceof CelParseError))
|
|
35
|
+
throw error;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
walkCel(ast, visit);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** `<scope>.<name>` read as a member access — `resources.db`, `steps.build`,
|
|
42
|
+
* `variables.apiUrl`. Returns the span of `name` alone. */
|
|
43
|
+
function scopeMemberSite(node, scope, name) {
|
|
44
|
+
if (node.kind !== "member" || node.property !== name)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (node.target.kind !== "ident" || node.target.name !== scope)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { range: node.propertyRange };
|
|
49
|
+
}
|
|
50
|
+
const SELF_PREFIX = "Self.";
|
|
51
|
+
/** The span of `name` inside a `!ref` scalar, or undefined when the scalar names
|
|
52
|
+
* something else. Accepts the bare form and the `Self.`-qualified one, which
|
|
53
|
+
* also resolves locally; `<Alias>.<name>` is a different module's export and is
|
|
54
|
+
* deliberately not matched. */
|
|
55
|
+
function refSite(scalar, name) {
|
|
56
|
+
if (scalar.tag !== "!ref")
|
|
57
|
+
return undefined;
|
|
58
|
+
const [start, end] = scalar.range;
|
|
59
|
+
const raw = refText(scalar);
|
|
60
|
+
if (raw === name)
|
|
61
|
+
return { range: [start, end] };
|
|
62
|
+
if (raw === `${SELF_PREFIX}${name}`)
|
|
63
|
+
return { range: [start + SELF_PREFIX.length, end] };
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/** A `!ref` scalar's resolved value is a `TaggedSentinel`, so the written text
|
|
67
|
+
* is read off the sentinel rather than assumed to be a plain string. */
|
|
68
|
+
function refText(scalar) {
|
|
69
|
+
const value = scalar.value;
|
|
70
|
+
if (typeof value === "string")
|
|
71
|
+
return value;
|
|
72
|
+
if (value && typeof value === "object" && typeof value.source === "string")
|
|
73
|
+
return value.source;
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
/** A resource's references within one document: `!ref <name>` (and its `Self.`
|
|
77
|
+
* form) plus `resources.<name>` in CEL. */
|
|
78
|
+
export function resourceSites(doc, name) {
|
|
79
|
+
const sites = [];
|
|
80
|
+
if (!doc.root)
|
|
81
|
+
return sites;
|
|
82
|
+
eachScalar(doc.root, (scalar) => {
|
|
83
|
+
const ref = refSite(scalar, name);
|
|
84
|
+
if (ref)
|
|
85
|
+
sites.push(ref);
|
|
86
|
+
eachCelNode(scalar, (node) => {
|
|
87
|
+
const site = scopeMemberSite(node, "resources", name);
|
|
88
|
+
if (site)
|
|
89
|
+
sites.push(site);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return sites;
|
|
93
|
+
}
|
|
94
|
+
/** A step's references within its declaring document: `steps.<name>` in CEL.
|
|
95
|
+
* Deliberately document-scoped — `steps.<name>.result` is readable only inside
|
|
96
|
+
* the resource whose body declares the step, and a resource is one document. */
|
|
97
|
+
export function stepSites(doc, name) {
|
|
98
|
+
const sites = [];
|
|
99
|
+
if (!doc.root)
|
|
100
|
+
return sites;
|
|
101
|
+
eachScalar(doc.root, (scalar) => {
|
|
102
|
+
eachCelNode(scalar, (node) => {
|
|
103
|
+
const site = scopeMemberSite(node, "steps", name);
|
|
104
|
+
if (site)
|
|
105
|
+
sites.push(site);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
return sites;
|
|
109
|
+
}
|
|
110
|
+
/** A `variables:` / `secrets:` / `ports:` entry's reads: `<block>.<name>`. */
|
|
111
|
+
export function declarationSites(doc, block, name) {
|
|
112
|
+
const sites = [];
|
|
113
|
+
if (!doc.root)
|
|
114
|
+
return sites;
|
|
115
|
+
eachScalar(doc.root, (scalar) => {
|
|
116
|
+
eachCelNode(scalar, (node) => {
|
|
117
|
+
const site = scopeMemberSite(node, block, name);
|
|
118
|
+
if (site)
|
|
119
|
+
sites.push(site);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
return sites;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every map in a document that declares a resource named `name` — a `kind:`
|
|
126
|
+
* beside a `metadata.name`.
|
|
127
|
+
*
|
|
128
|
+
* Used to detect a **shadowing scope declaration**: a resource declared inside
|
|
129
|
+
* another's `x-telo-scope` array shadows a module-level name of the same
|
|
130
|
+
* spelling within that scope's regions, so renaming the module-level one must
|
|
131
|
+
* not rewrite references that resolve to the scoped one. Detected structurally
|
|
132
|
+
* rather than by reading `x-telo-scope` off the kind's schema, because the
|
|
133
|
+
* question a rename needs answered is "is this spelling declared more than once
|
|
134
|
+
* in reach", which is true of any nested declaration whether or not the slot
|
|
135
|
+
* carrying it is annotated.
|
|
136
|
+
*/
|
|
137
|
+
export function resourceDeclarations(doc, name) {
|
|
138
|
+
const found = [];
|
|
139
|
+
const visit = (node) => {
|
|
140
|
+
if (node.kind === "map") {
|
|
141
|
+
let hasKind = false;
|
|
142
|
+
let nameNode;
|
|
143
|
+
for (const pair of node.entries) {
|
|
144
|
+
const key = scalarString(pair.key);
|
|
145
|
+
if (key === "kind")
|
|
146
|
+
hasKind = true;
|
|
147
|
+
if (key === "metadata" && pair.value?.kind === "map") {
|
|
148
|
+
for (const inner of pair.value.entries) {
|
|
149
|
+
if (scalarString(inner.key) === "name" && inner.value?.kind === "scalar") {
|
|
150
|
+
nameNode = inner.value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (hasKind && nameNode && scalarString(nameNode) === name)
|
|
156
|
+
found.push(nameNode.range);
|
|
157
|
+
for (const pair of node.entries)
|
|
158
|
+
if (pair.value)
|
|
159
|
+
visit(pair.value);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (node.kind === "seq") {
|
|
163
|
+
for (const item of node.items)
|
|
164
|
+
visit(item);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
if (doc.root)
|
|
168
|
+
visit(doc.root);
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
/** Every step in a document declaring `name:` — the span of the name scalar.
|
|
172
|
+
* More than one means the spelling is ambiguous within the resource, which is
|
|
173
|
+
* a refusal rather than a guess. */
|
|
174
|
+
export function stepDeclarations(doc, name) {
|
|
175
|
+
const found = [];
|
|
176
|
+
const visit = (node, inStepArray) => {
|
|
177
|
+
if (node.kind === "map") {
|
|
178
|
+
// A step is a map in a sequence carrying a `name:`; a resource's own
|
|
179
|
+
// `metadata.name` is nested under `metadata:` and so never matches here.
|
|
180
|
+
if (inStepArray) {
|
|
181
|
+
for (const pair of node.entries) {
|
|
182
|
+
if (scalarString(pair.key) === "name" &&
|
|
183
|
+
pair.value?.kind === "scalar" &&
|
|
184
|
+
scalarString(pair.value) === name) {
|
|
185
|
+
found.push(pair.value.range);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const pair of node.entries)
|
|
190
|
+
if (pair.value)
|
|
191
|
+
visit(pair.value, false);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (node.kind === "seq") {
|
|
195
|
+
for (const item of node.items)
|
|
196
|
+
visit(item, true);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
if (doc.root)
|
|
200
|
+
visit(doc.root, false);
|
|
201
|
+
return found;
|
|
202
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rename/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EACV,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildRename, prepareRename } from "./build-rename.js";
|