@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,84 @@
1
+ import type { Node as SyntaxNode } from "web-tree-sitter";
2
+ import type { BirdDeclaration, ParseIssue } from "../types.js";
3
+ import {
4
+ parseDefineDeclaration,
5
+ parseIncludeDeclaration,
6
+ parseRouterIdDeclaration,
7
+ parseTableDeclaration,
8
+ parseTemplateDeclaration,
9
+ } from "./basic.js";
10
+ import { parseFilterDeclaration, parseFunctionDeclaration } from "./filter.js";
11
+ import { parseProtocolDeclaration } from "./protocol.js";
12
+ import {
13
+ parseRouterIdFromStatement,
14
+ parseTableFromStatement,
15
+ } from "./top-level.js";
16
+
17
+ export const parseDeclarations = (
18
+ rootNode: SyntaxNode,
19
+ source: string,
20
+ issues: ParseIssue[],
21
+ ): BirdDeclaration[] => {
22
+ const declarations: BirdDeclaration[] = [];
23
+
24
+ for (const child of rootNode.namedChildren) {
25
+ if (child.type === "include_declaration") {
26
+ declarations.push(parseIncludeDeclaration(child, source, issues));
27
+ continue;
28
+ }
29
+
30
+ if (child.type === "define_declaration") {
31
+ declarations.push(parseDefineDeclaration(child, source, issues));
32
+ continue;
33
+ }
34
+
35
+ if (child.type === "router_id_declaration") {
36
+ declarations.push(parseRouterIdDeclaration(child, source, issues));
37
+ continue;
38
+ }
39
+
40
+ if (child.type === "table_declaration") {
41
+ declarations.push(parseTableDeclaration(child, source, issues));
42
+ continue;
43
+ }
44
+
45
+ if (child.type === "protocol_declaration") {
46
+ declarations.push(parseProtocolDeclaration(child, source, issues));
47
+ continue;
48
+ }
49
+
50
+ if (child.type === "template_declaration") {
51
+ declarations.push(parseTemplateDeclaration(child, source, issues));
52
+ continue;
53
+ }
54
+
55
+ if (child.type === "filter_declaration") {
56
+ declarations.push(parseFilterDeclaration(child, source, issues));
57
+ continue;
58
+ }
59
+
60
+ if (child.type === "function_declaration") {
61
+ declarations.push(parseFunctionDeclaration(child, source, issues));
62
+ continue;
63
+ }
64
+
65
+ if (child.type === "top_level_statement") {
66
+ const routerFromTopLevel = parseRouterIdFromStatement(
67
+ child,
68
+ source,
69
+ issues,
70
+ );
71
+ if (routerFromTopLevel) {
72
+ declarations.push(routerFromTopLevel);
73
+ continue;
74
+ }
75
+
76
+ const tableFromTopLevel = parseTableFromStatement(child, source, issues);
77
+ if (tableFromTopLevel) {
78
+ declarations.push(tableFromTopLevel);
79
+ }
80
+ }
81
+ }
82
+
83
+ return declarations;
84
+ };
@@ -0,0 +1,597 @@
1
+ import type { Node as SyntaxNode } from "web-tree-sitter";
2
+ import type {
3
+ ChannelEntry,
4
+ ExportStatement,
5
+ ImportStatement,
6
+ ParseIssue,
7
+ ProtocolStatement,
8
+ } from "../types.js";
9
+ import { pushMissingFieldIssue } from "../issues.js";
10
+ import { isPresentNode, mergeRanges, textOf, toRange } from "../tree.js";
11
+ import {
12
+ CHANNEL_DIRECTIONS,
13
+ PROTOCOL_STATEMENT_TYPES,
14
+ type ProtocolDeclaration,
15
+ isIpLiteralCandidate,
16
+ normalizeChannelType,
17
+ protocolTypeTextAndRange,
18
+ protocolStatementNodesOf,
19
+ } from "./shared.js";
20
+
21
+ // Keep API near parseProtocolStatements and channel fallback behavior.
22
+ const parseImportExportNode = (
23
+ statementNode: SyntaxNode,
24
+ source: string,
25
+ ): ImportStatement | ExportStatement => {
26
+ const statementRange = toRange(statementNode, source);
27
+ const clauseNode = statementNode.childForFieldName("clause");
28
+ const isImport = statementNode.type === "import_statement";
29
+
30
+ const base = {
31
+ kind: isImport ? ("import" as const) : ("export" as const),
32
+ ...statementRange,
33
+ };
34
+
35
+ if (!isPresentNode(clauseNode) || clauseNode.type === "all_clause") {
36
+ return {
37
+ ...base,
38
+ mode: "all",
39
+ };
40
+ }
41
+
42
+ if (clauseNode.type === "none_clause") {
43
+ return {
44
+ ...base,
45
+ mode: "none",
46
+ };
47
+ }
48
+
49
+ if (
50
+ clauseNode.type === "filter_name_clause" ||
51
+ clauseNode.type === "filter_block_clause"
52
+ ) {
53
+ const filterNameNode = clauseNode.childForFieldName("filter_name");
54
+
55
+ return {
56
+ ...base,
57
+ mode: "filter",
58
+ filterName: isPresentNode(filterNameNode)
59
+ ? textOf(filterNameNode, source)
60
+ : undefined,
61
+ filterNameRange: isPresentNode(filterNameNode)
62
+ ? toRange(filterNameNode, source)
63
+ : undefined,
64
+ };
65
+ }
66
+
67
+ if (clauseNode.type === "where_clause") {
68
+ const whereExpressionNode =
69
+ clauseNode.childForFieldName("where_expression");
70
+
71
+ return {
72
+ ...base,
73
+ mode: "where",
74
+ whereExpression: isPresentNode(whereExpressionNode)
75
+ ? textOf(whereExpressionNode, source)
76
+ : undefined,
77
+ whereExpressionRange: isPresentNode(whereExpressionNode)
78
+ ? toRange(whereExpressionNode, source)
79
+ : undefined,
80
+ clauseText: textOf(clauseNode, source),
81
+ };
82
+ }
83
+
84
+ const clauseText = textOf(clauseNode, source).trim();
85
+ const lowered = clauseText.toLowerCase();
86
+
87
+ if (lowered === "none" || lowered.startsWith("none ")) {
88
+ return {
89
+ ...base,
90
+ mode: "none",
91
+ clauseText,
92
+ };
93
+ }
94
+
95
+ if (lowered.startsWith("where ")) {
96
+ return {
97
+ ...base,
98
+ mode: "where",
99
+ whereExpression: clauseText.slice("where ".length).trim(),
100
+ clauseText,
101
+ };
102
+ }
103
+
104
+ if (lowered.startsWith("filter ")) {
105
+ const maybeName = clauseText.slice("filter ".length).trim();
106
+ return {
107
+ ...base,
108
+ mode: "filter",
109
+ filterName:
110
+ maybeName.length > 0 && !maybeName.startsWith("{")
111
+ ? maybeName
112
+ : undefined,
113
+ clauseText,
114
+ };
115
+ }
116
+
117
+ return {
118
+ ...base,
119
+ mode: "other",
120
+ clauseText,
121
+ };
122
+ };
123
+
124
+ const parseChannelEntries = (
125
+ channelBodyNode: SyntaxNode,
126
+ source: string,
127
+ ): ChannelEntry[] => {
128
+ const entries: ChannelEntry[] = [];
129
+ const namedChildren = channelBodyNode.namedChildren;
130
+
131
+ for (let index = 0; index < namedChildren.length; index += 1) {
132
+ const entryNode = namedChildren[index];
133
+ if (!entryNode) {
134
+ continue;
135
+ }
136
+
137
+ const entryRange = toRange(entryNode, source);
138
+
139
+ if (entryNode.type === "channel_table_statement") {
140
+ const tableNameNode = entryNode.childForFieldName("table_name");
141
+ entries.push({
142
+ kind: "table",
143
+ tableName: isPresentNode(tableNameNode)
144
+ ? textOf(tableNameNode, source)
145
+ : "",
146
+ tableNameRange: isPresentNode(tableNameNode)
147
+ ? toRange(tableNameNode, source)
148
+ : entryRange,
149
+ ...entryRange,
150
+ });
151
+ continue;
152
+ }
153
+
154
+ if (
155
+ entryNode.type === "identifier" &&
156
+ textOf(entryNode, source).toLowerCase() === "table" &&
157
+ namedChildren[index + 1]?.type === "identifier"
158
+ ) {
159
+ const tableNameNode = namedChildren[index + 1];
160
+ const tableRange = mergeRanges(
161
+ entryRange,
162
+ toRange(tableNameNode, source),
163
+ );
164
+ entries.push({
165
+ kind: "table",
166
+ tableName: textOf(tableNameNode, source),
167
+ tableNameRange: toRange(tableNameNode, source),
168
+ ...tableRange,
169
+ });
170
+ index += 1;
171
+ continue;
172
+ }
173
+
174
+ if (
175
+ entryNode.type === "import_statement" ||
176
+ entryNode.type === "export_statement"
177
+ ) {
178
+ const statement = parseImportExportNode(entryNode, source);
179
+ const clauseText = statement.clauseText?.toLowerCase() ?? "";
180
+
181
+ if (
182
+ statement.mode === "other" &&
183
+ (clauseText.startsWith("limit ") ||
184
+ clauseText.startsWith("keep filtered "))
185
+ ) {
186
+ if (clauseText.startsWith("keep filtered ")) {
187
+ entries.push({
188
+ kind: "keep-filtered",
189
+ value: (statement.clauseText ?? "")
190
+ .slice("keep filtered ".length)
191
+ .trim(),
192
+ valueRange: entryRange,
193
+ ...entryRange,
194
+ });
195
+ } else {
196
+ const payload = (statement.clauseText ?? "")
197
+ .slice("limit ".length)
198
+ .trim();
199
+ const actionMarker = " action ";
200
+ const actionIndex = payload.toLowerCase().indexOf(actionMarker);
201
+ const limitValue =
202
+ actionIndex === -1 ? payload : payload.slice(0, actionIndex).trim();
203
+ const limitAction =
204
+ actionIndex === -1
205
+ ? undefined
206
+ : payload.slice(actionIndex + actionMarker.length).trim();
207
+
208
+ entries.push({
209
+ kind: "limit",
210
+ direction: statement.kind === "export" ? "export" : "import",
211
+ value: limitValue,
212
+ valueRange: entryRange,
213
+ action: limitAction,
214
+ actionRange: limitAction ? entryRange : undefined,
215
+ ...entryRange,
216
+ });
217
+ }
218
+ continue;
219
+ }
220
+
221
+ if (statement.kind === "import") {
222
+ entries.push({
223
+ kind: "import",
224
+ mode: statement.mode,
225
+ filterName: statement.filterName,
226
+ filterNameRange: statement.filterNameRange,
227
+ whereExpression: statement.whereExpression,
228
+ whereExpressionRange: statement.whereExpressionRange,
229
+ clauseText: statement.clauseText,
230
+ ...entryRange,
231
+ });
232
+ } else {
233
+ entries.push({
234
+ kind: "export",
235
+ mode: statement.mode,
236
+ filterName: statement.filterName,
237
+ filterNameRange: statement.filterNameRange,
238
+ whereExpression: statement.whereExpression,
239
+ whereExpressionRange: statement.whereExpressionRange,
240
+ clauseText: statement.clauseText,
241
+ ...entryRange,
242
+ });
243
+ }
244
+ continue;
245
+ }
246
+
247
+ if (entryNode.type === "channel_limit_statement") {
248
+ const directionNode = entryNode.childForFieldName("direction");
249
+ const limitValueNode = entryNode.childForFieldName("limit_value");
250
+ const limitActionNode = entryNode.childForFieldName("limit_action");
251
+
252
+ const directionText = isPresentNode(directionNode)
253
+ ? textOf(directionNode, source).toLowerCase()
254
+ : "import";
255
+ const direction = CHANNEL_DIRECTIONS.has(directionText)
256
+ ? (directionText as "import" | "receive" | "export")
257
+ : "import";
258
+
259
+ entries.push({
260
+ kind: "limit",
261
+ direction,
262
+ value: isPresentNode(limitValueNode)
263
+ ? textOf(limitValueNode, source)
264
+ : "",
265
+ valueRange: isPresentNode(limitValueNode)
266
+ ? toRange(limitValueNode, source)
267
+ : entryRange,
268
+ action: isPresentNode(limitActionNode)
269
+ ? textOf(limitActionNode, source)
270
+ : undefined,
271
+ actionRange: isPresentNode(limitActionNode)
272
+ ? toRange(limitActionNode, source)
273
+ : undefined,
274
+ ...entryRange,
275
+ });
276
+ continue;
277
+ }
278
+
279
+ if (entryNode.type === "channel_debug_statement") {
280
+ const debugClauseNode = entryNode.childForFieldName("debug_clause");
281
+ entries.push({
282
+ kind: "debug",
283
+ clauseText: isPresentNode(debugClauseNode)
284
+ ? textOf(debugClauseNode, source)
285
+ : textOf(entryNode, source),
286
+ ...entryRange,
287
+ });
288
+ continue;
289
+ }
290
+
291
+ if (
292
+ entryNode.type === "identifier" &&
293
+ textOf(entryNode, source).toLowerCase() === "debug" &&
294
+ namedChildren[index + 1]
295
+ ) {
296
+ const clauseNode = namedChildren[index + 1];
297
+ const debugRange = mergeRanges(entryRange, toRange(clauseNode, source));
298
+ entries.push({
299
+ kind: "debug",
300
+ clauseText: textOf(clauseNode, source),
301
+ ...debugRange,
302
+ });
303
+ index += 1;
304
+ continue;
305
+ }
306
+
307
+ if (entryNode.type === "channel_keep_filtered_statement") {
308
+ const switchValueNode = entryNode.childForFieldName("switch_value");
309
+ entries.push({
310
+ kind: "keep-filtered",
311
+ value: isPresentNode(switchValueNode)
312
+ ? textOf(switchValueNode, source)
313
+ : "",
314
+ valueRange: isPresentNode(switchValueNode)
315
+ ? toRange(switchValueNode, source)
316
+ : entryRange,
317
+ ...entryRange,
318
+ });
319
+ continue;
320
+ }
321
+
322
+ entries.push({
323
+ kind: "other",
324
+ text: textOf(entryNode, source),
325
+ ...entryRange,
326
+ });
327
+ }
328
+
329
+ return entries;
330
+ };
331
+
332
+ export const parseProtocolStatements = (
333
+ blockNode: SyntaxNode,
334
+ source: string,
335
+ issues: ParseIssue[],
336
+ ): ProtocolStatement[] => {
337
+ const statements: ProtocolStatement[] = [];
338
+ const nodes = protocolStatementNodesOf(blockNode);
339
+ const childNodes = blockNode.namedChildren;
340
+ const fallbackChannelIndices = new Set<number>();
341
+
342
+ for (const statementNode of nodes) {
343
+ const statementRange = toRange(statementNode, source);
344
+
345
+ if (statementNode.type === "local_as_statement") {
346
+ const asnNode = statementNode.childForFieldName("asn");
347
+ if (!isPresentNode(asnNode)) {
348
+ pushMissingFieldIssue(
349
+ issues,
350
+ statementNode,
351
+ "Missing ASN in local as statement",
352
+ source,
353
+ );
354
+ }
355
+
356
+ statements.push({
357
+ kind: "local-as",
358
+ asn: isPresentNode(asnNode) ? textOf(asnNode, source) : "",
359
+ asnRange: isPresentNode(asnNode)
360
+ ? toRange(asnNode, source)
361
+ : statementRange,
362
+ ...statementRange,
363
+ });
364
+ continue;
365
+ }
366
+
367
+ if (statementNode.type === "neighbor_statement") {
368
+ const addressNode = statementNode.childForFieldName("address");
369
+ const asnNode = statementNode.childForFieldName("asn");
370
+
371
+ if (!isPresentNode(addressNode)) {
372
+ pushMissingFieldIssue(
373
+ issues,
374
+ statementNode,
375
+ "Missing neighbor address",
376
+ source,
377
+ );
378
+ }
379
+
380
+ const addressText = isPresentNode(addressNode)
381
+ ? textOf(addressNode, source)
382
+ : "";
383
+ const addressKind =
384
+ isPresentNode(addressNode) && isIpLiteralCandidate(addressText)
385
+ ? "ip"
386
+ : "other";
387
+
388
+ statements.push({
389
+ kind: "neighbor",
390
+ address: addressText,
391
+ addressRange: isPresentNode(addressNode)
392
+ ? toRange(addressNode, source)
393
+ : statementRange,
394
+ addressKind,
395
+ asn: isPresentNode(asnNode) ? textOf(asnNode, source) : undefined,
396
+ asnRange: isPresentNode(asnNode) ? toRange(asnNode, source) : undefined,
397
+ ...statementRange,
398
+ });
399
+ continue;
400
+ }
401
+
402
+ if (
403
+ statementNode.type === "import_statement" ||
404
+ statementNode.type === "export_statement"
405
+ ) {
406
+ statements.push(parseImportExportNode(statementNode, source));
407
+ continue;
408
+ }
409
+
410
+ if (statementNode.type === "channel_statement") {
411
+ const channelTypeNode = statementNode.childForFieldName("channel_type");
412
+ const channelBodyNode = statementNode.childForFieldName("body");
413
+ const channelTypeText = isPresentNode(channelTypeNode)
414
+ ? textOf(channelTypeNode, source)
415
+ : "";
416
+
417
+ statements.push({
418
+ kind: "channel",
419
+ channelType: normalizeChannelType(channelTypeText),
420
+ channelTypeRange: isPresentNode(channelTypeNode)
421
+ ? toRange(channelTypeNode, source)
422
+ : statementRange,
423
+ entries: isPresentNode(channelBodyNode)
424
+ ? parseChannelEntries(channelBodyNode, source)
425
+ : [],
426
+ ...statementRange,
427
+ });
428
+ continue;
429
+ }
430
+
431
+ if (statementNode.type === "expression_statement") {
432
+ statements.push({
433
+ kind: "other",
434
+ text: textOf(statementNode, source),
435
+ ...statementRange,
436
+ });
437
+ continue;
438
+ }
439
+ }
440
+
441
+ for (let index = 0; index < childNodes.length - 1; index += 1) {
442
+ const maybeChannelTypeNode = childNodes[index];
443
+ const maybeChannelBodyNode = childNodes[index + 1];
444
+ if (!maybeChannelTypeNode || !maybeChannelBodyNode) {
445
+ continue;
446
+ }
447
+
448
+ if (
449
+ maybeChannelTypeNode.type !== "identifier" ||
450
+ maybeChannelBodyNode.type !== "block"
451
+ ) {
452
+ continue;
453
+ }
454
+
455
+ const channelTypeText = textOf(maybeChannelTypeNode, source);
456
+ const channelType = normalizeChannelType(channelTypeText);
457
+ if (channelType === "unknown") {
458
+ continue;
459
+ }
460
+
461
+ const channelRange = mergeRanges(
462
+ toRange(maybeChannelTypeNode, source),
463
+ toRange(maybeChannelBodyNode, source),
464
+ );
465
+
466
+ statements.push({
467
+ kind: "channel",
468
+ channelType,
469
+ channelTypeRange: toRange(maybeChannelTypeNode, source),
470
+ entries: parseChannelEntries(maybeChannelBodyNode, source),
471
+ ...channelRange,
472
+ });
473
+
474
+ fallbackChannelIndices.add(index);
475
+ fallbackChannelIndices.add(index + 1);
476
+ index += 1;
477
+ }
478
+
479
+ for (let index = 0; index < childNodes.length; index += 1) {
480
+ const currentNode = childNodes[index];
481
+
482
+ if (
483
+ PROTOCOL_STATEMENT_TYPES.has(currentNode.type) ||
484
+ fallbackChannelIndices.has(index)
485
+ ) {
486
+ continue;
487
+ }
488
+
489
+ let endIndex = index;
490
+
491
+ while (endIndex + 1 < childNodes.length) {
492
+ const nextNode = childNodes[endIndex + 1];
493
+ if (
494
+ PROTOCOL_STATEMENT_TYPES.has(nextNode.type) ||
495
+ fallbackChannelIndices.has(endIndex + 1)
496
+ ) {
497
+ break;
498
+ }
499
+
500
+ endIndex += 1;
501
+ }
502
+
503
+ const lastNode = childNodes[endIndex];
504
+ const text = source.slice(currentNode.startIndex, lastNode.endIndex).trim();
505
+
506
+ if (text.length > 0) {
507
+ statements.push({
508
+ kind: "other",
509
+ text,
510
+ ...mergeRanges(toRange(currentNode, source), toRange(lastNode, source)),
511
+ });
512
+ }
513
+
514
+ index = endIndex;
515
+ }
516
+
517
+ return statements;
518
+ };
519
+
520
+ export const parseProtocolDeclaration = (
521
+ declarationNode: SyntaxNode,
522
+ source: string,
523
+ issues: ParseIssue[],
524
+ ): ProtocolDeclaration => {
525
+ const declarationRange = toRange(declarationNode, source);
526
+ const protocolTypeNode = declarationNode.childForFieldName("protocol_type");
527
+ const protocolVariantNode =
528
+ declarationNode.childForFieldName("protocol_variant");
529
+ const nameNode = declarationNode.childForFieldName("name");
530
+ const fromTemplateNode = declarationNode.childForFieldName("from_template");
531
+ const bodyNode = declarationNode.childForFieldName("body");
532
+ const hasFromKeyword = declarationNode.children.some(
533
+ (entry) => entry.type === "from",
534
+ );
535
+
536
+ if (!isPresentNode(protocolTypeNode)) {
537
+ pushMissingFieldIssue(
538
+ issues,
539
+ declarationNode,
540
+ "Missing protocol type for protocol declaration",
541
+ source,
542
+ );
543
+ }
544
+
545
+ if (!isPresentNode(nameNode)) {
546
+ pushMissingFieldIssue(
547
+ issues,
548
+ declarationNode,
549
+ "Missing name for protocol declaration",
550
+ source,
551
+ );
552
+ }
553
+
554
+ if (hasFromKeyword && !isPresentNode(fromTemplateNode)) {
555
+ pushMissingFieldIssue(
556
+ issues,
557
+ declarationNode,
558
+ "Missing template name after from clause",
559
+ source,
560
+ );
561
+ }
562
+
563
+ if (!isPresentNode(bodyNode)) {
564
+ issues.push({
565
+ code: "syntax/unbalanced-brace",
566
+ message: "Missing '{' for protocol declaration",
567
+ ...declarationRange,
568
+ });
569
+ }
570
+
571
+ const { protocolType, protocolTypeRange } = protocolTypeTextAndRange(
572
+ protocolTypeNode,
573
+ protocolVariantNode,
574
+ source,
575
+ declarationRange,
576
+ );
577
+
578
+ return {
579
+ kind: "protocol",
580
+ protocolType,
581
+ protocolTypeRange,
582
+ name: isPresentNode(nameNode) ? textOf(nameNode, source) : "",
583
+ nameRange: isPresentNode(nameNode)
584
+ ? toRange(nameNode, source)
585
+ : declarationRange,
586
+ fromTemplate: isPresentNode(fromTemplateNode)
587
+ ? textOf(fromTemplateNode, source)
588
+ : undefined,
589
+ fromTemplateRange: isPresentNode(fromTemplateNode)
590
+ ? toRange(fromTemplateNode, source)
591
+ : undefined,
592
+ statements: isPresentNode(bodyNode)
593
+ ? parseProtocolStatements(bodyNode, source, issues)
594
+ : [],
595
+ ...declarationRange,
596
+ };
597
+ };