@levr-one/cli 0.12.4 → 0.12.6
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/{addHandler-LO6SChns.js → addHandler-ybX54_3f.js} +6 -6
- package/dist/{appendHandler-DtxLpPu6.js → appendHandler-CKkl9ymw.js} +7 -7
- package/dist/{artifact-upload-BvBSevIx.js → artifact-upload-Ba801tb6.js} +1 -1
- package/dist/{attachHandler-DXZi2ILT.js → attachHandler-CfNvVAV2.js} +7 -7
- package/dist/cli.js +77 -18
- package/dist/{connect-DVjgyfls.js → connect-yq3fGXkW.js} +9 -8
- package/dist/{find-workspace-Bl4rfr73.js → find-workspace-4Us2vEb7.js} +1 -1
- package/dist/{importHandler-uFdalKtw.js → importHandler-bYBWYC_0.js} +8 -8
- package/dist/{listHandler-BW92JLCl.js → listHandler-Du_YGzre.js} +4 -4
- package/dist/{loginHandler-C9Oi92IY.js → loginHandler-PbKmDAGo.js} +4 -4
- package/dist/{logoutHandler-Cf5aI1wq.js → logoutHandler-DxnzGPYx.js} +1 -1
- package/dist/{pushHandler-CXFZt3SO.js → pushHandler-BulraEwP.js} +15 -13
- package/dist/refsHandler-DtcceTRP.js +1757 -0
- package/dist/{resolve-token-ByZq05Us.js → resolve-token-DKEXT7SA.js} +8 -5
- package/dist/{resolve-workspace-BbK3uYQc.js → resolve-workspace-D8y7twod.js} +37 -5
- package/dist/{runHandler-IyPhhiGs.js → runHandler-B-u-0eRk.js} +25 -13
- package/dist/{sdk-client-B4FMd_Fa.js → sdk-client-BgzKzVIR.js} +1 -1
- package/dist/{selectHandler-D6cvq60S.js → selectHandler-C0gs-Q70.js} +9 -4
- package/dist/{statusHandler-BQXn93-r.js → statusHandler-Dnnh80ZN.js} +7 -5
- package/dist/{token-refresh-CJB7Qlxy.js → token-refresh-BpFuU4Ij.js} +26 -2
- package/package.json +1 -1
- /package/dist/{currentHandler-QhCywr3S.js → currentHandler-PaLZKLGF.js} +0 -0
- /package/dist/{env-CvjMSRd7.js → env-DjeRwmZ7.js} +0 -0
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
//#region ../code-intel/dist/shared.js
|
|
8
|
+
/** Directories never worth walking — they dwarf the real source tree. */
|
|
9
|
+
const IGNORED_DIRS = [
|
|
10
|
+
"node_modules",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
"coverage",
|
|
14
|
+
".git",
|
|
15
|
+
".turbo",
|
|
16
|
+
".yarn",
|
|
17
|
+
".next",
|
|
18
|
+
"out"
|
|
19
|
+
];
|
|
20
|
+
/** The files the TypeScript engines load. */
|
|
21
|
+
const TS_SOURCE = /\.tsx?$/;
|
|
22
|
+
const TEST_FILE_RE = /\.(test|spec)\.[cm]?tsx?$/;
|
|
23
|
+
function isTestFile(filePath) {
|
|
24
|
+
return TEST_FILE_RE.test(filePath);
|
|
25
|
+
}
|
|
26
|
+
/** Walk up from `start` until a directory containing `.git` is found. */
|
|
27
|
+
function findProjectRoot(start = process.cwd()) {
|
|
28
|
+
let dir = path$1.resolve(start);
|
|
29
|
+
for (;;) {
|
|
30
|
+
if (fs.existsSync(path$1.join(dir, ".git"))) return dir;
|
|
31
|
+
const parent = path$1.dirname(dir);
|
|
32
|
+
if (parent === dir) return path$1.resolve(start);
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Nearest `tsconfig.json` at or above `scopeRoot`, bounded by `projectRoot`.
|
|
38
|
+
*
|
|
39
|
+
* Load-bearing, not a nicety. `apps/backender` mandates non-relative imports
|
|
40
|
+
* (`import { X } from 'src/service/x'`), resolved through the `paths` mapping in
|
|
41
|
+
* its tsconfig. Without those options the specifier does not resolve, the import
|
|
42
|
+
* never binds to the declaration, and a reference search cannot see the
|
|
43
|
+
* consumer — so the gate reports a well-wired service as orphaned and FAILS
|
|
44
|
+
* CLOSED ON CORRECT CODE.
|
|
45
|
+
*
|
|
46
|
+
* That is a worse failure than the one this gate replaced: Axon was confidently
|
|
47
|
+
* wrong, and a gate that fails correct work is confidently wrong with authority.
|
|
48
|
+
*/
|
|
49
|
+
function findTsConfig(scopeRoot, projectRoot) {
|
|
50
|
+
let dir = path$1.resolve(scopeRoot);
|
|
51
|
+
const stop = path$1.resolve(projectRoot);
|
|
52
|
+
for (;;) {
|
|
53
|
+
const candidate = path$1.join(dir, "tsconfig.json");
|
|
54
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
55
|
+
if (dir === stop) return null;
|
|
56
|
+
const parent = path$1.dirname(dir);
|
|
57
|
+
if (parent === dir) return null;
|
|
58
|
+
dir = parent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function listSourceFiles(scopeRoot, extension = TS_SOURCE, skipDir) {
|
|
62
|
+
if (extension.test(scopeRoot) && fs.statSync(scopeRoot, { throwIfNoEntry: false })?.isFile()) return [scopeRoot];
|
|
63
|
+
const files = [];
|
|
64
|
+
const walk = (dir) => {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
72
|
+
if (IGNORED_DIRS.includes(entry.name) || skipDir?.(dir, entry.name)) continue;
|
|
73
|
+
walk(path$1.join(dir, entry.name));
|
|
74
|
+
} else if (extension.test(entry.name)) files.push(path$1.join(dir, entry.name));
|
|
75
|
+
};
|
|
76
|
+
walk(scopeRoot);
|
|
77
|
+
return files.sort();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A name so common that answering for it would load the machine's memory
|
|
81
|
+
* rather than a project — `id`, `name`, `run` — unscoped or in a scope
|
|
82
|
+
* where it appears everywhere. A misconfigured GATE (exit 2 in the CLIs),
|
|
83
|
+
* never a verdict: the remedy is `--scope`, and running unbounded ended in
|
|
84
|
+
* a SIGKILLed 19 GB `tsgo --lsp` with no answer at all (ENG-5121 review
|
|
85
|
+
* F-025).
|
|
86
|
+
*/
|
|
87
|
+
var SymbolTooCommonError = class extends Error {
|
|
88
|
+
symbol;
|
|
89
|
+
count;
|
|
90
|
+
limit;
|
|
91
|
+
what;
|
|
92
|
+
constructor(symbol, count, limit, what) {
|
|
93
|
+
super(`"${symbol}" is too common to resolve here: ${count} ${what}, above the ${limit} limit. Narrow it with --scope (e.g. --scope apps/backender/src/<module>).`);
|
|
94
|
+
this.symbol = symbol;
|
|
95
|
+
this.count = count;
|
|
96
|
+
this.limit = limit;
|
|
97
|
+
this.what = what;
|
|
98
|
+
this.name = "SymbolTooCommonError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function escapeRegExp$1(s) {
|
|
102
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Does this source text LOOK like it declares `symbol` at top level?
|
|
106
|
+
*
|
|
107
|
+
* A candidate filter only — cheap enough to run over every file in scope so
|
|
108
|
+
* an engine knows which files to ask the compiler about. The compiler (or the
|
|
109
|
+
* LSP server's symbol tree) is what decides; this must merely never say "no"
|
|
110
|
+
* for a file that does declare the symbol.
|
|
111
|
+
*/
|
|
112
|
+
function looksLikeTopLevelDeclaration(symbol, text) {
|
|
113
|
+
const name = escapeRegExp$1(symbol);
|
|
114
|
+
if (new RegExp(String.raw`^[ \t]*(?:export\s+)?(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:class|function\s*\*?|interface|type|enum|const\s+enum|const|let|var)\s+${name}\b`, "m").test(text)) return true;
|
|
115
|
+
return new RegExp(String.raw`(?:^|,)[ \t]*${name}\s*[=:,;]`, "m").test(text);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Does this source text LOOK like it declares a class/interface MEMBER named
|
|
119
|
+
* `symbol`? Covers a decorator on the same line, a definite-assignment `!`,
|
|
120
|
+
* and a member on the class's own line (`class A { X = 1 }`) — all shapes
|
|
121
|
+
* ts-morph's `getMembers()` returns and the earlier filter missed
|
|
122
|
+
* (ENG-5121 review F-023).
|
|
123
|
+
*/
|
|
124
|
+
function looksLikeMemberDeclaration(symbol, text) {
|
|
125
|
+
return new RegExp(String.raw`(?:^[ \t]*|[{;][ \t]*)(?:@\w+(?:\([^)]*\))?\s*)*(?:(?:public|private|protected|static|readonly|abstract|override|async|declare|accessor|get|set)\s+)*\*?${escapeRegExp$1(symbol)}\s*[!?]?\s*[(<:=;,}]`, "m").test(text);
|
|
126
|
+
}
|
|
127
|
+
/** Convert an absolute path under `projectRoot` to the repo-relative form results use. */
|
|
128
|
+
function relativeTo(projectRoot, absolute) {
|
|
129
|
+
return path$1.relative(projectRoot, absolute) || absolute;
|
|
130
|
+
}
|
|
131
|
+
/** An empty "not found" result, so both engines produce a byte-identical one. */
|
|
132
|
+
function notFound(filesScanned, tsConfigPath) {
|
|
133
|
+
return {
|
|
134
|
+
found: false,
|
|
135
|
+
resolvedAsMember: false,
|
|
136
|
+
declarationFiles: [],
|
|
137
|
+
referenceCount: 0,
|
|
138
|
+
referencingFiles: [],
|
|
139
|
+
excludedTestReferences: 0,
|
|
140
|
+
filesScanned,
|
|
141
|
+
tsConfigPath
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Upper bound on source files one request may load.
|
|
146
|
+
*
|
|
147
|
+
* A TS program holds every loaded file's AST and type information in memory, so
|
|
148
|
+
* cost scales with the file count. Node's default old-space is ~4GB, and an
|
|
149
|
+
* UNSCOPED run over this monorepo (11,460 `.ts`/`.tsx` files) exceeds it:
|
|
150
|
+
*
|
|
151
|
+
* FATAL ERROR: Ineffective mark-compacts near heap limit
|
|
152
|
+
* Allocation failed - JavaScript heap out of memory
|
|
153
|
+
*
|
|
154
|
+
* That is the worst possible failure for a gate. It exits 134 with a V8 stack
|
|
155
|
+
* trace and no verdict — not a pass, not a fail, just noise a caller has to
|
|
156
|
+
* interpret. The gate framework exists to make "verified" and "not verified"
|
|
157
|
+
* distinguishable, and an OOM is neither.
|
|
158
|
+
*
|
|
159
|
+
* 8000 sits above the largest legitimate single scope in this repo
|
|
160
|
+
* (`apps/backender` at 6,314, which resolves comfortably) and below the
|
|
161
|
+
* whole-repo set that does not. Override deliberately with `--max-files` if a
|
|
162
|
+
* genuinely larger scope is ever needed — and expect to raise Node's heap with
|
|
163
|
+
* `NODE_OPTIONS=--max-old-space-size=...` at the same time.
|
|
164
|
+
*
|
|
165
|
+
* The cap applies to every engine. An LSP engine would survive a larger
|
|
166
|
+
* scope, but an unscoped gate must behave identically whichever engine the
|
|
167
|
+
* workspace has bound — "exit 2, narrow the scope" — or flipping the binding
|
|
168
|
+
* would change verdicts for reasons that have nothing to do with the code.
|
|
169
|
+
* (Moved from `scripts/gates/symbol-referenced.lib.ts`, ENG-7284 D5.)
|
|
170
|
+
*/
|
|
171
|
+
const DEFAULT_MAX_FILES = 8e3;
|
|
172
|
+
/** Thrown when the requested scope holds more files than the resolver will load. */
|
|
173
|
+
var ScopeTooBroadError = class extends Error {
|
|
174
|
+
fileCount;
|
|
175
|
+
maxFiles;
|
|
176
|
+
scopeLabel;
|
|
177
|
+
constructor(fileCount, maxFiles, scopeLabel) {
|
|
178
|
+
super(`scope "${scopeLabel}" contains ${fileCount} source files, above the ${maxFiles}-file limit. Loading them all would exhaust Node's heap and abort with no verdict. Narrow it with --scope (e.g. --scope apps/backender/src), or raise the cap deliberately with --max-files.`);
|
|
179
|
+
this.fileCount = fileCount;
|
|
180
|
+
this.maxFiles = maxFiles;
|
|
181
|
+
this.scopeLabel = scopeLabel;
|
|
182
|
+
this.name = "ScopeTooBroadError";
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* A toolchain the engine needs is missing or unusable in the target checkout
|
|
187
|
+
* (no `typescript`, no tsgo binary, no `rust-analyzer`, an index that never
|
|
188
|
+
* settles). Exit 2 in every CLI: a misconfigured environment, never a
|
|
189
|
+
* verdict. `remedy` says what to install.
|
|
190
|
+
*/
|
|
191
|
+
var ToolchainError = class extends Error {
|
|
192
|
+
remedy;
|
|
193
|
+
constructor(message, remedy) {
|
|
194
|
+
super(`${message} ${remedy}`);
|
|
195
|
+
this.remedy = remedy;
|
|
196
|
+
this.name = "ToolchainError";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region ../code-intel/dist/lsp-client.js
|
|
202
|
+
/** How long to wait for any single server response before failing CLOSED. */
|
|
203
|
+
const RESPONSE_TIMEOUT_MS = 12e4;
|
|
204
|
+
/** How much of the server's stderr to keep for the exit diagnostic. */
|
|
205
|
+
const STDERR_TAIL_BYTES = 4096;
|
|
206
|
+
var LspClient = class LspClient {
|
|
207
|
+
options;
|
|
208
|
+
proc;
|
|
209
|
+
buffer = Buffer.alloc(0);
|
|
210
|
+
nextId = 1;
|
|
211
|
+
pending = /* @__PURE__ */ new Map();
|
|
212
|
+
opened = /* @__PURE__ */ new Set();
|
|
213
|
+
exited = null;
|
|
214
|
+
stderrTail = "";
|
|
215
|
+
listeners = /* @__PURE__ */ new Set();
|
|
216
|
+
name;
|
|
217
|
+
responseTimeoutMs;
|
|
218
|
+
rootDir;
|
|
219
|
+
constructor(options) {
|
|
220
|
+
this.options = options;
|
|
221
|
+
this.rootDir = path$1.resolve(options.rootDir);
|
|
222
|
+
this.name = options.name;
|
|
223
|
+
this.responseTimeoutMs = options.responseTimeoutMs ?? RESPONSE_TIMEOUT_MS;
|
|
224
|
+
const bin = options.bin;
|
|
225
|
+
const [cmd, args] = bin.endsWith(".js") ? [process.execPath, [bin, ...options.args]] : [bin, [...options.args]];
|
|
226
|
+
this.proc = spawn(cmd, args, {
|
|
227
|
+
cwd: this.rootDir,
|
|
228
|
+
stdio: [
|
|
229
|
+
"pipe",
|
|
230
|
+
"pipe",
|
|
231
|
+
"pipe"
|
|
232
|
+
]
|
|
233
|
+
});
|
|
234
|
+
this.proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
235
|
+
this.proc.stderr.on("data", (chunk) => {
|
|
236
|
+
this.stderrTail = (this.stderrTail + chunk.toString("utf8")).slice(-STDERR_TAIL_BYTES);
|
|
237
|
+
});
|
|
238
|
+
this.proc.stdin.on("error", (err) => this.fail(err));
|
|
239
|
+
this.proc.on("exit", (code, signal) => {
|
|
240
|
+
const tail = this.stderrTail.trim();
|
|
241
|
+
this.fail(/* @__PURE__ */ new Error(`${this.name} exited (code=${code ?? "null"}, signal=${signal ?? "null"}) before answering` + (tail ? `\n--- ${this.name} stderr (tail) ---\n${tail}` : "")));
|
|
242
|
+
});
|
|
243
|
+
this.proc.on("error", (err) => this.fail(err));
|
|
244
|
+
}
|
|
245
|
+
/** Record the fatal error once and reject everything still waiting. */
|
|
246
|
+
fail(err) {
|
|
247
|
+
if (!this.exited) this.exited = err;
|
|
248
|
+
for (const p of this.pending.values()) p.reject(this.exited);
|
|
249
|
+
this.pending.clear();
|
|
250
|
+
}
|
|
251
|
+
/** Spawn the server and complete the `initialize` handshake. */
|
|
252
|
+
static async start(options) {
|
|
253
|
+
const client = new LspClient(options);
|
|
254
|
+
const rootUri = pathToFileURL(client.rootDir).href;
|
|
255
|
+
try {
|
|
256
|
+
await client.handshake(rootUri);
|
|
257
|
+
} catch (e) {
|
|
258
|
+
client.proc.kill();
|
|
259
|
+
throw e;
|
|
260
|
+
}
|
|
261
|
+
return client;
|
|
262
|
+
}
|
|
263
|
+
async handshake(rootUri) {
|
|
264
|
+
await this.request("initialize", {
|
|
265
|
+
processId: process.pid,
|
|
266
|
+
rootUri,
|
|
267
|
+
capabilities: mergeCapabilities({
|
|
268
|
+
workspace: { configuration: true },
|
|
269
|
+
textDocument: { documentSymbol: { hierarchicalDocumentSymbolSupport: true } }
|
|
270
|
+
}, this.options.capabilities ?? {}),
|
|
271
|
+
...this.options.initializationOptions !== void 0 ? { initializationOptions: this.options.initializationOptions } : {},
|
|
272
|
+
workspaceFolders: [{
|
|
273
|
+
uri: rootUri,
|
|
274
|
+
name: path$1.basename(this.rootDir)
|
|
275
|
+
}]
|
|
276
|
+
});
|
|
277
|
+
this.notify("initialized", {});
|
|
278
|
+
}
|
|
279
|
+
write(message) {
|
|
280
|
+
if (this.exited) throw this.exited;
|
|
281
|
+
const body = JSON.stringify(message);
|
|
282
|
+
this.proc.stdin.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
283
|
+
}
|
|
284
|
+
onData(chunk) {
|
|
285
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
286
|
+
for (;;) {
|
|
287
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
288
|
+
if (headerEnd < 0) return;
|
|
289
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
290
|
+
const lengthMatch = /Content-Length: (\d+)/i.exec(header);
|
|
291
|
+
if (!lengthMatch) {
|
|
292
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const length = Number(lengthMatch[1]);
|
|
296
|
+
const bodyStart = headerEnd + 4;
|
|
297
|
+
if (this.buffer.length < bodyStart + length) return;
|
|
298
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length);
|
|
299
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
300
|
+
let message;
|
|
301
|
+
try {
|
|
302
|
+
message = JSON.parse(body.toString("utf8"));
|
|
303
|
+
} catch (e) {
|
|
304
|
+
this.fail(/* @__PURE__ */ new Error(`${this.name} sent malformed JSON: ${e.message}`));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.dispatch(message);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
dispatch(message) {
|
|
311
|
+
if (message.method !== void 0 && message.id !== void 0) {
|
|
312
|
+
let result = null;
|
|
313
|
+
if (message.method === "workspace/configuration") result = (message.params?.items ?? []).map(() => null);
|
|
314
|
+
this.write({
|
|
315
|
+
jsonrpc: "2.0",
|
|
316
|
+
id: message.id,
|
|
317
|
+
result
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (message.method !== void 0) {
|
|
322
|
+
for (const listener of this.listeners) listener(message);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (message.id !== void 0) {
|
|
326
|
+
const waiter = this.pending.get(message.id);
|
|
327
|
+
if (waiter) {
|
|
328
|
+
this.pending.delete(message.id);
|
|
329
|
+
waiter.resolve(message);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
request(method, params) {
|
|
334
|
+
const id = this.nextId++;
|
|
335
|
+
return new Promise((resolve$1, reject) => {
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
this.pending.delete(id);
|
|
338
|
+
reject(/* @__PURE__ */ new Error(`${this.name} did not answer ${method} within ${this.responseTimeoutMs} ms`));
|
|
339
|
+
}, this.responseTimeoutMs);
|
|
340
|
+
this.pending.set(id, {
|
|
341
|
+
resolve: (m) => {
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
if (m.error) reject(/* @__PURE__ */ new Error(`${method} failed: ${m.error.message}`));
|
|
344
|
+
else resolve$1(m);
|
|
345
|
+
},
|
|
346
|
+
reject: (e) => {
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
reject(e);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
this.write({
|
|
353
|
+
jsonrpc: "2.0",
|
|
354
|
+
id,
|
|
355
|
+
method,
|
|
356
|
+
params
|
|
357
|
+
});
|
|
358
|
+
} catch (e) {
|
|
359
|
+
clearTimeout(timer);
|
|
360
|
+
this.pending.delete(id);
|
|
361
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
notify(method, params) {
|
|
366
|
+
this.write({
|
|
367
|
+
jsonrpc: "2.0",
|
|
368
|
+
method,
|
|
369
|
+
params
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Open a file so the server loads the project that contains it, and WAIT
|
|
374
|
+
* for that load to settle (obligation 2 + R1/F-11). Idempotent.
|
|
375
|
+
*/
|
|
376
|
+
async open(absolutePath) {
|
|
377
|
+
const resolved = path$1.resolve(absolutePath);
|
|
378
|
+
if (this.opened.has(resolved)) return;
|
|
379
|
+
this.opened.add(resolved);
|
|
380
|
+
this.notify("textDocument/didOpen", { textDocument: {
|
|
381
|
+
uri: pathToFileURL(resolved).href,
|
|
382
|
+
languageId: languageIdOf(resolved),
|
|
383
|
+
version: 1,
|
|
384
|
+
text: fs.readFileSync(resolved, "utf8")
|
|
385
|
+
} });
|
|
386
|
+
await this.request("textDocument/documentSymbol", { textDocument: { uri: pathToFileURL(resolved).href } });
|
|
387
|
+
}
|
|
388
|
+
/** Hierarchical symbols of an (already opened) file. */
|
|
389
|
+
async documentSymbols(absolutePath) {
|
|
390
|
+
await this.open(absolutePath);
|
|
391
|
+
return (await this.request("textDocument/documentSymbol", { textDocument: { uri: pathToFileURL(path$1.resolve(absolutePath)).href } })).result ?? [];
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Every reference to the symbol whose identifier is at `position` in
|
|
395
|
+
* `absolutePath`, INCLUDING the declaration itself. Answers only from loaded
|
|
396
|
+
* projects — see the header.
|
|
397
|
+
*
|
|
398
|
+
* `includeDeclaration` is deliberately `true`. In the compiler's model an
|
|
399
|
+
* import specifier (`import { X } from …`) is itself a declaration — of the
|
|
400
|
+
* local alias — so `false` drops every import line along with the real
|
|
401
|
+
* declaration, and the count no longer matches ts-morph's
|
|
402
|
+
* `findReferencesAsNodes()`, which lists them. The caller knows the real
|
|
403
|
+
* declaration's position (it asked for it) and removes exactly that one.
|
|
404
|
+
*/
|
|
405
|
+
async references(absolutePath, position) {
|
|
406
|
+
await this.open(absolutePath);
|
|
407
|
+
return (await this.request("textDocument/references", {
|
|
408
|
+
textDocument: { uri: pathToFileURL(path$1.resolve(absolutePath)).href },
|
|
409
|
+
position,
|
|
410
|
+
context: { includeDeclaration: true }
|
|
411
|
+
})).result ?? [];
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Where the symbol whose identifier is at `position` is DEFINED. Used to
|
|
415
|
+
* confirm a documentSymbol candidate is the declaration itself and not an
|
|
416
|
+
* alias of one — an import specifier in a wrapped `import { type X, … }`
|
|
417
|
+
* reports as a top-level symbol, and taking it as the declaration made the
|
|
418
|
+
* REAL declaration's identifier count as an external reference
|
|
419
|
+
* (ENG-5121 review F-021).
|
|
420
|
+
*/
|
|
421
|
+
async definition(absolutePath, position) {
|
|
422
|
+
await this.open(absolutePath);
|
|
423
|
+
const result = (await this.request("textDocument/definition", {
|
|
424
|
+
textDocument: { uri: pathToFileURL(path$1.resolve(absolutePath)).href },
|
|
425
|
+
position
|
|
426
|
+
})).result;
|
|
427
|
+
if (!result) return [];
|
|
428
|
+
return (Array.isArray(result) ? result : [result]).map((l) => "targetUri" in l ? {
|
|
429
|
+
uri: l.targetUri,
|
|
430
|
+
range: l.targetSelectionRange
|
|
431
|
+
} : l);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* `workspace/symbol` for `query`. Never the first call on tsgo (see the
|
|
435
|
+
* header); rust-analyzer answers it once indexing is quiescent.
|
|
436
|
+
*/
|
|
437
|
+
async workspaceSymbol(query) {
|
|
438
|
+
return (await this.request("workspace/symbol", { query })).result ?? [];
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Resolve with the first server notification named `method` that
|
|
442
|
+
* `accept` takes, or REJECT after `timeoutMs`, or as soon as the server
|
|
443
|
+
* dies. A timeout is a failure, never "it probably settled".
|
|
444
|
+
*/
|
|
445
|
+
waitForNotification(method, accept, timeoutMs) {
|
|
446
|
+
if (this.exited) return Promise.reject(this.exited);
|
|
447
|
+
return new Promise((resolve$1, reject) => {
|
|
448
|
+
const done = (fn) => {
|
|
449
|
+
clearTimeout(timer);
|
|
450
|
+
clearInterval(poll);
|
|
451
|
+
this.listeners.delete(listener);
|
|
452
|
+
fn();
|
|
453
|
+
};
|
|
454
|
+
const listener = (m) => {
|
|
455
|
+
if (m.method === method && accept(m.params)) done(() => resolve$1(m.params));
|
|
456
|
+
};
|
|
457
|
+
const timer = setTimeout(() => done(() => reject(/* @__PURE__ */ new Error(`${this.name} sent no matching ${method} within ${timeoutMs} ms`))), timeoutMs);
|
|
458
|
+
const poll = setInterval(() => {
|
|
459
|
+
if (this.exited) done(() => reject(this.exited));
|
|
460
|
+
}, 250);
|
|
461
|
+
this.listeners.add(listener);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
/** Shut the server down. Safe to call twice. */
|
|
465
|
+
async dispose() {
|
|
466
|
+
if (this.exited) return;
|
|
467
|
+
try {
|
|
468
|
+
await Promise.race([this.request("shutdown"), new Promise((resolve$1) => setTimeout(resolve$1, 2e3))]);
|
|
469
|
+
this.notify("exit");
|
|
470
|
+
} catch {}
|
|
471
|
+
this.proc.stdin?.end();
|
|
472
|
+
this.proc.kill();
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
/** The LSP `languageId` for a file, by extension. */
|
|
476
|
+
function languageIdOf(file) {
|
|
477
|
+
if (file.endsWith(".rs")) return "rust";
|
|
478
|
+
if (file.endsWith("x")) return "typescriptreact";
|
|
479
|
+
return "typescript";
|
|
480
|
+
}
|
|
481
|
+
/** `base` with `extra` merged in, one level deep (`workspace`, `textDocument`, …). */
|
|
482
|
+
function mergeCapabilities(base, extra) {
|
|
483
|
+
const out = { ...base };
|
|
484
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
485
|
+
const prev = out[key];
|
|
486
|
+
out[key] = prev && typeof prev === "object" && value && typeof value === "object" ? {
|
|
487
|
+
...prev,
|
|
488
|
+
...value
|
|
489
|
+
} : value;
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
/** Convert a `file://` URI the server returned to an absolute path. */
|
|
494
|
+
function uriToPath(uri) {
|
|
495
|
+
return fileURLToPath(uri);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region ../code-intel/dist/workspaces.js
|
|
500
|
+
/**
|
|
501
|
+
* Open one source file in every workspace whose `package.json` lists
|
|
502
|
+
* `packageName` as a dependency, so the server loads those projects and
|
|
503
|
+
* `references` can see cross-package consumers. `within` restricts the
|
|
504
|
+
* candidates to workspaces that intersect the search scope — a scope of
|
|
505
|
+
* `apps/backender` has no use for `apps/client`'s project.
|
|
506
|
+
*
|
|
507
|
+
* For `isAutomated` in `packages/shared` that is the difference between 3
|
|
508
|
+
* refs / 1 file and 11 refs / 6 files (ENG-5121 §3.4 E5 → E6). Opening ANY
|
|
509
|
+
* file of the project is enough (E7) — `firstSourceFile` picks one.
|
|
510
|
+
*
|
|
511
|
+
* Returns the files it opened, so callers (and tests) can see what happened.
|
|
512
|
+
*/
|
|
513
|
+
async function openDependentWorkspaces(client, packageName, within) {
|
|
514
|
+
const opened = [];
|
|
515
|
+
for (const ws of listWorkspaces(client.rootDir)) {
|
|
516
|
+
if (!ws.dependencies.has(packageName)) continue;
|
|
517
|
+
if (within && !within.some((scope) => intersects(ws.dir, scope))) continue;
|
|
518
|
+
const file = firstSourceFile(ws.dir);
|
|
519
|
+
if (!file) continue;
|
|
520
|
+
await client.open(file);
|
|
521
|
+
opened.push(file);
|
|
522
|
+
}
|
|
523
|
+
return opened;
|
|
524
|
+
}
|
|
525
|
+
let workspaceCache = null;
|
|
526
|
+
/**
|
|
527
|
+
* Every workspace package under `rootDir`, read from the root `package.json`'s
|
|
528
|
+
* `workspaces` globs. Only the one-level `dir/*` and literal `dir/name` forms
|
|
529
|
+
* are supported, which is every form this repo uses.
|
|
530
|
+
*/
|
|
531
|
+
function listWorkspaces(rootDir) {
|
|
532
|
+
const root = path$1.resolve(rootDir);
|
|
533
|
+
if (workspaceCache?.root === root) return workspaceCache.list;
|
|
534
|
+
const list = [];
|
|
535
|
+
const patterns = readPackageJson(path$1.join(root, "package.json"))?.workspaces ?? [];
|
|
536
|
+
const seen = /* @__PURE__ */ new Set();
|
|
537
|
+
for (const pattern of patterns) {
|
|
538
|
+
const dirs = pattern.endsWith("/*") ? listDirs(path$1.join(root, pattern.slice(0, -2))) : [path$1.join(root, pattern)];
|
|
539
|
+
for (const dir of dirs) {
|
|
540
|
+
if (seen.has(dir)) continue;
|
|
541
|
+
seen.add(dir);
|
|
542
|
+
const pkg = readPackageJson(path$1.join(dir, "package.json"));
|
|
543
|
+
if (!pkg?.name) continue;
|
|
544
|
+
list.push({
|
|
545
|
+
dir,
|
|
546
|
+
name: pkg.name,
|
|
547
|
+
dependencies: new Set([
|
|
548
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
549
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
550
|
+
...Object.keys(pkg.peerDependencies ?? {})
|
|
551
|
+
])
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
workspaceCache = {
|
|
556
|
+
root,
|
|
557
|
+
list
|
|
558
|
+
};
|
|
559
|
+
return list;
|
|
560
|
+
}
|
|
561
|
+
/** The workspace a file belongs to — nearest ancestor `package.json` with a name — or null. */
|
|
562
|
+
function workspaceOf(rootDir, absoluteFile) {
|
|
563
|
+
const root = path$1.resolve(rootDir);
|
|
564
|
+
const file = path$1.resolve(absoluteFile);
|
|
565
|
+
let best = null;
|
|
566
|
+
for (const ws of listWorkspaces(root)) if (file === ws.dir || file.startsWith(ws.dir + path$1.sep)) {
|
|
567
|
+
if (!best || ws.dir.length > best.dir.length) best = ws;
|
|
568
|
+
}
|
|
569
|
+
return best;
|
|
570
|
+
}
|
|
571
|
+
function readPackageJson(file) {
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
574
|
+
if (parsed.workspaces && !Array.isArray(parsed.workspaces)) parsed.workspaces = parsed.workspaces.packages ?? [];
|
|
575
|
+
return parsed;
|
|
576
|
+
} catch {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function listDirs(parent) {
|
|
581
|
+
try {
|
|
582
|
+
return fs.readdirSync(parent, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => path$1.join(parent, e.name));
|
|
583
|
+
} catch {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function intersects(a, b) {
|
|
588
|
+
const x = path$1.resolve(a);
|
|
589
|
+
const y = path$1.resolve(b);
|
|
590
|
+
return x === y || x.startsWith(y + path$1.sep) || y.startsWith(x + path$1.sep);
|
|
591
|
+
}
|
|
592
|
+
/** Root-level files that are in no workspace's source project. */
|
|
593
|
+
const CONFIG_FILE = /^(?:.*\.config|eslint|vitest|vite|playwright|tsup|tsdown|knip|commitlint|prettier)(?:\..*)?\.tsx?$/;
|
|
594
|
+
/** Where a workspace's source project lives, in the order to try. */
|
|
595
|
+
const SOURCE_DIRS = [
|
|
596
|
+
"src",
|
|
597
|
+
"app",
|
|
598
|
+
"lib",
|
|
599
|
+
"test",
|
|
600
|
+
"scripts"
|
|
601
|
+
];
|
|
602
|
+
/**
|
|
603
|
+
* The first `.ts`/`.tsx` source file under `dir` (not a `.d.ts`, not a test),
|
|
604
|
+
* used purely to make the server load `dir`'s project — any file will do (E7)
|
|
605
|
+
* AS LONG AS IT IS IN THE PROJECT. A breadth-first walk from the workspace
|
|
606
|
+
* root found `vite.config.ts` / `eslint.config.ts` first for ~35 of 41
|
|
607
|
+
* workspaces, and those are in no `src/**`-scoped tsconfig, so the open
|
|
608
|
+
* loaded nothing and cross-workspace consumers stayed invisible (ENG-5121
|
|
609
|
+
* review F-029). Source directories are tried first; root-level config
|
|
610
|
+
* files are never the answer.
|
|
611
|
+
*/
|
|
612
|
+
function firstSourceFile(dir) {
|
|
613
|
+
for (const sub of SOURCE_DIRS) {
|
|
614
|
+
const candidate = path$1.join(dir, sub);
|
|
615
|
+
if (fs.existsSync(candidate)) {
|
|
616
|
+
const found = firstSourceFileUnder(candidate);
|
|
617
|
+
if (found) return found;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return firstSourceFileUnder(dir, true);
|
|
621
|
+
}
|
|
622
|
+
function firstSourceFileUnder(dir, skipConfigs = false) {
|
|
623
|
+
const queue = [dir];
|
|
624
|
+
while (queue.length) {
|
|
625
|
+
const current = queue.shift();
|
|
626
|
+
let entries;
|
|
627
|
+
try {
|
|
628
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
629
|
+
} catch {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
633
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
634
|
+
if (!IGNORED_DIRS.includes(entry.name)) queue.push(path$1.join(current, entry.name));
|
|
635
|
+
} else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith(".d.ts") && !/\.(test|spec)\.tsx?$/.test(entry.name) && !(skipConfigs && CONFIG_FILE.test(entry.name))) return path$1.join(current, entry.name);
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region ../code-intel/dist/engines/tsgo.js
|
|
642
|
+
/**
|
|
643
|
+
* `typescript` and the tsgo binary, resolved from `projectRoot`'s own
|
|
644
|
+
* dependencies. See the header: never from this package's location.
|
|
645
|
+
*/
|
|
646
|
+
function resolveTsToolchain(projectRoot) {
|
|
647
|
+
const manifest = path$1.join(path$1.resolve(projectRoot), "package.json");
|
|
648
|
+
if (!fs.existsSync(manifest)) throw new ToolchainError(`No package.json at ${projectRoot}, so typescript cannot be resolved from the checkout.`, "Run this from the root of a TypeScript repository.");
|
|
649
|
+
const requireFromRepo = createRequire(manifest);
|
|
650
|
+
let typescript;
|
|
651
|
+
try {
|
|
652
|
+
typescript = requireFromRepo("typescript");
|
|
653
|
+
} catch {
|
|
654
|
+
throw new ToolchainError(`typescript is not installed in ${projectRoot}.`, "Install it in this checkout (e.g. `yarn add -D typescript`).");
|
|
655
|
+
}
|
|
656
|
+
const platformPackage = `@typescript/native-preview-${process.platform}-${process.arch}`;
|
|
657
|
+
const exe = process.platform === "win32" ? "tsgo.exe" : "tsgo";
|
|
658
|
+
try {
|
|
659
|
+
const dir = path$1.dirname(requireFromRepo.resolve(`${platformPackage}/package.json`));
|
|
660
|
+
const native = path$1.join(dir, "lib", exe);
|
|
661
|
+
if (fs.existsSync(native)) return {
|
|
662
|
+
ts: typescript,
|
|
663
|
+
tsgoBin: native
|
|
664
|
+
};
|
|
665
|
+
} catch {}
|
|
666
|
+
try {
|
|
667
|
+
const dir = path$1.dirname(requireFromRepo.resolve("@typescript/native-preview/package.json"));
|
|
668
|
+
const launcher = path$1.join(dir, "bin", "tsgo.js");
|
|
669
|
+
if (fs.existsSync(launcher)) return {
|
|
670
|
+
ts: typescript,
|
|
671
|
+
tsgoBin: launcher
|
|
672
|
+
};
|
|
673
|
+
} catch {}
|
|
674
|
+
throw new ToolchainError(`The tsgo binary (@typescript/native-preview) is not installed in ${projectRoot}.`, "Install it in this checkout (e.g. `yarn add -D @typescript/native-preview`).");
|
|
675
|
+
}
|
|
676
|
+
/** LSP `SymbolKind` values a top-level declaration can carry. Type aliases report as Class (5). */
|
|
677
|
+
const TOP_LEVEL_KINDS = new Set([
|
|
678
|
+
5,
|
|
679
|
+
10,
|
|
680
|
+
11,
|
|
681
|
+
12,
|
|
682
|
+
13,
|
|
683
|
+
14
|
|
684
|
+
]);
|
|
685
|
+
/** Class/interface members: Method 6, Property 7 (accessors also report as 7), Field 8. */
|
|
686
|
+
const MEMBER_KINDS = new Set([
|
|
687
|
+
6,
|
|
688
|
+
7,
|
|
689
|
+
8
|
|
690
|
+
]);
|
|
691
|
+
/** Containers whose children count as members. */
|
|
692
|
+
const CONTAINER_KINDS = new Set([5, 11]);
|
|
693
|
+
/**
|
|
694
|
+
* Files step 3 may load beyond the configured projects — extra opens plus
|
|
695
|
+
* the excluded files the compiler API answers. Each is a full module load
|
|
696
|
+
* with its import closure; past this many the name is not being resolved,
|
|
697
|
+
* it is being grepped, and the process is on its way to being OOM-killed.
|
|
698
|
+
*/
|
|
699
|
+
const MAX_EXTRA_FILES = 200;
|
|
700
|
+
/**
|
|
701
|
+
* Files whose TEXT looks like it declares the symbol. Each costs a
|
|
702
|
+
* `documentSymbol` round trip (~150 ms) plus a `definition` per hit; for
|
|
703
|
+
* `id` inside apps/backender/src that is thousands of object-literal keys
|
|
704
|
+
* and minutes of wall clock before a single reference is asked for.
|
|
705
|
+
*/
|
|
706
|
+
const MAX_DECLARATION_CANDIDATES = 200;
|
|
707
|
+
function escapeRegExp(text) {
|
|
708
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
709
|
+
}
|
|
710
|
+
/** Inside the repository and not under a `node_modules` (a dependency's .d.ts). */
|
|
711
|
+
function isRepoSource(file, projectRoot) {
|
|
712
|
+
const root = path$1.resolve(projectRoot) + path$1.sep;
|
|
713
|
+
return file.startsWith(root) && !file.includes(`${path$1.sep}node_modules${path$1.sep}`);
|
|
714
|
+
}
|
|
715
|
+
/** Files whose text looks like it declares `symbol` at top level. */
|
|
716
|
+
function topLevelCandidates(symbol, contents) {
|
|
717
|
+
return [...contents].filter(([, text]) => looksLikeTopLevelDeclaration(symbol, text)).map(([f]) => f);
|
|
718
|
+
}
|
|
719
|
+
/** Files whose text looks like it declares a MEMBER named `symbol`. */
|
|
720
|
+
function memberCandidates(symbol, contents) {
|
|
721
|
+
return [...contents].filter(([, text]) => looksLikeMemberDeclaration(symbol, text)).map(([f]) => f);
|
|
722
|
+
}
|
|
723
|
+
/** Files each tsconfig includes, computed once per config per engine call. */
|
|
724
|
+
var ProjectMembership = class {
|
|
725
|
+
ts;
|
|
726
|
+
projectRoot;
|
|
727
|
+
byConfig = /* @__PURE__ */ new Map();
|
|
728
|
+
constructor(ts, projectRoot) {
|
|
729
|
+
this.ts = ts;
|
|
730
|
+
this.projectRoot = projectRoot;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* The tsconfig whose file set contains `file`, or null when no governing
|
|
734
|
+
* config includes it (no tsconfig up the tree, or excluded by the one
|
|
735
|
+
* there is).
|
|
736
|
+
*/
|
|
737
|
+
coveringConfig(file) {
|
|
738
|
+
const config = findTsConfig(path$1.dirname(file), this.projectRoot);
|
|
739
|
+
if (!config) return null;
|
|
740
|
+
let files = this.byConfig.get(config);
|
|
741
|
+
if (!files) {
|
|
742
|
+
files = this.load(config);
|
|
743
|
+
this.byConfig.set(config, files);
|
|
744
|
+
}
|
|
745
|
+
return files.has(path$1.resolve(file)) ? config : null;
|
|
746
|
+
}
|
|
747
|
+
/** The governing tsconfig's compiler options (for a file it EXCLUDES). */
|
|
748
|
+
optionsFor(file) {
|
|
749
|
+
const config = findTsConfig(path$1.dirname(file), this.projectRoot);
|
|
750
|
+
if (!config) return {};
|
|
751
|
+
this.coveringConfig(file);
|
|
752
|
+
return this.optionsByConfig.get(config) ?? {};
|
|
753
|
+
}
|
|
754
|
+
optionsByConfig = /* @__PURE__ */ new Map();
|
|
755
|
+
load(config) {
|
|
756
|
+
const ts = this.ts;
|
|
757
|
+
const read = ts.readConfigFile(config, (p) => ts.sys.readFile(p));
|
|
758
|
+
if (read.error || !read.config) return /* @__PURE__ */ new Set();
|
|
759
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path$1.dirname(config));
|
|
760
|
+
this.optionsByConfig.set(config, parsed.options);
|
|
761
|
+
return new Set(parsed.fileNames.map((f) => path$1.resolve(f)));
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
/**
|
|
765
|
+
* References from files NO tsconfig covers, resolved by the compiler API
|
|
766
|
+
* with the governing tsconfig's OPTIONS. An excluded file opened into the
|
|
767
|
+
* server's inferred project has no `paths`, so an import through an alias
|
|
768
|
+
* (`~/x`) never resolves and its references vanish — correct code FAILED
|
|
769
|
+
* under the tsgo binding (ENG-5121 review F-022). tsgo exposes no way to
|
|
770
|
+
* give an inferred project compiler options (its LSP preferences are
|
|
771
|
+
* editor settings only), so those files are answered here instead: one
|
|
772
|
+
* language service over exactly those files, which pulls the declaring
|
|
773
|
+
* module in through the resolved import.
|
|
774
|
+
*/
|
|
775
|
+
function referencesFromUncoveredFiles(ts, uncovered, declarations, membership) {
|
|
776
|
+
const out = /* @__PURE__ */ new Map();
|
|
777
|
+
if (uncovered.length === 0) return out;
|
|
778
|
+
const options = {
|
|
779
|
+
...membership.optionsFor(uncovered[0]),
|
|
780
|
+
noEmit: true,
|
|
781
|
+
skipLibCheck: true
|
|
782
|
+
};
|
|
783
|
+
const roots = [...new Set([...uncovered, ...declarations.map((d) => d.file)])];
|
|
784
|
+
const service = ts.createLanguageService({
|
|
785
|
+
getCompilationSettings: () => options,
|
|
786
|
+
getScriptFileNames: () => roots,
|
|
787
|
+
getScriptVersion: () => "1",
|
|
788
|
+
getScriptSnapshot: (f) => fs.existsSync(f) ? ts.ScriptSnapshot.fromString(fs.readFileSync(f, "utf8")) : void 0,
|
|
789
|
+
getCurrentDirectory: () => process.cwd(),
|
|
790
|
+
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
|
791
|
+
fileExists: (p) => ts.sys.fileExists(p),
|
|
792
|
+
readFile: (p, encoding) => ts.sys.readFile(p, encoding),
|
|
793
|
+
readDirectory: (...args) => ts.sys.readDirectory(...args),
|
|
794
|
+
directoryExists: (p) => ts.sys.directoryExists(p),
|
|
795
|
+
getDirectories: (p) => ts.sys.getDirectories(p)
|
|
796
|
+
});
|
|
797
|
+
const wanted = new Set(uncovered.map((f) => path$1.resolve(f)));
|
|
798
|
+
try {
|
|
799
|
+
for (const decl of declarations) {
|
|
800
|
+
const source = service.getProgram()?.getSourceFile(decl.file);
|
|
801
|
+
if (!source) continue;
|
|
802
|
+
const offset = source.getPositionOfLineAndCharacter(decl.position.line, decl.position.character);
|
|
803
|
+
for (const entry of service.getReferencesAtPosition(decl.file, offset) ?? []) {
|
|
804
|
+
const file = path$1.resolve(entry.fileName);
|
|
805
|
+
if (!wanted.has(file)) continue;
|
|
806
|
+
const refSource = service.getProgram()?.getSourceFile(entry.fileName);
|
|
807
|
+
if (!refSource) continue;
|
|
808
|
+
const pos = refSource.getLineAndCharacterOfPosition(entry.textSpan.start);
|
|
809
|
+
(out.get(file) ?? out.set(file, []).get(file)).push(pos);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
} finally {
|
|
813
|
+
service.dispose();
|
|
814
|
+
}
|
|
815
|
+
return out;
|
|
816
|
+
}
|
|
817
|
+
/** Keep only candidates whose definition is the candidate itself. */
|
|
818
|
+
async function confirmDeclarations(client, candidates) {
|
|
819
|
+
const confirmed = [];
|
|
820
|
+
for (const c of candidates) {
|
|
821
|
+
const defs = await client.definition(c.file, c.position);
|
|
822
|
+
if (defs.some((d) => uriToPath(d.uri) === c.file && d.range.start.line === c.position.line) || defs.length === 0) confirmed.push(c);
|
|
823
|
+
}
|
|
824
|
+
return confirmed;
|
|
825
|
+
}
|
|
826
|
+
function topLevelDeclarations(symbols, name, file) {
|
|
827
|
+
return symbols.filter((s) => s.name === name && TOP_LEVEL_KINDS.has(s.kind)).map((s) => ({
|
|
828
|
+
file,
|
|
829
|
+
position: s.selectionRange.start
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
function memberDeclarations(symbols, name, file) {
|
|
833
|
+
const out = [];
|
|
834
|
+
for (const container of symbols) {
|
|
835
|
+
if (!CONTAINER_KINDS.has(container.kind)) continue;
|
|
836
|
+
for (const child of container.children ?? []) if (child.name === name && MEMBER_KINDS.has(child.kind)) out.push({
|
|
837
|
+
file,
|
|
838
|
+
position: child.selectionRange.start
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return out;
|
|
842
|
+
}
|
|
843
|
+
async function resolveOne$1(ts, client, symbol, request, contents, scopeFiles, membership) {
|
|
844
|
+
const { projectRoot, includeTests, tsConfigPath } = request;
|
|
845
|
+
const rel = (p) => relativeTo(projectRoot, p);
|
|
846
|
+
let declarations = [];
|
|
847
|
+
const topLevel = topLevelCandidates(symbol, contents);
|
|
848
|
+
if (topLevel.length > MAX_DECLARATION_CANDIDATES) throw new SymbolTooCommonError(symbol, topLevel.length, MAX_DECLARATION_CANDIDATES, "files in scope look like they declare it");
|
|
849
|
+
for (const file of topLevel) declarations.push(...topLevelDeclarations(await client.documentSymbols(file), symbol, file));
|
|
850
|
+
declarations = await confirmDeclarations(client, declarations);
|
|
851
|
+
let resolvedAsMember = false;
|
|
852
|
+
if (declarations.length === 0) {
|
|
853
|
+
const members = memberCandidates(symbol, contents);
|
|
854
|
+
if (members.length > MAX_DECLARATION_CANDIDATES) throw new SymbolTooCommonError(symbol, members.length, MAX_DECLARATION_CANDIDATES, "files in scope look like they declare a member of that name");
|
|
855
|
+
for (const file of members) declarations.push(...memberDeclarations(await client.documentSymbols(file), symbol, file));
|
|
856
|
+
declarations = await confirmDeclarations(client, declarations);
|
|
857
|
+
resolvedAsMember = declarations.length > 0;
|
|
858
|
+
}
|
|
859
|
+
if (declarations.length === 0) return {
|
|
860
|
+
symbol,
|
|
861
|
+
result: notFound(scopeFiles.size, tsConfigPath),
|
|
862
|
+
locations: []
|
|
863
|
+
};
|
|
864
|
+
for (const decl of new Set(declarations.map((d) => d.file))) {
|
|
865
|
+
const ws = workspaceOf(projectRoot, decl);
|
|
866
|
+
if (ws) await openDependentWorkspaces(client, ws.name, request.scopeRoots);
|
|
867
|
+
}
|
|
868
|
+
const mention = /* @__PURE__ */ new RegExp(`\\b${escapeRegExp(symbol)}\\b`);
|
|
869
|
+
const loadedConfigs = /* @__PURE__ */ new Set();
|
|
870
|
+
const uncovered = [];
|
|
871
|
+
let extraFiles = 0;
|
|
872
|
+
for (const [file, text] of contents) {
|
|
873
|
+
if (!mention.test(text)) continue;
|
|
874
|
+
if (!includeTests && isTestFile(file)) continue;
|
|
875
|
+
const config = membership.coveringConfig(file);
|
|
876
|
+
if (config) {
|
|
877
|
+
if (loadedConfigs.has(config)) continue;
|
|
878
|
+
loadedConfigs.add(config);
|
|
879
|
+
await client.open(file);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (++extraFiles > MAX_EXTRA_FILES) throw new SymbolTooCommonError(symbol, extraFiles, MAX_EXTRA_FILES, "files outside every tsconfig project mention it");
|
|
883
|
+
if (findTsConfig(path$1.dirname(file), projectRoot)) uncovered.push(file);
|
|
884
|
+
else await client.open(file);
|
|
885
|
+
}
|
|
886
|
+
const fromUncovered = referencesFromUncoveredFiles(ts, uncovered, declarations, membership);
|
|
887
|
+
const declarationFiles = /* @__PURE__ */ new Set();
|
|
888
|
+
const referencingFiles = /* @__PURE__ */ new Set();
|
|
889
|
+
const locations = [];
|
|
890
|
+
const seen = /* @__PURE__ */ new Set();
|
|
891
|
+
let referenceCount = 0;
|
|
892
|
+
let excludedTestReferences = 0;
|
|
893
|
+
for (const decl of declarations) {
|
|
894
|
+
declarationFiles.add(decl.file);
|
|
895
|
+
locations.push({
|
|
896
|
+
file: rel(decl.file),
|
|
897
|
+
line: decl.position.line + 1,
|
|
898
|
+
col: decl.position.character + 1,
|
|
899
|
+
kind: "declaration"
|
|
900
|
+
});
|
|
901
|
+
const merged = [...(await client.references(decl.file, decl.position)).map((r) => ({
|
|
902
|
+
file: uriToPath(r.uri),
|
|
903
|
+
start: r.range.start
|
|
904
|
+
})), ...[...fromUncovered].flatMap(([file, positions]) => positions.map((start) => ({
|
|
905
|
+
file,
|
|
906
|
+
start
|
|
907
|
+
})))];
|
|
908
|
+
for (const ref of merged) {
|
|
909
|
+
const refFile = ref.file;
|
|
910
|
+
if (refFile === decl.file) continue;
|
|
911
|
+
if (!isRepoSource(refFile, projectRoot)) continue;
|
|
912
|
+
const key = `${refFile}:${ref.start.line}:${ref.start.character}`;
|
|
913
|
+
if (seen.has(key)) continue;
|
|
914
|
+
seen.add(key);
|
|
915
|
+
const location = {
|
|
916
|
+
file: rel(refFile),
|
|
917
|
+
line: ref.start.line + 1,
|
|
918
|
+
col: ref.start.character + 1
|
|
919
|
+
};
|
|
920
|
+
if (!includeTests && isTestFile(refFile)) {
|
|
921
|
+
excludedTestReferences++;
|
|
922
|
+
locations.push({
|
|
923
|
+
...location,
|
|
924
|
+
kind: "test"
|
|
925
|
+
});
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
referenceCount++;
|
|
929
|
+
referencingFiles.add(refFile);
|
|
930
|
+
locations.push({
|
|
931
|
+
...location,
|
|
932
|
+
kind: "reference"
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
symbol,
|
|
938
|
+
result: {
|
|
939
|
+
found: true,
|
|
940
|
+
resolvedAsMember,
|
|
941
|
+
declarationFiles: [...declarationFiles].map(rel).sort(),
|
|
942
|
+
referenceCount,
|
|
943
|
+
referencingFiles: [...referencingFiles].map(rel).sort(),
|
|
944
|
+
excludedTestReferences,
|
|
945
|
+
filesScanned: scopeFiles.size,
|
|
946
|
+
tsConfigPath
|
|
947
|
+
},
|
|
948
|
+
locations
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* ONE server per call — and therefore one per process for the CLIs — for N
|
|
953
|
+
* symbols. The server's project loads are the expensive part (2–2.5 s for
|
|
954
|
+
* `apps/client`); each `references` request after that is tens of
|
|
955
|
+
* milliseconds, which is what keeps G47's eight symbols far inside the
|
|
956
|
+
* executor's 30 s cap.
|
|
957
|
+
*/
|
|
958
|
+
const tsgoEngine = async (request) => {
|
|
959
|
+
const { ts, tsgoBin } = resolveTsToolchain(request.toolchainRoot ?? request.projectRoot);
|
|
960
|
+
const contents = /* @__PURE__ */ new Map();
|
|
961
|
+
for (const file of request.files) try {
|
|
962
|
+
contents.set(file, fs.readFileSync(file, "utf8"));
|
|
963
|
+
} catch {}
|
|
964
|
+
const scopeFiles = new Set(request.files);
|
|
965
|
+
const client = await LspClient.start({
|
|
966
|
+
rootDir: request.projectRoot,
|
|
967
|
+
bin: tsgoBin,
|
|
968
|
+
args: ["--lsp", "--stdio"],
|
|
969
|
+
name: "tsgo --lsp"
|
|
970
|
+
});
|
|
971
|
+
try {
|
|
972
|
+
for (const scopeRoot of request.scopeRoots) {
|
|
973
|
+
if (findTsConfig(scopeRoot, request.projectRoot)) continue;
|
|
974
|
+
const root = path$1.resolve(scopeRoot);
|
|
975
|
+
for (const file of request.files) if (file === root || file.startsWith(root + path$1.sep)) await client.open(file);
|
|
976
|
+
}
|
|
977
|
+
const membership = new ProjectMembership(ts, request.projectRoot);
|
|
978
|
+
const results = [];
|
|
979
|
+
for (const symbol of request.symbols) results.push(await resolveOne$1(ts, client, symbol, request, contents, scopeFiles, membership));
|
|
980
|
+
return results;
|
|
981
|
+
} finally {
|
|
982
|
+
await client.dispose();
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
//#endregion
|
|
987
|
+
//#region ../code-intel/dist/engines/rust-test-code.js
|
|
988
|
+
/**
|
|
989
|
+
* [issue:ENG-7284] D5 — which lines of a Rust file are TEST code, so the
|
|
990
|
+
* rust-analyzer engine can discount references from them.
|
|
991
|
+
*
|
|
992
|
+
* Pure and linear: each file is lexed ONCE into its comment/string spans and
|
|
993
|
+
* its matching brace pairs, and every question after that (is this position
|
|
994
|
+
* code, where does this item end, which block encloses it) is a lookup.
|
|
995
|
+
* Review round 2 measured the earlier per-attribute rescan at 703 ms for one
|
|
996
|
+
* 100 KB file.
|
|
997
|
+
*
|
|
998
|
+
* ## What is test code
|
|
999
|
+
*
|
|
1000
|
+
* - an ITEM whose outer attribute is `#[test]`, `#[<path>::test]`, or a
|
|
1001
|
+
* `#[cfg(...)]` predicate that cannot hold when `test` is false
|
|
1002
|
+
* (`cfg(test)`, `cfg(all(test, unix))`; not `cfg(any(test, feature))`,
|
|
1003
|
+
* which a feature build turns on, and not `cfg(not(any(test, mock)))`).
|
|
1004
|
+
* Items with a body run to their matching `}`; body-less items (`use`,
|
|
1005
|
+
* `const`, `static`, `type`, `extern crate`, a tuple struct) run to their
|
|
1006
|
+
* `;`. An attribute on anything that is not an item — a struct field, an
|
|
1007
|
+
* enum variant, a parameter, a statement — opens no region;
|
|
1008
|
+
* - the whole file, for an inner `#![cfg(test)]` at file scope, or the
|
|
1009
|
+
* enclosing block for one inside `mod x { … }`;
|
|
1010
|
+
* - an out-of-line `#[cfg(test)] mod x;` is reported by name, and the caller
|
|
1011
|
+
* marks that module's file(s) as test code.
|
|
1012
|
+
*/
|
|
1013
|
+
/**
|
|
1014
|
+
* Cargo's build output and vendored crates: `target/` or `vendor/` beside a
|
|
1015
|
+
* `Cargo.toml`. A module that happens to be named `target` or `vendor`
|
|
1016
|
+
* (`src/target/mod.rs`) is source and is walked (review round 3 L7).
|
|
1017
|
+
*/
|
|
1018
|
+
const rustSkipDir = (parent, name) => (name === "target" || name === "vendor") && fs.existsSync(path$1.join(parent, "Cargo.toml"));
|
|
1019
|
+
const RAW_STRING = /(?:b|c)?r(#*)"/y;
|
|
1020
|
+
const CHAR_LITERAL = /'(?:\\(?:u\{[0-9a-fA-F]+\}|x[0-9a-fA-F]{2}|.)|[^\\'\n])'/uy;
|
|
1021
|
+
var RustSource = class {
|
|
1022
|
+
text;
|
|
1023
|
+
/** Sorted, non-overlapping `[start, end)` spans of comments and strings. */
|
|
1024
|
+
nonCode = [];
|
|
1025
|
+
/** Code `{` offset → its matching `}` offset. */
|
|
1026
|
+
braceClose = /* @__PURE__ */ new Map();
|
|
1027
|
+
/** Code brace pairs, in order of their `{`. */
|
|
1028
|
+
bracePairs = [];
|
|
1029
|
+
lineStarts = [0];
|
|
1030
|
+
constructor(text) {
|
|
1031
|
+
this.text = text;
|
|
1032
|
+
const stack = [];
|
|
1033
|
+
for (let i = 0; i < text.length;) {
|
|
1034
|
+
const ch = text[i];
|
|
1035
|
+
const next = text[i + 1];
|
|
1036
|
+
if (ch === "\n") {
|
|
1037
|
+
this.lineStarts.push(i + 1);
|
|
1038
|
+
i++;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const end = this.nonCodeEnd(i, ch, next);
|
|
1042
|
+
if (end > i) {
|
|
1043
|
+
this.nonCode.push([i, end]);
|
|
1044
|
+
for (let j = i; j < end; j++) if (text[j] === "\n") this.lineStarts.push(j + 1);
|
|
1045
|
+
i = end;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (ch === "{") stack.push(i);
|
|
1049
|
+
else if (ch === "}") {
|
|
1050
|
+
const open = stack.pop();
|
|
1051
|
+
if (open !== void 0) {
|
|
1052
|
+
this.braceClose.set(open, i);
|
|
1053
|
+
this.bracePairs.push([open, i]);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
i++;
|
|
1057
|
+
}
|
|
1058
|
+
this.bracePairs.sort((a, b) => a[0] - b[0]);
|
|
1059
|
+
}
|
|
1060
|
+
/** End of the comment/string starting at `i`, or `i` when there is none. */
|
|
1061
|
+
nonCodeEnd(i, ch, next) {
|
|
1062
|
+
const text = this.text;
|
|
1063
|
+
if (ch === "/" && next === "/") {
|
|
1064
|
+
const nl = text.indexOf("\n", i);
|
|
1065
|
+
return nl < 0 ? text.length : nl;
|
|
1066
|
+
}
|
|
1067
|
+
if (ch === "/" && next === "*") {
|
|
1068
|
+
let depth = 1;
|
|
1069
|
+
let j = i + 2;
|
|
1070
|
+
while (j < text.length && depth > 0) if (text[j] === "/" && text[j + 1] === "*") {
|
|
1071
|
+
depth++;
|
|
1072
|
+
j += 2;
|
|
1073
|
+
} else if (text[j] === "*" && text[j + 1] === "/") {
|
|
1074
|
+
depth--;
|
|
1075
|
+
j += 2;
|
|
1076
|
+
} else j++;
|
|
1077
|
+
return j;
|
|
1078
|
+
}
|
|
1079
|
+
if ((ch === "r" || ch === "b" || ch === "c") && !/\w/.test(text[i - 1] ?? "")) {
|
|
1080
|
+
RAW_STRING.lastIndex = i;
|
|
1081
|
+
const raw = RAW_STRING.exec(text);
|
|
1082
|
+
if (raw) {
|
|
1083
|
+
const close = `"${raw[1]}`;
|
|
1084
|
+
const end = text.indexOf(close, i + raw[0].length);
|
|
1085
|
+
return end < 0 ? text.length : end + close.length;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (ch === "\"") {
|
|
1089
|
+
let j = i + 1;
|
|
1090
|
+
while (j < text.length && text[j] !== "\"") j += text[j] === "\\" ? 2 : 1;
|
|
1091
|
+
return Math.min(j + 1, text.length);
|
|
1092
|
+
}
|
|
1093
|
+
if (ch === "'") {
|
|
1094
|
+
CHAR_LITERAL.lastIndex = i;
|
|
1095
|
+
const lit = CHAR_LITERAL.exec(text);
|
|
1096
|
+
if (lit) return i + lit[0].length;
|
|
1097
|
+
}
|
|
1098
|
+
return i;
|
|
1099
|
+
}
|
|
1100
|
+
/** The non-code span containing `pos`, or undefined when `pos` is code. */
|
|
1101
|
+
spanAt(pos) {
|
|
1102
|
+
let lo = 0;
|
|
1103
|
+
let hi = this.nonCode.length - 1;
|
|
1104
|
+
while (lo <= hi) {
|
|
1105
|
+
const mid = lo + hi >> 1;
|
|
1106
|
+
const [s, e] = this.nonCode[mid];
|
|
1107
|
+
if (pos < s) hi = mid - 1;
|
|
1108
|
+
else if (pos >= e) lo = mid + 1;
|
|
1109
|
+
else return this.nonCode[mid];
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
isCode(pos) {
|
|
1113
|
+
return this.spanAt(pos) === void 0;
|
|
1114
|
+
}
|
|
1115
|
+
/** `pos`, or the end of the non-code span it falls in. */
|
|
1116
|
+
skip(pos) {
|
|
1117
|
+
return this.spanAt(pos)?.[1] ?? pos;
|
|
1118
|
+
}
|
|
1119
|
+
lineAt(offset) {
|
|
1120
|
+
let lo = 0;
|
|
1121
|
+
let hi = this.lineStarts.length - 1;
|
|
1122
|
+
while (lo < hi) {
|
|
1123
|
+
const mid = lo + hi + 1 >> 1;
|
|
1124
|
+
if (this.lineStarts[mid] <= offset) lo = mid;
|
|
1125
|
+
else hi = mid - 1;
|
|
1126
|
+
}
|
|
1127
|
+
return lo;
|
|
1128
|
+
}
|
|
1129
|
+
/** The innermost brace pair enclosing `pos`, or undefined at file scope. */
|
|
1130
|
+
enclosing(pos) {
|
|
1131
|
+
let best;
|
|
1132
|
+
for (const pair of this.bracePairs) {
|
|
1133
|
+
if (pair[0] > pos) break;
|
|
1134
|
+
if (pair[1] > pos) best = pair;
|
|
1135
|
+
}
|
|
1136
|
+
return best;
|
|
1137
|
+
}
|
|
1138
|
+
/** Index of the `]` matching the code `[` at `open`, or -1. */
|
|
1139
|
+
bracketClose(open) {
|
|
1140
|
+
let depth = 0;
|
|
1141
|
+
for (let i = open; i < this.text.length;) {
|
|
1142
|
+
const skipped = this.skip(i);
|
|
1143
|
+
if (skipped !== i) {
|
|
1144
|
+
i = skipped;
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
const ch = this.text[i];
|
|
1148
|
+
if (ch === "[") depth++;
|
|
1149
|
+
else if (ch === "]" && --depth === 0) return i;
|
|
1150
|
+
i++;
|
|
1151
|
+
}
|
|
1152
|
+
return -1;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* The first code character in `stops` at bracket depth 0 at or after
|
|
1156
|
+
* `from`, or -1. `()`, `[]` and nested `{}` always count; with `angles`,
|
|
1157
|
+
* `<…>` does too (an item signature: `impl Foo<{ N + 1 }> {` must not stop
|
|
1158
|
+
* at the const-generic brace, review round 3 L3), where `->` and `=>` are
|
|
1159
|
+
* not closers.
|
|
1160
|
+
*/
|
|
1161
|
+
findAtDepth0(from, stops, angles = false) {
|
|
1162
|
+
let depth = 0;
|
|
1163
|
+
for (let i = from; i < this.text.length;) {
|
|
1164
|
+
const skipped = this.skip(i);
|
|
1165
|
+
if (skipped !== i) {
|
|
1166
|
+
i = skipped;
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const ch = this.text[i];
|
|
1170
|
+
const prev = this.text[i - 1];
|
|
1171
|
+
if (depth === 0 && stops.includes(ch)) return i;
|
|
1172
|
+
if (ch === "(" || ch === "[" || ch === "{" || angles && ch === "<") depth++;
|
|
1173
|
+
else if (ch === ")" || ch === "]" || ch === "}" || angles && ch === ">" && prev !== "-" && prev !== "=") {
|
|
1174
|
+
if (depth === 0) return -1;
|
|
1175
|
+
depth--;
|
|
1176
|
+
}
|
|
1177
|
+
i++;
|
|
1178
|
+
}
|
|
1179
|
+
return -1;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
/**
|
|
1183
|
+
* Whether a `cfg(...)` predicate can be true when `test` is false. Kleene
|
|
1184
|
+
* logic: `test` is false, every other option is unknown. The item is test
|
|
1185
|
+
* code exactly when the result is definitely false. A predicate that does
|
|
1186
|
+
* not parse is treated as production (never hides a reference).
|
|
1187
|
+
*/
|
|
1188
|
+
function cfgIsTestOnly(predicate) {
|
|
1189
|
+
let pos = 0;
|
|
1190
|
+
const src = predicate;
|
|
1191
|
+
const ws = () => {
|
|
1192
|
+
while (pos < src.length && /\s/.test(src[pos])) pos++;
|
|
1193
|
+
};
|
|
1194
|
+
const parse = () => {
|
|
1195
|
+
ws();
|
|
1196
|
+
const ident = /^[A-Za-z_][\w]*/.exec(src.slice(pos));
|
|
1197
|
+
if (!ident) throw new Error("cfg: expected an identifier");
|
|
1198
|
+
const name = ident[0];
|
|
1199
|
+
pos += name.length;
|
|
1200
|
+
ws();
|
|
1201
|
+
if (src[pos] === "=") {
|
|
1202
|
+
pos++;
|
|
1203
|
+
ws();
|
|
1204
|
+
const str = /^"(?:[^"\\]|\\.)*"/.exec(src.slice(pos));
|
|
1205
|
+
if (!str) throw new Error("cfg: expected a string");
|
|
1206
|
+
pos += str[0].length;
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
if (src[pos] !== "(") return name === "test" ? false : null;
|
|
1210
|
+
pos++;
|
|
1211
|
+
const args = [];
|
|
1212
|
+
ws();
|
|
1213
|
+
while (src[pos] !== ")") {
|
|
1214
|
+
args.push(parse());
|
|
1215
|
+
ws();
|
|
1216
|
+
if (src[pos] === ",") {
|
|
1217
|
+
pos++;
|
|
1218
|
+
ws();
|
|
1219
|
+
} else if (src[pos] !== ")") throw new Error("cfg: expected , or )");
|
|
1220
|
+
}
|
|
1221
|
+
pos++;
|
|
1222
|
+
if (name === "not") {
|
|
1223
|
+
if (args.length !== 1) throw new Error("cfg: not takes one argument");
|
|
1224
|
+
return args[0] === null ? null : !args[0];
|
|
1225
|
+
}
|
|
1226
|
+
if (name === "all") {
|
|
1227
|
+
if (args.includes(false)) return false;
|
|
1228
|
+
return args.every((a) => a === true) ? true : null;
|
|
1229
|
+
}
|
|
1230
|
+
if (name === "any") {
|
|
1231
|
+
if (args.includes(true)) return true;
|
|
1232
|
+
return args.every((a) => a === false) ? false : null;
|
|
1233
|
+
}
|
|
1234
|
+
return null;
|
|
1235
|
+
};
|
|
1236
|
+
try {
|
|
1237
|
+
const result = parse();
|
|
1238
|
+
ws();
|
|
1239
|
+
return pos === src.length && result === false;
|
|
1240
|
+
} catch {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/** Attribute names (last path segment) that make an item a test. */
|
|
1245
|
+
const TEST_ITEM_ATTRIBUTES = new Set([
|
|
1246
|
+
"test",
|
|
1247
|
+
"rstest",
|
|
1248
|
+
"test_case",
|
|
1249
|
+
"bench"
|
|
1250
|
+
]);
|
|
1251
|
+
/** Whether an attribute (name, arguments) marks its item as test code. */
|
|
1252
|
+
function isTestAttribute(name, args) {
|
|
1253
|
+
const last = name.split("::").pop() ?? name;
|
|
1254
|
+
if (TEST_ITEM_ATTRIBUTES.has(last)) return true;
|
|
1255
|
+
return name === "cfg" && args !== void 0 && cfgIsTestOnly(args);
|
|
1256
|
+
}
|
|
1257
|
+
const ATTRIBUTE_START = /#(!?)\[/g;
|
|
1258
|
+
const ATTRIBUTE_BODY = /^\s*([\w:]+)\s*(?:\(([\s\S]*)\))?\s*$/;
|
|
1259
|
+
const ITEM = /^(?:pub(?:\s*\([^)]*\))?\s+)?(?:(?:default|async|unsafe|const|extern(?:\s+"[^"]*")?)\s+)*(mod|fn|impl|struct|enum|union|trait|const|static|use|type|extern\s+crate|macro_rules!)(?![\w])/;
|
|
1260
|
+
/** `extern "C" { … }` (optionally `unsafe`), review round 3 L1. */
|
|
1261
|
+
const EXTERN_BLOCK = /^(?:pub(?:\s*\([^)]*\))?\s+)?(?:unsafe\s+)?extern(?:\s+"[^"]*")?\s*\{/;
|
|
1262
|
+
/** A macro invocation used as an item: `mock! {…}`, `thread_local!(…);` (L2). */
|
|
1263
|
+
const MACRO_ITEM = /^(?:[A-Za-z_]\w*::)*[A-Za-z_]\w*!\s*([({[])/;
|
|
1264
|
+
/** `#[path = "…"]` immediately before (possibly among other attributes). */
|
|
1265
|
+
const PATH_ATTRIBUTE = /#\[\s*path\s*=\s*"([^"]+)"\s*\]/;
|
|
1266
|
+
/** A `#[path]` attached to the attribute run that ends at the match. */
|
|
1267
|
+
const PATH_BEFORE = /#\[\s*path\s*=\s*"([^"]+)"\s*\](?:\s*#\[[^\]]*\])*\s*$/;
|
|
1268
|
+
/** An out-of-line module declaration. */
|
|
1269
|
+
const MOD_DECL = /\bmod\s+([A-Za-z_]\w*)\s*;/g;
|
|
1270
|
+
/** Items whose extent is a `{ … }` body (or a `;` when they have none). */
|
|
1271
|
+
const BLOCK_ITEMS = new Set([
|
|
1272
|
+
"mod",
|
|
1273
|
+
"fn",
|
|
1274
|
+
"impl",
|
|
1275
|
+
"struct",
|
|
1276
|
+
"enum",
|
|
1277
|
+
"union",
|
|
1278
|
+
"trait",
|
|
1279
|
+
"macro_rules!"
|
|
1280
|
+
]);
|
|
1281
|
+
function cfgTestRegions(text) {
|
|
1282
|
+
const src = new RustSource(text);
|
|
1283
|
+
const ranges = [];
|
|
1284
|
+
const outOfLine = [];
|
|
1285
|
+
const outOfLinePaths = [];
|
|
1286
|
+
let wholeFile = false;
|
|
1287
|
+
for (const match of text.matchAll(ATTRIBUTE_START)) {
|
|
1288
|
+
const at = match.index;
|
|
1289
|
+
if (!src.isCode(at)) continue;
|
|
1290
|
+
const open = at + match[0].length - 1;
|
|
1291
|
+
const close = src.bracketClose(open);
|
|
1292
|
+
if (close < 0) continue;
|
|
1293
|
+
const body = ATTRIBUTE_BODY.exec(text.slice(open + 1, close));
|
|
1294
|
+
if (!body || !isTestAttribute(body[1] ?? "", body[2])) continue;
|
|
1295
|
+
if (match[1] === "!") {
|
|
1296
|
+
const block = src.enclosing(at);
|
|
1297
|
+
if (!block) wholeFile = true;
|
|
1298
|
+
else ranges.push([src.lineAt(block[0]), src.lineAt(block[1])]);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
let p = close + 1;
|
|
1302
|
+
let pathTarget = PATH_BEFORE.exec(text.slice(Math.max(0, at - 300), at))?.[1];
|
|
1303
|
+
for (;;) {
|
|
1304
|
+
while (p < text.length && (/\s/.test(text[p]) || !src.isCode(p))) p = src.isCode(p) ? p + 1 : src.skip(p);
|
|
1305
|
+
if (text[p] === "#" && text[p + 1] === "[") {
|
|
1306
|
+
const next = src.bracketClose(p + 1);
|
|
1307
|
+
if (next < 0) break;
|
|
1308
|
+
pathTarget = PATH_ATTRIBUTE.exec(text.slice(p, next + 1))?.[1] ?? pathTarget;
|
|
1309
|
+
p = next + 1;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
const head = text.slice(p, p + 200);
|
|
1315
|
+
const externBlock = EXTERN_BLOCK.exec(head);
|
|
1316
|
+
if (externBlock) {
|
|
1317
|
+
const end$1 = src.braceClose.get(p + externBlock[0].length - 1);
|
|
1318
|
+
if (end$1 !== void 0) ranges.push([src.lineAt(at), src.lineAt(end$1)]);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const item = ITEM.exec(head);
|
|
1322
|
+
if (!item) {
|
|
1323
|
+
const macro = MACRO_ITEM.exec(head);
|
|
1324
|
+
if (macro) {
|
|
1325
|
+
const open$1 = p + macro[0].length - 1;
|
|
1326
|
+
const end$1 = text[open$1] === "{" ? src.braceClose.get(open$1) : src.findAtDepth0(open$1, ";");
|
|
1327
|
+
if (end$1 !== void 0 && end$1 >= 0) ranges.push([src.lineAt(at), src.lineAt(end$1)]);
|
|
1328
|
+
}
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
const kind = item[1].replace(/\s+/g, " ");
|
|
1332
|
+
const afterKeyword = p + item[0].length;
|
|
1333
|
+
if (kind === "mod") {
|
|
1334
|
+
const decl = /^\s*([A-Za-z_]\w*)\s*;/.exec(text.slice(afterKeyword));
|
|
1335
|
+
if (decl) {
|
|
1336
|
+
if (pathTarget) outOfLinePaths.push(pathTarget);
|
|
1337
|
+
else outOfLine.push(decl[1]);
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
let end;
|
|
1342
|
+
if (BLOCK_ITEMS.has(kind)) {
|
|
1343
|
+
const stop = src.findAtDepth0(afterKeyword, "{;", true);
|
|
1344
|
+
if (stop < 0) continue;
|
|
1345
|
+
end = text[stop] === "{" ? src.braceClose.get(stop) ?? -1 : stop;
|
|
1346
|
+
} else end = src.findAtDepth0(afterKeyword, ";");
|
|
1347
|
+
if (end < 0) continue;
|
|
1348
|
+
ranges.push([src.lineAt(at), src.lineAt(end)]);
|
|
1349
|
+
}
|
|
1350
|
+
const declaredMods = [];
|
|
1351
|
+
for (const m of text.matchAll(MOD_DECL)) if (src.isCode(m.index)) declaredMods.push(m[1]);
|
|
1352
|
+
return {
|
|
1353
|
+
ranges,
|
|
1354
|
+
outOfLine,
|
|
1355
|
+
wholeFile,
|
|
1356
|
+
outOfLinePaths,
|
|
1357
|
+
declaredMods
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region ../code-intel/dist/engines/rust-analyzer.js
|
|
1363
|
+
/** The files the Rust engine treats as source. */
|
|
1364
|
+
const RUST_SOURCE = /\.rs$/;
|
|
1365
|
+
const RA_BIN = "rust-analyzer";
|
|
1366
|
+
const INSTALL_REMEDY = "Install it with `rustup component add rust-analyzer` and make sure `rust-analyzer` is on PATH.";
|
|
1367
|
+
/** Default bound on the indexing wait; `CODE_INTEL_RA_TIMEOUT_MS` overrides. */
|
|
1368
|
+
const DEFAULT_INDEX_TIMEOUT_MS = 3e5;
|
|
1369
|
+
function indexTimeoutMs(env = process.env) {
|
|
1370
|
+
const raw = Number(env.CODE_INTEL_RA_TIMEOUT_MS);
|
|
1371
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_INDEX_TIMEOUT_MS;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Refuse up front when `rust-analyzer` cannot run. rustup installs a proxy
|
|
1375
|
+
* named `rust-analyzer` for every toolchain, which exits non-zero with
|
|
1376
|
+
* "Unknown binary" until the component is added, so existence on PATH is not
|
|
1377
|
+
* enough: `--version` must succeed.
|
|
1378
|
+
*/
|
|
1379
|
+
function assertRustAnalyzer(bin = RA_BIN) {
|
|
1380
|
+
const probe = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
1381
|
+
if (probe.error || probe.status !== 0) {
|
|
1382
|
+
const detail = (probe.stderr || probe.error?.message || "").trim();
|
|
1383
|
+
throw new ToolchainError(`rust-analyzer is not available${detail ? ` (${detail.split("\n")[0]})` : ""}.`, INSTALL_REMEDY);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* The directory rust-analyzer should treat as the workspace: the HIGHEST
|
|
1388
|
+
* directory between the scope and the repository root that holds a
|
|
1389
|
+
* `Cargo.toml`, so a scope inside one member of a Cargo workspace still
|
|
1390
|
+
* loads the whole workspace (and its cross-crate references).
|
|
1391
|
+
*/
|
|
1392
|
+
function cargoRoot(scopeRoot, projectRoot) {
|
|
1393
|
+
const stop = path$1.resolve(projectRoot);
|
|
1394
|
+
let dir = path$1.resolve(scopeRoot);
|
|
1395
|
+
if (fs.statSync(dir, { throwIfNoEntry: false })?.isFile()) dir = path$1.dirname(dir);
|
|
1396
|
+
let found = null;
|
|
1397
|
+
for (;;) {
|
|
1398
|
+
if (fs.existsSync(path$1.join(dir, "Cargo.toml"))) found = dir;
|
|
1399
|
+
if (dir === stop) break;
|
|
1400
|
+
const parent = path$1.dirname(dir);
|
|
1401
|
+
if (parent === dir) break;
|
|
1402
|
+
dir = parent;
|
|
1403
|
+
}
|
|
1404
|
+
if (!found) throw new ToolchainError(`No Cargo.toml at or above ${relativeTo(projectRoot, scopeRoot)}.`, "Point --scope inside a Cargo crate or workspace.");
|
|
1405
|
+
return found;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* A file that is test code in full, by its path alone: under a `tests/`
|
|
1409
|
+
* directory of the crate, or named `tests.rs` / `*_test.rs` / `*_tests.rs`.
|
|
1410
|
+
* Judged relative to `crateRoot`, so a checkout that itself sits under some
|
|
1411
|
+
* `tests/` directory is not all test code.
|
|
1412
|
+
*/
|
|
1413
|
+
function isRustTestPath(file, crateRoot) {
|
|
1414
|
+
const resolved = path$1.resolve(file);
|
|
1415
|
+
const parts = (crateRoot ? path$1.relative(crateRoot, resolved) : resolved).split(path$1.sep);
|
|
1416
|
+
if (parts.slice(0, -1).includes("tests")) return true;
|
|
1417
|
+
const base = parts[parts.length - 1] ?? "";
|
|
1418
|
+
return base === "tests.rs" || base.endsWith("_test.rs") || base.endsWith("_tests.rs");
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Answers "is this location test code?" for one crate. Every `.rs` file of
|
|
1422
|
+
* the crate is scanned up front for out-of-line `#[cfg(test)] mod x;`
|
|
1423
|
+
* declarations, so a reference in `x.rs` is test code whichever file the
|
|
1424
|
+
* server happens to return first.
|
|
1425
|
+
*/
|
|
1426
|
+
var TestCode = class {
|
|
1427
|
+
crateRoot;
|
|
1428
|
+
regions = /* @__PURE__ */ new Map();
|
|
1429
|
+
testFiles = /* @__PURE__ */ new Set();
|
|
1430
|
+
constructor(crateRoot) {
|
|
1431
|
+
this.crateRoot = crateRoot;
|
|
1432
|
+
for (const file of listSourceFiles(crateRoot, RUST_SOURCE, rustSkipDir)) this.regionsOf(path$1.resolve(file));
|
|
1433
|
+
}
|
|
1434
|
+
isTest(file, line) {
|
|
1435
|
+
const resolved = path$1.resolve(file);
|
|
1436
|
+
if (isRustTestPath(resolved, this.crateRoot) || this.testFiles.has(resolved)) return true;
|
|
1437
|
+
const { ranges, wholeFile } = this.regionsOf(resolved);
|
|
1438
|
+
return wholeFile || ranges.some(([a, b]) => line >= a && line <= b);
|
|
1439
|
+
}
|
|
1440
|
+
regionsOf(file) {
|
|
1441
|
+
const cached = this.regions.get(file);
|
|
1442
|
+
if (cached) return cached;
|
|
1443
|
+
let text = "";
|
|
1444
|
+
try {
|
|
1445
|
+
text = fs.readFileSync(file, "utf8");
|
|
1446
|
+
} catch {}
|
|
1447
|
+
const regions = cfgTestRegions(text);
|
|
1448
|
+
const dir = path$1.dirname(file);
|
|
1449
|
+
const stem = path$1.basename(file, ".rs");
|
|
1450
|
+
const moduleDir = [
|
|
1451
|
+
"mod",
|
|
1452
|
+
"lib",
|
|
1453
|
+
"main"
|
|
1454
|
+
].includes(stem) ? dir : path$1.join(dir, stem);
|
|
1455
|
+
const markModule = (name) => {
|
|
1456
|
+
this.testFiles.add(path$1.join(moduleDir, `${name}.rs`));
|
|
1457
|
+
for (const f of listSourceFiles(path$1.join(moduleDir, name), RUST_SOURCE, rustSkipDir)) this.testFiles.add(path$1.resolve(f));
|
|
1458
|
+
};
|
|
1459
|
+
for (const name of regions.outOfLine) markModule(name);
|
|
1460
|
+
for (const target of regions.outOfLinePaths) this.testFiles.add(path$1.resolve(dir, target));
|
|
1461
|
+
if (regions.wholeFile || this.testFiles.has(file)) for (const name of regions.declaredMods) markModule(name);
|
|
1462
|
+
this.regions.set(file, regions);
|
|
1463
|
+
return regions;
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
function inside(file, roots) {
|
|
1467
|
+
const f = path$1.resolve(file);
|
|
1468
|
+
return roots.some((r) => {
|
|
1469
|
+
const root = path$1.resolve(r);
|
|
1470
|
+
return f === root || f.startsWith(root + path$1.sep);
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
/** The identifier position of `name` declared in `file` within `range`. */
|
|
1474
|
+
function identifierIn(symbols, name, range) {
|
|
1475
|
+
const within = (p) => (p.line > range.start.line || p.line === range.start.line && p.character >= range.start.character) && (p.line < range.end.line || p.line === range.end.line && p.character <= range.end.character);
|
|
1476
|
+
const stack = [...symbols];
|
|
1477
|
+
while (stack.length) {
|
|
1478
|
+
const s = stack.pop();
|
|
1479
|
+
if (s.name === name && within(s.selectionRange.start)) return s.selectionRange.start;
|
|
1480
|
+
stack.push(...s.children ?? []);
|
|
1481
|
+
}
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
async function resolveOne(client, symbol, request, testCode) {
|
|
1485
|
+
const { projectRoot, includeTests } = request;
|
|
1486
|
+
const rel = (p) => relativeTo(projectRoot, p);
|
|
1487
|
+
const hits = (await client.workspaceSymbol(symbol)).filter((s) => s.name === symbol && s.location.uri.startsWith("file:") && inside(uriToPath(s.location.uri), request.scopeRoots));
|
|
1488
|
+
const declarations = [];
|
|
1489
|
+
const seenDecl = /* @__PURE__ */ new Set();
|
|
1490
|
+
for (const hit of hits) {
|
|
1491
|
+
const file = uriToPath(hit.location.uri);
|
|
1492
|
+
const position = identifierIn(await client.documentSymbols(file), symbol, hit.location.range) ?? hit.location.range.start;
|
|
1493
|
+
const key = `${file}:${position.line}:${position.character}`;
|
|
1494
|
+
if (seenDecl.has(key)) continue;
|
|
1495
|
+
seenDecl.add(key);
|
|
1496
|
+
declarations.push({
|
|
1497
|
+
file,
|
|
1498
|
+
position
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
if (declarations.length === 0) return {
|
|
1502
|
+
symbol,
|
|
1503
|
+
result: notFound(request.files.length, null),
|
|
1504
|
+
locations: []
|
|
1505
|
+
};
|
|
1506
|
+
const declarationFiles = /* @__PURE__ */ new Set();
|
|
1507
|
+
const referencingFiles = /* @__PURE__ */ new Set();
|
|
1508
|
+
const locations = [];
|
|
1509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1510
|
+
let referenceCount = 0;
|
|
1511
|
+
let excludedTestReferences = 0;
|
|
1512
|
+
const root = path$1.resolve(projectRoot) + path$1.sep;
|
|
1513
|
+
for (const decl of declarations) {
|
|
1514
|
+
declarationFiles.add(decl.file);
|
|
1515
|
+
locations.push({
|
|
1516
|
+
file: rel(decl.file),
|
|
1517
|
+
line: decl.position.line + 1,
|
|
1518
|
+
col: decl.position.character + 1,
|
|
1519
|
+
kind: "declaration"
|
|
1520
|
+
});
|
|
1521
|
+
const refs = await client.references(decl.file, decl.position);
|
|
1522
|
+
for (const ref of refs) {
|
|
1523
|
+
if (!ref.uri.startsWith("file:")) continue;
|
|
1524
|
+
const refFile = uriToPath(ref.uri);
|
|
1525
|
+
if (refFile === decl.file) continue;
|
|
1526
|
+
if (!refFile.startsWith(root)) continue;
|
|
1527
|
+
const start = ref.range.start;
|
|
1528
|
+
const key = `${refFile}:${start.line}:${start.character}`;
|
|
1529
|
+
if (seen.has(key)) continue;
|
|
1530
|
+
seen.add(key);
|
|
1531
|
+
const location = {
|
|
1532
|
+
file: rel(refFile),
|
|
1533
|
+
line: start.line + 1,
|
|
1534
|
+
col: start.character + 1
|
|
1535
|
+
};
|
|
1536
|
+
if (!includeTests && testCode.isTest(refFile, start.line)) {
|
|
1537
|
+
excludedTestReferences++;
|
|
1538
|
+
locations.push({
|
|
1539
|
+
...location,
|
|
1540
|
+
kind: "test"
|
|
1541
|
+
});
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
referenceCount++;
|
|
1545
|
+
referencingFiles.add(refFile);
|
|
1546
|
+
locations.push({
|
|
1547
|
+
...location,
|
|
1548
|
+
kind: "reference"
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return {
|
|
1553
|
+
symbol,
|
|
1554
|
+
result: {
|
|
1555
|
+
found: true,
|
|
1556
|
+
resolvedAsMember: false,
|
|
1557
|
+
declarationFiles: [...declarationFiles].map(rel).sort(),
|
|
1558
|
+
referenceCount,
|
|
1559
|
+
referencingFiles: [...referencingFiles].map(rel).sort(),
|
|
1560
|
+
excludedTestReferences,
|
|
1561
|
+
filesScanned: request.files.length,
|
|
1562
|
+
tsConfigPath: null
|
|
1563
|
+
},
|
|
1564
|
+
locations
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* ONE server per call, for N symbols: indexing is the expensive part, each
|
|
1569
|
+
* `references` request after it is cheap.
|
|
1570
|
+
*/
|
|
1571
|
+
const rustAnalyzerEngine = async (request) => {
|
|
1572
|
+
assertRustAnalyzer();
|
|
1573
|
+
const workspaceRoot = cargoRoot(request.scopeRoots[0], request.projectRoot);
|
|
1574
|
+
const timeoutMs = indexTimeoutMs();
|
|
1575
|
+
let client;
|
|
1576
|
+
try {
|
|
1577
|
+
client = await LspClient.start({
|
|
1578
|
+
rootDir: workspaceRoot,
|
|
1579
|
+
bin: RA_BIN,
|
|
1580
|
+
args: [],
|
|
1581
|
+
name: "rust-analyzer",
|
|
1582
|
+
capabilities: {
|
|
1583
|
+
workspace: {
|
|
1584
|
+
configuration: false,
|
|
1585
|
+
symbol: {}
|
|
1586
|
+
},
|
|
1587
|
+
experimental: { serverStatusNotification: true },
|
|
1588
|
+
window: { workDoneProgress: true }
|
|
1589
|
+
},
|
|
1590
|
+
initializationOptions: {
|
|
1591
|
+
checkOnSave: false,
|
|
1592
|
+
workspace: { symbol: { search: {
|
|
1593
|
+
kind: "all_symbols",
|
|
1594
|
+
scope: "workspace"
|
|
1595
|
+
} } }
|
|
1596
|
+
},
|
|
1597
|
+
responseTimeoutMs: timeoutMs
|
|
1598
|
+
});
|
|
1599
|
+
} catch (e) {
|
|
1600
|
+
throw new ToolchainError(`rust-analyzer did not start: ${e.message}`, INSTALL_REMEDY);
|
|
1601
|
+
}
|
|
1602
|
+
try {
|
|
1603
|
+
let status;
|
|
1604
|
+
try {
|
|
1605
|
+
status = await client.waitForNotification("experimental/serverStatus", (p) => p?.quiescent === true, timeoutMs);
|
|
1606
|
+
} catch (e) {
|
|
1607
|
+
throw new ToolchainError(`rust-analyzer did not finish indexing ${relativeTo(request.projectRoot, workspaceRoot) || "."}: ${e.message}.`, "Raise CODE_INTEL_RA_TIMEOUT_MS for a large workspace, or check that `cargo metadata` succeeds there.");
|
|
1608
|
+
}
|
|
1609
|
+
if (status.health === "error") throw new ToolchainError(`rust-analyzer reports an error loading the workspace${status.message ? `: ${status.message}` : ""}.`, "Check that `cargo metadata` succeeds in the crate.");
|
|
1610
|
+
const testCode = new TestCode(workspaceRoot);
|
|
1611
|
+
const results = [];
|
|
1612
|
+
for (const symbol of request.symbols) results.push(await resolveOne(client, symbol, request, testCode));
|
|
1613
|
+
return results;
|
|
1614
|
+
} finally {
|
|
1615
|
+
await client.dispose();
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
//#endregion
|
|
1620
|
+
//#region ../code-intel/dist/registry.js
|
|
1621
|
+
const engines = {
|
|
1622
|
+
ts: {
|
|
1623
|
+
resolve: tsgoEngine,
|
|
1624
|
+
source: TS_SOURCE,
|
|
1625
|
+
usesTsConfig: true
|
|
1626
|
+
},
|
|
1627
|
+
rust: {
|
|
1628
|
+
resolve: rustAnalyzerEngine,
|
|
1629
|
+
source: RUST_SOURCE,
|
|
1630
|
+
usesTsConfig: false,
|
|
1631
|
+
skipDir: rustSkipDir
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
const REFS_LANGUAGES = Object.keys(engines);
|
|
1635
|
+
function isRefsLanguage(value) {
|
|
1636
|
+
return Object.prototype.hasOwnProperty.call(engines, value);
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* The request an engine receives: absolute scope roots, every source file
|
|
1640
|
+
* in them, and the scope cap applied BEFORE anything loads.
|
|
1641
|
+
*/
|
|
1642
|
+
function buildEngineRequest(options) {
|
|
1643
|
+
const engine = engines[options.language];
|
|
1644
|
+
const projectRoot = path$1.resolve(options.projectRoot);
|
|
1645
|
+
const scopeRoots = options.scopes.map((s) => path$1.resolve(projectRoot, s));
|
|
1646
|
+
const files = [...new Set(scopeRoots.flatMap((r) => listSourceFiles(r, engine.source, "skipDir" in engine ? engine.skipDir : void 0)))].sort();
|
|
1647
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
1648
|
+
if (files.length > maxFiles) throw new ScopeTooBroadError(files.length, maxFiles, options.scopes.join(", "));
|
|
1649
|
+
const tsConfig = engine.usesTsConfig && scopeRoots[0] ? findTsConfig(scopeRoots[0], projectRoot) : null;
|
|
1650
|
+
return {
|
|
1651
|
+
symbols: options.symbols,
|
|
1652
|
+
projectRoot,
|
|
1653
|
+
scopeRoots,
|
|
1654
|
+
files,
|
|
1655
|
+
includeTests: options.includeTests,
|
|
1656
|
+
tsConfigPath: tsConfig ? relativeTo(projectRoot, tsConfig) : null
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
//#endregion
|
|
1661
|
+
//#region src/commands/gates/refsHandler.ts
|
|
1662
|
+
/** Locations printed after the verdict. */
|
|
1663
|
+
const MAX_LOCATIONS = 20;
|
|
1664
|
+
function usage(message) {
|
|
1665
|
+
return {
|
|
1666
|
+
exitCode: 2,
|
|
1667
|
+
stdout: "",
|
|
1668
|
+
stderr: `gates refs: ${message}\n`
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
/** A relative path that leaves its base (`..` or `../x`, not `..cache`). */
|
|
1672
|
+
function escapes(rel) {
|
|
1673
|
+
return rel === ".." || rel.startsWith(`..${path$1.sep}`) || path$1.isAbsolute(rel);
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* The scope, repository-relative, or a reason it is refused. It must exist
|
|
1677
|
+
* and resolve INSIDE the repository, never to the root itself: the root is
|
|
1678
|
+
* the whole repository, which no engine loads (the resolver's ENG-5106
|
|
1679
|
+
* rule). Symlinks are resolved first, so a link cannot point the search
|
|
1680
|
+
* outside.
|
|
1681
|
+
*/
|
|
1682
|
+
function resolveScope(projectRoot, scope) {
|
|
1683
|
+
const trimmed = scope.trim();
|
|
1684
|
+
if (!trimmed) return { error: "--scope is required" };
|
|
1685
|
+
const root = fs.realpathSync(projectRoot);
|
|
1686
|
+
const target = path$1.resolve(root, trimmed);
|
|
1687
|
+
let real;
|
|
1688
|
+
try {
|
|
1689
|
+
real = fs.realpathSync(target);
|
|
1690
|
+
} catch {
|
|
1691
|
+
if (escapes(path$1.relative(root, target))) return { error: `--scope "${scope}" resolves outside the repository (${root})` };
|
|
1692
|
+
return { error: `--scope "${scope}" does not exist` };
|
|
1693
|
+
}
|
|
1694
|
+
const rel = path$1.relative(root, real);
|
|
1695
|
+
if (escapes(rel)) return { error: `--scope "${scope}" resolves outside the repository (${root})` };
|
|
1696
|
+
if (rel === "") return { error: `--scope "${scope}" is the repository root; name a directory narrower than the root (e.g. apps/backender/src)` };
|
|
1697
|
+
return { scope: rel };
|
|
1698
|
+
}
|
|
1699
|
+
function formatLocation(loc) {
|
|
1700
|
+
return ` ${loc.kind.padEnd(11)} ${loc.file}:${loc.line}:${loc.col}`;
|
|
1701
|
+
}
|
|
1702
|
+
/** One-line verdict plus up to MAX_LOCATIONS locations. */
|
|
1703
|
+
function formatRefs(resolution, language, scope) {
|
|
1704
|
+
const { symbol, result } = resolution;
|
|
1705
|
+
const where = `[${language}, scope ${scope}]`;
|
|
1706
|
+
if (!result.found) return `FAIL ${symbol}: not found in ${result.filesScanned} file(s) ${where}\n`;
|
|
1707
|
+
const tests = result.excludedTestReferences ? `; ${result.excludedTestReferences} in test code not counted (--include-tests counts them)` : "";
|
|
1708
|
+
const lines = [result.referenceCount > 0 ? `PASS ${symbol}: declared in ${result.declarationFiles.join(", ")}; ${result.referenceCount} reference(s) from ${result.referencingFiles.length} other module(s)${tests} ${where}` : `FAIL ${symbol}: declared in ${result.declarationFiles.join(", ")} but not referenced from any other module${tests} ${where}`];
|
|
1709
|
+
const rank = {
|
|
1710
|
+
declaration: 0,
|
|
1711
|
+
reference: 1,
|
|
1712
|
+
test: 2
|
|
1713
|
+
};
|
|
1714
|
+
const ordered = [...resolution.locations].sort((a, b) => rank[a.kind] - rank[b.kind]);
|
|
1715
|
+
const shown = ordered.slice(0, MAX_LOCATIONS);
|
|
1716
|
+
lines.push(...shown.map(formatLocation));
|
|
1717
|
+
const more = ordered.length - shown.length;
|
|
1718
|
+
if (more > 0) lines.push(` … ${more} more`);
|
|
1719
|
+
return lines.join("\n") + "\n";
|
|
1720
|
+
}
|
|
1721
|
+
/** The whole command minus process I/O, so tests can drive it. */
|
|
1722
|
+
async function executeRefs(flags, cwd = process.cwd()) {
|
|
1723
|
+
if (!isRefsLanguage(flags.lang)) return usage(`--lang "${flags.lang}" is not a language this CLI resolves; expected one of: ${REFS_LANGUAGES.join(", ")}`);
|
|
1724
|
+
const symbol = flags.symbol.trim();
|
|
1725
|
+
if (!symbol) return usage("--symbol is required");
|
|
1726
|
+
const projectRoot = findProjectRoot(cwd);
|
|
1727
|
+
const scoped = resolveScope(projectRoot, flags.scope);
|
|
1728
|
+
if ("error" in scoped) return usage(scoped.error);
|
|
1729
|
+
try {
|
|
1730
|
+
const request = buildEngineRequest({
|
|
1731
|
+
language: flags.lang,
|
|
1732
|
+
symbols: [symbol],
|
|
1733
|
+
projectRoot,
|
|
1734
|
+
scopes: [scoped.scope],
|
|
1735
|
+
includeTests: flags["include-tests"]
|
|
1736
|
+
});
|
|
1737
|
+
const [resolution] = await engines[flags.lang].resolve(request);
|
|
1738
|
+
if (!resolution) return usage(`no answer for "${symbol}"`);
|
|
1739
|
+
return {
|
|
1740
|
+
exitCode: resolution.result.found && resolution.result.referenceCount > 0 ? 0 : 1,
|
|
1741
|
+
stdout: formatRefs(resolution, flags.lang, scoped.scope),
|
|
1742
|
+
stderr: ""
|
|
1743
|
+
};
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (error instanceof ToolchainError || error instanceof ScopeTooBroadError || error instanceof SymbolTooCommonError) return usage(error.message);
|
|
1746
|
+
return usage(`resolution failed for "${symbol}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function gatesRefsHandler(flags) {
|
|
1750
|
+
const result = await executeRefs(flags);
|
|
1751
|
+
if (result.stdout.length > 0) this.process.stdout.write(result.stdout);
|
|
1752
|
+
if (result.stderr.length > 0) this.process.stderr.write(result.stderr);
|
|
1753
|
+
this.process.exitCode = result.exitCode;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
//#endregion
|
|
1757
|
+
export { gatesRefsHandler };
|