@alex-dbs/coderuler 1.4.0 → 1.6.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.
Files changed (38) hide show
  1. package/README.md +3 -1
  2. package/dist/adapter/cli/commands/check.js +21 -10
  3. package/dist/adapter/cli/program.js +2 -2
  4. package/dist/adapter/cli/reports.js +1 -1
  5. package/dist/adapter/cli/services.d.ts +4 -2
  6. package/dist/adapter/decision/jev.js +32 -15
  7. package/dist/adapter/decision/openai.d.ts +11 -0
  8. package/dist/adapter/decision/openai.js +160 -0
  9. package/dist/adapter/filesystem/rule-test-lsp.d.ts +6 -0
  10. package/dist/adapter/filesystem/rule-test-lsp.js +189 -0
  11. package/dist/adapter/filesystem/rule-test-worker.d.ts +5 -0
  12. package/dist/adapter/filesystem/rule-test-worker.js +48 -26
  13. package/dist/adapter/filesystem/rule-tests.d.ts +2 -2
  14. package/dist/adapter/filesystem/rule-tests.js +170 -29
  15. package/dist/adapter/git/changes.d.ts +2 -1
  16. package/dist/adapter/git/changes.js +369 -35
  17. package/dist/adapter/git/rules.d.ts +5 -0
  18. package/dist/adapter/git/rules.js +126 -0
  19. package/dist/adapter/github/pull-request.d.ts +2 -0
  20. package/dist/adapter/github/pull-request.js +9 -3
  21. package/dist/adapter/lsp/backend.d.ts +12 -0
  22. package/dist/adapter/lsp/backend.js +826 -0
  23. package/dist/app/concurrency.d.ts +1 -1
  24. package/dist/app/concurrency.js +12 -8
  25. package/dist/app/execute.js +84 -22
  26. package/dist/app/lsp.d.ts +6 -0
  27. package/dist/app/lsp.js +141 -0
  28. package/dist/app/ports.d.ts +13 -1
  29. package/dist/app/rule.d.ts +52 -10
  30. package/dist/app/rule.js +14 -0
  31. package/dist/app/validate.js +3 -2
  32. package/dist/index.d.ts +3 -3
  33. package/dist/index.js +1 -1
  34. package/dist/infra/runtime.js +27 -8
  35. package/dist/infra/settings.js +19 -8
  36. package/package.json +3 -1
  37. package/skills/coderuler-new-rule/SKILL.md +7 -3
  38. package/skills/coderuler-new-rule/reference.md +80 -13
@@ -0,0 +1,826 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { parse } from '@ast-grep/napi';
6
+ import { ApplyWorkspaceEditRequest, CallHierarchyIncomingCallsRequest, CallHierarchyOutgoingCallsRequest, CallHierarchyPrepareRequest, DeclarationRequest, DefinitionRequest, DidOpenTextDocumentNotification, DocumentSymbolRequest, ExitNotification, HoverRequest, ImplementationRequest, InitializeRequest, InitializedNotification, MessageType, ReferencesRequest, ShowMessageNotification, ShowMessageRequest, ShutdownRequest, SymbolKind, TypeDefinitionRequest, WorkDoneProgress, WorkDoneProgressCreateRequest, WorkspaceSymbolRequest, } from 'vscode-languageserver-protocol';
7
+ import { createProtocolConnection } from 'vscode-languageserver-protocol/node';
8
+ import { resolveAstGrepLanguage } from '../scope/astgrep-languages.js';
9
+ import { textLineSpan } from '../scope/lines.js';
10
+ const DEFAULT_OPTIONS = {
11
+ command: 'gopls',
12
+ args: ['serve'],
13
+ languageId: 'go',
14
+ };
15
+ const MAX_STDERR_BYTES = 8 * 1024;
16
+ const SHUTDOWN_GRACE_MS = 2_000;
17
+ const AST_DECLARATION_KINDS = {
18
+ function_declaration: true,
19
+ method_declaration: true,
20
+ var_declaration: true,
21
+ const_declaration: true,
22
+ type_declaration: true,
23
+ };
24
+ function describe(error) {
25
+ return error instanceof Error ? error.message : String(error);
26
+ }
27
+ function createTextIndex(text) {
28
+ const lineStarts = [0];
29
+ const lineEnds = [];
30
+ for (let index = 0; index < text.length; index += 1) {
31
+ const code = text.charCodeAt(index);
32
+ if (code === 10 || code === 13) {
33
+ lineEnds.push(index);
34
+ if (code === 13 && text.charCodeAt(index + 1) === 10)
35
+ index += 1;
36
+ lineStarts.push(index + 1);
37
+ }
38
+ }
39
+ lineEnds.push(text.length);
40
+ const offset = (position) => {
41
+ if (!Number.isSafeInteger(position.line) || position.line < 0 || position.line >= lineStarts.length) {
42
+ throw new Error(`LSP position line ${position.line + 1} is outside the document`);
43
+ }
44
+ if (!Number.isSafeInteger(position.character) || position.character < 0) {
45
+ throw new Error('LSP position character must be a nonnegative safe integer');
46
+ }
47
+ const start = lineStarts[position.line];
48
+ const end = lineEnds[position.line];
49
+ if (position.character > end - start) {
50
+ throw new Error(`LSP position column ${position.character + 1} is outside line ${position.line + 1}`);
51
+ }
52
+ return start + position.character;
53
+ };
54
+ return {
55
+ lineStarts,
56
+ lineEnds,
57
+ lineCount: lineStarts.length,
58
+ position(line, column) {
59
+ if (!Number.isSafeInteger(line) || line < 1) {
60
+ throw new Error('LSP line must be a positive safe integer');
61
+ }
62
+ if (!Number.isSafeInteger(column) || column < 1) {
63
+ throw new Error('LSP column must be a positive safe integer');
64
+ }
65
+ const zeroLine = line - 1;
66
+ if (zeroLine >= lineStarts.length)
67
+ throw new Error(`LSP line ${line} is outside the document`);
68
+ const character = column - 1;
69
+ if (character > lineEnds[zeroLine] - lineStarts[zeroLine]) {
70
+ throw new Error(`LSP column ${column} is outside line ${line}`);
71
+ }
72
+ return { line: zeroLine, character };
73
+ },
74
+ offset,
75
+ };
76
+ }
77
+ function isWithinRoot(root, filepath) {
78
+ const relativePath = relative(root, filepath);
79
+ if (relativePath === '')
80
+ return true;
81
+ const firstSegment = relativePath.split(sep)[0];
82
+ return firstSegment !== '..' && !isAbsolute(relativePath);
83
+ }
84
+ function comparePosition(left, right) {
85
+ return left.line - right.line || left.character - right.character;
86
+ }
87
+ function compareRanges(left, right) {
88
+ return comparePosition(left.start, right.start) || comparePosition(left.end, right.end);
89
+ }
90
+ function rangeContains(outer, inner) {
91
+ return comparePosition(outer.start, inner.start) <= 0 && comparePosition(outer.end, inner.end) >= 0;
92
+ }
93
+ function rangeStrictlyContains(outer, inner) {
94
+ return rangeContains(outer, inner) && compareRanges(outer, inner) !== 0;
95
+ }
96
+ function sameRange(left, right) {
97
+ return compareRanges(left, right) === 0;
98
+ }
99
+ function normalizeLocations(result) {
100
+ if (result === null || result === undefined)
101
+ return [];
102
+ const values = Array.isArray(result) ? result : [result];
103
+ const normalized = [];
104
+ for (const value of values) {
105
+ if (typeof value !== 'object' || value === null)
106
+ continue;
107
+ if ('targetUri' in value && 'targetSelectionRange' in value) {
108
+ const link = value;
109
+ normalized.push({ uri: link.targetUri, range: link.targetSelectionRange });
110
+ }
111
+ else if ('uri' in value && 'range' in value) {
112
+ normalized.push(value);
113
+ }
114
+ }
115
+ return normalized;
116
+ }
117
+ function isDocumentSymbol(value) {
118
+ return 'selectionRange' in value;
119
+ }
120
+ function flattenSymbols(symbols) {
121
+ const declarations = [];
122
+ const visit = (symbol) => {
123
+ declarations.push({
124
+ name: symbol.name,
125
+ kind: symbol.kind,
126
+ range: symbol.range,
127
+ selectionRange: symbol.selectionRange,
128
+ hasFullRange: true,
129
+ });
130
+ for (const child of symbol.children ?? [])
131
+ visit(child);
132
+ };
133
+ for (const symbol of symbols) {
134
+ if (isDocumentSymbol(symbol))
135
+ visit(symbol);
136
+ else
137
+ declarations.push({
138
+ name: symbol.name,
139
+ kind: symbol.kind,
140
+ range: symbol.location.range,
141
+ selectionRange: symbol.location.range,
142
+ hasFullRange: false,
143
+ });
144
+ }
145
+ return declarations;
146
+ }
147
+ function compareText(left, right) {
148
+ return left < right ? -1 : left > right ? 1 : 0;
149
+ }
150
+ function createError(message, cause) {
151
+ return cause === undefined ? new Error(message) : new Error(message, { cause });
152
+ }
153
+ export async function createLspBackend(project, signal, options) {
154
+ const settings = options ?? DEFAULT_OPTIONS;
155
+ const timeoutMs = settings.requestTimeoutMs ?? 60_000;
156
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
157
+ throw new Error('LSP requestTimeoutMs must be a positive safe integer');
158
+ }
159
+ if (signal.aborted)
160
+ throw createError('LSP startup was aborted', signal.reason);
161
+ const root = resolve(project.root);
162
+ const rootUri = pathToFileURL(root).href;
163
+ const workspaceFolder = { uri: rootUri, name: basename(root) };
164
+ const usesProcessGroup = settings.languageId === 'ruby' && process.platform !== 'win32';
165
+ const child = spawn(settings.command, [...(settings.args ?? [])], {
166
+ cwd: root,
167
+ env: settings.env,
168
+ shell: process.platform === 'win32' && settings.languageId === 'typescript',
169
+ detached: usesProcessGroup,
170
+ stdio: ['pipe', 'pipe', 'pipe'],
171
+ });
172
+ let stderr = Buffer.alloc(0);
173
+ let terminalError;
174
+ let initialized = false;
175
+ let closing = false;
176
+ let processClosed = false;
177
+ let releasePromise;
178
+ let closePromise;
179
+ let connection;
180
+ let capabilities;
181
+ let rubyPositionNeedsSurrogateAdjustment = false;
182
+ const controller = new AbortController();
183
+ const documentPromises = new Map();
184
+ const symbolPromises = new Map();
185
+ const astPromises = new Map();
186
+ const { promise: processClosedPromise, resolve: resolveProcessClosed } = Promise.withResolvers();
187
+ const progressToken = settings.initializationProgressToken;
188
+ const readiness = progressToken === undefined ? undefined : Promise.withResolvers();
189
+ let readinessPending = readiness !== undefined;
190
+ const stderrText = () => stderr.toString('utf8').trim();
191
+ const withStderr = (error) => {
192
+ const message = describe(error);
193
+ const detail = stderrText();
194
+ return createError(detail === '' ? message : `${message}\nLSP stderr (latest ${MAX_STDERR_BYTES} bytes):\n${detail}`, error);
195
+ };
196
+ const closedError = () => terminalError ?? new Error('LSP session is closed');
197
+ const signalChild = (signalName) => {
198
+ if (usesProcessGroup) {
199
+ const pid = child.pid;
200
+ if (pid === undefined)
201
+ return;
202
+ try {
203
+ process.kill(-pid, signalName);
204
+ }
205
+ catch (error) {
206
+ if (error?.code !== 'ESRCH')
207
+ throw error;
208
+ }
209
+ return;
210
+ }
211
+ child.kill(signalName);
212
+ };
213
+ const cleanup = (graceful) => {
214
+ if (releasePromise !== undefined)
215
+ return releasePromise;
216
+ releasePromise = (async () => {
217
+ if (graceful && initialized && !processClosed && connection !== undefined) {
218
+ const shutdown = (async () => {
219
+ try {
220
+ await connection.sendRequest(ShutdownRequest.type);
221
+ await connection.sendNotification(ExitNotification.type);
222
+ }
223
+ catch {
224
+ // A bounded process release below handles failed shutdown handshakes.
225
+ }
226
+ })();
227
+ void shutdown.catch(() => undefined);
228
+ }
229
+ else if (!processClosed) {
230
+ signalChild('SIGTERM');
231
+ }
232
+ if (!processClosed) {
233
+ const { promise: expired, resolve: resolveExpired } = Promise.withResolvers();
234
+ const releaseTimer = setTimeout(() => resolveExpired('timeout'), SHUTDOWN_GRACE_MS);
235
+ const closed = processClosedPromise.then(() => 'closed');
236
+ const result = await Promise.race([closed, expired]);
237
+ clearTimeout(releaseTimer);
238
+ if (result === 'timeout' && !processClosed)
239
+ signalChild('SIGKILL');
240
+ if (!processClosed)
241
+ await processClosedPromise;
242
+ }
243
+ connection?.dispose();
244
+ signal.removeEventListener('abort', onRunAbort);
245
+ child.removeAllListeners('error');
246
+ child.removeAllListeners('close');
247
+ child.stderr?.removeAllListeners('data');
248
+ })();
249
+ return releasePromise;
250
+ };
251
+ const fail = (error) => {
252
+ if (terminalError !== undefined || closing)
253
+ return;
254
+ terminalError = withStderr(error);
255
+ controller.abort(terminalError);
256
+ void cleanup(false).catch(() => undefined);
257
+ };
258
+ const onRunAbort = () => {
259
+ if (!closing)
260
+ fail(createError('LSP session was aborted', signal.reason));
261
+ };
262
+ signal.addEventListener('abort', onRunAbort, { once: true });
263
+ if (signal.aborted)
264
+ onRunAbort();
265
+ child.stderr?.on('data', (chunk) => {
266
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
267
+ stderr = Buffer.concat([stderr, bytes]).subarray(-MAX_STDERR_BYTES);
268
+ });
269
+ child.once('error', (error) => {
270
+ fail(createError(`LSP process '${settings.command}' failed: ${describe(error)}`, error));
271
+ });
272
+ child.once('close', (code, processSignal) => {
273
+ processClosed = true;
274
+ resolveProcessClosed();
275
+ if (!closing && terminalError === undefined) {
276
+ fail(createError(`LSP process exited unexpectedly (code ${String(code)}, signal ${String(processSignal)})`));
277
+ }
278
+ });
279
+ const bounded = (method, operation) => {
280
+ if (terminalError !== undefined || closing || controller.signal.aborted) {
281
+ return Promise.reject(closedError());
282
+ }
283
+ let abortListener;
284
+ const { promise: aborted, reject: rejectAbort } = Promise.withResolvers();
285
+ abortListener = () => rejectAbort(closedError());
286
+ controller.signal.addEventListener('abort', abortListener, { once: true });
287
+ let operationPromise;
288
+ try {
289
+ operationPromise = operation();
290
+ }
291
+ catch (error) {
292
+ operationPromise = Promise.reject(error);
293
+ }
294
+ const { promise: timeout, reject: rejectTimeout } = Promise.withResolvers();
295
+ const timer = setTimeout(() => {
296
+ const error = createError(`LSP request ${method} timed out after ${timeoutMs}ms`);
297
+ rejectTimeout(error);
298
+ fail(error);
299
+ }, timeoutMs);
300
+ return Promise.race([operationPromise, aborted, timeout]).finally(() => {
301
+ clearTimeout(timer);
302
+ if (abortListener !== undefined)
303
+ controller.signal.removeEventListener('abort', abortListener);
304
+ });
305
+ };
306
+ const ensureAvailable = () => {
307
+ if (terminalError !== undefined || closing || controller.signal.aborted)
308
+ throw closedError();
309
+ if (connection === undefined)
310
+ throw new Error('LSP connection is not initialized');
311
+ };
312
+ const assertCapability = (method, capability) => {
313
+ ensureAvailable();
314
+ if (capabilities?.[capability] === undefined || capabilities[capability] === false) {
315
+ throw new Error(`LSP server does not support ${method}`);
316
+ }
317
+ };
318
+ const request = (type, method, params, capability) => {
319
+ ensureAvailable();
320
+ if (capability !== undefined)
321
+ assertCapability(method, capability);
322
+ return bounded(method, () => connection.sendRequest(type, params));
323
+ };
324
+ const notify = (method, operation) => {
325
+ ensureAvailable();
326
+ return bounded(method, operation);
327
+ };
328
+ try {
329
+ connection = createProtocolConnection(child.stdout, child.stdin);
330
+ connection.onError((details) => {
331
+ const [error, message, code] = details;
332
+ fail(createError(`LSP transport failed${message !== undefined && 'method' in message ? ` for ${message.method}` : ''}${code === undefined ? '' : ` (${code})`}: ${describe(error)}`, error));
333
+ });
334
+ connection.onClose(() => {
335
+ if (!closing && terminalError === undefined)
336
+ fail(new Error('LSP transport closed unexpectedly'));
337
+ });
338
+ connection.onRequest(ApplyWorkspaceEditRequest.type, () => ({
339
+ applied: false,
340
+ failureReason: 'Read-only LSP session',
341
+ }));
342
+ connection.onRequest(ShowMessageRequest.type, () => null);
343
+ if (readiness !== undefined && progressToken !== undefined) {
344
+ connection.onProgress(WorkDoneProgress.type, progressToken, progress => {
345
+ if (progress.kind !== 'end')
346
+ return;
347
+ readinessPending = false;
348
+ readiness.resolve();
349
+ });
350
+ connection.onRequest(WorkDoneProgressCreateRequest.type, () => undefined);
351
+ connection.onNotification(ShowMessageNotification.type, ({ type, message }) => {
352
+ if (readinessPending &&
353
+ type === MessageType.Error &&
354
+ message.startsWith('Error while indexing ('))
355
+ fail(new Error(message));
356
+ });
357
+ }
358
+ connection.listen();
359
+ const initializeResult = await bounded('initialize', () => connection.sendRequest(InitializeRequest.type, {
360
+ processId: process.pid,
361
+ rootUri,
362
+ capabilities: {
363
+ general: { positionEncodings: ['utf-16'] },
364
+ textDocument: { documentSymbol: { hierarchicalDocumentSymbolSupport: true } },
365
+ ...(readiness === undefined ? {} : { window: { workDoneProgress: true } }),
366
+ },
367
+ workspaceFolders: [workspaceFolder],
368
+ }));
369
+ capabilities = initializeResult.capabilities;
370
+ // Ruby LSP 0.26.11 counts a preceding surrogate pair as one cursor unit.
371
+ rubyPositionNeedsSurrogateAdjustment =
372
+ settings.languageId === 'ruby' &&
373
+ initializeResult.serverInfo?.name === 'Ruby LSP' &&
374
+ initializeResult.serverInfo.version === '0.26.11';
375
+ await notify('initialized', () => connection.sendNotification(InitializedNotification.type, {}));
376
+ initialized = true;
377
+ if (readiness !== undefined)
378
+ await bounded('$/progress', () => readiness.promise);
379
+ }
380
+ catch (error) {
381
+ fail(error);
382
+ await cleanup(false).catch(() => undefined);
383
+ throw terminalError ?? withStderr(error);
384
+ }
385
+ const canonicalPathFromUri = (uri) => {
386
+ let filepath;
387
+ try {
388
+ const parsed = new URL(uri);
389
+ if (parsed.protocol !== 'file:')
390
+ throw new Error(`Unsupported LSP source URI scheme '${parsed.protocol}'`);
391
+ filepath = fileURLToPath(parsed);
392
+ }
393
+ catch (error) {
394
+ throw createError(`Invalid LSP file URI '${uri}': ${describe(error)}`, error);
395
+ }
396
+ return resolve(filepath);
397
+ };
398
+ const isAllowedUri = (uri, includeDependencies) => {
399
+ let parsed;
400
+ try {
401
+ parsed = new URL(uri);
402
+ }
403
+ catch (error) {
404
+ throw createError(`Invalid LSP URI '${uri}': ${describe(error)}`, error);
405
+ }
406
+ if (parsed.protocol !== 'file:')
407
+ return includeDependencies;
408
+ return includeDependencies || isWithinRoot(root, canonicalPathFromUri(uri));
409
+ };
410
+ const documentForPath = (filepath, includeDependencies) => {
411
+ const absolute = resolve(root, filepath);
412
+ const relativePath = isWithinRoot(root, absolute) ? relative(root, absolute) : undefined;
413
+ if (relativePath === undefined && !includeDependencies) {
414
+ return Promise.reject(new Error(`LSP source is outside the project: ${filepath}`));
415
+ }
416
+ const uri = pathToFileURL(absolute).href;
417
+ const cached = documentPromises.get(uri);
418
+ if (cached !== undefined)
419
+ return cached;
420
+ const pending = (async () => {
421
+ let text;
422
+ if (relativePath !== undefined)
423
+ text = await project.readText(relativePath);
424
+ else {
425
+ const data = await readFile(absolute);
426
+ try {
427
+ text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(data);
428
+ }
429
+ catch (error) {
430
+ throw createError(`LSP source '${absolute}' is not valid UTF-8`, error);
431
+ }
432
+ }
433
+ const document = {
434
+ uri,
435
+ text,
436
+ index: createTextIndex(text),
437
+ };
438
+ await notify('textDocument/didOpen', () => connection.sendNotification(DidOpenTextDocumentNotification.type, {
439
+ textDocument: {
440
+ uri,
441
+ languageId: settings.languageId === 'typescript' && extname(absolute) === '.tsx'
442
+ ? 'typescriptreact'
443
+ : settings.languageId,
444
+ version: 1,
445
+ text,
446
+ },
447
+ }));
448
+ return document;
449
+ })();
450
+ documentPromises.set(uri, pending);
451
+ return pending;
452
+ };
453
+ const documentForUri = (uri, includeDependencies) => {
454
+ if (!isAllowedUri(uri, includeDependencies)) {
455
+ return Promise.reject(new Error(`LSP source is outside the project: ${uri}`));
456
+ }
457
+ if (!uri.startsWith('file:')) {
458
+ return Promise.reject(new Error(`LSP source URI is not a file URI: ${uri}`));
459
+ }
460
+ return documentForPath(canonicalPathFromUri(uri), includeDependencies);
461
+ };
462
+ const positionForTarget = (document, line, column) => {
463
+ const position = document.index.position(line, column);
464
+ if (!rubyPositionNeedsSurrogateAdjustment)
465
+ return position;
466
+ const start = document.index.lineStarts[position.line];
467
+ const end = start + position.character;
468
+ let surrogatePairs = 0;
469
+ for (let index = start; index + 1 < end; index += 1) {
470
+ const code = document.text.charCodeAt(index);
471
+ const next = document.text.charCodeAt(index + 1);
472
+ if (code >= 0xd800 && code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
473
+ surrogatePairs += 1;
474
+ index += 1;
475
+ }
476
+ }
477
+ if (surrogatePairs === 0)
478
+ return position;
479
+ const character = position.character + surrogatePairs;
480
+ const lineLength = document.index.lineEnds[position.line] - start;
481
+ return character > lineLength ? position : { ...position, character };
482
+ };
483
+ const filterLocations = (result, includeDependencies) => normalizeLocations(result).filter(location => isAllowedUri(location.uri, includeDependencies));
484
+ const symbolQuery = async (document, includeDependencies) => {
485
+ assertCapability('textDocument/documentSymbol', 'documentSymbolProvider');
486
+ let pending = symbolPromises.get(document.uri);
487
+ if (pending === undefined) {
488
+ pending = request(DocumentSymbolRequest.type, 'textDocument/documentSymbol', {
489
+ textDocument: { uri: document.uri },
490
+ }, 'documentSymbolProvider').then(result => result ?? []);
491
+ symbolPromises.set(document.uri, pending);
492
+ }
493
+ const symbols = await pending;
494
+ if (symbols.every(isDocumentSymbol))
495
+ return symbols;
496
+ return symbols.filter(symbol => isAllowedUri(symbol.location.uri, includeDependencies));
497
+ };
498
+ const astFor = (document) => {
499
+ let pending = astPromises.get(document.uri);
500
+ if (pending === undefined) {
501
+ pending = Promise.resolve().then(() => parse(resolveAstGrepLanguage('go'), document.text));
502
+ astPromises.set(document.uri, pending);
503
+ }
504
+ return pending;
505
+ };
506
+ const astNodeRange = (node) => {
507
+ const native = node.range();
508
+ return {
509
+ start: { line: native.start.line, character: native.start.column },
510
+ end: { line: native.end.line, character: native.end.column },
511
+ };
512
+ };
513
+ const rangeOffsets = (document, range) => {
514
+ const start = document.index.offset(range.start);
515
+ const end = document.index.offset(range.end);
516
+ if (end < start)
517
+ throw new Error('LSP source range ends before it starts');
518
+ return { start, end };
519
+ };
520
+ const declarationFromRange = (document, range) => {
521
+ const { start, end } = rangeOffsets(document, range);
522
+ const source = document.text.slice(start, end);
523
+ if (source.length === 0)
524
+ throw new Error('LSP declaration range is empty');
525
+ return { source, lines: textLineSpan(range.start.line + 1, source) };
526
+ };
527
+ const bestSymbolRange = (document, declarations, selectionRange) => {
528
+ const completeDeclarations = declarations.filter(symbol => symbol.hasFullRange);
529
+ const exact = completeDeclarations.filter(symbol => sameRange(symbol.selectionRange, selectionRange));
530
+ const matching = exact.length > 0
531
+ ? exact
532
+ : completeDeclarations.filter(symbol => rangeContains(symbol.selectionRange, selectionRange));
533
+ return matching
534
+ .map(symbol => {
535
+ const offsets = rangeOffsets(document, symbol.range);
536
+ return { range: symbol.range, length: offsets.end - offsets.start };
537
+ })
538
+ .sort((left, right) => left.length - right.length || compareRanges(left.range, right.range))[0]?.range;
539
+ };
540
+ const astDeclarationRange = async (document, selectionRange) => {
541
+ const rootNode = (await astFor(document)).root();
542
+ const selectionStart = selectionRange.start;
543
+ const selectionEnd = selectionRange.end;
544
+ let best;
545
+ const pendingNodes = [rootNode];
546
+ while (pendingNodes.length > 0) {
547
+ const node = pendingNodes.pop();
548
+ for (const child of node.children())
549
+ pendingNodes.push(child);
550
+ if (AST_DECLARATION_KINDS[String(node.kind())] !== true)
551
+ continue;
552
+ const range = astNodeRange(node);
553
+ if (!rangeContains(range, { start: selectionStart, end: selectionEnd }))
554
+ continue;
555
+ const offsets = rangeOffsets(document, range);
556
+ const length = offsets.end - offsets.start;
557
+ if (best === undefined || length < best.length)
558
+ best = { node, range, length };
559
+ }
560
+ if (best === undefined)
561
+ return undefined;
562
+ const range = astNodeRange(best.node);
563
+ const native = best.node.range();
564
+ return { range, source: best.node.text(), startLine: native.start.line + 1 };
565
+ };
566
+ const declarationSource = async (item, includeDependencies) => {
567
+ const document = await documentForUri(item.uri, includeDependencies);
568
+ if (settings.languageId === 'go' && item.kind === SymbolKind.Variable) {
569
+ const astDeclaration = await astDeclarationRange(document, item.selectionRange);
570
+ if (astDeclaration !== undefined) {
571
+ return {
572
+ source: astDeclaration.source,
573
+ lines: textLineSpan(astDeclaration.startLine, astDeclaration.source),
574
+ range: astDeclaration.range,
575
+ };
576
+ }
577
+ }
578
+ const documentSymbolProvider = capabilities?.documentSymbolProvider;
579
+ if (documentSymbolProvider !== undefined && documentSymbolProvider !== false) {
580
+ const symbols = await symbolQuery(document, includeDependencies);
581
+ const declarations = flattenSymbols(symbols);
582
+ const symbolRange = bestSymbolRange(document, declarations, item.selectionRange);
583
+ if (symbolRange !== undefined) {
584
+ return { ...declarationFromRange(document, symbolRange), range: symbolRange };
585
+ }
586
+ }
587
+ if (rangeStrictlyContains(item.range, item.selectionRange)) {
588
+ return { ...declarationFromRange(document, item.range), range: item.range };
589
+ }
590
+ if (settings.languageId === 'go') {
591
+ const astDeclaration = await astDeclarationRange(document, item.selectionRange);
592
+ if (astDeclaration !== undefined) {
593
+ return {
594
+ source: astDeclaration.source,
595
+ lines: textLineSpan(astDeclaration.startLine, astDeclaration.source),
596
+ range: astDeclaration.range,
597
+ };
598
+ }
599
+ }
600
+ throw new Error(`Could not find the full declaration for LSP call hierarchy item '${item.name}' at ${item.uri}`);
601
+ };
602
+ const itemIdentity = (item) => {
603
+ const filepath = canonicalPathFromUri(item.uri);
604
+ const selection = item.selectionRange;
605
+ return `${filepath}\0${selection.start.line}:${selection.start.character}-${selection.end.line}:${selection.end.character}`;
606
+ };
607
+ const itemPath = (item) => {
608
+ const filepath = canonicalPathFromUri(item.uri);
609
+ const relativePath = relative(root, filepath);
610
+ return isWithinRoot(root, filepath) ? relativePath : filepath;
611
+ };
612
+ const symbolsForTarget = async (document, targetLine, includeDependencies) => {
613
+ const symbols = flattenSymbols(await symbolQuery(document, includeDependencies));
614
+ const line = targetLine - 1;
615
+ const sortCandidates = (candidates) => [...candidates].sort((left, right) => {
616
+ const leftRange = rangeOffsets(document, left.range);
617
+ const rightRange = rangeOffsets(document, right.range);
618
+ const leftSize = leftRange.end - leftRange.start;
619
+ const rightSize = rightRange.end - rightRange.start;
620
+ return leftSize - rightSize ||
621
+ comparePosition(left.selectionRange.start, right.selectionRange.start) ||
622
+ left.kind - right.kind ||
623
+ compareText(left.name, right.name);
624
+ });
625
+ const candidates = symbols.filter(symbol => (settings.languageId !== 'go' || symbol.hasFullRange) &&
626
+ (symbol.kind === SymbolKind.Function || symbol.kind === SymbolKind.Method) &&
627
+ symbol.name.trim() !== '' &&
628
+ symbol.range.start.line <= line &&
629
+ symbol.range.end.line >= line);
630
+ if (candidates.length > 0 || settings.languageId !== 'go') {
631
+ return sortCandidates(candidates);
632
+ }
633
+ const pendingNodes = [(await astFor(document)).root()];
634
+ const astCandidates = [];
635
+ while (pendingNodes.length > 0) {
636
+ const node = pendingNodes.pop();
637
+ for (const child of node.children())
638
+ pendingNodes.push(child);
639
+ const nodeKind = String(node.kind());
640
+ if (nodeKind !== 'function_declaration' &&
641
+ nodeKind !== 'method_declaration' &&
642
+ nodeKind !== 'method_elem')
643
+ continue;
644
+ const range = astNodeRange(node);
645
+ if (range.start.line > line || range.end.line < line)
646
+ continue;
647
+ const nameNode = node.field('name');
648
+ if (nameNode === null)
649
+ continue;
650
+ const name = nameNode.text();
651
+ if (name.trim() === '')
652
+ continue;
653
+ astCandidates.push({
654
+ name,
655
+ kind: nodeKind === 'function_declaration' ? SymbolKind.Function : SymbolKind.Method,
656
+ range,
657
+ selectionRange: astNodeRange(nameNode),
658
+ hasFullRange: true,
659
+ });
660
+ }
661
+ return sortCandidates(astCandidates);
662
+ };
663
+ const prepareRoots = async (target, includeDependencies) => {
664
+ assertCapability('textDocument/prepareCallHierarchy', 'callHierarchyProvider');
665
+ const document = await documentForPath(target.filepath, includeDependencies);
666
+ let position;
667
+ if (target.column !== undefined)
668
+ position = positionForTarget(document, target.line, target.column);
669
+ else {
670
+ if (!Number.isSafeInteger(target.line) || target.line < 1 || target.line > document.index.lineCount) {
671
+ throw new Error(`LSP line ${target.line} is outside the document`);
672
+ }
673
+ const candidates = await symbolsForTarget(document, target.line, includeDependencies);
674
+ if (candidates.length === 0)
675
+ return [];
676
+ position = candidates[0].selectionRange.start;
677
+ }
678
+ const prepared = await request(CallHierarchyPrepareRequest.type, 'textDocument/prepareCallHierarchy', {
679
+ textDocument: { uri: document.uri },
680
+ position,
681
+ }, 'callHierarchyProvider');
682
+ return prepared ?? [];
683
+ };
684
+ const traverse = async (target, options, direction) => {
685
+ if (!Number.isSafeInteger(options.depth) || options.depth < 0) {
686
+ throw new Error('LSP call depth must be a nonnegative safe integer');
687
+ }
688
+ if (options.depth === 0)
689
+ return [];
690
+ const includeDependencies = options.includeDependencies === true;
691
+ const roots = await prepareRoots(target, includeDependencies);
692
+ if (roots.length === 0)
693
+ return [];
694
+ const permittedRoots = roots.filter(item => isAllowedUri(item.uri, includeDependencies));
695
+ const rootIdentities = new Set(permittedRoots.map(itemIdentity));
696
+ const accepted = new Map();
697
+ const expanded = new Set();
698
+ const queue = [];
699
+ for (const item of permittedRoots) {
700
+ const identity = itemIdentity(item);
701
+ if (!expanded.has(identity)) {
702
+ expanded.add(identity);
703
+ queue.push({ item, depth: 0 });
704
+ }
705
+ }
706
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
707
+ const current = queue[cursor];
708
+ if (current.depth >= options.depth)
709
+ continue;
710
+ const response = direction === 'callers'
711
+ ? (await request(CallHierarchyIncomingCallsRequest.type, 'callHierarchy/incomingCalls', { item: current.item }, 'callHierarchyProvider')) ?? []
712
+ : (await request(CallHierarchyOutgoingCallsRequest.type, 'callHierarchy/outgoingCalls', { item: current.item }, 'callHierarchyProvider')) ?? [];
713
+ for (const edge of response) {
714
+ const item = direction === 'callers'
715
+ ? edge.from
716
+ : edge.to;
717
+ if (!isAllowedUri(item.uri, includeDependencies))
718
+ continue;
719
+ const identity = itemIdentity(item);
720
+ const depth = current.depth + 1;
721
+ if (!rootIdentities.has(identity)) {
722
+ const previous = accepted.get(identity);
723
+ if (previous === undefined || depth < previous.depth)
724
+ accepted.set(identity, { item, depth });
725
+ }
726
+ if (!expanded.has(identity)) {
727
+ expanded.add(identity);
728
+ queue.push({ item, depth });
729
+ }
730
+ }
731
+ }
732
+ const results = [];
733
+ for (const node of accepted.values()) {
734
+ const declaration = await declarationSource(node.item, includeDependencies);
735
+ results.push({
736
+ call: {
737
+ name: node.item.name,
738
+ filepath: itemPath(node.item),
739
+ source: declaration.source,
740
+ lines: declaration.lines,
741
+ depth: node.depth,
742
+ },
743
+ declarationPosition: declaration.range.start,
744
+ });
745
+ }
746
+ results.sort((left, right) => left.call.depth - right.call.depth ||
747
+ compareText(left.call.filepath, right.call.filepath) ||
748
+ comparePosition(left.declarationPosition, right.declarationPosition) ||
749
+ compareText(left.call.name, right.call.name));
750
+ return results.map(result => result.call);
751
+ };
752
+ const navigate = async (method, capability, type, target, options) => {
753
+ const includeDependencies = options?.includeDependencies === true;
754
+ const document = await documentForPath(target.filepath, includeDependencies);
755
+ const position = positionForTarget(document, target.line, target.column ?? 1);
756
+ const result = await request(type, method, {
757
+ textDocument: { uri: document.uri },
758
+ position,
759
+ }, capability);
760
+ return filterLocations(result, includeDependencies);
761
+ };
762
+ const tools = {
763
+ definitions: (target, options) => navigate('textDocument/definition', 'definitionProvider', DefinitionRequest.type, target, options),
764
+ declarations: (target, options) => navigate('textDocument/declaration', 'declarationProvider', DeclarationRequest.type, target, options),
765
+ typeDefinitions: (target, options) => navigate('textDocument/typeDefinition', 'typeDefinitionProvider', TypeDefinitionRequest.type, target, options),
766
+ implementations: (target, options) => navigate('textDocument/implementation', 'implementationProvider', ImplementationRequest.type, target, options),
767
+ references: async (target, options) => {
768
+ const includeDependencies = options?.includeDependencies === true;
769
+ const document = await documentForPath(target.filepath, includeDependencies);
770
+ const position = positionForTarget(document, target.line, target.column ?? 1);
771
+ const result = await request(ReferencesRequest.type, 'textDocument/references', {
772
+ textDocument: { uri: document.uri },
773
+ position,
774
+ context: { includeDeclaration: options?.includeDeclaration ?? false },
775
+ }, 'referencesProvider');
776
+ return filterLocations(result, includeDependencies);
777
+ },
778
+ hover: async (target, options) => {
779
+ const includeDependencies = options?.includeDependencies === true;
780
+ const document = await documentForPath(target.filepath, includeDependencies);
781
+ const position = positionForTarget(document, target.line, target.column ?? 1);
782
+ const result = await request(HoverRequest.type, 'textDocument/hover', {
783
+ textDocument: { uri: document.uri },
784
+ position,
785
+ }, 'hoverProvider');
786
+ return result ?? null;
787
+ },
788
+ documentSymbols: async (filepath, options) => {
789
+ const document = await documentForPath(filepath, options?.includeDependencies === true);
790
+ return symbolQuery(document, options?.includeDependencies === true);
791
+ },
792
+ workspaceSymbols: async (query, options) => {
793
+ if (query.length === 0)
794
+ return [];
795
+ const result = await request(WorkspaceSymbolRequest.type, 'workspace/symbol', { query }, 'workspaceSymbolProvider');
796
+ const includeDependencies = options?.includeDependencies === true;
797
+ return (result ?? []).filter(symbol => isAllowedUri(symbol.location.uri, includeDependencies));
798
+ },
799
+ callers: (target, options) => traverse(target, options, 'callers'),
800
+ callees: (target, options) => traverse(target, options, 'callees'),
801
+ };
802
+ const close = () => {
803
+ if (closePromise !== undefined)
804
+ return closePromise;
805
+ closing = true;
806
+ controller.abort(new Error('LSP session is closed'));
807
+ closePromise = cleanup(initialized && terminalError === undefined);
808
+ return closePromise;
809
+ };
810
+ return { ...tools, close };
811
+ }
812
+ export function createRubyLspBackend(project, signal) {
813
+ return createLspBackend(project, signal, {
814
+ command: 'ruby-lsp',
815
+ args: [],
816
+ languageId: 'ruby',
817
+ initializationProgressToken: 'indexing-progress',
818
+ });
819
+ }
820
+ export function createTypescriptLspBackend(project, signal) {
821
+ return createLspBackend(project, signal, {
822
+ command: 'tsc',
823
+ args: ['--lsp', '--stdio'],
824
+ languageId: 'typescript',
825
+ });
826
+ }