@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.
- package/.oxfmtrc.json +16 -0
- package/LICENSE +674 -0
- package/README.md +312 -0
- package/dist/declarations/basic.d.ts +9 -0
- package/dist/declarations/basic.d.ts.map +1 -0
- package/dist/declarations/basic.js +180 -0
- package/dist/declarations/basic.js.map +1 -0
- package/dist/declarations/filter.d.ts +6 -0
- package/dist/declarations/filter.d.ts.map +1 -0
- package/dist/declarations/filter.js +330 -0
- package/dist/declarations/filter.js.map +1 -0
- package/dist/declarations/parse-declarations.d.ts +4 -0
- package/dist/declarations/parse-declarations.d.ts.map +1 -0
- package/dist/declarations/parse-declarations.js +54 -0
- package/dist/declarations/parse-declarations.js.map +1 -0
- package/dist/declarations/protocol.d.ts +6 -0
- package/dist/declarations/protocol.d.ts.map +1 -0
- package/dist/declarations/protocol.js +444 -0
- package/dist/declarations/protocol.js.map +1 -0
- package/dist/declarations/shared.d.ts +56 -0
- package/dist/declarations/shared.d.ts.map +1 -0
- package/dist/declarations/shared.js +169 -0
- package/dist/declarations/shared.js.map +1 -0
- package/dist/declarations/top-level.d.ts +6 -0
- package/dist/declarations/top-level.d.ts.map +1 -0
- package/dist/declarations/top-level.js +141 -0
- package/dist/declarations/top-level.js.map +1 -0
- package/dist/declarations.d.ts +2 -0
- package/dist/declarations.d.ts.map +1 -0
- package/dist/declarations.js +2 -0
- package/dist/declarations.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/issues.d.ts +9 -0
- package/dist/issues.d.ts.map +1 -0
- package/dist/issues.js +119 -0
- package/dist/issues.js.map +1 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +51 -0
- package/dist/runtime.js.map +1 -0
- package/dist/tree-sitter-birdcc.wasm +0 -0
- package/dist/tree.d.ts +16 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +150 -0
- package/dist/tree.js.map +1 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/grammar.js +601 -0
- package/package.json +46 -0
- package/scripts/sync-wasm-paths.mjs +21 -0
- package/src/declarations/basic.ts +272 -0
- package/src/declarations/filter.ts +437 -0
- package/src/declarations/parse-declarations.ts +84 -0
- package/src/declarations/protocol.ts +597 -0
- package/src/declarations/shared.ts +275 -0
- package/src/declarations/top-level.ts +185 -0
- package/src/declarations.ts +1 -0
- package/src/index.ts +102 -0
- package/src/issues.ts +154 -0
- package/src/runtime.ts +64 -0
- package/src/tree-sitter-birdcc.wasm +0 -0
- package/src/tree.ts +210 -0
- package/src/types.ts +329 -0
- package/test/fixtures.test.ts +48 -0
- package/test/ip-literal-candidate.test.ts +39 -0
- package/test/parser.test.ts +475 -0
- package/test/realworld-smoke.test.ts +46 -0
- package/test/runtime.test.ts +51 -0
- package/test/tree.test.ts +83 -0
- package/tree-sitter.json +37 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { Node as SyntaxNode } from "web-tree-sitter";
|
|
2
|
+
import type { ParseIssue } from "../types.js";
|
|
3
|
+
import { pushMissingFieldIssue } from "../issues.js";
|
|
4
|
+
import { isPresentNode, stripQuotes, textOf, toRange } from "../tree.js";
|
|
5
|
+
import {
|
|
6
|
+
type DefineDeclaration,
|
|
7
|
+
type IncludeDeclaration,
|
|
8
|
+
type RouterIdDeclaration,
|
|
9
|
+
type TableDeclaration,
|
|
10
|
+
type TemplateDeclaration,
|
|
11
|
+
isStrictIpv4Literal,
|
|
12
|
+
nodeOrSelf,
|
|
13
|
+
normalizeTableType,
|
|
14
|
+
} from "./shared.js";
|
|
15
|
+
|
|
16
|
+
export const parseIncludeDeclaration = (
|
|
17
|
+
declarationNode: SyntaxNode,
|
|
18
|
+
source: string,
|
|
19
|
+
issues: ParseIssue[],
|
|
20
|
+
): IncludeDeclaration => {
|
|
21
|
+
const declarationRange = toRange(declarationNode, source);
|
|
22
|
+
const pathNode = declarationNode.childForFieldName("path");
|
|
23
|
+
if (!isPresentNode(pathNode)) {
|
|
24
|
+
pushMissingFieldIssue(
|
|
25
|
+
issues,
|
|
26
|
+
declarationNode,
|
|
27
|
+
"Missing path for include declaration",
|
|
28
|
+
source,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
kind: "include",
|
|
34
|
+
path: isPresentNode(pathNode) ? stripQuotes(textOf(pathNode, source)) : "",
|
|
35
|
+
pathRange: isPresentNode(pathNode)
|
|
36
|
+
? toRange(pathNode, source)
|
|
37
|
+
: declarationRange,
|
|
38
|
+
...declarationRange,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const parseDefineDeclaration = (
|
|
43
|
+
declarationNode: SyntaxNode,
|
|
44
|
+
source: string,
|
|
45
|
+
issues: ParseIssue[],
|
|
46
|
+
): DefineDeclaration => {
|
|
47
|
+
const declarationRange = toRange(declarationNode, source);
|
|
48
|
+
const nameNode = declarationNode.childForFieldName("name");
|
|
49
|
+
if (!isPresentNode(nameNode)) {
|
|
50
|
+
pushMissingFieldIssue(
|
|
51
|
+
issues,
|
|
52
|
+
declarationNode,
|
|
53
|
+
"Missing name for define declaration",
|
|
54
|
+
source,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
kind: "define",
|
|
60
|
+
name: isPresentNode(nameNode) ? textOf(nameNode, source) : "",
|
|
61
|
+
nameRange: isPresentNode(nameNode)
|
|
62
|
+
? toRange(nameNode, source)
|
|
63
|
+
: declarationRange,
|
|
64
|
+
...declarationRange,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const parseRouterIdDeclaration = (
|
|
69
|
+
declarationNode: SyntaxNode,
|
|
70
|
+
source: string,
|
|
71
|
+
issues: ParseIssue[],
|
|
72
|
+
): RouterIdDeclaration => {
|
|
73
|
+
const declarationRange = toRange(declarationNode, source);
|
|
74
|
+
const rawValueNode = declarationNode.childForFieldName("value");
|
|
75
|
+
|
|
76
|
+
if (!isPresentNode(rawValueNode)) {
|
|
77
|
+
pushMissingFieldIssue(
|
|
78
|
+
issues,
|
|
79
|
+
declarationNode,
|
|
80
|
+
"Missing value for router id declaration",
|
|
81
|
+
source,
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
kind: "router-id",
|
|
85
|
+
value: "",
|
|
86
|
+
valueKind: "unknown",
|
|
87
|
+
valueRange: declarationRange,
|
|
88
|
+
...declarationRange,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const valueNode = nodeOrSelf(rawValueNode);
|
|
93
|
+
|
|
94
|
+
if (valueNode.type === "router_id_from_clause") {
|
|
95
|
+
const fromSourceNode = valueNode.childForFieldName("from_source");
|
|
96
|
+
const fromSourceText = isPresentNode(fromSourceNode)
|
|
97
|
+
? textOf(fromSourceNode, source).toLowerCase()
|
|
98
|
+
: "";
|
|
99
|
+
if (fromSourceText !== "routing" && fromSourceText !== "dynamic") {
|
|
100
|
+
return {
|
|
101
|
+
kind: "router-id",
|
|
102
|
+
value: textOf(valueNode, source),
|
|
103
|
+
valueKind: "unknown",
|
|
104
|
+
valueRange: toRange(valueNode, source),
|
|
105
|
+
...declarationRange,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
kind: "router-id",
|
|
111
|
+
value: textOf(valueNode, source),
|
|
112
|
+
valueKind: "from",
|
|
113
|
+
valueRange: toRange(valueNode, source),
|
|
114
|
+
fromSource: fromSourceText,
|
|
115
|
+
...declarationRange,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
valueNode.type === "ipv4_literal" &&
|
|
121
|
+
isStrictIpv4Literal(textOf(valueNode, source))
|
|
122
|
+
) {
|
|
123
|
+
return {
|
|
124
|
+
kind: "router-id",
|
|
125
|
+
value: textOf(valueNode, source),
|
|
126
|
+
valueKind: "ip",
|
|
127
|
+
valueRange: toRange(valueNode, source),
|
|
128
|
+
...declarationRange,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (valueNode.type === "number") {
|
|
133
|
+
return {
|
|
134
|
+
kind: "router-id",
|
|
135
|
+
value: textOf(valueNode, source),
|
|
136
|
+
valueKind: "number",
|
|
137
|
+
valueRange: toRange(valueNode, source),
|
|
138
|
+
...declarationRange,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
kind: "router-id",
|
|
144
|
+
value: textOf(valueNode, source),
|
|
145
|
+
valueKind: "unknown",
|
|
146
|
+
valueRange: toRange(valueNode, source),
|
|
147
|
+
...declarationRange,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const parseTableDeclaration = (
|
|
152
|
+
declarationNode: SyntaxNode,
|
|
153
|
+
source: string,
|
|
154
|
+
issues: ParseIssue[],
|
|
155
|
+
): TableDeclaration => {
|
|
156
|
+
const declarationRange = toRange(declarationNode, source);
|
|
157
|
+
const tableTypeNode = declarationNode.childForFieldName("table_type");
|
|
158
|
+
const nameNode = declarationNode.childForFieldName("name");
|
|
159
|
+
const attrsNode = declarationNode.childForFieldName("attrs");
|
|
160
|
+
|
|
161
|
+
if (!isPresentNode(nameNode)) {
|
|
162
|
+
pushMissingFieldIssue(
|
|
163
|
+
issues,
|
|
164
|
+
declarationNode,
|
|
165
|
+
"Missing name for table declaration",
|
|
166
|
+
source,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const firstToken = declarationNode.children[0];
|
|
171
|
+
const tableTypeText = isPresentNode(tableTypeNode)
|
|
172
|
+
? textOf(tableTypeNode, source)
|
|
173
|
+
: firstToken
|
|
174
|
+
? textOf(firstToken, source)
|
|
175
|
+
: "unknown";
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
kind: "table",
|
|
179
|
+
tableType: normalizeTableType(tableTypeText),
|
|
180
|
+
tableTypeRange: isPresentNode(tableTypeNode)
|
|
181
|
+
? toRange(tableTypeNode, source)
|
|
182
|
+
: declarationRange,
|
|
183
|
+
name: isPresentNode(nameNode) ? textOf(nameNode, source) : "",
|
|
184
|
+
nameRange: isPresentNode(nameNode)
|
|
185
|
+
? toRange(nameNode, source)
|
|
186
|
+
: declarationRange,
|
|
187
|
+
attrsText: isPresentNode(attrsNode) ? textOf(attrsNode, source) : undefined,
|
|
188
|
+
attrsRange: isPresentNode(attrsNode)
|
|
189
|
+
? toRange(attrsNode, source)
|
|
190
|
+
: undefined,
|
|
191
|
+
...declarationRange,
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const parseTemplateDeclaration = (
|
|
196
|
+
declarationNode: SyntaxNode,
|
|
197
|
+
source: string,
|
|
198
|
+
issues: ParseIssue[],
|
|
199
|
+
): TemplateDeclaration => {
|
|
200
|
+
const declarationRange = toRange(declarationNode, source);
|
|
201
|
+
const declarationText = textOf(declarationNode, source);
|
|
202
|
+
const declarationHeader = declarationText.split("{", 1)[0] ?? declarationText;
|
|
203
|
+
const templateTypeNode = declarationNode.childForFieldName("template_type");
|
|
204
|
+
const nameNode = declarationNode.childForFieldName("name");
|
|
205
|
+
const fromTemplateNode = declarationNode.childForFieldName("from_template");
|
|
206
|
+
const bodyNode = declarationNode.childForFieldName("body");
|
|
207
|
+
const inferredFromTemplateMatch = declarationHeader.match(
|
|
208
|
+
/\bfrom\s+([A-Za-z_][A-Za-z0-9_-]*)\b/i,
|
|
209
|
+
);
|
|
210
|
+
const inferredFromTemplate = inferredFromTemplateMatch?.[1];
|
|
211
|
+
const hasFromKeyword =
|
|
212
|
+
declarationNode.children.some((entry) => entry.type === "from") ||
|
|
213
|
+
/\bfrom\b/i.test(declarationHeader);
|
|
214
|
+
|
|
215
|
+
if (!isPresentNode(templateTypeNode)) {
|
|
216
|
+
pushMissingFieldIssue(
|
|
217
|
+
issues,
|
|
218
|
+
declarationNode,
|
|
219
|
+
"Missing template type for template declaration",
|
|
220
|
+
source,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!isPresentNode(nameNode)) {
|
|
225
|
+
pushMissingFieldIssue(
|
|
226
|
+
issues,
|
|
227
|
+
declarationNode,
|
|
228
|
+
"Missing name for template declaration",
|
|
229
|
+
source,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (hasFromKeyword && !isPresentNode(fromTemplateNode)) {
|
|
234
|
+
pushMissingFieldIssue(
|
|
235
|
+
issues,
|
|
236
|
+
declarationNode,
|
|
237
|
+
"Missing template name after from clause",
|
|
238
|
+
source,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!isPresentNode(bodyNode)) {
|
|
243
|
+
issues.push({
|
|
244
|
+
code: "syntax/unbalanced-brace",
|
|
245
|
+
message: "Missing '{' for template declaration",
|
|
246
|
+
...declarationRange,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
kind: "template",
|
|
252
|
+
templateType: isPresentNode(templateTypeNode)
|
|
253
|
+
? textOf(templateTypeNode, source)
|
|
254
|
+
: "",
|
|
255
|
+
templateTypeRange: isPresentNode(templateTypeNode)
|
|
256
|
+
? toRange(templateTypeNode, source)
|
|
257
|
+
: declarationRange,
|
|
258
|
+
name: isPresentNode(nameNode) ? textOf(nameNode, source) : "",
|
|
259
|
+
nameRange: isPresentNode(nameNode)
|
|
260
|
+
? toRange(nameNode, source)
|
|
261
|
+
: declarationRange,
|
|
262
|
+
fromTemplate: isPresentNode(fromTemplateNode)
|
|
263
|
+
? textOf(fromTemplateNode, source)
|
|
264
|
+
: inferredFromTemplate,
|
|
265
|
+
fromTemplateRange: isPresentNode(fromTemplateNode)
|
|
266
|
+
? toRange(fromTemplateNode, source)
|
|
267
|
+
: inferredFromTemplate
|
|
268
|
+
? declarationRange
|
|
269
|
+
: undefined,
|
|
270
|
+
...declarationRange,
|
|
271
|
+
};
|
|
272
|
+
};
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import type { Node as SyntaxNode } from "web-tree-sitter";
|
|
2
|
+
import type { FilterBodyStatement, ParseIssue } from "../types.js";
|
|
3
|
+
import { pushMissingFieldIssue } from "../issues.js";
|
|
4
|
+
import { isPresentNode, mergeRanges, textOf, toRange } from "../tree.js";
|
|
5
|
+
import {
|
|
6
|
+
type ExtractedLiteral,
|
|
7
|
+
type FilterDeclaration,
|
|
8
|
+
type FunctionDeclaration,
|
|
9
|
+
type MatchExpression,
|
|
10
|
+
isStrictIpLiteral,
|
|
11
|
+
} from "./shared.js";
|
|
12
|
+
|
|
13
|
+
const parseControlStatements = (
|
|
14
|
+
bodyNode: SyntaxNode,
|
|
15
|
+
source: string,
|
|
16
|
+
): FilterBodyStatement[] => {
|
|
17
|
+
const statements: FilterBodyStatement[] = [];
|
|
18
|
+
const bodyRange = toRange(bodyNode, source);
|
|
19
|
+
const bodyText = textOf(bodyNode, source);
|
|
20
|
+
const tokenTexts = bodyNode.namedChildren.map((node) =>
|
|
21
|
+
textOf(node, source).toLowerCase(),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
for (const statementNode of bodyNode.namedChildren) {
|
|
25
|
+
const statementRange = toRange(statementNode, source);
|
|
26
|
+
const text = textOf(statementNode, source).trim();
|
|
27
|
+
const lowered = text.toLowerCase();
|
|
28
|
+
|
|
29
|
+
if (statementNode.type === "if_statement" || lowered === "if") {
|
|
30
|
+
const thenIndex = lowered.indexOf(" then ");
|
|
31
|
+
const conditionText =
|
|
32
|
+
lowered.startsWith("if ") && thenIndex > 0
|
|
33
|
+
? text.slice(3, thenIndex).trim()
|
|
34
|
+
: undefined;
|
|
35
|
+
|
|
36
|
+
statements.push({
|
|
37
|
+
kind: "if",
|
|
38
|
+
conditionText,
|
|
39
|
+
thenText: "",
|
|
40
|
+
...statementRange,
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (statementNode.type === "accept_statement" || lowered === "accept") {
|
|
46
|
+
statements.push({
|
|
47
|
+
kind: "accept",
|
|
48
|
+
...statementRange,
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (statementNode.type === "reject_statement" || lowered === "reject") {
|
|
54
|
+
statements.push({
|
|
55
|
+
kind: "reject",
|
|
56
|
+
...statementRange,
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (statementNode.type === "return_statement" || lowered === "return") {
|
|
62
|
+
statements.push({
|
|
63
|
+
kind: "return",
|
|
64
|
+
...statementRange,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (statementNode.type === "case_statement" || lowered === "case") {
|
|
70
|
+
statements.push({
|
|
71
|
+
kind: "case",
|
|
72
|
+
...statementRange,
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (statementNode.type === "expression_statement") {
|
|
78
|
+
const expressionNode = statementNode.childForFieldName("expression");
|
|
79
|
+
statements.push({
|
|
80
|
+
kind: "expression",
|
|
81
|
+
expressionText: isPresentNode(expressionNode)
|
|
82
|
+
? textOf(expressionNode, source)
|
|
83
|
+
: textOf(statementNode, source),
|
|
84
|
+
...statementRange,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
tokenTexts.includes("if") &&
|
|
92
|
+
!statements.some((item) => item.kind === "if")
|
|
93
|
+
) {
|
|
94
|
+
statements.push({
|
|
95
|
+
kind: "if",
|
|
96
|
+
conditionText: undefined,
|
|
97
|
+
thenText: "",
|
|
98
|
+
...bodyRange,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
tokenTexts.includes("case") &&
|
|
104
|
+
!statements.some((item) => item.kind === "case")
|
|
105
|
+
) {
|
|
106
|
+
statements.push({
|
|
107
|
+
kind: "case",
|
|
108
|
+
subjectText: undefined,
|
|
109
|
+
...bodyRange,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
tokenTexts.includes("accept") &&
|
|
115
|
+
!statements.some((item) => item.kind === "accept")
|
|
116
|
+
) {
|
|
117
|
+
statements.push({
|
|
118
|
+
kind: "accept",
|
|
119
|
+
...bodyRange,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
tokenTexts.includes("reject") &&
|
|
125
|
+
!statements.some((item) => item.kind === "reject")
|
|
126
|
+
) {
|
|
127
|
+
statements.push({
|
|
128
|
+
kind: "reject",
|
|
129
|
+
...bodyRange,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
tokenTexts.includes("return") &&
|
|
135
|
+
!statements.some((item) => item.kind === "return")
|
|
136
|
+
) {
|
|
137
|
+
statements.push({
|
|
138
|
+
kind: "return",
|
|
139
|
+
valueText: undefined,
|
|
140
|
+
...bodyRange,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const hasExpressionStatement = statements.some(
|
|
145
|
+
(item) => item.kind === "expression",
|
|
146
|
+
);
|
|
147
|
+
if (!hasExpressionStatement) {
|
|
148
|
+
const segments = bodyText
|
|
149
|
+
.split(";")
|
|
150
|
+
.map((segment) => segment.trim())
|
|
151
|
+
.filter((segment) => segment.length > 0);
|
|
152
|
+
|
|
153
|
+
for (const segment of segments) {
|
|
154
|
+
const normalizedSegment = segment
|
|
155
|
+
.replace(/^[\s{]+/, "")
|
|
156
|
+
.replace(/[\s}]+$/, "");
|
|
157
|
+
if (normalizedSegment.length === 0) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
normalizedSegment.startsWith("if ") ||
|
|
163
|
+
normalizedSegment.startsWith("case ") ||
|
|
164
|
+
normalizedSegment === "accept" ||
|
|
165
|
+
normalizedSegment === "reject" ||
|
|
166
|
+
normalizedSegment.startsWith("return")
|
|
167
|
+
) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
statements.push({
|
|
172
|
+
kind: "expression",
|
|
173
|
+
expressionText: normalizedSegment,
|
|
174
|
+
...bodyRange,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return statements;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const collectLiteralsAndMatches = (
|
|
183
|
+
bodyNode: SyntaxNode,
|
|
184
|
+
source: string,
|
|
185
|
+
): { literals: ExtractedLiteral[]; matches: MatchExpression[] } => {
|
|
186
|
+
const literals: ExtractedLiteral[] = [];
|
|
187
|
+
const matches: MatchExpression[] = [];
|
|
188
|
+
const isIpLike = (token: string): boolean => isStrictIpLiteral(token);
|
|
189
|
+
|
|
190
|
+
const extractPrefixSuffix = (token: string): string | null => {
|
|
191
|
+
const slashIndex = token.indexOf("/");
|
|
192
|
+
if (slashIndex === -1) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const suffix = token.slice(slashIndex);
|
|
197
|
+
const matched = suffix.match(
|
|
198
|
+
/^\/(?:\d{1,3}(?:[+-]|\{\d{1,3}(?:,\d{1,3})?\})?)/,
|
|
199
|
+
);
|
|
200
|
+
return matched?.[0] ?? null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const collectNode = (node: SyntaxNode): void => {
|
|
204
|
+
const namedChildren = node.namedChildren;
|
|
205
|
+
|
|
206
|
+
for (let index = 0; index < namedChildren.length; index += 1) {
|
|
207
|
+
const current = namedChildren[index];
|
|
208
|
+
if (!current) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const currentText = textOf(current, source);
|
|
213
|
+
const currentRange = toRange(current, source);
|
|
214
|
+
|
|
215
|
+
if (current.type === "ip_literal" && isStrictIpLiteral(currentText)) {
|
|
216
|
+
literals.push({
|
|
217
|
+
kind: "ip",
|
|
218
|
+
value: currentText,
|
|
219
|
+
...currentRange,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (current.type === "prefix_literal") {
|
|
224
|
+
literals.push({
|
|
225
|
+
kind: "prefix",
|
|
226
|
+
value: currentText,
|
|
227
|
+
...currentRange,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (current.type === "number" || current.type === "raw_token") {
|
|
232
|
+
const ownSuffix = extractPrefixSuffix(currentText);
|
|
233
|
+
if (ownSuffix) {
|
|
234
|
+
const ipPart = currentText.slice(0, currentText.indexOf("/"));
|
|
235
|
+
if (isIpLike(ipPart)) {
|
|
236
|
+
literals.push({
|
|
237
|
+
kind: "prefix",
|
|
238
|
+
value: `${ipPart}${ownSuffix}`,
|
|
239
|
+
...currentRange,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
const nextNode = namedChildren[index + 1];
|
|
244
|
+
const nextText = nextNode ? textOf(nextNode, source) : "";
|
|
245
|
+
const nextSuffix = nextNode ? extractPrefixSuffix(nextText) : null;
|
|
246
|
+
|
|
247
|
+
if (nextSuffix && isIpLike(currentText)) {
|
|
248
|
+
const mergedRange = mergeRanges(
|
|
249
|
+
currentRange,
|
|
250
|
+
toRange(nextNode, source),
|
|
251
|
+
);
|
|
252
|
+
literals.push({
|
|
253
|
+
kind: "prefix",
|
|
254
|
+
value: `${currentText}${nextSuffix}`,
|
|
255
|
+
...mergedRange,
|
|
256
|
+
});
|
|
257
|
+
} else if (isIpLike(currentText)) {
|
|
258
|
+
literals.push({
|
|
259
|
+
kind: "ip",
|
|
260
|
+
value: currentText,
|
|
261
|
+
...currentRange,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (current.type === "binary_expression") {
|
|
268
|
+
const operatorNode = current.childForFieldName("operator");
|
|
269
|
+
const leftNode = current.childForFieldName("left");
|
|
270
|
+
const rightNode = current.childForFieldName("right");
|
|
271
|
+
|
|
272
|
+
if (
|
|
273
|
+
isPresentNode(operatorNode) &&
|
|
274
|
+
textOf(operatorNode, source) === "~"
|
|
275
|
+
) {
|
|
276
|
+
matches.push({
|
|
277
|
+
operator: "~",
|
|
278
|
+
left: isPresentNode(leftNode) ? textOf(leftNode, source) : "",
|
|
279
|
+
right: isPresentNode(rightNode) ? textOf(rightNode, source) : "",
|
|
280
|
+
...toRange(current, source),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (currentText.trim() === "~") {
|
|
286
|
+
const leftNode = namedChildren[index - 1];
|
|
287
|
+
const immediateRightNode = namedChildren[index + 1];
|
|
288
|
+
|
|
289
|
+
if (!leftNode || !immediateRightNode) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const leftText = textOf(leftNode, source).trim();
|
|
294
|
+
const immediateRightText = textOf(immediateRightNode, source).trim();
|
|
295
|
+
const rightNode =
|
|
296
|
+
immediateRightText === "["
|
|
297
|
+
? (namedChildren[index + 2] ?? immediateRightNode)
|
|
298
|
+
: immediateRightNode;
|
|
299
|
+
const rightText = textOf(rightNode, source).trim();
|
|
300
|
+
|
|
301
|
+
if (leftText.length === 0 || rightText.length === 0) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
matches.push({
|
|
306
|
+
operator: "~",
|
|
307
|
+
left: leftText,
|
|
308
|
+
right: rightText,
|
|
309
|
+
...currentRange,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
collectNode(current);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
collectNode(bodyNode);
|
|
318
|
+
|
|
319
|
+
const literalKeys = new Set<string>();
|
|
320
|
+
const uniqueLiterals: ExtractedLiteral[] = [];
|
|
321
|
+
for (const literal of literals) {
|
|
322
|
+
const key = `${literal.kind}:${literal.value}:${literal.line}:${literal.column}:${literal.endLine}:${literal.endColumn}`;
|
|
323
|
+
if (literalKeys.has(key)) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
literalKeys.add(key);
|
|
328
|
+
uniqueLiterals.push(literal);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const matchKeys = new Set<string>();
|
|
332
|
+
const uniqueMatches: MatchExpression[] = [];
|
|
333
|
+
for (const match of matches) {
|
|
334
|
+
const key = `${match.operator}:${match.left}:${match.right}:${match.line}:${match.column}:${match.endLine}:${match.endColumn}`;
|
|
335
|
+
if (matchKeys.has(key)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
matchKeys.add(key);
|
|
340
|
+
uniqueMatches.push(match);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
literals: uniqueLiterals,
|
|
345
|
+
matches: uniqueMatches,
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const parseFilterDeclaration = (
|
|
350
|
+
declarationNode: SyntaxNode,
|
|
351
|
+
source: string,
|
|
352
|
+
issues: ParseIssue[],
|
|
353
|
+
): FilterDeclaration => {
|
|
354
|
+
const declarationRange = toRange(declarationNode, source);
|
|
355
|
+
const nameNode = declarationNode.childForFieldName("name");
|
|
356
|
+
const bodyNode = declarationNode.childForFieldName("body");
|
|
357
|
+
|
|
358
|
+
if (!isPresentNode(nameNode)) {
|
|
359
|
+
pushMissingFieldIssue(
|
|
360
|
+
issues,
|
|
361
|
+
declarationNode,
|
|
362
|
+
"Missing name for filter declaration",
|
|
363
|
+
source,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!isPresentNode(bodyNode)) {
|
|
368
|
+
issues.push({
|
|
369
|
+
code: "syntax/unbalanced-brace",
|
|
370
|
+
message: "Missing '{' for filter declaration",
|
|
371
|
+
...declarationRange,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const extracted = isPresentNode(bodyNode)
|
|
376
|
+
? collectLiteralsAndMatches(bodyNode, source)
|
|
377
|
+
: { literals: [], matches: [] };
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
kind: "filter",
|
|
381
|
+
name: isPresentNode(nameNode) ? textOf(nameNode, source) : "",
|
|
382
|
+
nameRange: isPresentNode(nameNode)
|
|
383
|
+
? toRange(nameNode, source)
|
|
384
|
+
: declarationRange,
|
|
385
|
+
statements: isPresentNode(bodyNode)
|
|
386
|
+
? parseControlStatements(bodyNode, source)
|
|
387
|
+
: [],
|
|
388
|
+
literals: extracted.literals,
|
|
389
|
+
matches: extracted.matches,
|
|
390
|
+
...declarationRange,
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
export const parseFunctionDeclaration = (
|
|
395
|
+
declarationNode: SyntaxNode,
|
|
396
|
+
source: string,
|
|
397
|
+
issues: ParseIssue[],
|
|
398
|
+
): FunctionDeclaration => {
|
|
399
|
+
const declarationRange = toRange(declarationNode, source);
|
|
400
|
+
const nameNode = declarationNode.childForFieldName("name");
|
|
401
|
+
const bodyNode = declarationNode.childForFieldName("body");
|
|
402
|
+
|
|
403
|
+
if (!isPresentNode(nameNode)) {
|
|
404
|
+
pushMissingFieldIssue(
|
|
405
|
+
issues,
|
|
406
|
+
declarationNode,
|
|
407
|
+
"Missing name for function declaration",
|
|
408
|
+
source,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!isPresentNode(bodyNode)) {
|
|
413
|
+
issues.push({
|
|
414
|
+
code: "syntax/unbalanced-brace",
|
|
415
|
+
message: "Missing '{' for function declaration",
|
|
416
|
+
...declarationRange,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const extracted = isPresentNode(bodyNode)
|
|
421
|
+
? collectLiteralsAndMatches(bodyNode, source)
|
|
422
|
+
: { literals: [], matches: [] };
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
kind: "function",
|
|
426
|
+
name: isPresentNode(nameNode) ? textOf(nameNode, source) : "",
|
|
427
|
+
nameRange: isPresentNode(nameNode)
|
|
428
|
+
? toRange(nameNode, source)
|
|
429
|
+
: declarationRange,
|
|
430
|
+
statements: isPresentNode(bodyNode)
|
|
431
|
+
? parseControlStatements(bodyNode, source)
|
|
432
|
+
: [],
|
|
433
|
+
literals: extracted.literals,
|
|
434
|
+
matches: extracted.matches,
|
|
435
|
+
...declarationRange,
|
|
436
|
+
};
|
|
437
|
+
};
|