@birdcc/parser 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 (76) hide show
  1. package/.oxfmtrc.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +312 -0
  4. package/dist/declarations/basic.d.ts +9 -0
  5. package/dist/declarations/basic.d.ts.map +1 -0
  6. package/dist/declarations/basic.js +180 -0
  7. package/dist/declarations/basic.js.map +1 -0
  8. package/dist/declarations/filter.d.ts +6 -0
  9. package/dist/declarations/filter.d.ts.map +1 -0
  10. package/dist/declarations/filter.js +330 -0
  11. package/dist/declarations/filter.js.map +1 -0
  12. package/dist/declarations/parse-declarations.d.ts +4 -0
  13. package/dist/declarations/parse-declarations.d.ts.map +1 -0
  14. package/dist/declarations/parse-declarations.js +54 -0
  15. package/dist/declarations/parse-declarations.js.map +1 -0
  16. package/dist/declarations/protocol.d.ts +6 -0
  17. package/dist/declarations/protocol.d.ts.map +1 -0
  18. package/dist/declarations/protocol.js +444 -0
  19. package/dist/declarations/protocol.js.map +1 -0
  20. package/dist/declarations/shared.d.ts +56 -0
  21. package/dist/declarations/shared.d.ts.map +1 -0
  22. package/dist/declarations/shared.js +169 -0
  23. package/dist/declarations/shared.js.map +1 -0
  24. package/dist/declarations/top-level.d.ts +6 -0
  25. package/dist/declarations/top-level.d.ts.map +1 -0
  26. package/dist/declarations/top-level.js +141 -0
  27. package/dist/declarations/top-level.js.map +1 -0
  28. package/dist/declarations.d.ts +2 -0
  29. package/dist/declarations.d.ts.map +1 -0
  30. package/dist/declarations.js +2 -0
  31. package/dist/declarations.js.map +1 -0
  32. package/dist/index.d.ts +8 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +49 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/issues.d.ts +9 -0
  37. package/dist/issues.d.ts.map +1 -0
  38. package/dist/issues.js +119 -0
  39. package/dist/issues.js.map +1 -0
  40. package/dist/runtime.d.ts +5 -0
  41. package/dist/runtime.d.ts.map +1 -0
  42. package/dist/runtime.js +51 -0
  43. package/dist/runtime.js.map +1 -0
  44. package/dist/tree-sitter-birdcc.wasm +0 -0
  45. package/dist/tree.d.ts +16 -0
  46. package/dist/tree.d.ts.map +1 -0
  47. package/dist/tree.js +150 -0
  48. package/dist/tree.js.map +1 -0
  49. package/dist/types.d.ts +222 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/grammar.js +601 -0
  54. package/package.json +46 -0
  55. package/scripts/sync-wasm-paths.mjs +21 -0
  56. package/src/declarations/basic.ts +272 -0
  57. package/src/declarations/filter.ts +437 -0
  58. package/src/declarations/parse-declarations.ts +84 -0
  59. package/src/declarations/protocol.ts +597 -0
  60. package/src/declarations/shared.ts +275 -0
  61. package/src/declarations/top-level.ts +185 -0
  62. package/src/declarations.ts +1 -0
  63. package/src/index.ts +102 -0
  64. package/src/issues.ts +154 -0
  65. package/src/runtime.ts +64 -0
  66. package/src/tree-sitter-birdcc.wasm +0 -0
  67. package/src/tree.ts +210 -0
  68. package/src/types.ts +329 -0
  69. package/test/fixtures.test.ts +48 -0
  70. package/test/ip-literal-candidate.test.ts +39 -0
  71. package/test/parser.test.ts +475 -0
  72. package/test/realworld-smoke.test.ts +46 -0
  73. package/test/runtime.test.ts +51 -0
  74. package/test/tree.test.ts +83 -0
  75. package/tree-sitter.json +37 -0
  76. package/tsconfig.json +8 -0
@@ -0,0 +1,475 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseBirdConfig } from "../src/index.js";
3
+
4
+ describe("@birdcc/parser tree-sitter", () => {
5
+ it("builds top-level DSL declarations", async () => {
6
+ const sample = `
7
+ include "base.conf";
8
+
9
+ template bgp edge_tpl {
10
+ }
11
+
12
+ protocol bgp edge from edge_tpl {
13
+ local as 65001;
14
+ }
15
+
16
+ filter export_policy {
17
+ accept;
18
+ }
19
+
20
+ function is_ok() -> bool {
21
+ return true;
22
+ }
23
+ `;
24
+
25
+ const parsed = await parseBirdConfig(sample);
26
+ const kinds = parsed.program.declarations.map((item) => item.kind);
27
+
28
+ expect(kinds).toEqual([
29
+ "include",
30
+ "template",
31
+ "protocol",
32
+ "filter",
33
+ "function",
34
+ ]);
35
+
36
+ const protocol = parsed.program.declarations.find(
37
+ (item) => item.kind === "protocol",
38
+ );
39
+ expect(protocol).toBeDefined();
40
+ if (protocol?.kind === "protocol") {
41
+ expect(protocol.protocolType).toBe("bgp");
42
+ expect(protocol.name).toBe("edge");
43
+ expect(protocol.fromTemplate).toBe("edge_tpl");
44
+ }
45
+ });
46
+
47
+ it("parses template inheritance via from clause", async () => {
48
+ const sample = `
49
+ template bgp base_tpl {
50
+ }
51
+
52
+ template bgp edge_tpl from base_tpl {
53
+ }
54
+ `;
55
+
56
+ const parsed = await parseBirdConfig(sample);
57
+ const templates = parsed.program.declarations.filter(
58
+ (item) => item.kind === "template",
59
+ );
60
+ expect(templates).toHaveLength(2);
61
+
62
+ const edgeTemplate = templates[1];
63
+ if (edgeTemplate?.kind === "template") {
64
+ expect(edgeTemplate.name).toBe("edge_tpl");
65
+ expect(edgeTemplate.fromTemplate).toBe("base_tpl");
66
+ }
67
+ });
68
+
69
+ it("parses router id and table declarations", async () => {
70
+ const sample = `
71
+ router id 192.0.2.1;
72
+ router id 12345;
73
+ router id from routing;
74
+ router id 999.0.0.1;
75
+ routing table master;
76
+ ipv4 table edge4;
77
+ vpn4 table core attrs (extended, foo);
78
+ `;
79
+
80
+ const parsed = await parseBirdConfig(sample);
81
+
82
+ const routerDeclarations = parsed.program.declarations.filter(
83
+ (item) => item.kind === "router-id",
84
+ );
85
+ const tableDeclarations = parsed.program.declarations.filter(
86
+ (item) => item.kind === "table",
87
+ );
88
+
89
+ expect(routerDeclarations).toHaveLength(4);
90
+ expect(tableDeclarations).toHaveLength(3);
91
+
92
+ const firstRouter = routerDeclarations[0];
93
+ if (firstRouter?.kind === "router-id") {
94
+ expect(firstRouter.valueKind).toBe("ip");
95
+ expect(firstRouter.value).toBe("192.0.2.1");
96
+ }
97
+
98
+ const fromRouter = routerDeclarations[2];
99
+ if (fromRouter?.kind === "router-id") {
100
+ expect(fromRouter.valueKind).toBe("from");
101
+ expect(fromRouter.fromSource).toBe("routing");
102
+ }
103
+
104
+ const invalidRouter = routerDeclarations[3];
105
+ if (invalidRouter?.kind === "router-id") {
106
+ expect(invalidRouter.valueKind).toBe("unknown");
107
+ expect(invalidRouter.value).toBe("999.0.0.1");
108
+ }
109
+
110
+ const vpnTable = tableDeclarations[2];
111
+ if (vpnTable?.kind === "table") {
112
+ expect(vpnTable.tableType).toBe("vpn4");
113
+ expect(vpnTable.name).toBe("core");
114
+ expect(vpnTable.attrsText).toContain("extended");
115
+ }
116
+ });
117
+
118
+ it("recognizes all supported table type declarations", async () => {
119
+ const sample = `
120
+ routing table t_routing;
121
+ ipv4 table t_ipv4;
122
+ ipv6 table t_ipv6;
123
+ vpn4 table t_vpn4;
124
+ vpn6 table t_vpn6;
125
+ roa4 table t_roa4;
126
+ roa6 table t_roa6;
127
+ flow4 table t_flow4;
128
+ flow6 table t_flow6;
129
+ `;
130
+
131
+ const parsed = await parseBirdConfig(sample);
132
+ const tables = parsed.program.declarations.filter(
133
+ (item) => item.kind === "table",
134
+ );
135
+
136
+ const tableTypes = tables.map((item) =>
137
+ item.kind === "table" ? item.tableType : "unknown",
138
+ );
139
+
140
+ expect(tableTypes).toEqual([
141
+ "routing",
142
+ "ipv4",
143
+ "ipv6",
144
+ "vpn4",
145
+ "vpn6",
146
+ "roa4",
147
+ "roa6",
148
+ "flow4",
149
+ "flow6",
150
+ ]);
151
+ expect(tableTypes).not.toContain("unknown");
152
+ });
153
+
154
+ it("extracts protocol common statements and channel entries", async () => {
155
+ const sample = `
156
+ protocol bgp edge_peer {
157
+ local as 65001;
158
+ neighbor 192.0.2.1 as 65002;
159
+ import all;
160
+ export filter policy_out;
161
+ ipv4 {
162
+ table master4;
163
+ import none;
164
+ export where net.len <= 24;
165
+ import limit 1000 action block;
166
+ debug all;
167
+ import keep filtered on;
168
+ };
169
+ }
170
+ `;
171
+
172
+ const parsed = await parseBirdConfig(sample);
173
+ const protocol = parsed.program.declarations.find(
174
+ (item) => item.kind === "protocol",
175
+ );
176
+
177
+ expect(protocol).toBeDefined();
178
+ if (protocol?.kind === "protocol") {
179
+ const localAs = protocol.statements.find(
180
+ (item) => item.kind === "local-as",
181
+ );
182
+ const neighbor = protocol.statements.find(
183
+ (item) => item.kind === "neighbor",
184
+ );
185
+ const importStatement = protocol.statements.find(
186
+ (item) => item.kind === "import",
187
+ );
188
+ const exportStatement = protocol.statements.find(
189
+ (item) => item.kind === "export",
190
+ );
191
+ const channel = protocol.statements.find(
192
+ (item) => item.kind === "channel",
193
+ );
194
+
195
+ expect(localAs?.kind).toBe("local-as");
196
+ expect(neighbor?.kind).toBe("neighbor");
197
+ expect(importStatement?.kind).toBe("import");
198
+ if (importStatement?.kind === "import") {
199
+ expect(importStatement.mode).toBe("all");
200
+ }
201
+
202
+ expect(exportStatement?.kind).toBe("export");
203
+ if (exportStatement?.kind === "export") {
204
+ expect(exportStatement.mode).toBe("filter");
205
+ expect(exportStatement.filterName).toBe("policy_out");
206
+ }
207
+
208
+ expect(channel?.kind).toBe("channel");
209
+ if (channel?.kind === "channel") {
210
+ expect(channel.channelType).toBe("ipv4");
211
+ expect(channel.entries.some((item) => item.kind === "table")).toBe(
212
+ true,
213
+ );
214
+ expect(
215
+ channel.entries.some(
216
+ (item) => item.kind === "import" && item.mode === "none",
217
+ ),
218
+ ).toBe(true);
219
+ expect(
220
+ channel.entries.some(
221
+ (item) => item.kind === "export" && item.mode === "where",
222
+ ),
223
+ ).toBe(true);
224
+ expect(channel.entries.some((item) => item.kind === "limit")).toBe(
225
+ true,
226
+ );
227
+ expect(channel.entries.some((item) => item.kind === "debug")).toBe(
228
+ true,
229
+ );
230
+ expect(
231
+ channel.entries.some((item) => item.kind === "keep-filtered"),
232
+ ).toBe(true);
233
+ }
234
+ }
235
+ });
236
+
237
+ it("preserves generic protocol statements as other entries", async () => {
238
+ const sample = `
239
+ protocol ospf core {
240
+ area 0;
241
+ }
242
+ `;
243
+
244
+ const parsed = await parseBirdConfig(sample);
245
+ const protocol = parsed.program.declarations.find(
246
+ (item) => item.kind === "protocol",
247
+ );
248
+
249
+ expect(protocol).toBeDefined();
250
+ if (protocol?.kind === "protocol") {
251
+ const otherStatement = protocol.statements.find(
252
+ (item) => item.kind === "other",
253
+ );
254
+ expect(otherStatement?.kind).toBe("other");
255
+ if (otherStatement?.kind === "other") {
256
+ expect(otherStatement.text.toLowerCase()).toContain("area");
257
+ }
258
+ }
259
+ });
260
+
261
+ it("preserves multi-line protocol statements as a single other entry", async () => {
262
+ const sample = `
263
+ protocol ospf core {
264
+ area 0
265
+ stub;
266
+ }
267
+ `;
268
+
269
+ const parsed = await parseBirdConfig(sample);
270
+ const protocol = parsed.program.declarations.find(
271
+ (item) => item.kind === "protocol",
272
+ );
273
+
274
+ expect(protocol).toBeDefined();
275
+ if (protocol?.kind === "protocol") {
276
+ const otherStatements = protocol.statements.filter(
277
+ (item) => item.kind === "other",
278
+ );
279
+ expect(otherStatements).toHaveLength(1);
280
+ const text =
281
+ otherStatements[0]?.kind === "other" ? otherStatements[0].text : "";
282
+ expect(text.toLowerCase()).toContain("area");
283
+ expect(text.toLowerCase()).toContain("stub");
284
+ }
285
+ });
286
+
287
+ it("keeps invalid neighbor IP as ip-like candidate for semantic validation", async () => {
288
+ const sample = `
289
+ protocol bgp edge_peer {
290
+ neighbor 203.0.113.999 as 65002;
291
+ }
292
+ `;
293
+
294
+ const parsed = await parseBirdConfig(sample);
295
+ const protocol = parsed.program.declarations.find(
296
+ (item) => item.kind === "protocol",
297
+ );
298
+
299
+ expect(protocol).toBeDefined();
300
+ if (protocol?.kind === "protocol") {
301
+ const neighbor = protocol.statements.find(
302
+ (item) => item.kind === "neighbor",
303
+ );
304
+ expect(neighbor?.kind).toBe("neighbor");
305
+ if (neighbor?.kind === "neighbor") {
306
+ expect(neighbor.addressKind).toBe("ip");
307
+ }
308
+ }
309
+ });
310
+
311
+ it("extracts filter/function control statements, literals and match expressions", async () => {
312
+ const sample = `
313
+ function is_private() -> bool {
314
+ if net ~ [ 10.0.0.0/8+, 2001:db8::/32{33,128} ] then return true;
315
+ return false;
316
+ }
317
+
318
+ filter export_policy {
319
+ if bgp_path ~ [= * 65003 * =] then reject;
320
+ case net.type {
321
+ NET_IP4: accept;
322
+ else: reject;
323
+ }
324
+ accept;
325
+ }
326
+ `;
327
+
328
+ const parsed = await parseBirdConfig(sample);
329
+ const fn = parsed.program.declarations.find(
330
+ (item) => item.kind === "function",
331
+ );
332
+ const filter = parsed.program.declarations.find(
333
+ (item) => item.kind === "filter",
334
+ );
335
+
336
+ expect(fn).toBeDefined();
337
+ if (fn?.kind === "function") {
338
+ expect(fn.statements.some((item) => item.kind === "if")).toBe(true);
339
+ expect(fn.statements.some((item) => item.kind === "return")).toBe(true);
340
+ expect(fn.literals.some((item) => item.kind === "prefix")).toBe(true);
341
+ expect(fn.matches.some((item) => item.operator === "~")).toBe(true);
342
+ }
343
+
344
+ expect(filter).toBeDefined();
345
+ if (filter?.kind === "filter") {
346
+ expect(filter.statements.some((item) => item.kind === "if")).toBe(true);
347
+ expect(filter.statements.some((item) => item.kind === "case")).toBe(true);
348
+ expect(filter.statements.some((item) => item.kind === "accept")).toBe(
349
+ true,
350
+ );
351
+ expect(filter.statements.some((item) => item.kind === "reject")).toBe(
352
+ true,
353
+ );
354
+ expect(filter.matches.some((item) => item.operator === "~")).toBe(true);
355
+ }
356
+ });
357
+
358
+ it("does not collect nested protocol statements inside inline filter blocks", async () => {
359
+ const sample = `
360
+ protocol bgp edge_peer {
361
+ local as 65001;
362
+ import filter {
363
+ local as 65003;
364
+ neighbor 198.51.100.1 as 65004;
365
+ };
366
+ neighbor 192.0.2.1 as 65002;
367
+ }
368
+ `;
369
+
370
+ const parsed = await parseBirdConfig(sample);
371
+ const protocol = parsed.program.declarations.find(
372
+ (item) => item.kind === "protocol",
373
+ );
374
+
375
+ expect(protocol).toBeDefined();
376
+ if (protocol?.kind === "protocol") {
377
+ const localAsStatements = protocol.statements.filter(
378
+ (item) => item.kind === "local-as",
379
+ );
380
+ const neighborStatements = protocol.statements.filter(
381
+ (item) => item.kind === "neighbor",
382
+ );
383
+ const importStatements = protocol.statements.filter(
384
+ (item) => item.kind === "import",
385
+ );
386
+
387
+ expect(localAsStatements).toHaveLength(1);
388
+ expect(neighborStatements).toHaveLength(1);
389
+ expect(importStatements).toHaveLength(1);
390
+ }
391
+ });
392
+
393
+ it("extracts declaration text correctly with non-ASCII content on the same line", async () => {
394
+ const sample = `include "路由.conf"; protocol bgp edge { local as 65001; };`;
395
+ const parsed = await parseBirdConfig(sample);
396
+
397
+ const includeDeclaration = parsed.program.declarations.find(
398
+ (item) => item.kind === "include",
399
+ );
400
+ const protocolDeclaration = parsed.program.declarations.find(
401
+ (item) => item.kind === "protocol",
402
+ );
403
+
404
+ expect(includeDeclaration).toBeDefined();
405
+ if (includeDeclaration?.kind === "include") {
406
+ expect(includeDeclaration.path).toBe("路由.conf");
407
+ }
408
+
409
+ expect(protocolDeclaration).toBeDefined();
410
+ if (protocolDeclaration?.kind === "protocol") {
411
+ expect(protocolDeclaration.protocolType).toBe("bgp");
412
+ expect(protocolDeclaration.name).toBe("edge");
413
+ expect(protocolDeclaration.statements.map((item) => item.kind)).toEqual([
414
+ "local-as",
415
+ ]);
416
+ }
417
+ });
418
+
419
+ it("reports missing declaration symbols for incomplete headers", async () => {
420
+ const sample = `
421
+ include;
422
+ define;
423
+ router id;
424
+ table;
425
+ ipv4 table;
426
+ protocol bgp {
427
+ }
428
+ template bgp {
429
+ }
430
+ filter {
431
+ }
432
+ function {
433
+ }
434
+ `;
435
+
436
+ const parsed = await parseBirdConfig(sample);
437
+ const messages = parsed.issues.map((item) => item.message);
438
+
439
+ expect(messages).toContain("Missing path for include declaration");
440
+ expect(messages).toContain("Missing name for define declaration");
441
+ expect(messages).toContain("Missing value for router id declaration");
442
+ expect(messages).toContain("Missing name for table declaration");
443
+ expect(messages).toContain("Missing name for protocol declaration");
444
+ expect(messages).toContain("Missing name for template declaration");
445
+ expect(messages).toContain("Missing name for filter declaration");
446
+ expect(messages).toContain("Missing name for function declaration");
447
+ });
448
+
449
+ it("reports unbalanced brace recovery issues", async () => {
450
+ const sample = `
451
+ protocol bgp edge {
452
+ ipv4 {
453
+ import where net.len <= 24;
454
+ `;
455
+
456
+ const parsed = await parseBirdConfig(sample);
457
+ expect(
458
+ parsed.issues.some((item) => item.code === "syntax/unbalanced-brace"),
459
+ ).toBe(true);
460
+ });
461
+
462
+ it("reports missing semicolon recovery issues", async () => {
463
+ const sample = `
464
+ protocol bgp edge {
465
+ local as 65001
466
+ neighbor 192.0.2.1 as 65002;
467
+ }
468
+ `;
469
+
470
+ const parsed = await parseBirdConfig(sample);
471
+ expect(
472
+ parsed.issues.some((item) => item.code === "syntax/missing-semicolon"),
473
+ ).toBe(true);
474
+ });
475
+ });
@@ -0,0 +1,46 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import { parseBirdConfig } from "../src/index.js";
8
+
9
+ import { collectBirdConfigCandidates } from "../../../../tools/realworld-config-files.mjs";
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ const repoRoot = resolve(here, "../../../../");
13
+ const examplesRoot = resolve(repoRoot, "refer/config-examples");
14
+
15
+ const DEFAULT_MAX_BYTES = 256 * 1024;
16
+ const DEFAULT_LIMIT = 20;
17
+
18
+ describe("real-world config examples (smoke)", () => {
19
+ it("parses selected config examples without parser runtime failure", async () => {
20
+ const maxBytes = Number(
21
+ process.env.BIRDCC_REALWORLD_MAX_BYTES ?? DEFAULT_MAX_BYTES,
22
+ );
23
+ const limit = Number(process.env.BIRDCC_REALWORLD_LIMIT ?? DEFAULT_LIMIT);
24
+
25
+ const candidates = await collectBirdConfigCandidates({
26
+ root: examplesRoot,
27
+ maxBytes,
28
+ });
29
+
30
+ candidates.sort((left, right) => right.bytes - left.bytes);
31
+ const selected = candidates.slice(0, Math.max(0, limit));
32
+ if (selected.length === 0) {
33
+ return;
34
+ }
35
+
36
+ for (const file of selected) {
37
+ const text = await readFile(file.path, "utf8");
38
+ const parsed = await parseBirdConfig(text);
39
+
40
+ const runtimeIssues = parsed.issues.filter(
41
+ (issue) => issue.code === "parser/runtime-error",
42
+ );
43
+ expect(runtimeIssues, file.path).toHaveLength(0);
44
+ }
45
+ });
46
+ });
@@ -0,0 +1,51 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { fileURLToPath } from "node:url";
3
+ import { parseBirdConfig } from "../src/index.js";
4
+ import {
5
+ getParser,
6
+ resetParserRuntimeForTests,
7
+ setLanguageWasmPathsForTests,
8
+ } from "../src/runtime.js";
9
+
10
+ const VALID_WASM_PATH = fileURLToPath(
11
+ new URL("../src/tree-sitter-birdcc.wasm", import.meta.url),
12
+ );
13
+
14
+ describe("@birdcc/parser runtime", () => {
15
+ afterEach(() => {
16
+ setLanguageWasmPathsForTests([VALID_WASM_PATH]);
17
+ resetParserRuntimeForTests();
18
+ });
19
+
20
+ it("reuses a single parser instance for concurrent callers", async () => {
21
+ const [parserA, parserB, parserC] = await Promise.all([
22
+ getParser(),
23
+ getParser(),
24
+ getParser(),
25
+ ]);
26
+
27
+ expect(parserA).toBe(parserB);
28
+ expect(parserA).toBe(parserC);
29
+ });
30
+
31
+ it("recovers from initialization failure and supports retry", async () => {
32
+ setLanguageWasmPathsForTests(["/tmp/not-found-tree-sitter-birdcc.wasm"]);
33
+ await expect(getParser()).rejects.toThrow(
34
+ "Unable to load Tree-sitter WASM language",
35
+ );
36
+
37
+ setLanguageWasmPathsForTests([VALID_WASM_PATH]);
38
+ await expect(getParser()).resolves.toBeDefined();
39
+ });
40
+
41
+ it("returns degraded parse result when runtime cannot be initialized", async () => {
42
+ setLanguageWasmPathsForTests(["/tmp/not-found-tree-sitter-birdcc.wasm"]);
43
+ const parsed = await parseBirdConfig(
44
+ "protocol bgp edge { local as 65001; }",
45
+ );
46
+
47
+ expect(parsed.program.declarations).toHaveLength(0);
48
+ expect(parsed.issues).toHaveLength(1);
49
+ expect(parsed.issues[0].code).toBe("parser/runtime-error");
50
+ });
51
+ });
@@ -0,0 +1,83 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ cacheUtf8BytesForTests,
4
+ getUtf8CacheStateForTests,
5
+ resetUtf8CacheForTests,
6
+ toRange,
7
+ } from "../src/tree.js";
8
+
9
+ describe("@birdcc/parser utf8 cache", () => {
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ resetUtf8CacheForTests();
13
+ });
14
+
15
+ it("reuses cache for the same source within ttl window", () => {
16
+ const nowSpy = vi.spyOn(Date, "now");
17
+ nowSpy.mockReturnValue(1_000);
18
+
19
+ cacheUtf8BytesForTests("protocol bgp edge {}");
20
+ const firstState = getUtf8CacheStateForTests();
21
+
22
+ nowSpy.mockReturnValue(1_500);
23
+ cacheUtf8BytesForTests("protocol bgp edge {}");
24
+ const secondState = getUtf8CacheStateForTests();
25
+
26
+ expect(firstState.hasCache).toBe(true);
27
+ expect(firstState.utf8Version).toBe(1);
28
+ expect(secondState.utf8Version).toBe(1);
29
+ });
30
+
31
+ it("rebuilds cache after ttl expires", () => {
32
+ const nowSpy = vi.spyOn(Date, "now");
33
+ nowSpy.mockReturnValue(2_000);
34
+
35
+ cacheUtf8BytesForTests("protocol bgp edge {}");
36
+ const firstState = getUtf8CacheStateForTests();
37
+
38
+ nowSpy.mockReturnValue(2_000 + 31_000);
39
+ cacheUtf8BytesForTests("protocol bgp edge {}");
40
+ const secondState = getUtf8CacheStateForTests();
41
+
42
+ expect(firstState.utf8Version).toBe(1);
43
+ expect(secondState.utf8Version).toBe(2);
44
+ });
45
+
46
+ it("reuses line start cache and rebuilds after ttl expires", () => {
47
+ const node = {
48
+ startIndex: 0,
49
+ endIndex: 8,
50
+ text: "protocol",
51
+ startPosition: { row: 0, column: 0 },
52
+ endPosition: { row: 0, column: 8 },
53
+ };
54
+ const source = "protocol\\nbgp edge";
55
+ const nowSpy = vi.spyOn(Date, "now");
56
+ nowSpy.mockReturnValue(3_000);
57
+
58
+ toRange(node as never, source);
59
+ const firstState = getUtf8CacheStateForTests();
60
+
61
+ nowSpy.mockReturnValue(3_500);
62
+ toRange(node as never, source);
63
+ const secondState = getUtf8CacheStateForTests();
64
+
65
+ nowSpy.mockReturnValue(3_000 + 31_000);
66
+ toRange(node as never, source);
67
+ const thirdState = getUtf8CacheStateForTests();
68
+
69
+ expect(firstState.lineStartsVersion).toBe(1);
70
+ expect(secondState.lineStartsVersion).toBe(1);
71
+ expect(thirdState.lineStartsVersion).toBe(2);
72
+ });
73
+
74
+ it("does not store oversized source into cache", () => {
75
+ const largeSource = "a".repeat(4 * 1024 * 1024 + 64);
76
+ const byteLength = cacheUtf8BytesForTests(largeSource);
77
+ const state = getUtf8CacheStateForTests();
78
+
79
+ expect(byteLength).toBeGreaterThan(4 * 1024 * 1024);
80
+ expect(state.hasCache).toBe(false);
81
+ expect(state.byteLength).toBe(0);
82
+ });
83
+ });
@@ -0,0 +1,37 @@
1
+ {
2
+ "grammars": [
3
+ {
4
+ "name": "bird",
5
+ "camelcase": "BIRD",
6
+ "scope": "source.bird",
7
+ "path": ".",
8
+ "file-types": [
9
+ "bird",
10
+ "bird.conf",
11
+ "conf",
12
+ "bird",
13
+ "bird2",
14
+ "bird3",
15
+ "bird6"
16
+ ]
17
+ }
18
+ ],
19
+ "metadata": {
20
+ "version": "0.0.1-alpha.0",
21
+ "license": "GPL-3.0",
22
+ "description": "BIRD configuration grammar for Tree-sitter",
23
+ "authors": [
24
+ {
25
+ "name": "BIRD Chinese Community",
26
+ "email": "grammar-dev@birdcc.link"
27
+ }
28
+ ],
29
+
30
+ "links": {
31
+ "repository": "https://github.com/bird-chinese-community/BIRD-LSP/tree/main/packages/%40birdcc/parser"
32
+ }
33
+ },
34
+ "bindings": {
35
+ "c": true
36
+ }
37
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src/**/*.ts"]
8
+ }