@birdcc/linter 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 +210 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/bgp.d.ts +5 -0
- package/dist/rules/bgp.d.ts.map +1 -0
- package/dist/rules/bgp.js +131 -0
- package/dist/rules/bgp.js.map +1 -0
- package/dist/rules/catalog.d.ts +14 -0
- package/dist/rules/catalog.d.ts.map +1 -0
- package/dist/rules/catalog.js +61 -0
- package/dist/rules/catalog.js.map +1 -0
- package/dist/rules/cfg.d.ts +5 -0
- package/dist/rules/cfg.d.ts.map +1 -0
- package/dist/rules/cfg.js +264 -0
- package/dist/rules/cfg.js.map +1 -0
- package/dist/rules/net.d.ts +5 -0
- package/dist/rules/net.d.ts.map +1 -0
- package/dist/rules/net.js +140 -0
- package/dist/rules/net.js.map +1 -0
- package/dist/rules/normalize.d.ts +6 -0
- package/dist/rules/normalize.d.ts.map +1 -0
- package/dist/rules/normalize.js +65 -0
- package/dist/rules/normalize.js.map +1 -0
- package/dist/rules/ospf.d.ts +5 -0
- package/dist/rules/ospf.d.ts.map +1 -0
- package/dist/rules/ospf.js +136 -0
- package/dist/rules/ospf.js.map +1 -0
- package/dist/rules/shared.d.ts +46 -0
- package/dist/rules/shared.d.ts.map +1 -0
- package/dist/rules/shared.js +184 -0
- package/dist/rules/shared.js.map +1 -0
- package/dist/rules/sym.d.ts +5 -0
- package/dist/rules/sym.d.ts.map +1 -0
- package/dist/rules/sym.js +188 -0
- package/dist/rules/sym.js.map +1 -0
- package/dist/rules/type.d.ts +5 -0
- package/dist/rules/type.d.ts.map +1 -0
- package/dist/rules/type.js +130 -0
- package/dist/rules/type.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +155 -0
- package/src/rules/bgp.ts +239 -0
- package/src/rules/catalog.ts +80 -0
- package/src/rules/cfg.ts +562 -0
- package/src/rules/net.ts +262 -0
- package/src/rules/normalize.ts +90 -0
- package/src/rules/ospf.ts +221 -0
- package/src/rules/shared.ts +354 -0
- package/src/rules/sym.ts +342 -0
- package/src/rules/type.ts +210 -0
- package/test/linter.bgp-ospf.test.ts +129 -0
- package/test/linter.migration.test.ts +66 -0
- package/test/linter.net-type.test.ts +132 -0
- package/test/linter.sym-cfg.test.ts +224 -0
- package/test/linter.test.ts +21 -0
- package/tsconfig.json +8 -0
package/src/rules/net.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import type { BirdDiagnostic } from "@birdcc/core";
|
|
3
|
+
import type { FilterBodyStatement, SourceRange } from "@birdcc/parser";
|
|
4
|
+
import {
|
|
5
|
+
createRuleDiagnostic,
|
|
6
|
+
filterAndFunctionDeclarations,
|
|
7
|
+
protocolDeclarations,
|
|
8
|
+
pushUniqueDiagnostic,
|
|
9
|
+
type BirdRule,
|
|
10
|
+
} from "./shared.js";
|
|
11
|
+
|
|
12
|
+
interface PrefixParts {
|
|
13
|
+
address: string;
|
|
14
|
+
length: number | null;
|
|
15
|
+
raw: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PREFIX_PATTERN_SOURCE = "([0-9A-Za-z:.]+\\/[^\\s,;{}[\\]]+)";
|
|
19
|
+
|
|
20
|
+
const extractPrefixes = (text: string): PrefixParts[] => {
|
|
21
|
+
const prefixes: PrefixParts[] = [];
|
|
22
|
+
const prefixPattern = new RegExp(PREFIX_PATTERN_SOURCE, "g");
|
|
23
|
+
let matched = prefixPattern.exec(text);
|
|
24
|
+
|
|
25
|
+
while (matched) {
|
|
26
|
+
const raw = matched[1] ?? "";
|
|
27
|
+
const [address, lengthText] = raw.split("/");
|
|
28
|
+
if (!address || !lengthText) {
|
|
29
|
+
matched = prefixPattern.exec(text);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalizedLength = lengthText.trim();
|
|
34
|
+
const length = /^\d+$/.test(normalizedLength)
|
|
35
|
+
? Number.parseInt(normalizedLength, 10)
|
|
36
|
+
: null;
|
|
37
|
+
prefixes.push({ address: address.trim(), length, raw });
|
|
38
|
+
|
|
39
|
+
matched = prefixPattern.exec(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return prefixes;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const statementText = (statement: FilterBodyStatement): string => {
|
|
46
|
+
if (statement.kind === "expression") {
|
|
47
|
+
return statement.expressionText;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (statement.kind === "other") {
|
|
51
|
+
return statement.text;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (statement.kind === "if") {
|
|
55
|
+
return statement.conditionText ?? "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (statement.kind === "return") {
|
|
59
|
+
return statement.valueText ?? "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (statement.kind === "case") {
|
|
63
|
+
return statement.subjectText ?? "";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return "";
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const analyzePrefix = (
|
|
70
|
+
diagnostics: BirdDiagnostic[],
|
|
71
|
+
seen: Set<string>,
|
|
72
|
+
prefix: PrefixParts,
|
|
73
|
+
range: SourceRange,
|
|
74
|
+
): void => {
|
|
75
|
+
const family = isIP(prefix.address);
|
|
76
|
+
|
|
77
|
+
if (prefix.length === null || prefix.length < 0 || prefix.length > 128) {
|
|
78
|
+
pushUniqueDiagnostic(
|
|
79
|
+
diagnostics,
|
|
80
|
+
seen,
|
|
81
|
+
createRuleDiagnostic(
|
|
82
|
+
"net/invalid-prefix-length",
|
|
83
|
+
`Invalid prefix length in '${prefix.raw}'`,
|
|
84
|
+
range,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (prefix.address.includes(".")) {
|
|
91
|
+
if (family !== 4) {
|
|
92
|
+
pushUniqueDiagnostic(
|
|
93
|
+
diagnostics,
|
|
94
|
+
seen,
|
|
95
|
+
createRuleDiagnostic(
|
|
96
|
+
"net/invalid-ipv4-prefix",
|
|
97
|
+
`Invalid IPv4 prefix '${prefix.raw}'`,
|
|
98
|
+
range,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (prefix.length > 32) {
|
|
105
|
+
pushUniqueDiagnostic(
|
|
106
|
+
diagnostics,
|
|
107
|
+
seen,
|
|
108
|
+
createRuleDiagnostic(
|
|
109
|
+
"net/max-prefix-length",
|
|
110
|
+
`Invalid max prefix length ${prefix.length} for IPv4 prefix '${prefix.raw}'`,
|
|
111
|
+
range,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (prefix.address.includes(":")) {
|
|
119
|
+
if (family !== 6) {
|
|
120
|
+
pushUniqueDiagnostic(
|
|
121
|
+
diagnostics,
|
|
122
|
+
seen,
|
|
123
|
+
createRuleDiagnostic(
|
|
124
|
+
"net/invalid-ipv6-prefix",
|
|
125
|
+
`Invalid IPv6 prefix '${prefix.raw}'`,
|
|
126
|
+
range,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (prefix.length > 128) {
|
|
133
|
+
pushUniqueDiagnostic(
|
|
134
|
+
diagnostics,
|
|
135
|
+
seen,
|
|
136
|
+
createRuleDiagnostic(
|
|
137
|
+
"net/max-prefix-length",
|
|
138
|
+
`Invalid max prefix length ${prefix.length} for IPv6 prefix '${prefix.raw}'`,
|
|
139
|
+
range,
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pushUniqueDiagnostic(
|
|
147
|
+
diagnostics,
|
|
148
|
+
seen,
|
|
149
|
+
createRuleDiagnostic(
|
|
150
|
+
"net/invalid-ipv4-prefix",
|
|
151
|
+
`Invalid IP prefix '${prefix.raw}'`,
|
|
152
|
+
range,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const netPrefixRules: BirdRule = ({ parsed }) => {
|
|
158
|
+
const diagnostics: BirdDiagnostic[] = [];
|
|
159
|
+
const seen = new Set<string>();
|
|
160
|
+
|
|
161
|
+
for (const declaration of filterAndFunctionDeclarations(parsed)) {
|
|
162
|
+
for (const statement of declaration.statements) {
|
|
163
|
+
const text = statementText(statement);
|
|
164
|
+
if (!text) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const prefix of extractPrefixes(text)) {
|
|
169
|
+
analyzePrefix(diagnostics, seen, prefix, statement);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const match of declaration.matches) {
|
|
174
|
+
for (const prefix of extractPrefixes(match.right)) {
|
|
175
|
+
analyzePrefix(diagnostics, seen, prefix, match);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const literal of declaration.literals) {
|
|
180
|
+
if (literal.kind !== "prefix") {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const prefix of extractPrefixes(literal.value)) {
|
|
185
|
+
analyzePrefix(diagnostics, seen, prefix, literal);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return diagnostics;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const netMaxPrefixRule: BirdRule = ({ parsed }) => {
|
|
194
|
+
const diagnostics: BirdDiagnostic[] = [];
|
|
195
|
+
const seen = new Set<string>();
|
|
196
|
+
|
|
197
|
+
for (const declaration of protocolDeclarations(parsed)) {
|
|
198
|
+
for (const statement of declaration.statements) {
|
|
199
|
+
if (statement.kind !== "channel") {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
statement.channelType !== "ipv4" &&
|
|
205
|
+
statement.channelType !== "ipv6"
|
|
206
|
+
) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const channelText = statement.entries
|
|
211
|
+
.filter((entry) => entry.kind === "other")
|
|
212
|
+
.map((entry) => entry.text)
|
|
213
|
+
.join(" ");
|
|
214
|
+
const matched = channelText.match(/\bmax\s+prefix\s+([^;\s]+)/i);
|
|
215
|
+
if (!matched) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const token = (matched[1] ?? "").trim();
|
|
220
|
+
if (!/^\d+$/.test(token)) {
|
|
221
|
+
pushUniqueDiagnostic(
|
|
222
|
+
diagnostics,
|
|
223
|
+
seen,
|
|
224
|
+
createRuleDiagnostic(
|
|
225
|
+
"net/invalid-prefix-length",
|
|
226
|
+
`Invalid prefix length in 'max prefix ${token}'`,
|
|
227
|
+
statement,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const length = Number.parseInt(token, 10);
|
|
234
|
+
if (Number.isNaN(length)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const maxAllowed = statement.channelType === "ipv4" ? 32 : 128;
|
|
239
|
+
if (length > maxAllowed) {
|
|
240
|
+
pushUniqueDiagnostic(
|
|
241
|
+
diagnostics,
|
|
242
|
+
seen,
|
|
243
|
+
createRuleDiagnostic(
|
|
244
|
+
"net/max-prefix-length",
|
|
245
|
+
`Invalid max prefix length ${length} for ${statement.channelType}`,
|
|
246
|
+
statement,
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return diagnostics;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const netRules: BirdRule[] = [netPrefixRules, netMaxPrefixRule];
|
|
257
|
+
|
|
258
|
+
export const collectNetRuleDiagnostics = (
|
|
259
|
+
context: Parameters<BirdRule>[0],
|
|
260
|
+
): BirdDiagnostic[] => {
|
|
261
|
+
return netRules.flatMap((rule) => rule(context));
|
|
262
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { BirdDiagnostic } from "@birdcc/core";
|
|
2
|
+
import type { ParsedBirdDocument } from "@birdcc/parser";
|
|
3
|
+
import { isRuleCode, type RuleCode, RULE_SEVERITY } from "./catalog.js";
|
|
4
|
+
|
|
5
|
+
const CORE_CODE_MAP: Record<string, RuleCode> = {
|
|
6
|
+
"semantic/duplicate-definition": "sym/duplicate",
|
|
7
|
+
"semantic/undefined-reference": "sym/undefined",
|
|
8
|
+
"semantic/circular-template": "cfg/circular-template",
|
|
9
|
+
"type/mismatch": "type/mismatch",
|
|
10
|
+
"type/undefined-variable": "sym/variable-scope",
|
|
11
|
+
"type/unknown-expression": "cfg/incompatible-type",
|
|
12
|
+
"semantic/invalid-router-id": "cfg/incompatible-type",
|
|
13
|
+
"semantic/invalid-neighbor-address": "cfg/incompatible-type",
|
|
14
|
+
"semantic/invalid-cidr": "net/invalid-prefix-length",
|
|
15
|
+
"semantic/missing-include": "sym/undefined",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PARSER_TO_CFG_SYNTAX = new Set([
|
|
19
|
+
"syntax/missing-semicolon",
|
|
20
|
+
"syntax/unbalanced-brace",
|
|
21
|
+
"parser/syntax-error",
|
|
22
|
+
"parser/missing-symbol",
|
|
23
|
+
"parser/runtime-error",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const normalizeCode = (code: string): RuleCode => {
|
|
27
|
+
if (isRuleCode(code)) {
|
|
28
|
+
return code;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (code in CORE_CODE_MAP) {
|
|
32
|
+
return CORE_CODE_MAP[code] as RuleCode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (PARSER_TO_CFG_SYNTAX.has(code)) {
|
|
36
|
+
return "cfg/syntax-error";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return "cfg/incompatible-type";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const normalizeMessage = (
|
|
43
|
+
code: string,
|
|
44
|
+
mappedCode: RuleCode,
|
|
45
|
+
message: string,
|
|
46
|
+
): string => {
|
|
47
|
+
if (code === mappedCode) {
|
|
48
|
+
return message;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return `[${code}] ${message}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const toNormalizedDiagnostic = (
|
|
55
|
+
diagnostic: BirdDiagnostic,
|
|
56
|
+
fallbackUri?: string,
|
|
57
|
+
): BirdDiagnostic => {
|
|
58
|
+
const mappedCode = normalizeCode(diagnostic.code);
|
|
59
|
+
return {
|
|
60
|
+
...diagnostic,
|
|
61
|
+
code: mappedCode,
|
|
62
|
+
severity: RULE_SEVERITY[mappedCode],
|
|
63
|
+
message: normalizeMessage(diagnostic.code, mappedCode, diagnostic.message),
|
|
64
|
+
uri: diagnostic.uri ?? fallbackUri,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const normalizeBaseDiagnostics = (
|
|
69
|
+
parsed: ParsedBirdDocument,
|
|
70
|
+
coreDiagnostics: BirdDiagnostic[],
|
|
71
|
+
options: { uri?: string } = {},
|
|
72
|
+
): BirdDiagnostic[] => {
|
|
73
|
+
const parserDiagnostics: BirdDiagnostic[] = parsed.issues.map((issue) => ({
|
|
74
|
+
code: issue.code,
|
|
75
|
+
message: issue.message,
|
|
76
|
+
severity: "error",
|
|
77
|
+
source: "parser",
|
|
78
|
+
uri: options.uri,
|
|
79
|
+
range: {
|
|
80
|
+
line: issue.line,
|
|
81
|
+
column: issue.column,
|
|
82
|
+
endLine: issue.endLine,
|
|
83
|
+
endColumn: issue.endColumn,
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
return [...parserDiagnostics, ...coreDiagnostics].map((diagnostic) =>
|
|
88
|
+
toNormalizedDiagnostic(diagnostic, options.uri),
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { BirdDiagnostic } from "@birdcc/core";
|
|
2
|
+
import type { SourceRange } from "@birdcc/parser";
|
|
3
|
+
import {
|
|
4
|
+
createProtocolDiagnostic,
|
|
5
|
+
createRuleDiagnostic,
|
|
6
|
+
isProtocolType,
|
|
7
|
+
protocolDeclarations,
|
|
8
|
+
protocolOtherTextEntries,
|
|
9
|
+
type BirdRule,
|
|
10
|
+
} from "./shared.js";
|
|
11
|
+
|
|
12
|
+
interface OspfAreaSegment {
|
|
13
|
+
areaId: string;
|
|
14
|
+
text: string;
|
|
15
|
+
range: SourceRange;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BACKBONE_AREA_IDS = new Set(["0", "0.0.0.0"]);
|
|
19
|
+
|
|
20
|
+
const normalizeAreaId = (value: string): string => value.trim().toLowerCase();
|
|
21
|
+
|
|
22
|
+
const isBackboneArea = (value: string): boolean =>
|
|
23
|
+
BACKBONE_AREA_IDS.has(normalizeAreaId(value));
|
|
24
|
+
|
|
25
|
+
const parseAreaSegments = (
|
|
26
|
+
text: string,
|
|
27
|
+
range: SourceRange,
|
|
28
|
+
): OspfAreaSegment[] => {
|
|
29
|
+
const segments: OspfAreaSegment[] = [];
|
|
30
|
+
const consumedRanges: Array<{ start: number; end: number }> = [];
|
|
31
|
+
const blockPattern = /\barea\s+([^\s{;]+)([^{};]*)\{/gi;
|
|
32
|
+
let matched = blockPattern.exec(text);
|
|
33
|
+
|
|
34
|
+
while (matched) {
|
|
35
|
+
const matchedText = matched[0] ?? "";
|
|
36
|
+
const areaId = normalizeAreaId(matched[1] ?? "");
|
|
37
|
+
const header = (matched[2] ?? "").trim();
|
|
38
|
+
const openBraceIndex = (matched.index ?? 0) + matchedText.length - 1;
|
|
39
|
+
|
|
40
|
+
let cursor = openBraceIndex + 1;
|
|
41
|
+
let depth = 1;
|
|
42
|
+
while (cursor < text.length && depth > 0) {
|
|
43
|
+
if (text[cursor] === "{") {
|
|
44
|
+
depth += 1;
|
|
45
|
+
} else if (text[cursor] === "}") {
|
|
46
|
+
depth -= 1;
|
|
47
|
+
}
|
|
48
|
+
cursor += 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const closeBraceIndex = depth === 0 ? cursor - 1 : text.length - 1;
|
|
52
|
+
const body = text.slice(openBraceIndex + 1, closeBraceIndex);
|
|
53
|
+
const scopeText = `${header} ${body}`.trim();
|
|
54
|
+
if (areaId.length > 0) {
|
|
55
|
+
segments.push({ areaId, text: scopeText, range });
|
|
56
|
+
consumedRanges.push({
|
|
57
|
+
start: matched.index ?? 0,
|
|
58
|
+
end: closeBraceIndex + 1,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
blockPattern.lastIndex = Math.max(
|
|
63
|
+
closeBraceIndex + 1,
|
|
64
|
+
blockPattern.lastIndex,
|
|
65
|
+
);
|
|
66
|
+
matched = blockPattern.exec(text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const inlinePattern = /\barea\s+([^\s{;]+)\s+([^{};\n]+)\s*;?/gi;
|
|
70
|
+
let inline = inlinePattern.exec(text);
|
|
71
|
+
while (inline) {
|
|
72
|
+
const start = inline.index ?? 0;
|
|
73
|
+
const insideBlock = consumedRanges.some(
|
|
74
|
+
(item) => start >= item.start && start < item.end,
|
|
75
|
+
);
|
|
76
|
+
if (!insideBlock) {
|
|
77
|
+
const areaId = normalizeAreaId(inline[1] ?? "");
|
|
78
|
+
const inlineText = (inline[2] ?? "").trim();
|
|
79
|
+
if (areaId.length > 0) {
|
|
80
|
+
segments.push({ areaId, text: inlineText, range });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
inline = inlinePattern.exec(text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return segments;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const collectAreas = (
|
|
91
|
+
entries: Array<{ text: string; range: SourceRange }>,
|
|
92
|
+
): OspfAreaSegment[] => {
|
|
93
|
+
const areas: OspfAreaSegment[] = [];
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
areas.push(...parseAreaSegments(entry.text, entry.range));
|
|
96
|
+
}
|
|
97
|
+
return areas;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const ospfMissingAreaRule: BirdRule = ({ parsed }) => {
|
|
101
|
+
const diagnostics: BirdDiagnostic[] = [];
|
|
102
|
+
|
|
103
|
+
for (const declaration of protocolDeclarations(parsed)) {
|
|
104
|
+
if (!isProtocolType(declaration, "ospf")) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const areas = collectAreas(protocolOtherTextEntries(declaration));
|
|
109
|
+
if (areas.length > 0) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
diagnostics.push(
|
|
114
|
+
createProtocolDiagnostic(
|
|
115
|
+
"ospf/missing-area",
|
|
116
|
+
`OSPF protocol '${declaration.name}' has no configured areas`,
|
|
117
|
+
declaration,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return diagnostics;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const ospfBackboneStubRule: BirdRule = ({ parsed }) => {
|
|
126
|
+
const diagnostics: BirdDiagnostic[] = [];
|
|
127
|
+
|
|
128
|
+
for (const declaration of protocolDeclarations(parsed)) {
|
|
129
|
+
if (!isProtocolType(declaration, "ospf")) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const areas = collectAreas(protocolOtherTextEntries(declaration));
|
|
134
|
+
for (const area of areas) {
|
|
135
|
+
if (!isBackboneArea(area.areaId) || !/\bstub\b/i.test(area.text)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
diagnostics.push(
|
|
140
|
+
createRuleDiagnostic(
|
|
141
|
+
"ospf/backbone-stub",
|
|
142
|
+
`OSPF protocol '${declaration.name}' configures backbone area as stub`,
|
|
143
|
+
area.range,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return diagnostics;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const ospfVlinkInBackboneRule: BirdRule = ({ parsed }) => {
|
|
153
|
+
const diagnostics: BirdDiagnostic[] = [];
|
|
154
|
+
|
|
155
|
+
for (const declaration of protocolDeclarations(parsed)) {
|
|
156
|
+
if (!isProtocolType(declaration, "ospf")) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const areas = collectAreas(protocolOtherTextEntries(declaration));
|
|
161
|
+
for (const area of areas) {
|
|
162
|
+
if (!isBackboneArea(area.areaId) || !/\bvlink\b/i.test(area.text)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
diagnostics.push(
|
|
167
|
+
createRuleDiagnostic(
|
|
168
|
+
"ospf/vlink-in-backbone",
|
|
169
|
+
`OSPF protocol '${declaration.name}' cannot configure vlink in backbone area`,
|
|
170
|
+
area.range,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return diagnostics;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const ospfAsbrStubAreaRule: BirdRule = ({ parsed }) => {
|
|
180
|
+
const diagnostics: BirdDiagnostic[] = [];
|
|
181
|
+
|
|
182
|
+
for (const declaration of protocolDeclarations(parsed)) {
|
|
183
|
+
if (!isProtocolType(declaration, "ospf")) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const areas = collectAreas(protocolOtherTextEntries(declaration));
|
|
188
|
+
for (const area of areas) {
|
|
189
|
+
if (isBackboneArea(area.areaId)) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!/\bstub\b/i.test(area.text) || !/\basbr\b/i.test(area.text)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
diagnostics.push(
|
|
198
|
+
createRuleDiagnostic(
|
|
199
|
+
"ospf/asbr-stub-area",
|
|
200
|
+
`OSPF protocol '${declaration.name}' declares ASBR inside stub area ${area.areaId}`,
|
|
201
|
+
area.range,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return diagnostics;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const ospfRules: BirdRule[] = [
|
|
211
|
+
ospfMissingAreaRule,
|
|
212
|
+
ospfBackboneStubRule,
|
|
213
|
+
ospfVlinkInBackboneRule,
|
|
214
|
+
ospfAsbrStubAreaRule,
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
export const collectOspfRuleDiagnostics = (
|
|
218
|
+
context: Parameters<BirdRule>[0],
|
|
219
|
+
): BirdDiagnostic[] => {
|
|
220
|
+
return ospfRules.flatMap((rule) => rule(context));
|
|
221
|
+
};
|