@birdcc/lsp 0.0.1-alpha.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 (77) hide show
  1. package/.oxfmtrc.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +343 -0
  4. package/dist/completion.d.ts +8 -0
  5. package/dist/completion.d.ts.map +1 -0
  6. package/dist/completion.js +137 -0
  7. package/dist/completion.js.map +1 -0
  8. package/dist/definition.d.ts +5 -0
  9. package/dist/definition.d.ts.map +1 -0
  10. package/dist/definition.js +21 -0
  11. package/dist/definition.js.map +1 -0
  12. package/dist/diagnostic.d.ts +6 -0
  13. package/dist/diagnostic.d.ts.map +1 -0
  14. package/dist/diagnostic.js +38 -0
  15. package/dist/diagnostic.js.map +1 -0
  16. package/dist/document-symbol.d.ts +4 -0
  17. package/dist/document-symbol.d.ts.map +1 -0
  18. package/dist/document-symbol.js +20 -0
  19. package/dist/document-symbol.js.map +1 -0
  20. package/dist/hover-docs.d.ts +5 -0
  21. package/dist/hover-docs.d.ts.map +1 -0
  22. package/dist/hover-docs.js +141 -0
  23. package/dist/hover-docs.js.map +1 -0
  24. package/dist/hover-docs.yaml +600 -0
  25. package/dist/hover.d.ts +5 -0
  26. package/dist/hover.d.ts.map +1 -0
  27. package/dist/hover.js +81 -0
  28. package/dist/hover.js.map +1 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/lsp-server.d.ts +3 -0
  34. package/dist/lsp-server.d.ts.map +1 -0
  35. package/dist/lsp-server.js +250 -0
  36. package/dist/lsp-server.js.map +1 -0
  37. package/dist/references.d.ts +5 -0
  38. package/dist/references.d.ts.map +1 -0
  39. package/dist/references.js +48 -0
  40. package/dist/references.js.map +1 -0
  41. package/dist/server.d.ts +3 -0
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +4 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/shared.d.ts +17 -0
  46. package/dist/shared.d.ts.map +1 -0
  47. package/dist/shared.js +150 -0
  48. package/dist/shared.js.map +1 -0
  49. package/dist/symbol-utils.d.ts +17 -0
  50. package/dist/symbol-utils.d.ts.map +1 -0
  51. package/dist/symbol-utils.js +84 -0
  52. package/dist/symbol-utils.js.map +1 -0
  53. package/dist/validation.d.ts +21 -0
  54. package/dist/validation.d.ts.map +1 -0
  55. package/dist/validation.js +47 -0
  56. package/dist/validation.js.map +1 -0
  57. package/package.json +45 -0
  58. package/scripts/copy-hover-yaml.mjs +14 -0
  59. package/src/completion.ts +223 -0
  60. package/src/definition.ts +50 -0
  61. package/src/diagnostic.ts +48 -0
  62. package/src/document-symbol.ts +27 -0
  63. package/src/hover-docs.ts +223 -0
  64. package/src/hover-docs.yaml +600 -0
  65. package/src/hover.ts +122 -0
  66. package/src/index.ts +8 -0
  67. package/src/lsp-server.ts +350 -0
  68. package/src/references.ts +107 -0
  69. package/src/server.ts +4 -0
  70. package/src/shared.ts +182 -0
  71. package/src/symbol-utils.ts +126 -0
  72. package/src/validation.ts +85 -0
  73. package/test/hover-docs.test.ts +18 -0
  74. package/test/lsp.test.ts +304 -0
  75. package/test/perf-baseline.test.ts +96 -0
  76. package/test/validation.test.ts +212 -0
  77. package/tsconfig.json +8 -0
@@ -0,0 +1,126 @@
1
+ import type { SymbolDefinition, SymbolReference } from "@birdcc/core";
2
+ import type { Location, Position } from "vscode-languageserver/node.js";
3
+
4
+ export interface SymbolLookupIndex {
5
+ definitionsByName: Map<string, SymbolDefinition[]>;
6
+ referencesByName: Map<string, SymbolReference[]>;
7
+ }
8
+
9
+ const addToMapList = <T>(
10
+ map: Map<string, T[]>,
11
+ key: string,
12
+ value: T,
13
+ ): void => {
14
+ const existing = map.get(key);
15
+ if (existing) {
16
+ existing.push(value);
17
+ return;
18
+ }
19
+
20
+ map.set(key, [value]);
21
+ };
22
+
23
+ export const containsPosition = (
24
+ range: { line: number; column: number; endLine: number; endColumn: number },
25
+ position: Position,
26
+ ): boolean => {
27
+ const line = position.line + 1;
28
+ const column = position.character + 1;
29
+
30
+ if (line < range.line || line > range.endLine) {
31
+ return false;
32
+ }
33
+
34
+ if (line === range.line && column < range.column) {
35
+ return false;
36
+ }
37
+
38
+ if (line === range.endLine && column > range.endColumn) {
39
+ return false;
40
+ }
41
+
42
+ return true;
43
+ };
44
+
45
+ export const toLocation = (
46
+ symbol: SymbolDefinition | SymbolReference,
47
+ ): Location => ({
48
+ uri: symbol.uri,
49
+ range: {
50
+ start: {
51
+ line: Math.max(0, symbol.line - 1),
52
+ character: Math.max(0, symbol.column - 1),
53
+ },
54
+ end: {
55
+ line: Math.max(0, symbol.endLine - 1),
56
+ character: Math.max(0, symbol.endColumn - 1),
57
+ },
58
+ },
59
+ });
60
+
61
+ export const extractWordAtPosition = (
62
+ text: string,
63
+ position: Position,
64
+ ): string => {
65
+ const lineText = text.split(/\r?\n/)[position.line] ?? "";
66
+ if (position.character < 0 || position.character > lineText.length) {
67
+ return "";
68
+ }
69
+
70
+ let start = position.character;
71
+ while (start > 0 && /[A-Za-z0-9_]/.test(lineText[start - 1] ?? "")) {
72
+ start -= 1;
73
+ }
74
+
75
+ let end = position.character;
76
+ while (end < lineText.length && /[A-Za-z0-9_]/.test(lineText[end] ?? "")) {
77
+ end += 1;
78
+ }
79
+
80
+ return lineText.slice(start, end).trim();
81
+ };
82
+
83
+ export const dedupeLocations = (locations: Location[]): Location[] => {
84
+ const seen = new Set<string>();
85
+ const output: Location[] = [];
86
+
87
+ for (const location of locations) {
88
+ const key = [
89
+ location.uri,
90
+ location.range.start.line,
91
+ location.range.start.character,
92
+ location.range.end.line,
93
+ location.range.end.character,
94
+ ].join(":");
95
+
96
+ if (seen.has(key)) {
97
+ continue;
98
+ }
99
+
100
+ seen.add(key);
101
+ output.push(location);
102
+ }
103
+
104
+ return output;
105
+ };
106
+
107
+ export const createSymbolLookupIndex = (
108
+ definitions: SymbolDefinition[],
109
+ references: SymbolReference[],
110
+ ): SymbolLookupIndex => {
111
+ const definitionsByName = new Map<string, SymbolDefinition[]>();
112
+ const referencesByName = new Map<string, SymbolReference[]>();
113
+
114
+ for (const definition of definitions) {
115
+ addToMapList(definitionsByName, definition.name.toLowerCase(), definition);
116
+ }
117
+
118
+ for (const reference of references) {
119
+ addToMapList(referencesByName, reference.name.toLowerCase(), reference);
120
+ }
121
+
122
+ return {
123
+ definitionsByName,
124
+ referencesByName,
125
+ };
126
+ };
@@ -0,0 +1,85 @@
1
+ export interface ValidationDocument {
2
+ uri: string;
3
+ version: number;
4
+ getText(): string;
5
+ }
6
+
7
+ export interface ValidationPublishPayload<TDiagnostic> {
8
+ uri: string;
9
+ version?: number;
10
+ diagnostics: TDiagnostic[];
11
+ }
12
+
13
+ export interface ValidationSchedulerOptions<
14
+ TDocument extends ValidationDocument,
15
+ TDiagnostic,
16
+ > {
17
+ debounceMs: number;
18
+ validate(document: TDocument): Promise<TDiagnostic[]>;
19
+ publish(payload: ValidationPublishPayload<TDiagnostic>): void;
20
+ }
21
+
22
+ export interface ValidationScheduler<TDocument extends ValidationDocument> {
23
+ schedule(document: TDocument): void;
24
+ close(uri: string): void;
25
+ }
26
+
27
+ export const createValidationScheduler = <
28
+ TDocument extends ValidationDocument,
29
+ TDiagnostic,
30
+ >(
31
+ options: ValidationSchedulerOptions<TDocument, TDiagnostic>,
32
+ ): ValidationScheduler<TDocument> => {
33
+ const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>();
34
+ const latestTicketByUri = new Map<string, number>();
35
+ const latestVersionByUri = new Map<string, number>();
36
+ let nextTicket = 0;
37
+
38
+ const clearPending = (uri: string): void => {
39
+ const pendingTimer = pendingTimers.get(uri);
40
+ if (!pendingTimer) {
41
+ return;
42
+ }
43
+
44
+ clearTimeout(pendingTimer);
45
+ pendingTimers.delete(uri);
46
+ };
47
+
48
+ const runValidation = async (document: TDocument): Promise<void> => {
49
+ const uri = document.uri;
50
+ const ticket = ++nextTicket;
51
+ latestTicketByUri.set(uri, ticket);
52
+ latestVersionByUri.set(uri, document.version);
53
+
54
+ const diagnostics = await options.validate(document);
55
+ if (latestTicketByUri.get(uri) !== ticket) {
56
+ return;
57
+ }
58
+
59
+ options.publish({ uri, version: document.version, diagnostics });
60
+ };
61
+
62
+ return {
63
+ schedule: (document: TDocument): void => {
64
+ clearPending(document.uri);
65
+
66
+ const timer = setTimeout(() => {
67
+ pendingTimers.delete(document.uri);
68
+ void runValidation(document);
69
+ }, options.debounceMs);
70
+
71
+ pendingTimers.set(document.uri, timer);
72
+ },
73
+ close: (uri: string): void => {
74
+ clearPending(uri);
75
+ latestTicketByUri.delete(uri);
76
+ const version = latestVersionByUri.get(uri);
77
+ latestVersionByUri.delete(uri);
78
+ options.publish({
79
+ uri,
80
+ version,
81
+ diagnostics: [],
82
+ });
83
+ },
84
+ };
85
+ };
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { HOVER_KEYWORDS, HOVER_KEYWORD_DOCS } from "../src/hover-docs.js";
4
+ import { KEYWORD_DOCS } from "../src/shared.js";
5
+
6
+ describe("hover docs catalog", () => {
7
+ it("loads generated hover docs map with stable keyword coverage", () => {
8
+ expect(HOVER_KEYWORDS.length).toBeGreaterThanOrEqual(80);
9
+ expect(HOVER_KEYWORD_DOCS["thread group"]).toContain("Diff: `added`");
10
+ expect(HOVER_KEYWORD_DOCS["thread group"]).toContain("Version: `v3+`");
11
+ });
12
+
13
+ it("exposes merged keyword docs through shared keyword map", () => {
14
+ expect(KEYWORD_DOCS["router id"]).toContain("Version: `v2+`");
15
+ expect(KEYWORD_DOCS["router id"]).toContain("BIRD v2.18 / v3.2.0");
16
+ expect(KEYWORD_DOCS["router id"]).toContain("bird-2.18.html");
17
+ });
18
+ });
@@ -0,0 +1,304 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DiagnosticSeverity } from "vscode-languageserver/node.js";
3
+ import { TextDocument } from "vscode-languageserver-textdocument";
4
+ import { parseBirdConfig } from "@birdcc/parser";
5
+ import {
6
+ createCompletionItemsFromParsed,
7
+ createDefinitionLocations,
8
+ createDocumentSymbolsFromParsed,
9
+ createHoverFromParsed,
10
+ createReferenceLocations,
11
+ toLspDiagnostic,
12
+ } from "../src/index.js";
13
+
14
+ describe("@birdcc/lsp", () => {
15
+ it("maps bird diagnostics to lsp diagnostics", () => {
16
+ const output = toLspDiagnostic({
17
+ code: "semantic/undefined-reference",
18
+ message: "undefined ref",
19
+ severity: "error",
20
+ source: "core",
21
+ range: {
22
+ line: 10,
23
+ column: 3,
24
+ endLine: 10,
25
+ endColumn: 8,
26
+ },
27
+ });
28
+
29
+ expect(output.severity).toBe(DiagnosticSeverity.Error);
30
+ expect(output.range.start.line).toBe(9);
31
+ expect(output.range.start.character).toBe(2);
32
+ });
33
+
34
+ it("creates document symbols for key declaration kinds", async () => {
35
+ const parsed = await parseBirdConfig(`
36
+ include "base.conf";
37
+ define ASN = 65001;
38
+ ipv4 table main4;
39
+ router id 1.1.1.1;
40
+ template bgp edge_tpl {}
41
+ protocol bgp edge from edge_tpl {}
42
+ filter export_policy { accept; }
43
+ function helper() -> bool { return true; }
44
+ `);
45
+
46
+ const symbols = createDocumentSymbolsFromParsed(parsed);
47
+ const names = symbols.map((item) => item.name);
48
+
49
+ expect(names).toContain("base.conf");
50
+ expect(names).toContain("ASN");
51
+ expect(names).toContain("main4");
52
+ expect(names).toContain("router id 1.1.1.1");
53
+ expect(names).toContain("edge_tpl");
54
+ expect(names).toContain("edge");
55
+ expect(names).toContain("export_policy");
56
+ expect(names).toContain("helper");
57
+ });
58
+
59
+ it("creates hover for declaration symbol", async () => {
60
+ const text = `protocol bgp edge {}`;
61
+ const parsed = await parseBirdConfig(text);
62
+ const document = TextDocument.create("file:///bird.conf", "bird", 1, text);
63
+
64
+ const hover = createHoverFromParsed(parsed, document, {
65
+ line: 0,
66
+ character: 14,
67
+ });
68
+
69
+ expect(hover?.contents).toBeDefined();
70
+ });
71
+
72
+ it("creates hover for keyword", async () => {
73
+ const text = `include "base.conf";`;
74
+ const parsed = await parseBirdConfig(text);
75
+ const document = TextDocument.create("file:///bird.conf", "bird", 1, text);
76
+
77
+ const hover = createHoverFromParsed(parsed, document, {
78
+ line: 0,
79
+ character: 1,
80
+ });
81
+
82
+ expect(hover?.contents).toBeDefined();
83
+ });
84
+
85
+ it("escapes markdown code content in include hover", async () => {
86
+ const text = 'include "unsafe`name.conf";';
87
+ const parsed = await parseBirdConfig(text);
88
+ const document = TextDocument.create("file:///bird.conf", "bird", 1, text);
89
+
90
+ const hover = createHoverFromParsed(parsed, document, {
91
+ line: 0,
92
+ character: 12,
93
+ });
94
+
95
+ expect(hover?.contents).toBeDefined();
96
+ expect(
97
+ hover && typeof hover.contents !== "string" ? hover.contents.value : "",
98
+ ).toContain("unsafe\\`name.conf");
99
+ });
100
+
101
+ it("creates hover for multi-word keyword phrase", async () => {
102
+ const text = `protocol bgp edge { local as 65001; }`;
103
+ const parsed = await parseBirdConfig(text);
104
+ const document = TextDocument.create("file:///bird.conf", "bird", 1, text);
105
+
106
+ const hover = createHoverFromParsed(parsed, document, {
107
+ line: 0,
108
+ character: 28,
109
+ });
110
+
111
+ expect(hover?.contents).toBeDefined();
112
+ });
113
+
114
+ it("creates hover for define declaration name", async () => {
115
+ const text = `define ASN = 65001;`;
116
+ const parsed = await parseBirdConfig(text);
117
+ const document = TextDocument.create("file:///bird.conf", "bird", 1, text);
118
+
119
+ const hover = createHoverFromParsed(parsed, document, {
120
+ line: 0,
121
+ character: 8,
122
+ });
123
+
124
+ expect(hover?.contents).toBeDefined();
125
+ });
126
+
127
+ it("creates completion items with keywords and symbols", async () => {
128
+ const parsed = await parseBirdConfig(`
129
+ include "base.conf";
130
+ define ASN = 65001;
131
+ ipv4 table main4;
132
+ router id 1.1.1.1;
133
+ template bgp edge_tpl {}
134
+ filter export_policy { accept; }
135
+ `);
136
+
137
+ const items = createCompletionItemsFromParsed(parsed);
138
+ const labels = items.map((item) => item.label);
139
+
140
+ expect(labels).toContain("define");
141
+ expect(labels).toContain("protocol");
142
+ expect(labels).toContain("template");
143
+ expect(labels).toContain("base.conf");
144
+ expect(labels).toContain("ASN");
145
+ expect(labels).toContain("main4");
146
+ expect(labels).toContain("router id 1.1.1.1");
147
+ expect(labels).toContain("edge_tpl");
148
+ expect(labels).toContain("export_policy");
149
+ });
150
+
151
+ it("returns template completions in protocol from context", async () => {
152
+ const parsed = await parseBirdConfig(`
153
+ template bgp edge_tpl {}
154
+ template bgp core_tpl {}
155
+ define ASN = 65001;
156
+ filter export_policy { accept; }
157
+ `);
158
+
159
+ const items = createCompletionItemsFromParsed(parsed, {
160
+ linePrefix: "protocol bgp edge from ",
161
+ });
162
+ const labels = items.map((item) => item.label);
163
+
164
+ expect(labels).toContain("edge_tpl");
165
+ expect(labels).toContain("core_tpl");
166
+ expect(labels).not.toContain("ASN");
167
+ expect(labels).not.toContain("export_policy");
168
+ });
169
+
170
+ it("returns include path candidates inside include string context", async () => {
171
+ const parsed = await parseBirdConfig(`
172
+ include "base.conf";
173
+ include "upstream/edge.conf";
174
+ template bgp edge_tpl {}
175
+ `);
176
+
177
+ const items = createCompletionItemsFromParsed(parsed, {
178
+ linePrefix: 'include "',
179
+ });
180
+ const labels = items.map((item) => item.label);
181
+
182
+ expect(labels).toContain("base.conf");
183
+ expect(labels).toContain("upstream/edge.conf");
184
+ expect(labels).not.toContain("template");
185
+ });
186
+
187
+ it("returns empty completion list for include context without include declarations", async () => {
188
+ const parsed = await parseBirdConfig(`
189
+ template bgp edge_tpl {}
190
+ `);
191
+
192
+ const items = createCompletionItemsFromParsed(parsed, {
193
+ linePrefix: 'include "',
194
+ });
195
+
196
+ expect(items).toEqual([]);
197
+ });
198
+
199
+ it("returns empty completion list for from context without template declarations", async () => {
200
+ const parsed = await parseBirdConfig(`
201
+ define ASN = 65001;
202
+ filter export_policy { accept; }
203
+ `);
204
+
205
+ const items = createCompletionItemsFromParsed(parsed, {
206
+ linePrefix: "protocol bgp edge from ",
207
+ });
208
+
209
+ expect(items).toEqual([]);
210
+ });
211
+
212
+ it("includes snippet completion items in generic context", async () => {
213
+ const parsed = await parseBirdConfig(`
214
+ template bgp edge_tpl {}
215
+ `);
216
+
217
+ const items = createCompletionItemsFromParsed(parsed);
218
+ const labels = items.map((item) => item.label);
219
+
220
+ expect(labels).toContain('include "..."');
221
+ expect(labels).toContain("define NAME = value;");
222
+ expect(labels).toContain("router id 1.1.1.1;");
223
+ expect(labels).toContain("protocol bgp ...");
224
+ });
225
+
226
+ it("merges additional declarations into from/filter/table completion contexts", async () => {
227
+ const entryParsed = await parseBirdConfig(`
228
+ protocol bgp edge from
229
+ `);
230
+ const includeParsed = await parseBirdConfig(`
231
+ template bgp core_tpl {}
232
+ filter import_policy { accept; }
233
+ ipv4 table main4;
234
+ `);
235
+
236
+ const additionalDeclarations = includeParsed.program.declarations;
237
+ const fromItems = createCompletionItemsFromParsed(entryParsed, {
238
+ linePrefix: "protocol bgp edge from ",
239
+ additionalDeclarations,
240
+ });
241
+ expect(fromItems.map((item) => item.label)).toContain("core_tpl");
242
+
243
+ const filterItems = createCompletionItemsFromParsed(entryParsed, {
244
+ linePrefix: "import filter ",
245
+ additionalDeclarations,
246
+ });
247
+ expect(filterItems.map((item) => item.label)).toContain("import_policy");
248
+
249
+ const tableItems = createCompletionItemsFromParsed(entryParsed, {
250
+ linePrefix: "table ",
251
+ additionalDeclarations,
252
+ });
253
+ expect(tableItems.map((item) => item.label)).toContain("main4");
254
+ });
255
+
256
+ it("resolves cross-file definition and references from merged symbol table", () => {
257
+ const symbolTable = {
258
+ definitions: [
259
+ {
260
+ kind: "template" as const,
261
+ name: "edge_tpl",
262
+ line: 1,
263
+ column: 14,
264
+ endLine: 1,
265
+ endColumn: 22,
266
+ uri: "file:///templates.conf",
267
+ },
268
+ ],
269
+ references: [
270
+ {
271
+ kind: "template" as const,
272
+ name: "edge_tpl",
273
+ line: 1,
274
+ column: 24,
275
+ endLine: 1,
276
+ endColumn: 32,
277
+ uri: "file:///main.conf",
278
+ },
279
+ ],
280
+ };
281
+
282
+ const definitionLocations = createDefinitionLocations(
283
+ symbolTable,
284
+ "file:///main.conf",
285
+ { line: 0, character: 25 },
286
+ "protocol bgp edge from edge_tpl {}",
287
+ );
288
+ expect(definitionLocations).toHaveLength(1);
289
+ expect(definitionLocations[0]?.uri).toBe("file:///templates.conf");
290
+
291
+ const referenceLocations = createReferenceLocations(
292
+ symbolTable,
293
+ "file:///main.conf",
294
+ { line: 0, character: 25 },
295
+ "protocol bgp edge from edge_tpl {}",
296
+ );
297
+ expect(referenceLocations.map((item) => item.uri)).toContain(
298
+ "file:///templates.conf",
299
+ );
300
+ expect(referenceLocations.map((item) => item.uri)).toContain(
301
+ "file:///main.conf",
302
+ );
303
+ });
304
+ });
@@ -0,0 +1,96 @@
1
+ import { resolveCrossFileReferences } from "@birdcc/core";
2
+ import { lintResolvedCrossFileGraph } from "@birdcc/linter";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ interface PerfCase {
6
+ name: string;
7
+ includeCount: number;
8
+ linesPerInclude: number;
9
+ baselineMs: number;
10
+ }
11
+
12
+ const PERF_CASES: PerfCase[] = [
13
+ { name: "small", includeCount: 2, linesPerInclude: 80, baselineMs: 120 },
14
+ { name: "medium", includeCount: 8, linesPerInclude: 220, baselineMs: 320 },
15
+ { name: "large", includeCount: 20, linesPerInclude: 500, baselineMs: 600 },
16
+ ];
17
+
18
+ const createIncludeBody = (index: number, linesPerInclude: number): string => {
19
+ const lines: string[] = [];
20
+ lines.push(`template bgp tpl_${index} { local as ${65000 + index}; }`);
21
+ lines.push(`filter export_${index} { accept; }`);
22
+
23
+ for (let line = 0; line < linesPerInclude; line += 1) {
24
+ lines.push(`define DEF_${index}_${line} = ${line};`);
25
+ }
26
+
27
+ return `${lines.join("\n")}\n`;
28
+ };
29
+
30
+ const createCaseDocuments = (
31
+ testCase: PerfCase,
32
+ ): { entryUri: string; documents: Array<{ uri: string; text: string }> } => {
33
+ const entryUri = `memory://perf/${testCase.name}/main.conf`;
34
+ const documents: Array<{ uri: string; text: string }> = [];
35
+ const entryLines: string[] = ["router id 192.0.2.1;"];
36
+
37
+ for (let index = 0; index < testCase.includeCount; index += 1) {
38
+ const includePath = `include-${index}.conf`;
39
+ const includeUri = `memory://perf/${testCase.name}/${includePath}`;
40
+
41
+ entryLines.push(`include "${includePath}";`);
42
+ entryLines.push(`protocol bgp edge_${index} from tpl_${index} {`);
43
+ entryLines.push(` neighbor 192.0.2.${index + 1} as ${65100 + index};`);
44
+ entryLines.push(` local as ${65000 + index};`);
45
+ entryLines.push(` import filter export_${index};`);
46
+ entryLines.push("}");
47
+
48
+ documents.push({
49
+ uri: includeUri,
50
+ text: createIncludeBody(index, testCase.linesPerInclude),
51
+ });
52
+ }
53
+
54
+ documents.push({
55
+ uri: entryUri,
56
+ text: `${entryLines.join("\n")}\n`,
57
+ });
58
+
59
+ return { entryUri, documents };
60
+ };
61
+
62
+ const runPerfCase = async (testCase: PerfCase): Promise<number> => {
63
+ const { entryUri, documents } = createCaseDocuments(testCase);
64
+ const start = performance.now();
65
+
66
+ const resolved = await resolveCrossFileReferences({
67
+ entryUri,
68
+ documents,
69
+ loadFromFileSystem: false,
70
+ maxDepth: 16,
71
+ maxFiles: 256,
72
+ });
73
+ await lintResolvedCrossFileGraph(resolved);
74
+
75
+ return performance.now() - start;
76
+ };
77
+
78
+ describe("@birdcc/lsp perf baseline", () => {
79
+ it("collects cross-file baseline and emits threshold warnings", async () => {
80
+ for (const testCase of PERF_CASES) {
81
+ await runPerfCase(testCase);
82
+ const elapsed = await runPerfCase(testCase);
83
+
84
+ if (elapsed > testCase.baselineMs * 2) {
85
+ // Non-blocking alert: keep collecting baseline before enabling hard gates.
86
+ console.warn(
87
+ `[perf-warning] ${testCase.name} case took ${elapsed.toFixed(1)}ms (baseline ${testCase.baselineMs}ms, threshold ${
88
+ testCase.baselineMs * 2
89
+ }ms)`,
90
+ );
91
+ }
92
+
93
+ expect(elapsed).toBeGreaterThan(0);
94
+ }
95
+ });
96
+ });