@birdcc/parser 0.0.1-alpha.0 → 0.0.1-alpha.1

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.
@@ -1,275 +0,0 @@
1
- import { isIP } from "node:net";
2
- import type { Node as SyntaxNode } from "web-tree-sitter";
3
- import type {
4
- BirdDeclaration,
5
- ProtocolStatement,
6
- SourceRange,
7
- } from "../types.js";
8
- import { isPresentNode, mergeRanges, textOf, toRange } from "../tree.js";
9
-
10
- export type IncludeDeclaration = Extract<BirdDeclaration, { kind: "include" }>;
11
- export type DefineDeclaration = Extract<BirdDeclaration, { kind: "define" }>;
12
- export type RouterIdDeclaration = Extract<
13
- BirdDeclaration,
14
- { kind: "router-id" }
15
- >;
16
- export type TableDeclaration = Extract<BirdDeclaration, { kind: "table" }>;
17
- export type ProtocolDeclaration = Extract<
18
- BirdDeclaration,
19
- { kind: "protocol" }
20
- >;
21
- export type TemplateDeclaration = Extract<
22
- BirdDeclaration,
23
- { kind: "template" }
24
- >;
25
- export type FilterDeclaration = Extract<BirdDeclaration, { kind: "filter" }>;
26
- export type FunctionDeclaration = Extract<
27
- BirdDeclaration,
28
- { kind: "function" }
29
- >;
30
- export type ExtractedLiteral = FilterDeclaration["literals"][number];
31
- export type MatchExpression = FilterDeclaration["matches"][number];
32
-
33
- type ChannelStatement = Extract<ProtocolStatement, { kind: "channel" }>;
34
-
35
- export const PROTOCOL_STATEMENT_TYPES = new Set([
36
- "local_as_statement",
37
- "neighbor_statement",
38
- "import_statement",
39
- "export_statement",
40
- "channel_statement",
41
- "expression_statement",
42
- ]);
43
-
44
- export const TABLE_TYPES = new Set([
45
- "routing",
46
- "ipv4",
47
- "ipv6",
48
- "vpn4",
49
- "vpn6",
50
- "roa4",
51
- "roa6",
52
- "flow4",
53
- "flow6",
54
- ]);
55
-
56
- const CHANNEL_TYPES = new Set([
57
- "ipv4",
58
- "ipv6",
59
- "vpn4",
60
- "vpn6",
61
- "roa4",
62
- "roa6",
63
- "flow4",
64
- "flow6",
65
- "mpls",
66
- ]);
67
-
68
- export const isStrictIpv4Literal = (value: string): boolean =>
69
- isIP(value) === 4;
70
-
71
- export const isStrictIpv6Literal = (value: string): boolean =>
72
- isIP(value) === 6;
73
-
74
- export const isStrictIpLiteral = (value: string): boolean =>
75
- isStrictIpv4Literal(value) || isStrictIpv6Literal(value);
76
-
77
- const isAsciiDigit = (char: string): boolean => char >= "0" && char <= "9";
78
-
79
- const isAsciiHexDigit = (char: string): boolean =>
80
- isAsciiDigit(char) ||
81
- (char >= "a" && char <= "f") ||
82
- (char >= "A" && char <= "F");
83
-
84
- const isIpv4CandidateShape = (value: string): boolean => {
85
- let dotCount = 0;
86
- let segmentLength = 0;
87
-
88
- for (const char of value) {
89
- if (isAsciiDigit(char)) {
90
- segmentLength += 1;
91
- if (segmentLength > 3) {
92
- return false;
93
- }
94
- continue;
95
- }
96
-
97
- if (char !== ".") {
98
- return false;
99
- }
100
-
101
- if (segmentLength === 0) {
102
- return false;
103
- }
104
-
105
- dotCount += 1;
106
- segmentLength = 0;
107
- }
108
-
109
- return dotCount === 3 && segmentLength > 0;
110
- };
111
-
112
- const isIpv6CandidateShape = (value: string): boolean => {
113
- let hasColon = false;
114
-
115
- for (const char of value) {
116
- if (char === ":") {
117
- hasColon = true;
118
- continue;
119
- }
120
-
121
- if (char === "." || isAsciiHexDigit(char)) {
122
- continue;
123
- }
124
-
125
- return false;
126
- }
127
-
128
- return hasColon;
129
- };
130
-
131
- export const isIpLiteralCandidate = (value: string): boolean => {
132
- const trimmed = value.trim();
133
- if (trimmed.length === 0) {
134
- return false;
135
- }
136
-
137
- if (isIP(trimmed) !== 0) {
138
- return true;
139
- }
140
-
141
- if (trimmed.includes(".")) {
142
- return isIpv4CandidateShape(trimmed);
143
- }
144
-
145
- if (trimmed.includes(":")) {
146
- return isIpv6CandidateShape(trimmed);
147
- }
148
-
149
- return false;
150
- };
151
-
152
- export const protocolStatementNodesOf = (
153
- blockNode: SyntaxNode,
154
- ): SyntaxNode[] => {
155
- return blockNode.namedChildren.filter((child) =>
156
- PROTOCOL_STATEMENT_TYPES.has(child.type),
157
- );
158
- };
159
-
160
- export const protocolTypeTextAndRange = (
161
- protocolTypeNode: SyntaxNode | null,
162
- protocolVariantNode: SyntaxNode | null,
163
- source: string,
164
- declarationRange: SourceRange,
165
- ): { protocolType: string; protocolTypeRange: SourceRange } => {
166
- const protocolType = isPresentNode(protocolTypeNode)
167
- ? [
168
- textOf(protocolTypeNode, source),
169
- isPresentNode(protocolVariantNode)
170
- ? textOf(protocolVariantNode, source)
171
- : "",
172
- ]
173
- .filter(Boolean)
174
- .join(" ")
175
- : "";
176
-
177
- const protocolTypeRange =
178
- isPresentNode(protocolTypeNode) && isPresentNode(protocolVariantNode)
179
- ? mergeRanges(
180
- toRange(protocolTypeNode, source),
181
- toRange(protocolVariantNode, source),
182
- )
183
- : isPresentNode(protocolTypeNode)
184
- ? toRange(protocolTypeNode, source)
185
- : declarationRange;
186
-
187
- return { protocolType, protocolTypeRange };
188
- };
189
-
190
- export const normalizeTableType = (
191
- value: string,
192
- ): TableDeclaration["tableType"] => {
193
- const lowered = value.toLowerCase();
194
- return TABLE_TYPES.has(lowered)
195
- ? (lowered as TableDeclaration["tableType"])
196
- : "unknown";
197
- };
198
-
199
- export const normalizeChannelType = (
200
- value: string,
201
- ): ChannelStatement["channelType"] => {
202
- const lowered = value.toLowerCase();
203
- return CHANNEL_TYPES.has(lowered)
204
- ? (lowered as ChannelStatement["channelType"])
205
- : "unknown";
206
- };
207
-
208
- export const nodeOrSelf = (node: SyntaxNode): SyntaxNode => {
209
- if (node.namedChildCount === 1) {
210
- const child = node.namedChildren[0];
211
- if (child) {
212
- return child;
213
- }
214
- }
215
-
216
- return node;
217
- };
218
-
219
- export const CHANNEL_DIRECTIONS = new Set(["import", "receive", "export"]);
220
-
221
- export const isNumericToken = (value: string): boolean => {
222
- if (value.length === 0) {
223
- return false;
224
- }
225
-
226
- for (const char of value) {
227
- if (char < "0" || char > "9") {
228
- return false;
229
- }
230
- }
231
-
232
- return true;
233
- };
234
-
235
- export interface TopLevelToken {
236
- text: string;
237
- lowered: string;
238
- range: SourceRange;
239
- }
240
-
241
- export const topLevelTokensOf = (
242
- statementNode: SyntaxNode,
243
- source: string,
244
- ): TopLevelToken[] => {
245
- const tokens: TopLevelToken[] = [];
246
- for (const tokenNode of statementNode.namedChildren) {
247
- const tokenText = textOf(tokenNode, source).trim();
248
- if (tokenText.length === 0) {
249
- continue;
250
- }
251
-
252
- tokens.push({
253
- text: tokenText,
254
- lowered: tokenText.toLowerCase(),
255
- range: toRange(tokenNode),
256
- });
257
- }
258
-
259
- return tokens;
260
- };
261
-
262
- export const mergedTokenRange = (
263
- declarationRange: SourceRange,
264
- tokens: TopLevelToken[],
265
- startIndex: number,
266
- endIndex: number,
267
- ): SourceRange => {
268
- const startToken = tokens[startIndex];
269
- const endToken = tokens[endIndex];
270
- if (!startToken || !endToken) {
271
- return declarationRange;
272
- }
273
-
274
- return mergeRanges(startToken.range, endToken.range);
275
- };
@@ -1,185 +0,0 @@
1
- import type { Node as SyntaxNode } from "web-tree-sitter";
2
- import type { ParseIssue } from "../types.js";
3
- import { toRange } from "../tree.js";
4
- import {
5
- TABLE_TYPES,
6
- type RouterIdDeclaration,
7
- type TableDeclaration,
8
- isNumericToken,
9
- isStrictIpv4Literal,
10
- mergedTokenRange,
11
- normalizeTableType,
12
- topLevelTokensOf,
13
- } from "./shared.js";
14
-
15
- export const parseRouterIdFromStatement = (
16
- statementNode: SyntaxNode,
17
- source: string,
18
- issues: ParseIssue[],
19
- ): RouterIdDeclaration | null => {
20
- const declarationRange = toRange(statementNode, source);
21
- const tokens = topLevelTokensOf(statementNode, source);
22
-
23
- if (tokens[0]?.lowered !== "router" || tokens[1]?.lowered !== "id") {
24
- return null;
25
- }
26
-
27
- const valueTokens = tokens.slice(2);
28
- const value = valueTokens
29
- .map((token) => token.text)
30
- .join(" ")
31
- .trim();
32
- const valueRange = mergedTokenRange(
33
- declarationRange,
34
- tokens,
35
- 2,
36
- Math.max(tokens.length - 1, 2),
37
- );
38
-
39
- if (value.length === 0) {
40
- issues.push({
41
- code: "parser/missing-symbol",
42
- message: "Missing value for router id declaration",
43
- ...declarationRange,
44
- });
45
-
46
- return {
47
- kind: "router-id",
48
- value: "",
49
- valueKind: "unknown",
50
- valueRange: valueRange,
51
- ...declarationRange,
52
- };
53
- }
54
-
55
- if (valueTokens.length === 2 && valueTokens[0]?.lowered === "from") {
56
- const fromSourceToken = valueTokens[1]?.lowered;
57
- if (fromSourceToken !== "routing" && fromSourceToken !== "dynamic") {
58
- return {
59
- kind: "router-id",
60
- value,
61
- valueKind: "unknown",
62
- valueRange: valueRange,
63
- ...declarationRange,
64
- };
65
- }
66
-
67
- return {
68
- kind: "router-id",
69
- value,
70
- valueKind: "from",
71
- valueRange: valueRange,
72
- fromSource: fromSourceToken,
73
- ...declarationRange,
74
- };
75
- }
76
-
77
- if (valueTokens.length === 1 && isStrictIpv4Literal(value)) {
78
- return {
79
- kind: "router-id",
80
- value,
81
- valueKind: "ip",
82
- valueRange: valueRange,
83
- ...declarationRange,
84
- };
85
- }
86
-
87
- if (valueTokens.length === 1 && isNumericToken(value)) {
88
- return {
89
- kind: "router-id",
90
- value,
91
- valueKind: "number",
92
- valueRange: valueRange,
93
- ...declarationRange,
94
- };
95
- }
96
-
97
- return {
98
- kind: "router-id",
99
- value,
100
- valueKind: "unknown",
101
- valueRange: valueRange,
102
- ...declarationRange,
103
- };
104
- };
105
-
106
- export const parseTableFromStatement = (
107
- statementNode: SyntaxNode,
108
- source: string,
109
- issues: ParseIssue[],
110
- ): TableDeclaration | null => {
111
- const declarationRange = toRange(statementNode, source);
112
- const tokens = topLevelTokensOf(statementNode, source);
113
- if (tokens.length === 0) {
114
- return null;
115
- }
116
-
117
- let tableType: TableDeclaration["tableType"] = "unknown";
118
- let name = "";
119
- let attrsText: string | undefined;
120
- let tableTypeRange = declarationRange;
121
- let nameRange = declarationRange;
122
- let attrsRange: TableDeclaration["attrsRange"];
123
- let nameTokenIndex = -1;
124
- let attrsStartIndex = -1;
125
-
126
- if (tokens[0]?.lowered === "routing" && tokens[1]?.lowered === "table") {
127
- tableType = "routing";
128
- tableTypeRange = tokens[0].range;
129
- name = tokens[2]?.text ?? "";
130
- nameTokenIndex = 2;
131
- attrsStartIndex = 3;
132
- } else if (
133
- TABLE_TYPES.has(tokens[0]?.lowered ?? "") &&
134
- tokens[1]?.lowered === "table"
135
- ) {
136
- tableType = normalizeTableType(tokens[0]?.text ?? "");
137
- tableTypeRange = tokens[0]?.range ?? declarationRange;
138
- name = tokens[2]?.text ?? "";
139
- nameTokenIndex = 2;
140
- attrsStartIndex = 3;
141
- } else if (tokens[0]?.lowered === "table") {
142
- tableType = "unknown";
143
- name = tokens[1]?.text ?? "";
144
- nameTokenIndex = 1;
145
- attrsStartIndex = 2;
146
- } else {
147
- return null;
148
- }
149
-
150
- if (nameTokenIndex >= 0 && tokens[nameTokenIndex]) {
151
- nameRange = tokens[nameTokenIndex].range;
152
- }
153
-
154
- if (attrsStartIndex >= 0 && attrsStartIndex < tokens.length) {
155
- attrsText = tokens
156
- .slice(attrsStartIndex)
157
- .map((token) => token.text)
158
- .join(" ");
159
- attrsRange = mergedTokenRange(
160
- declarationRange,
161
- tokens,
162
- attrsStartIndex,
163
- tokens.length - 1,
164
- );
165
- }
166
-
167
- if (name.length === 0) {
168
- issues.push({
169
- code: "parser/missing-symbol",
170
- message: "Missing name for table declaration",
171
- ...declarationRange,
172
- });
173
- }
174
-
175
- return {
176
- kind: "table",
177
- tableType,
178
- tableTypeRange,
179
- name,
180
- nameRange,
181
- attrsText,
182
- attrsRange,
183
- ...declarationRange,
184
- };
185
- };
@@ -1 +0,0 @@
1
- export { parseDeclarations } from "./declarations/parse-declarations.js";
package/src/index.ts DELETED
@@ -1,102 +0,0 @@
1
- import { getParser } from "./runtime.js";
2
- import {
3
- collectTreeIssues,
4
- dedupeIssues,
5
- ensureBraceBalanceIssue,
6
- parseFailureIssue,
7
- runtimeFailureIssue,
8
- } from "./issues.js";
9
- import { parseDeclarations } from "./declarations.js";
10
- import type { ParseIssue, ParsedBirdDocument } from "./types.js";
11
-
12
- export type {
13
- AcceptStatement,
14
- BirdDeclaration,
15
- BirdProgram,
16
- CaseStatement,
17
- ChannelDebugEntry,
18
- ChannelEntry,
19
- ChannelExportEntry,
20
- ChannelImportEntry,
21
- ChannelKeepFilteredEntry,
22
- ChannelLimitEntry,
23
- ChannelOtherEntry,
24
- ChannelStatement,
25
- ChannelTableEntry,
26
- DefineDeclaration,
27
- ExpressionStatement,
28
- ExtractedLiteral,
29
- ExportStatement,
30
- FilterBodyStatement,
31
- FilterDeclaration,
32
- FunctionDeclaration,
33
- IfStatement,
34
- ImportStatement,
35
- IncludeDeclaration,
36
- LocalAsStatement,
37
- MatchExpression,
38
- NeighborStatement,
39
- OtherProtocolStatement,
40
- OtherStatement,
41
- ParseIssue,
42
- ParsedBirdDocument,
43
- ProtocolDeclaration,
44
- ProtocolStatement,
45
- RejectStatement,
46
- ReturnStatement,
47
- RouterIdDeclaration,
48
- SourceRange,
49
- TableDeclaration,
50
- TemplateDeclaration,
51
- } from "./types.js";
52
-
53
- /**
54
- * Parse one BIRD configuration text into AST V2 declarations and parser diagnostics.
55
- * Returns a degraded document with `parser/runtime-error` when Tree-sitter runtime cannot initialize.
56
- */
57
- export const parseBirdConfig = async (
58
- input: string,
59
- ): Promise<ParsedBirdDocument> => {
60
- let parser;
61
- try {
62
- parser = await getParser();
63
- } catch (error) {
64
- return {
65
- program: {
66
- kind: "program",
67
- declarations: [],
68
- },
69
- issues: [runtimeFailureIssue(error)],
70
- };
71
- }
72
-
73
- const tree = parser.parse(input);
74
-
75
- if (!tree) {
76
- return {
77
- program: {
78
- kind: "program",
79
- declarations: [],
80
- },
81
- issues: [parseFailureIssue()],
82
- };
83
- }
84
-
85
- try {
86
- const issues: ParseIssue[] = [];
87
- collectTreeIssues(tree.rootNode, input, issues);
88
- ensureBraceBalanceIssue(input, issues);
89
-
90
- const declarations = parseDeclarations(tree.rootNode, input, issues);
91
-
92
- return {
93
- program: {
94
- kind: "program",
95
- declarations,
96
- },
97
- issues: dedupeIssues(issues),
98
- };
99
- } finally {
100
- tree.delete();
101
- }
102
- };
package/src/issues.ts DELETED
@@ -1,154 +0,0 @@
1
- import type { Node as SyntaxNode } from "web-tree-sitter";
2
- import type { ParseIssue } from "./types.js";
3
- import { toRange } from "./tree.js";
4
-
5
- export const collectTreeIssues = (
6
- rootNode: SyntaxNode,
7
- source: string,
8
- issues: ParseIssue[],
9
- ): void => {
10
- if (!rootNode.hasError) {
11
- return;
12
- }
13
-
14
- const stack: SyntaxNode[] = [rootNode];
15
-
16
- while (stack.length > 0) {
17
- const current = stack.pop();
18
- if (!current) {
19
- continue;
20
- }
21
-
22
- if (current.isError) {
23
- const snippet = current.text.replace(/\s+/g, " ").trim();
24
- issues.push({
25
- code: "parser/syntax-error",
26
- message: `Syntax error near '${snippet || current.type}'`,
27
- ...toRange(current, source),
28
- });
29
- }
30
-
31
- if (current.isMissing) {
32
- const code =
33
- current.type === "}"
34
- ? "syntax/unbalanced-brace"
35
- : current.type === ";"
36
- ? "syntax/missing-semicolon"
37
- : "parser/missing-symbol";
38
- const message =
39
- current.type === "}"
40
- ? "Missing '}' to close block"
41
- : current.type === ";"
42
- ? "Missing ';' at end of statement"
43
- : `Missing symbol '${current.type}'`;
44
-
45
- issues.push({
46
- code,
47
- message,
48
- ...toRange(current, source),
49
- });
50
- }
51
-
52
- for (const child of current.children) {
53
- stack.push(child);
54
- }
55
- }
56
- };
57
-
58
- export const pushMissingFieldIssue = (
59
- issues: ParseIssue[],
60
- declarationNode: SyntaxNode,
61
- message: string,
62
- source: string,
63
- ): void => {
64
- issues.push({
65
- code: "parser/missing-symbol",
66
- message,
67
- ...toRange(declarationNode, source),
68
- });
69
- };
70
-
71
- export const dedupeIssues = (issues: ParseIssue[]): ParseIssue[] => {
72
- const seen = new Set<string>();
73
- const unique: ParseIssue[] = [];
74
-
75
- for (const issue of issues) {
76
- const key = `${issue.code}:${issue.message}:${issue.line}:${issue.column}:${issue.endLine}:${issue.endColumn}`;
77
- if (seen.has(key)) {
78
- continue;
79
- }
80
-
81
- seen.add(key);
82
- unique.push(issue);
83
- }
84
-
85
- return unique;
86
- };
87
-
88
- export const ensureBraceBalanceIssue = (
89
- source: string,
90
- issues: ParseIssue[],
91
- ): void => {
92
- let balance = 0;
93
- let line = 1;
94
- let column = 1;
95
- let endLine = 1;
96
- let endColumn = 1;
97
-
98
- for (const char of source) {
99
- if (char === "{") {
100
- balance += 1;
101
- endLine = line;
102
- endColumn = column;
103
- } else if (char === "}") {
104
- balance -= 1;
105
- endLine = line;
106
- endColumn = column;
107
- }
108
-
109
- if (char === "\n") {
110
- line += 1;
111
- column = 1;
112
- } else {
113
- column += 1;
114
- }
115
- }
116
-
117
- if (balance <= 0) {
118
- return;
119
- }
120
-
121
- const alreadyHasUnbalanced = issues.some(
122
- (item) => item.code === "syntax/unbalanced-brace",
123
- );
124
- if (alreadyHasUnbalanced) {
125
- return;
126
- }
127
-
128
- issues.push({
129
- code: "syntax/unbalanced-brace",
130
- message: "Missing '}' to close block",
131
- line: endLine,
132
- column: endColumn,
133
- endLine,
134
- endColumn,
135
- });
136
- };
137
-
138
- export const parseFailureIssue = (): ParseIssue => ({
139
- code: "parser/syntax-error",
140
- message: "Failed to parse input",
141
- line: 1,
142
- column: 1,
143
- endLine: 1,
144
- endColumn: 1,
145
- });
146
-
147
- export const runtimeFailureIssue = (error: unknown): ParseIssue => ({
148
- code: "parser/runtime-error",
149
- message: `Parser runtime unavailable: ${error instanceof Error ? error.message : String(error)}`,
150
- line: 1,
151
- column: 1,
152
- endLine: 1,
153
- endColumn: 1,
154
- });