@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.
- package/package.json +10 -3
- package/.oxfmtrc.json +0 -16
- package/grammar.js +0 -601
- package/scripts/sync-wasm-paths.mjs +0 -21
- package/src/declarations/basic.ts +0 -272
- package/src/declarations/filter.ts +0 -437
- package/src/declarations/parse-declarations.ts +0 -84
- package/src/declarations/protocol.ts +0 -597
- package/src/declarations/shared.ts +0 -275
- package/src/declarations/top-level.ts +0 -185
- package/src/declarations.ts +0 -1
- package/src/index.ts +0 -102
- package/src/issues.ts +0 -154
- package/src/runtime.ts +0 -64
- package/src/tree-sitter-birdcc.wasm +0 -0
- package/src/tree.ts +0 -210
- package/src/types.ts +0 -329
- package/test/fixtures.test.ts +0 -48
- package/test/ip-literal-candidate.test.ts +0 -39
- package/test/parser.test.ts +0 -475
- package/test/realworld-smoke.test.ts +0 -46
- package/test/runtime.test.ts +0 -51
- package/test/tree.test.ts +0 -83
- package/tree-sitter.json +0 -37
- package/tsconfig.json +0 -8
package/src/runtime.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { Language, Parser } from "web-tree-sitter";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const DEFAULT_LANGUAGE_WASM_PATHS = [
|
|
6
|
-
fileURLToPath(new URL("./tree-sitter-birdcc.wasm", import.meta.url)),
|
|
7
|
-
fileURLToPath(new URL("../src/tree-sitter-birdcc.wasm", import.meta.url)),
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
let parserPromise: Promise<Parser> | null = null;
|
|
11
|
-
let languageWasmPaths: string[] = [...DEFAULT_LANGUAGE_WASM_PATHS];
|
|
12
|
-
|
|
13
|
-
const loadLanguage = async (): Promise<Language> => {
|
|
14
|
-
let lastError: unknown;
|
|
15
|
-
|
|
16
|
-
for (const path of languageWasmPaths) {
|
|
17
|
-
if (!existsSync(path)) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
return await Language.load(path);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
lastError = error;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const errorMessage =
|
|
29
|
-
lastError instanceof Error ? lastError.message : String(lastError);
|
|
30
|
-
const availablePaths = languageWasmPaths.join(", ");
|
|
31
|
-
throw new Error(
|
|
32
|
-
`Unable to load Tree-sitter WASM language from any candidate path: ${availablePaths}. Last error: ${errorMessage}`,
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const createParser = async (): Promise<Parser> => {
|
|
37
|
-
await Parser.init();
|
|
38
|
-
const language = await loadLanguage();
|
|
39
|
-
|
|
40
|
-
const parser = new Parser();
|
|
41
|
-
parser.setLanguage(language);
|
|
42
|
-
return parser;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const getParser = async (): Promise<Parser> => {
|
|
46
|
-
if (!parserPromise) {
|
|
47
|
-
parserPromise = createParser().catch((error: unknown) => {
|
|
48
|
-
parserPromise = null;
|
|
49
|
-
throw error;
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return parserPromise;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const resetParserRuntimeForTests = (): void => {
|
|
57
|
-
parserPromise = null;
|
|
58
|
-
languageWasmPaths = [...DEFAULT_LANGUAGE_WASM_PATHS];
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export const setLanguageWasmPathsForTests = (paths: string[]): void => {
|
|
62
|
-
languageWasmPaths = [...paths];
|
|
63
|
-
parserPromise = null;
|
|
64
|
-
};
|
|
Binary file
|
package/src/tree.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import type { Node as SyntaxNode } from "web-tree-sitter";
|
|
2
|
-
import type { SourceRange } from "./types.js";
|
|
3
|
-
|
|
4
|
-
const byteOffsetToCodeUnitIndex = (
|
|
5
|
-
bytes: Buffer,
|
|
6
|
-
byteOffset: number,
|
|
7
|
-
): number => {
|
|
8
|
-
const clampedOffset = Math.max(0, Math.min(byteOffset, bytes.length));
|
|
9
|
-
return bytes.subarray(0, clampedOffset).toString("utf8").length;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
let cachedSourceForBytes: string | null = null;
|
|
13
|
-
let cachedUtf8Bytes: Buffer | null = null;
|
|
14
|
-
let cachedSourceForLineStarts: string | null = null;
|
|
15
|
-
let cachedLineStarts: number[] | null = null;
|
|
16
|
-
const UTF8_CACHE_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
17
|
-
const UTF8_CACHE_TTL_MS = 30_000;
|
|
18
|
-
let cachedUtf8BytesUpdatedAt = 0;
|
|
19
|
-
let cachedUtf8BytesVersion = 0;
|
|
20
|
-
let cachedLineStartsUpdatedAt = 0;
|
|
21
|
-
let cachedLineStartsVersion = 0;
|
|
22
|
-
|
|
23
|
-
const clearTextCaches = (): void => {
|
|
24
|
-
cachedSourceForBytes = null;
|
|
25
|
-
cachedUtf8Bytes = null;
|
|
26
|
-
cachedSourceForLineStarts = null;
|
|
27
|
-
cachedLineStarts = null;
|
|
28
|
-
cachedUtf8BytesUpdatedAt = 0;
|
|
29
|
-
cachedLineStartsUpdatedAt = 0;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const isUtf8CacheExpired = (): boolean => {
|
|
33
|
-
if (!cachedUtf8Bytes || cachedUtf8BytesUpdatedAt === 0) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return Date.now() - cachedUtf8BytesUpdatedAt > UTF8_CACHE_TTL_MS;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const utf8BytesOf = (source: string): Buffer => {
|
|
41
|
-
const estimatedBytes = Buffer.byteLength(source, "utf8");
|
|
42
|
-
if (estimatedBytes > UTF8_CACHE_LIMIT_BYTES) {
|
|
43
|
-
if (
|
|
44
|
-
cachedUtf8Bytes &&
|
|
45
|
-
cachedUtf8Bytes.length > UTF8_CACHE_LIMIT_BYTES / 2
|
|
46
|
-
) {
|
|
47
|
-
clearTextCaches();
|
|
48
|
-
}
|
|
49
|
-
return Buffer.from(source, "utf8");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
cachedSourceForBytes !== source ||
|
|
54
|
-
!cachedUtf8Bytes ||
|
|
55
|
-
isUtf8CacheExpired()
|
|
56
|
-
) {
|
|
57
|
-
cachedSourceForBytes = source;
|
|
58
|
-
cachedUtf8Bytes = Buffer.from(source, "utf8");
|
|
59
|
-
cachedUtf8BytesVersion += 1;
|
|
60
|
-
}
|
|
61
|
-
cachedUtf8BytesUpdatedAt = Date.now();
|
|
62
|
-
|
|
63
|
-
return cachedUtf8Bytes;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const nodeCodeUnitSpan = (
|
|
67
|
-
node: SyntaxNode,
|
|
68
|
-
source: string,
|
|
69
|
-
): {
|
|
70
|
-
startIndex: number;
|
|
71
|
-
endIndex: number;
|
|
72
|
-
} => {
|
|
73
|
-
const startIndex = node.startIndex;
|
|
74
|
-
const endIndex = node.endIndex;
|
|
75
|
-
|
|
76
|
-
const inBounds =
|
|
77
|
-
startIndex >= 0 && endIndex >= startIndex && endIndex <= source.length;
|
|
78
|
-
if (inBounds && source.slice(startIndex, endIndex) === node.text) {
|
|
79
|
-
return { startIndex, endIndex };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const bytes = utf8BytesOf(source);
|
|
83
|
-
return {
|
|
84
|
-
startIndex: byteOffsetToCodeUnitIndex(bytes, startIndex),
|
|
85
|
-
endIndex: byteOffsetToCodeUnitIndex(bytes, endIndex),
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const lineStartsOf = (source: string): number[] => {
|
|
90
|
-
const estimatedBytes = Buffer.byteLength(source, "utf8");
|
|
91
|
-
const canUseCache = estimatedBytes <= UTF8_CACHE_LIMIT_BYTES;
|
|
92
|
-
if (
|
|
93
|
-
canUseCache &&
|
|
94
|
-
cachedSourceForLineStarts === source &&
|
|
95
|
-
cachedLineStarts &&
|
|
96
|
-
Date.now() - cachedLineStartsUpdatedAt <= UTF8_CACHE_TTL_MS
|
|
97
|
-
) {
|
|
98
|
-
cachedLineStartsUpdatedAt = Date.now();
|
|
99
|
-
return cachedLineStarts;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const starts = [0];
|
|
103
|
-
|
|
104
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
105
|
-
if (source[index] === "\n") {
|
|
106
|
-
starts.push(index + 1);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (canUseCache) {
|
|
111
|
-
cachedSourceForLineStarts = source;
|
|
112
|
-
cachedLineStarts = starts;
|
|
113
|
-
cachedLineStartsUpdatedAt = Date.now();
|
|
114
|
-
cachedLineStartsVersion += 1;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return starts;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const indexToLineColumn = (
|
|
121
|
-
codeUnitIndex: number,
|
|
122
|
-
lineStarts: number[],
|
|
123
|
-
): { line: number; column: number } => {
|
|
124
|
-
let low = 0;
|
|
125
|
-
let high = lineStarts.length - 1;
|
|
126
|
-
let bestLineIndex = 0;
|
|
127
|
-
|
|
128
|
-
while (low <= high) {
|
|
129
|
-
const middle = Math.floor((low + high) / 2);
|
|
130
|
-
const lineStart = lineStarts[middle] ?? 0;
|
|
131
|
-
|
|
132
|
-
if (lineStart <= codeUnitIndex) {
|
|
133
|
-
bestLineIndex = middle;
|
|
134
|
-
low = middle + 1;
|
|
135
|
-
} else {
|
|
136
|
-
high = middle - 1;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const lineStart = lineStarts[bestLineIndex] ?? 0;
|
|
141
|
-
return {
|
|
142
|
-
line: bestLineIndex + 1,
|
|
143
|
-
column: codeUnitIndex - lineStart + 1,
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
export const toRange = (node: SyntaxNode, source?: string): SourceRange => {
|
|
148
|
-
if (!source) {
|
|
149
|
-
return {
|
|
150
|
-
line: node.startPosition.row + 1,
|
|
151
|
-
column: node.startPosition.column + 1,
|
|
152
|
-
endLine: node.endPosition.row + 1,
|
|
153
|
-
endColumn: node.endPosition.column + 1,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const { startIndex, endIndex } = nodeCodeUnitSpan(node, source);
|
|
158
|
-
const lineStarts = lineStartsOf(source);
|
|
159
|
-
const start = indexToLineColumn(startIndex, lineStarts);
|
|
160
|
-
const end = indexToLineColumn(endIndex, lineStarts);
|
|
161
|
-
|
|
162
|
-
return {
|
|
163
|
-
line: start.line,
|
|
164
|
-
column: start.column,
|
|
165
|
-
endLine: end.line,
|
|
166
|
-
endColumn: end.column,
|
|
167
|
-
};
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
export const mergeRanges = (
|
|
171
|
-
start: SourceRange,
|
|
172
|
-
end: SourceRange,
|
|
173
|
-
): SourceRange => ({
|
|
174
|
-
line: start.line,
|
|
175
|
-
column: start.column,
|
|
176
|
-
endLine: end.endLine,
|
|
177
|
-
endColumn: end.endColumn,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
export const textOf = (node: SyntaxNode, source: string): string => {
|
|
181
|
-
const { startIndex, endIndex } = nodeCodeUnitSpan(node, source);
|
|
182
|
-
return source.slice(startIndex, endIndex);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
export const stripQuotes = (value: string): string =>
|
|
186
|
-
value.replace(/^['"]|['"]$/g, "");
|
|
187
|
-
|
|
188
|
-
export const isPresentNode = (node: SyntaxNode | null): node is SyntaxNode =>
|
|
189
|
-
Boolean(node && !node.isMissing && !node.isError);
|
|
190
|
-
|
|
191
|
-
export const cacheUtf8BytesForTests = (source: string): number =>
|
|
192
|
-
utf8BytesOf(source).length;
|
|
193
|
-
|
|
194
|
-
export const getUtf8CacheStateForTests = (): {
|
|
195
|
-
hasCache: boolean;
|
|
196
|
-
byteLength: number;
|
|
197
|
-
utf8Version: number;
|
|
198
|
-
lineStartsVersion: number;
|
|
199
|
-
} => ({
|
|
200
|
-
hasCache: Boolean(cachedUtf8Bytes),
|
|
201
|
-
byteLength: cachedUtf8Bytes?.length ?? 0,
|
|
202
|
-
utf8Version: cachedUtf8BytesVersion,
|
|
203
|
-
lineStartsVersion: cachedLineStartsVersion,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
export const resetUtf8CacheForTests = (): void => {
|
|
207
|
-
clearTextCaches();
|
|
208
|
-
cachedUtf8BytesVersion = 0;
|
|
209
|
-
cachedLineStartsVersion = 0;
|
|
210
|
-
};
|
package/src/types.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
/** 1-based source range used across parser/core/linter diagnostics. */
|
|
2
|
-
export interface SourceRange {
|
|
3
|
-
line: number;
|
|
4
|
-
column: number;
|
|
5
|
-
endLine: number;
|
|
6
|
-
endColumn: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Parser issue emitted from Tree-sitter syntax recovery or runtime initialization. */
|
|
10
|
-
export interface ParseIssue extends SourceRange {
|
|
11
|
-
code:
|
|
12
|
-
| "syntax/missing-semicolon"
|
|
13
|
-
| "syntax/unbalanced-brace"
|
|
14
|
-
| "parser/missing-symbol"
|
|
15
|
-
| "parser/syntax-error"
|
|
16
|
-
| "parser/runtime-error";
|
|
17
|
-
message: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface DeclarationBase extends SourceRange {
|
|
21
|
-
kind:
|
|
22
|
-
| "include"
|
|
23
|
-
| "define"
|
|
24
|
-
| "router-id"
|
|
25
|
-
| "table"
|
|
26
|
-
| "protocol"
|
|
27
|
-
| "template"
|
|
28
|
-
| "filter"
|
|
29
|
-
| "function";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface IncludeDeclaration extends DeclarationBase {
|
|
33
|
-
kind: "include";
|
|
34
|
-
path: string;
|
|
35
|
-
pathRange: SourceRange;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface DefineDeclaration extends DeclarationBase {
|
|
39
|
-
kind: "define";
|
|
40
|
-
name: string;
|
|
41
|
-
nameRange: SourceRange;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface RouterIdDeclaration extends DeclarationBase {
|
|
45
|
-
kind: "router-id";
|
|
46
|
-
value: string;
|
|
47
|
-
valueKind: "ip" | "number" | "from" | "unknown";
|
|
48
|
-
valueRange: SourceRange;
|
|
49
|
-
fromSource?: "routing" | "dynamic";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface TableDeclaration extends DeclarationBase {
|
|
53
|
-
kind: "table";
|
|
54
|
-
tableType:
|
|
55
|
-
| "routing"
|
|
56
|
-
| "ipv4"
|
|
57
|
-
| "ipv6"
|
|
58
|
-
| "vpn4"
|
|
59
|
-
| "vpn6"
|
|
60
|
-
| "roa4"
|
|
61
|
-
| "roa6"
|
|
62
|
-
| "flow4"
|
|
63
|
-
| "flow6"
|
|
64
|
-
| "unknown";
|
|
65
|
-
tableTypeRange: SourceRange;
|
|
66
|
-
name: string;
|
|
67
|
-
nameRange: SourceRange;
|
|
68
|
-
attrsText?: string;
|
|
69
|
-
attrsRange?: SourceRange;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
interface StatementBase extends SourceRange {
|
|
73
|
-
kind: "local-as" | "neighbor" | "import" | "export" | "channel";
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface LocalAsStatement extends StatementBase {
|
|
77
|
-
kind: "local-as";
|
|
78
|
-
asn: string;
|
|
79
|
-
asnRange: SourceRange;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface NeighborStatement extends StatementBase {
|
|
83
|
-
kind: "neighbor";
|
|
84
|
-
address: string;
|
|
85
|
-
addressRange: SourceRange;
|
|
86
|
-
addressKind: "ip" | "other";
|
|
87
|
-
asn?: string;
|
|
88
|
-
asnRange?: SourceRange;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface ImportStatement extends StatementBase {
|
|
92
|
-
kind: "import";
|
|
93
|
-
mode: "all" | "none" | "filter" | "where" | "other";
|
|
94
|
-
filterName?: string;
|
|
95
|
-
filterNameRange?: SourceRange;
|
|
96
|
-
whereExpression?: string;
|
|
97
|
-
whereExpressionRange?: SourceRange;
|
|
98
|
-
clauseText?: string;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface ExportStatement extends StatementBase {
|
|
102
|
-
kind: "export";
|
|
103
|
-
mode: "all" | "none" | "filter" | "where" | "other";
|
|
104
|
-
filterName?: string;
|
|
105
|
-
filterNameRange?: SourceRange;
|
|
106
|
-
whereExpression?: string;
|
|
107
|
-
whereExpressionRange?: SourceRange;
|
|
108
|
-
clauseText?: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface ChannelEntryBase extends SourceRange {
|
|
112
|
-
kind:
|
|
113
|
-
| "table"
|
|
114
|
-
| "import"
|
|
115
|
-
| "export"
|
|
116
|
-
| "limit"
|
|
117
|
-
| "debug"
|
|
118
|
-
| "keep-filtered"
|
|
119
|
-
| "other";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface ChannelTableEntry extends ChannelEntryBase {
|
|
123
|
-
kind: "table";
|
|
124
|
-
tableName: string;
|
|
125
|
-
tableNameRange: SourceRange;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface ChannelImportEntry extends ChannelEntryBase {
|
|
129
|
-
kind: "import";
|
|
130
|
-
mode: "all" | "none" | "filter" | "where" | "other";
|
|
131
|
-
filterName?: string;
|
|
132
|
-
filterNameRange?: SourceRange;
|
|
133
|
-
whereExpression?: string;
|
|
134
|
-
whereExpressionRange?: SourceRange;
|
|
135
|
-
clauseText?: string;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface ChannelExportEntry extends ChannelEntryBase {
|
|
139
|
-
kind: "export";
|
|
140
|
-
mode: "all" | "none" | "filter" | "where" | "other";
|
|
141
|
-
filterName?: string;
|
|
142
|
-
filterNameRange?: SourceRange;
|
|
143
|
-
whereExpression?: string;
|
|
144
|
-
whereExpressionRange?: SourceRange;
|
|
145
|
-
clauseText?: string;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface ChannelLimitEntry extends ChannelEntryBase {
|
|
149
|
-
kind: "limit";
|
|
150
|
-
direction: "import" | "receive" | "export";
|
|
151
|
-
value: string;
|
|
152
|
-
valueRange: SourceRange;
|
|
153
|
-
action?: string;
|
|
154
|
-
actionRange?: SourceRange;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export interface ChannelDebugEntry extends ChannelEntryBase {
|
|
158
|
-
kind: "debug";
|
|
159
|
-
clauseText: string;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface ChannelKeepFilteredEntry extends ChannelEntryBase {
|
|
163
|
-
kind: "keep-filtered";
|
|
164
|
-
value: string;
|
|
165
|
-
valueRange: SourceRange;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface ChannelOtherEntry extends ChannelEntryBase {
|
|
169
|
-
kind: "other";
|
|
170
|
-
text: string;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export type ChannelEntry =
|
|
174
|
-
| ChannelTableEntry
|
|
175
|
-
| ChannelImportEntry
|
|
176
|
-
| ChannelExportEntry
|
|
177
|
-
| ChannelLimitEntry
|
|
178
|
-
| ChannelDebugEntry
|
|
179
|
-
| ChannelKeepFilteredEntry
|
|
180
|
-
| ChannelOtherEntry;
|
|
181
|
-
|
|
182
|
-
export interface ChannelStatement extends StatementBase {
|
|
183
|
-
kind: "channel";
|
|
184
|
-
channelType:
|
|
185
|
-
| "ipv4"
|
|
186
|
-
| "ipv6"
|
|
187
|
-
| "vpn4"
|
|
188
|
-
| "vpn6"
|
|
189
|
-
| "roa4"
|
|
190
|
-
| "roa6"
|
|
191
|
-
| "flow4"
|
|
192
|
-
| "flow6"
|
|
193
|
-
| "mpls"
|
|
194
|
-
| "unknown";
|
|
195
|
-
channelTypeRange: SourceRange;
|
|
196
|
-
entries: ChannelEntry[];
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface OtherProtocolStatement extends SourceRange {
|
|
200
|
-
kind: "other";
|
|
201
|
-
text: string;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export type ProtocolStatement =
|
|
205
|
-
| LocalAsStatement
|
|
206
|
-
| NeighborStatement
|
|
207
|
-
| ImportStatement
|
|
208
|
-
| ExportStatement
|
|
209
|
-
| ChannelStatement
|
|
210
|
-
| OtherProtocolStatement;
|
|
211
|
-
|
|
212
|
-
export interface ProtocolDeclaration extends DeclarationBase {
|
|
213
|
-
kind: "protocol";
|
|
214
|
-
protocolType: string;
|
|
215
|
-
protocolTypeRange: SourceRange;
|
|
216
|
-
name: string;
|
|
217
|
-
nameRange: SourceRange;
|
|
218
|
-
fromTemplate?: string;
|
|
219
|
-
fromTemplateRange?: SourceRange;
|
|
220
|
-
statements: ProtocolStatement[];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface TemplateDeclaration extends DeclarationBase {
|
|
224
|
-
kind: "template";
|
|
225
|
-
templateType: string;
|
|
226
|
-
templateTypeRange: SourceRange;
|
|
227
|
-
name: string;
|
|
228
|
-
nameRange: SourceRange;
|
|
229
|
-
fromTemplate?: string;
|
|
230
|
-
fromTemplateRange?: SourceRange;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export interface ExtractedLiteral extends SourceRange {
|
|
234
|
-
kind: "ip" | "prefix";
|
|
235
|
-
value: string;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export interface MatchExpression extends SourceRange {
|
|
239
|
-
operator: "~";
|
|
240
|
-
left: string;
|
|
241
|
-
right: string;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
interface ControlStatementBase extends SourceRange {
|
|
245
|
-
kind: "if" | "accept" | "reject" | "return" | "case" | "expression" | "other";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface IfStatement extends ControlStatementBase {
|
|
249
|
-
kind: "if";
|
|
250
|
-
conditionText?: string;
|
|
251
|
-
thenText: string;
|
|
252
|
-
elseText?: string;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export interface AcceptStatement extends ControlStatementBase {
|
|
256
|
-
kind: "accept";
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export interface RejectStatement extends ControlStatementBase {
|
|
260
|
-
kind: "reject";
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export interface ReturnStatement extends ControlStatementBase {
|
|
264
|
-
kind: "return";
|
|
265
|
-
valueText?: string;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export interface CaseStatement extends ControlStatementBase {
|
|
269
|
-
kind: "case";
|
|
270
|
-
subjectText?: string;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export interface ExpressionStatement extends ControlStatementBase {
|
|
274
|
-
kind: "expression";
|
|
275
|
-
expressionText: string;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export interface OtherStatement extends ControlStatementBase {
|
|
279
|
-
kind: "other";
|
|
280
|
-
text: string;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export type FilterBodyStatement =
|
|
284
|
-
| IfStatement
|
|
285
|
-
| AcceptStatement
|
|
286
|
-
| RejectStatement
|
|
287
|
-
| ReturnStatement
|
|
288
|
-
| CaseStatement
|
|
289
|
-
| ExpressionStatement
|
|
290
|
-
| OtherStatement;
|
|
291
|
-
|
|
292
|
-
export interface FilterDeclaration extends DeclarationBase {
|
|
293
|
-
kind: "filter";
|
|
294
|
-
name: string;
|
|
295
|
-
nameRange: SourceRange;
|
|
296
|
-
statements: FilterBodyStatement[];
|
|
297
|
-
literals: ExtractedLiteral[];
|
|
298
|
-
matches: MatchExpression[];
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export interface FunctionDeclaration extends DeclarationBase {
|
|
302
|
-
kind: "function";
|
|
303
|
-
name: string;
|
|
304
|
-
nameRange: SourceRange;
|
|
305
|
-
statements: FilterBodyStatement[];
|
|
306
|
-
literals: ExtractedLiteral[];
|
|
307
|
-
matches: MatchExpression[];
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export type BirdDeclaration =
|
|
311
|
-
| IncludeDeclaration
|
|
312
|
-
| DefineDeclaration
|
|
313
|
-
| RouterIdDeclaration
|
|
314
|
-
| TableDeclaration
|
|
315
|
-
| ProtocolDeclaration
|
|
316
|
-
| TemplateDeclaration
|
|
317
|
-
| FilterDeclaration
|
|
318
|
-
| FunctionDeclaration;
|
|
319
|
-
|
|
320
|
-
export interface BirdProgram {
|
|
321
|
-
kind: "program";
|
|
322
|
-
declarations: BirdDeclaration[];
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/** Result of parsing one BIRD config document. */
|
|
326
|
-
export interface ParsedBirdDocument {
|
|
327
|
-
program: BirdProgram;
|
|
328
|
-
issues: ParseIssue[];
|
|
329
|
-
}
|
package/test/fixtures.test.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { parseBirdConfig } from "../src/index.js";
|
|
4
|
-
|
|
5
|
-
const fixtureFiles = [
|
|
6
|
-
"basic.conf",
|
|
7
|
-
"bgp_advanced.conf",
|
|
8
|
-
"bogon.conf",
|
|
9
|
-
"protocol_phrases.conf",
|
|
10
|
-
] as const;
|
|
11
|
-
|
|
12
|
-
const readFixture = (name: string): string => {
|
|
13
|
-
const url = new URL(
|
|
14
|
-
`../../../../refer/BIRD-tm-language-grammar/sample/${name}`,
|
|
15
|
-
import.meta.url,
|
|
16
|
-
);
|
|
17
|
-
return readFileSync(url, "utf8");
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
describe("@birdcc/parser fixtures", () => {
|
|
21
|
-
for (const fileName of fixtureFiles) {
|
|
22
|
-
it(`parses fixture ${fileName}`, async () => {
|
|
23
|
-
const text = readFixture(fileName);
|
|
24
|
-
const parsed = await parseBirdConfig(text);
|
|
25
|
-
|
|
26
|
-
expect(parsed.program.declarations.length).toBeGreaterThan(0);
|
|
27
|
-
expect(
|
|
28
|
-
parsed.issues.filter((item) => item.code === "parser/syntax-error"),
|
|
29
|
-
).toHaveLength(0);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
it("extracts protocol statements from protocol_phrases.conf", async () => {
|
|
34
|
-
const text = readFixture("protocol_phrases.conf");
|
|
35
|
-
const parsed = await parseBirdConfig(text);
|
|
36
|
-
|
|
37
|
-
const protocols = parsed.program.declarations.filter(
|
|
38
|
-
(item) => item.kind === "protocol",
|
|
39
|
-
);
|
|
40
|
-
const statements = protocols.flatMap((protocol) =>
|
|
41
|
-
protocol.statements.map((item) => item.kind),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
expect(statements).toContain("local-as");
|
|
45
|
-
expect(statements).toContain("import");
|
|
46
|
-
expect(statements).toContain("export");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
isIpLiteralCandidate,
|
|
4
|
-
isStrictIpLiteral,
|
|
5
|
-
isStrictIpv4Literal,
|
|
6
|
-
isStrictIpv6Literal,
|
|
7
|
-
} from "../src/declarations/shared.js";
|
|
8
|
-
|
|
9
|
-
describe("ip literal candidate helpers", () => {
|
|
10
|
-
it("accepts strict IPv4/IPv6 literals", () => {
|
|
11
|
-
expect(isStrictIpv4Literal("192.0.2.1")).toBe(true);
|
|
12
|
-
expect(isStrictIpv6Literal("2001:db8::1")).toBe(true);
|
|
13
|
-
expect(isStrictIpLiteral("192.0.2.1")).toBe(true);
|
|
14
|
-
expect(isStrictIpLiteral("2001:db8::1")).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("rejects invalid strict literals", () => {
|
|
18
|
-
expect(isStrictIpv4Literal("203.0.113.999")).toBe(false);
|
|
19
|
-
expect(isStrictIpv6Literal("2001:db8::gg")).toBe(false);
|
|
20
|
-
expect(isStrictIpLiteral("not-an-ip")).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("keeps ip-like malformed inputs as candidates for semantic diagnostics", () => {
|
|
24
|
-
expect(isIpLiteralCandidate("203.0.113.999")).toBe(true);
|
|
25
|
-
expect(isIpLiteralCandidate("2001:db8::fffff")).toBe(true);
|
|
26
|
-
expect(isIpLiteralCandidate("2001:db8:::1")).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("filters out non-ip tokens and malformed shapes", () => {
|
|
30
|
-
expect(isIpLiteralCandidate("")).toBe(false);
|
|
31
|
-
expect(isIpLiteralCandidate(" ")).toBe(false);
|
|
32
|
-
expect(isIpLiteralCandidate("router-id")).toBe(false);
|
|
33
|
-
expect(isIpLiteralCandidate("203.0.113")).toBe(false);
|
|
34
|
-
expect(isIpLiteralCandidate("203..113.1")).toBe(false);
|
|
35
|
-
expect(isIpLiteralCandidate("::ffff:203.0.113.999")).toBe(false);
|
|
36
|
-
expect(isIpLiteralCandidate("2001:db8::gg")).toBe(false);
|
|
37
|
-
expect(isIpLiteralCandidate("2001:db8::1/64")).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
});
|