@birdcc/core 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 +11 -4
- package/.oxfmtrc.json +0 -16
- package/scripts/benchmark-node-vs-regex.js +0 -86
- package/src/cross-file.ts +0 -412
- package/src/index.ts +0 -42
- package/src/prefix.ts +0 -94
- package/src/range.ts +0 -12
- package/src/semantic-diagnostics.ts +0 -93
- package/src/snapshot.ts +0 -32
- package/src/symbol-table.ts +0 -171
- package/src/template-cycles.ts +0 -142
- package/src/type-checker.ts +0 -595
- package/src/types.ts +0 -101
- package/test/core.test.ts +0 -503
- package/test/prefix.test.ts +0 -40
- package/tsconfig.json +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@birdcc/core",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -9,17 +9,24 @@
|
|
|
9
9
|
"email": "npm-dev@birdcc.link",
|
|
10
10
|
"url": "https://github.com/bird-chinese-community/"
|
|
11
11
|
},
|
|
12
|
+
"description": "Semantic analysis core for BIRD2 configuration files.",
|
|
12
13
|
"main": "./dist/index.js",
|
|
13
|
-
"types": "./
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
16
|
-
"types": "./
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
17
18
|
"default": "./dist/index.js"
|
|
18
19
|
}
|
|
19
20
|
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"files": [
|
|
23
|
+
"dist/**",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
20
27
|
"dependencies": {
|
|
21
28
|
"fast-cidr-tools": "^0.3.4",
|
|
22
|
-
"@birdcc/parser": "0.0.1-alpha.
|
|
29
|
+
"@birdcc/parser": "0.0.1-alpha.1"
|
|
23
30
|
},
|
|
24
31
|
"devDependencies": {
|
|
25
32
|
"mitata": "^1.0.34"
|
package/.oxfmtrc.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "../../../node_modules/oxfmt/configuration_schema.json",
|
|
3
|
-
"ignorePatterns": [
|
|
4
|
-
"node_modules/*",
|
|
5
|
-
"package.json",
|
|
6
|
-
"dist/*",
|
|
7
|
-
".turbo/*",
|
|
8
|
-
"target/*",
|
|
9
|
-
"*.lock",
|
|
10
|
-
"pnpm-*.yaml",
|
|
11
|
-
"*-lock.json"
|
|
12
|
-
],
|
|
13
|
-
"printWidth": 80,
|
|
14
|
-
"tabWidth": 2,
|
|
15
|
-
"useTabs": false
|
|
16
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// Run: pnpm --filter @birdcc/core bench:ip-check
|
|
2
|
-
|
|
3
|
-
import { bench, run, summary, barplot } from "mitata";
|
|
4
|
-
import { isIP } from "node:net";
|
|
5
|
-
|
|
6
|
-
// --- regex implementations ---
|
|
7
|
-
// Validates each octet: 0-255, no leading zeros
|
|
8
|
-
const IPv4_RE =
|
|
9
|
-
/^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)){3}$/;
|
|
10
|
-
|
|
11
|
-
// Covers all RFC 4291 compressed forms (::, ::1, 1::, fe80::1, etc.)
|
|
12
|
-
// Zone ID (fe80::1%eth0) intentionally excluded — match node:net behavior = include it
|
|
13
|
-
const IPv6_RE =
|
|
14
|
-
/^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?\d)?\d)\.){3}(25[0-5]|(2[0-4]|1?\d)?\d))$/;
|
|
15
|
-
|
|
16
|
-
/** Returns 4 | 6 | 0 — same contract as node:net isIP */
|
|
17
|
-
const isIP_regex = (input) => {
|
|
18
|
-
if (IPv4_RE.test(input)) return 4;
|
|
19
|
-
if (IPv6_RE.test(input)) return 6;
|
|
20
|
-
return 0;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// --- test fixtures: mix of ipv4 / ipv6 / invalid ---
|
|
24
|
-
const samples = [
|
|
25
|
-
"127.0.0.1", // valid ipv4
|
|
26
|
-
"192.168.1.255", // valid ipv4
|
|
27
|
-
"0.0.0.0", // valid ipv4
|
|
28
|
-
"255.255.255.255", // valid ipv4
|
|
29
|
-
"::1", // valid ipv6 loopback
|
|
30
|
-
"2001:db8::1", // valid ipv6
|
|
31
|
-
"fe80::1%eth0", // ipv6 with zone id (isIP → 0, regex → 0)
|
|
32
|
-
"not-an-ip", // invalid
|
|
33
|
-
"127.000.000.001", // invalid (leading zeros)
|
|
34
|
-
"999.999.999.999", // invalid (out of range)
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const parseSampleSize = (value) => {
|
|
38
|
-
const parsed = Number.parseInt(value ?? "", 10);
|
|
39
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
40
|
-
return 100000;
|
|
41
|
-
}
|
|
42
|
-
return parsed;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const sampleSize = parseSampleSize(process.env.BENCH_SAMPLE_SIZE);
|
|
46
|
-
const samplePool = Array.from({ length: sampleSize }, (_, index) => {
|
|
47
|
-
return samples[index % samples.length];
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Sanity-check: both implementations must agree on every sample
|
|
51
|
-
for (const s of samples) {
|
|
52
|
-
const r = isIP_regex(s);
|
|
53
|
-
const n = isIP(s);
|
|
54
|
-
if (r !== n) {
|
|
55
|
-
throw new Error(`[mismatch] "${s}" regex=${r} node=${n}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --- benchmarks ---
|
|
60
|
-
summary(() => {
|
|
61
|
-
barplot(() => {
|
|
62
|
-
bench("regex · single sample (127.0.0.1)", () => isIP_regex("127.0.0.1"));
|
|
63
|
-
bench("node · single sample (127.0.0.1)", () => isIP("127.0.0.1"));
|
|
64
|
-
|
|
65
|
-
bench("regex · single sample (::1)", () => isIP_regex("::1"));
|
|
66
|
-
bench("node · single sample (::1)", () => isIP("::1"));
|
|
67
|
-
|
|
68
|
-
bench("regex · single sample (invalid)", () => isIP_regex("not-an-ip"));
|
|
69
|
-
bench("node · single sample (invalid)", () => isIP("not-an-ip"));
|
|
70
|
-
|
|
71
|
-
bench("regex · 10-sample mixed loop", () => {
|
|
72
|
-
for (const s of samples) isIP_regex(s);
|
|
73
|
-
});
|
|
74
|
-
bench("node · 10-sample mixed loop", () => {
|
|
75
|
-
for (const s of samples) isIP(s);
|
|
76
|
-
});
|
|
77
|
-
bench(`regex · ${sampleSize}-sample deterministic loop`, () => {
|
|
78
|
-
for (const s of samplePool) isIP_regex(s);
|
|
79
|
-
});
|
|
80
|
-
bench(`node · ${sampleSize}-sample deterministic loop`, () => {
|
|
81
|
-
for (const s of samplePool) isIP(s);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
await run();
|
package/src/cross-file.ts
DELETED
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, normalize, relative, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
import type { ParsedBirdDocument, SourceRange } from "@birdcc/parser";
|
|
5
|
-
import { parseBirdConfig } from "@birdcc/parser";
|
|
6
|
-
import type {
|
|
7
|
-
BirdDiagnostic,
|
|
8
|
-
CoreSnapshot,
|
|
9
|
-
CrossFileResolveOptions,
|
|
10
|
-
CrossFileResolutionResult,
|
|
11
|
-
CrossFileResolutionStats,
|
|
12
|
-
SymbolTable,
|
|
13
|
-
} from "./types.js";
|
|
14
|
-
import { buildCoreSnapshotFromParsed } from "./snapshot.js";
|
|
15
|
-
import {
|
|
16
|
-
mergeSymbolTables,
|
|
17
|
-
pushSymbolTableDiagnostics,
|
|
18
|
-
} from "./symbol-table.js";
|
|
19
|
-
import { collectCircularTemplateDiagnostics } from "./template-cycles.js";
|
|
20
|
-
|
|
21
|
-
export const DEFAULT_CROSS_FILE_MAX_DEPTH = 16;
|
|
22
|
-
export const DEFAULT_CROSS_FILE_MAX_FILES = 256;
|
|
23
|
-
|
|
24
|
-
const PARSED_DOCUMENT_CACHE_LIMIT = 512;
|
|
25
|
-
const parsedDocumentCache = new Map<
|
|
26
|
-
string,
|
|
27
|
-
{ text: string; parsed: ParsedBirdDocument }
|
|
28
|
-
>();
|
|
29
|
-
|
|
30
|
-
const DEFAULT_RANGE = {
|
|
31
|
-
line: 1,
|
|
32
|
-
column: 1,
|
|
33
|
-
endLine: 1,
|
|
34
|
-
endColumn: 1,
|
|
35
|
-
} as const;
|
|
36
|
-
|
|
37
|
-
const isFileUri = (uri: string): boolean => uri.startsWith("file://");
|
|
38
|
-
|
|
39
|
-
const toFilePath = (uri: string): string | null => {
|
|
40
|
-
if (isFileUri(uri)) {
|
|
41
|
-
return fileURLToPath(uri);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (uri.startsWith("/")) {
|
|
45
|
-
return uri;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return null;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const normalizeUriForPrefixMatch = (uri: string): string =>
|
|
52
|
-
uri.replace(/\/+$/, "");
|
|
53
|
-
|
|
54
|
-
const toDefaultWorkspaceRootUri = (entryUri: string): string => {
|
|
55
|
-
const entryPath = toFilePath(entryUri);
|
|
56
|
-
if (entryPath) {
|
|
57
|
-
return pathToFileURL(dirname(entryPath)).toString();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return normalize(dirname(entryUri));
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const isWithinWorkspaceRoot = (
|
|
64
|
-
candidateUri: string,
|
|
65
|
-
workspaceRootUri: string,
|
|
66
|
-
): boolean => {
|
|
67
|
-
const candidatePath = toFilePath(candidateUri);
|
|
68
|
-
const workspaceRootPath = toFilePath(workspaceRootUri);
|
|
69
|
-
|
|
70
|
-
if (candidatePath && workspaceRootPath) {
|
|
71
|
-
const resolvedRoot = normalize(workspaceRootPath);
|
|
72
|
-
const resolvedCandidate = normalize(candidatePath);
|
|
73
|
-
const relPath = relative(resolvedRoot, resolvedCandidate);
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
relPath.length === 0 ||
|
|
77
|
-
(!relPath.startsWith("..") && !isAbsolute(relPath))
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const normalizedRoot = normalizeUriForPrefixMatch(
|
|
82
|
-
normalize(workspaceRootUri),
|
|
83
|
-
);
|
|
84
|
-
const normalizedCandidate = normalizeUriForPrefixMatch(
|
|
85
|
-
normalize(candidateUri),
|
|
86
|
-
);
|
|
87
|
-
return (
|
|
88
|
-
normalizedCandidate === normalizedRoot ||
|
|
89
|
-
normalizedCandidate.startsWith(`${normalizedRoot}/`)
|
|
90
|
-
);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const defaultReadFileText = async (uri: string): Promise<string> => {
|
|
94
|
-
const filePath = toFilePath(uri);
|
|
95
|
-
if (!filePath) {
|
|
96
|
-
throw new Error(`Unsupported non-file URI '${uri}'`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return readFile(filePath, "utf8");
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const resolveIncludeUri = (baseUri: string, includePath: string): string => {
|
|
103
|
-
if (includePath.startsWith("file://")) {
|
|
104
|
-
return pathToFileURL(normalize(fileURLToPath(includePath))).toString();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (isFileUri(baseUri)) {
|
|
108
|
-
const basePath = fileURLToPath(baseUri);
|
|
109
|
-
const resolvedPath = resolve(dirname(basePath), includePath);
|
|
110
|
-
return pathToFileURL(resolvedPath).toString();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return normalize(resolve(dirname(baseUri), includePath));
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const includeDiagnostic = (
|
|
117
|
-
uri: string,
|
|
118
|
-
message: string,
|
|
119
|
-
range: SourceRange = DEFAULT_RANGE,
|
|
120
|
-
): BirdDiagnostic => ({
|
|
121
|
-
code: "semantic/missing-include",
|
|
122
|
-
message,
|
|
123
|
-
severity: "warning",
|
|
124
|
-
source: "core",
|
|
125
|
-
uri,
|
|
126
|
-
range: {
|
|
127
|
-
line: range.line,
|
|
128
|
-
column: range.column,
|
|
129
|
-
endLine: range.endLine,
|
|
130
|
-
endColumn: range.endColumn,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const dedupeDiagnostics = (diagnostics: BirdDiagnostic[]): BirdDiagnostic[] => {
|
|
135
|
-
const seen = new Set<string>();
|
|
136
|
-
const output: BirdDiagnostic[] = [];
|
|
137
|
-
|
|
138
|
-
for (const diagnostic of diagnostics) {
|
|
139
|
-
const key = [
|
|
140
|
-
diagnostic.code,
|
|
141
|
-
diagnostic.message,
|
|
142
|
-
diagnostic.uri ?? "",
|
|
143
|
-
diagnostic.range.line,
|
|
144
|
-
diagnostic.range.column,
|
|
145
|
-
diagnostic.range.endLine,
|
|
146
|
-
diagnostic.range.endColumn,
|
|
147
|
-
].join(":");
|
|
148
|
-
|
|
149
|
-
if (seen.has(key)) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
seen.add(key);
|
|
154
|
-
output.push(diagnostic);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return output;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const parseDocumentWithCache = async (
|
|
161
|
-
uri: string,
|
|
162
|
-
text: string,
|
|
163
|
-
stats: CrossFileResolutionStats,
|
|
164
|
-
): Promise<ParsedBirdDocument> => {
|
|
165
|
-
const cached = parsedDocumentCache.get(uri);
|
|
166
|
-
if (cached && cached.text === text) {
|
|
167
|
-
stats.parsedCacheHits += 1;
|
|
168
|
-
return cached.parsed;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
stats.parsedCacheMisses += 1;
|
|
172
|
-
const parsed = await parseBirdConfig(text);
|
|
173
|
-
|
|
174
|
-
if (parsedDocumentCache.size >= PARSED_DOCUMENT_CACHE_LIMIT) {
|
|
175
|
-
const oldestKey = parsedDocumentCache.keys().next().value;
|
|
176
|
-
if (oldestKey) {
|
|
177
|
-
parsedDocumentCache.delete(oldestKey);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
parsedDocumentCache.set(uri, { text, parsed });
|
|
182
|
-
return parsed;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
interface QueueItem {
|
|
186
|
-
uri: string;
|
|
187
|
-
depth: number;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export const resolveCrossFileReferences = async (
|
|
191
|
-
options: CrossFileResolveOptions,
|
|
192
|
-
): Promise<CrossFileResolutionResult> => {
|
|
193
|
-
const maxDepth = options.maxDepth ?? DEFAULT_CROSS_FILE_MAX_DEPTH;
|
|
194
|
-
const maxFiles = options.maxFiles ?? DEFAULT_CROSS_FILE_MAX_FILES;
|
|
195
|
-
const loadFromFileSystem = options.loadFromFileSystem ?? true;
|
|
196
|
-
const readFileText = options.readFileText ?? defaultReadFileText;
|
|
197
|
-
const workspaceRootUri =
|
|
198
|
-
options.workspaceRootUri ?? toDefaultWorkspaceRootUri(options.entryUri);
|
|
199
|
-
const allowIncludeOutsideWorkspace =
|
|
200
|
-
options.allowIncludeOutsideWorkspace ?? false;
|
|
201
|
-
|
|
202
|
-
const stats: CrossFileResolutionStats = {
|
|
203
|
-
loadedFromMemory: options.documents?.length ?? 0,
|
|
204
|
-
loadedFromFileSystem: 0,
|
|
205
|
-
skippedByDepth: 0,
|
|
206
|
-
skippedByFileLimit: 0,
|
|
207
|
-
missingIncludes: 0,
|
|
208
|
-
parsedCacheHits: 0,
|
|
209
|
-
parsedCacheMisses: 0,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const documentMap = new Map(
|
|
213
|
-
(options.documents ?? []).map((document) => [
|
|
214
|
-
document.uri,
|
|
215
|
-
{ uri: document.uri, text: document.text },
|
|
216
|
-
]),
|
|
217
|
-
);
|
|
218
|
-
const parsedDocuments = new Map<string, ParsedBirdDocument>();
|
|
219
|
-
const snapshots: Record<string, CoreSnapshot> = {};
|
|
220
|
-
const queue: QueueItem[] = [{ uri: options.entryUri, depth: 0 }];
|
|
221
|
-
const queued = new Set<string>([options.entryUri]);
|
|
222
|
-
const visited = new Set<string>();
|
|
223
|
-
const diagnostics: BirdDiagnostic[] = [];
|
|
224
|
-
|
|
225
|
-
const ensureDocument = async (uri: string): Promise<boolean> => {
|
|
226
|
-
if (documentMap.has(uri)) {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!loadFromFileSystem) {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
try {
|
|
235
|
-
const text = await readFileText(uri);
|
|
236
|
-
documentMap.set(uri, { uri, text });
|
|
237
|
-
stats.loadedFromFileSystem += 1;
|
|
238
|
-
return true;
|
|
239
|
-
} catch {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
if (!(await ensureDocument(options.entryUri))) {
|
|
245
|
-
return {
|
|
246
|
-
entryUri: options.entryUri,
|
|
247
|
-
visitedUris: [],
|
|
248
|
-
symbolTable: { definitions: [], references: [] },
|
|
249
|
-
snapshots: {},
|
|
250
|
-
documents: {},
|
|
251
|
-
diagnostics: [
|
|
252
|
-
includeDiagnostic(
|
|
253
|
-
options.entryUri,
|
|
254
|
-
`Entry file not found or not readable: '${options.entryUri}'`,
|
|
255
|
-
),
|
|
256
|
-
],
|
|
257
|
-
stats,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
while (queue.length > 0) {
|
|
262
|
-
const current = queue.shift();
|
|
263
|
-
if (!current) {
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (visited.has(current.uri)) {
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (current.depth > maxDepth) {
|
|
272
|
-
stats.skippedByDepth += 1;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (visited.size >= maxFiles) {
|
|
277
|
-
stats.skippedByFileLimit += 1;
|
|
278
|
-
diagnostics.push(
|
|
279
|
-
includeDiagnostic(
|
|
280
|
-
current.uri,
|
|
281
|
-
`Cross-file analysis stopped after reaching max files limit (${maxFiles})`,
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
visited.add(current.uri);
|
|
288
|
-
|
|
289
|
-
const document = documentMap.get(current.uri);
|
|
290
|
-
if (!document) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const parsed = await parseDocumentWithCache(
|
|
295
|
-
current.uri,
|
|
296
|
-
document.text,
|
|
297
|
-
stats,
|
|
298
|
-
);
|
|
299
|
-
parsedDocuments.set(current.uri, parsed);
|
|
300
|
-
snapshots[current.uri] = buildCoreSnapshotFromParsed(parsed, {
|
|
301
|
-
uri: current.uri,
|
|
302
|
-
typeCheck: options.typeCheck,
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
for (const declaration of parsed.program.declarations) {
|
|
306
|
-
if (declaration.kind !== "include" || declaration.path.length === 0) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const includeUri = resolveIncludeUri(current.uri, declaration.path);
|
|
311
|
-
const includeRange = declaration.pathRange;
|
|
312
|
-
|
|
313
|
-
if (
|
|
314
|
-
!allowIncludeOutsideWorkspace &&
|
|
315
|
-
!isWithinWorkspaceRoot(includeUri, workspaceRootUri)
|
|
316
|
-
) {
|
|
317
|
-
diagnostics.push(
|
|
318
|
-
includeDiagnostic(
|
|
319
|
-
current.uri,
|
|
320
|
-
`Include skipped outside workspace root '${workspaceRootUri}': '${declaration.path}'`,
|
|
321
|
-
includeRange,
|
|
322
|
-
),
|
|
323
|
-
);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (visited.has(includeUri) || queued.has(includeUri)) {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (current.depth + 1 > maxDepth) {
|
|
332
|
-
stats.skippedByDepth += 1;
|
|
333
|
-
diagnostics.push(
|
|
334
|
-
includeDiagnostic(
|
|
335
|
-
current.uri,
|
|
336
|
-
`Include skipped due to max depth (${maxDepth}): '${declaration.path}'`,
|
|
337
|
-
includeRange,
|
|
338
|
-
),
|
|
339
|
-
);
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (visited.size + queue.length >= maxFiles) {
|
|
344
|
-
stats.skippedByFileLimit += 1;
|
|
345
|
-
diagnostics.push(
|
|
346
|
-
includeDiagnostic(
|
|
347
|
-
current.uri,
|
|
348
|
-
`Include skipped due to max files limit (${maxFiles}): '${declaration.path}'`,
|
|
349
|
-
includeRange,
|
|
350
|
-
),
|
|
351
|
-
);
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const loaded = await ensureDocument(includeUri);
|
|
356
|
-
if (!loaded) {
|
|
357
|
-
stats.missingIncludes += 1;
|
|
358
|
-
diagnostics.push(
|
|
359
|
-
includeDiagnostic(
|
|
360
|
-
current.uri,
|
|
361
|
-
`Included file not found in workspace: '${declaration.path}'`,
|
|
362
|
-
includeRange,
|
|
363
|
-
),
|
|
364
|
-
);
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
queue.push({ uri: includeUri, depth: current.depth + 1 });
|
|
369
|
-
queued.add(includeUri);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const mergedSymbolTable: SymbolTable = mergeSymbolTables(
|
|
374
|
-
Object.values(snapshots).map((snapshot) => snapshot.symbolTable),
|
|
375
|
-
);
|
|
376
|
-
pushSymbolTableDiagnostics(mergedSymbolTable, diagnostics);
|
|
377
|
-
|
|
378
|
-
for (const [uri, snapshot] of Object.entries(snapshots)) {
|
|
379
|
-
for (const diagnostic of snapshot.diagnostics) {
|
|
380
|
-
if (
|
|
381
|
-
diagnostic.code === "semantic/duplicate-definition" ||
|
|
382
|
-
diagnostic.code === "semantic/undefined-reference" ||
|
|
383
|
-
diagnostic.code === "semantic/circular-template"
|
|
384
|
-
) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
diagnostics.push({
|
|
389
|
-
...diagnostic,
|
|
390
|
-
uri,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
diagnostics.push(
|
|
396
|
-
...collectCircularTemplateDiagnostics(
|
|
397
|
-
[...parsedDocuments.entries()].map(([uri, parsed]) => ({ uri, parsed })),
|
|
398
|
-
),
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
entryUri: options.entryUri,
|
|
403
|
-
visitedUris: [...visited],
|
|
404
|
-
symbolTable: mergedSymbolTable,
|
|
405
|
-
snapshots,
|
|
406
|
-
documents: Object.fromEntries(
|
|
407
|
-
[...documentMap.entries()].map(([uri, document]) => [uri, document.text]),
|
|
408
|
-
),
|
|
409
|
-
diagnostics: dedupeDiagnostics(diagnostics),
|
|
410
|
-
stats,
|
|
411
|
-
};
|
|
412
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { parseBirdConfig } from "@birdcc/parser";
|
|
2
|
-
import { resolveCrossFileReferences } from "./cross-file.js";
|
|
3
|
-
import { buildCoreSnapshotFromParsed } from "./snapshot.js";
|
|
4
|
-
|
|
5
|
-
export type {
|
|
6
|
-
BirdDiagnostic,
|
|
7
|
-
BirdDiagnosticSeverity,
|
|
8
|
-
BirdRange,
|
|
9
|
-
BirdSymbolKind,
|
|
10
|
-
CoreSnapshot,
|
|
11
|
-
CrossFileDocumentInput,
|
|
12
|
-
CrossFileResolveOptions,
|
|
13
|
-
CrossFileResolutionResult,
|
|
14
|
-
CrossFileResolutionStats,
|
|
15
|
-
SymbolDefinition,
|
|
16
|
-
SymbolReference,
|
|
17
|
-
SymbolTable,
|
|
18
|
-
TypeCheckOptions,
|
|
19
|
-
TypeValue,
|
|
20
|
-
} from "./types.js";
|
|
21
|
-
|
|
22
|
-
export { checkTypes } from "./type-checker.js";
|
|
23
|
-
export {
|
|
24
|
-
DEFAULT_DOCUMENT_URI,
|
|
25
|
-
buildSymbolTableFromParsed,
|
|
26
|
-
mergeSymbolTables,
|
|
27
|
-
pushSymbolTableDiagnostics,
|
|
28
|
-
} from "./symbol-table.js";
|
|
29
|
-
export { buildCoreSnapshotFromParsed } from "./snapshot.js";
|
|
30
|
-
export {
|
|
31
|
-
DEFAULT_CROSS_FILE_MAX_DEPTH,
|
|
32
|
-
DEFAULT_CROSS_FILE_MAX_FILES,
|
|
33
|
-
resolveCrossFileReferences,
|
|
34
|
-
} from "./cross-file.js";
|
|
35
|
-
|
|
36
|
-
/** Parses and builds semantic snapshot in one async call. */
|
|
37
|
-
export const buildCoreSnapshot = async (text: string) => {
|
|
38
|
-
const parsed = await parseBirdConfig(text);
|
|
39
|
-
return buildCoreSnapshotFromParsed(parsed);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type ResolveCrossFileReferences = typeof resolveCrossFileReferences;
|
package/src/prefix.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { isIP } from "node:net";
|
|
2
|
-
import { parse as parseCidr } from "fast-cidr-tools";
|
|
3
|
-
|
|
4
|
-
const parsePrefixRange = (
|
|
5
|
-
suffix: string,
|
|
6
|
-
): { min: number; max: number } | null => {
|
|
7
|
-
if (!suffix.startsWith("{") || !suffix.endsWith("}")) {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const inner = suffix.slice(1, -1);
|
|
12
|
-
const commaIndex = inner.indexOf(",");
|
|
13
|
-
if (commaIndex <= 0 || commaIndex >= inner.length - 1) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const min = Number(inner.slice(0, commaIndex));
|
|
18
|
-
const max = Number(inner.slice(commaIndex + 1));
|
|
19
|
-
|
|
20
|
-
if (!Number.isInteger(min) || !Number.isInteger(max)) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return { min, max };
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const isValidPrefixLiteral = (literal: string): boolean => {
|
|
28
|
-
let value = literal.trim();
|
|
29
|
-
if (value.length === 0) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let range: { min: number; max: number } | null = null;
|
|
34
|
-
|
|
35
|
-
if (value.endsWith("+")) {
|
|
36
|
-
value = value.slice(0, -1);
|
|
37
|
-
} else if (value.endsWith("-")) {
|
|
38
|
-
value = value.slice(0, -1);
|
|
39
|
-
} else if (value.endsWith("}")) {
|
|
40
|
-
const braceStart = value.lastIndexOf("{");
|
|
41
|
-
if (braceStart === -1) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
range = parsePrefixRange(value.slice(braceStart));
|
|
46
|
-
if (!range) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
value = value.slice(0, braceStart);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const slashIndex = value.lastIndexOf("/");
|
|
54
|
-
if (slashIndex <= 0 || slashIndex >= value.length - 1) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const ipPart = value.slice(0, slashIndex);
|
|
59
|
-
const prefixPart = value.slice(slashIndex + 1);
|
|
60
|
-
const prefix = Number(prefixPart);
|
|
61
|
-
|
|
62
|
-
if (!Number.isInteger(prefix)) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const version = isIP(ipPart);
|
|
67
|
-
if (version === 0) {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const maxBits = version === 4 ? 32 : 128;
|
|
72
|
-
if (prefix < 0 || prefix > maxBits) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (range) {
|
|
77
|
-
if (
|
|
78
|
-
range.min < prefix ||
|
|
79
|
-
range.min > maxBits ||
|
|
80
|
-
range.max < range.min ||
|
|
81
|
-
range.max > maxBits
|
|
82
|
-
) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
void parseCidr(value);
|
|
89
|
-
} catch {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return true;
|
|
94
|
-
};
|
package/src/range.ts
DELETED