madr-lint 0.1.0 → 0.2.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/CHANGELOG.md +31 -0
- package/README.md +19 -2
- package/dist/cli.js +543 -69
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +97 -6
- package/dist/index.js +306 -34
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -6,14 +6,152 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import { readFileSync as
|
|
9
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
10
10
|
import { relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
11
11
|
import { defineCommand, runMain } from "citty";
|
|
12
12
|
|
|
13
|
-
// src/core/
|
|
14
|
-
import { createHash } from "crypto";
|
|
13
|
+
// src/core/baseline.ts
|
|
15
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
16
15
|
import { dirname, join } from "path";
|
|
16
|
+
|
|
17
|
+
// src/core/types.ts
|
|
18
|
+
var INTERNAL_ERROR_RULE_NAME = "core/internal-error";
|
|
19
|
+
function isProjectRule(rule9) {
|
|
20
|
+
return "check" in rule9;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/core/baseline.ts
|
|
24
|
+
var BASELINE_VERSION = 1;
|
|
25
|
+
var BASELINE_DISPLAY_PATH = ".madr-lint/baseline.json";
|
|
26
|
+
function baselinePath(cwd) {
|
|
27
|
+
return join(cwd, ".madr-lint", "baseline.json");
|
|
28
|
+
}
|
|
29
|
+
function nullProtoMap() {
|
|
30
|
+
return /* @__PURE__ */ Object.create(null);
|
|
31
|
+
}
|
|
32
|
+
function buildBaseline(diagnostics) {
|
|
33
|
+
const entries = nullProtoMap();
|
|
34
|
+
for (const d of diagnostics) {
|
|
35
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) continue;
|
|
36
|
+
const byRule = entries[d.path] ??= nullProtoMap();
|
|
37
|
+
const byMessage = byRule[d.ruleName] ??= nullProtoMap();
|
|
38
|
+
byMessage[d.messageId] = (byMessage[d.messageId] ?? 0) + 1;
|
|
39
|
+
}
|
|
40
|
+
return { version: BASELINE_VERSION, entries };
|
|
41
|
+
}
|
|
42
|
+
function serializeBaseline(baseline) {
|
|
43
|
+
const sortedEntries = nullProtoMap();
|
|
44
|
+
for (const path of Object.keys(baseline.entries).toSorted()) {
|
|
45
|
+
const byRule = baseline.entries[path] ?? {};
|
|
46
|
+
const sortedByRule = nullProtoMap();
|
|
47
|
+
for (const rule9 of Object.keys(byRule).toSorted()) {
|
|
48
|
+
const byMessage = byRule[rule9] ?? {};
|
|
49
|
+
const sortedByMessage = nullProtoMap();
|
|
50
|
+
for (const messageId of Object.keys(byMessage).toSorted()) {
|
|
51
|
+
sortedByMessage[messageId] = byMessage[messageId] ?? 0;
|
|
52
|
+
}
|
|
53
|
+
sortedByRule[rule9] = sortedByMessage;
|
|
54
|
+
}
|
|
55
|
+
sortedEntries[path] = sortedByRule;
|
|
56
|
+
}
|
|
57
|
+
const ordered = { version: baseline.version, entries: sortedEntries };
|
|
58
|
+
return JSON.stringify(ordered, null, 2) + "\n";
|
|
59
|
+
}
|
|
60
|
+
function loadBaseline(path) {
|
|
61
|
+
if (!existsSync(path)) return null;
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
64
|
+
if (typeof data.entries !== "object" || data.entries === null) return null;
|
|
65
|
+
return {
|
|
66
|
+
version: typeof data.version === "number" ? data.version : BASELINE_VERSION,
|
|
67
|
+
entries: rebuildEntries(data.entries)
|
|
68
|
+
};
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function rebuildEntries(raw) {
|
|
74
|
+
const entries = nullProtoMap();
|
|
75
|
+
for (const [path, rawByRule] of Object.entries(raw)) {
|
|
76
|
+
if (typeof rawByRule !== "object" || rawByRule === null) continue;
|
|
77
|
+
const byRule = nullProtoMap();
|
|
78
|
+
for (const [rule9, rawByMessage] of Object.entries(rawByRule)) {
|
|
79
|
+
if (typeof rawByMessage !== "object" || rawByMessage === null) continue;
|
|
80
|
+
const byMessage = nullProtoMap();
|
|
81
|
+
for (const [messageId, count] of Object.entries(
|
|
82
|
+
rawByMessage
|
|
83
|
+
)) {
|
|
84
|
+
if (typeof count === "number") byMessage[messageId] = count;
|
|
85
|
+
}
|
|
86
|
+
byRule[rule9] = byMessage;
|
|
87
|
+
}
|
|
88
|
+
entries[path] = byRule;
|
|
89
|
+
}
|
|
90
|
+
return entries;
|
|
91
|
+
}
|
|
92
|
+
function writeBaseline(path, baseline) {
|
|
93
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
94
|
+
writeFileSync(path, serializeBaseline(baseline), "utf8");
|
|
95
|
+
}
|
|
96
|
+
var KEY_SEP = "\0";
|
|
97
|
+
function fingerprint(d) {
|
|
98
|
+
return d.path + KEY_SEP + d.ruleName + KEY_SEP + d.messageId;
|
|
99
|
+
}
|
|
100
|
+
function applyBaseline(diagnostics, baseline) {
|
|
101
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const [path, byRule] of Object.entries(baseline.entries)) {
|
|
103
|
+
for (const [rule9, byMessage] of Object.entries(byRule)) {
|
|
104
|
+
if (rule9 === INTERNAL_ERROR_RULE_NAME) continue;
|
|
105
|
+
for (const [messageId, count] of Object.entries(byMessage)) {
|
|
106
|
+
remaining.set(path + KEY_SEP + rule9 + KEY_SEP + messageId, count);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const kept = [];
|
|
111
|
+
let hidden = 0;
|
|
112
|
+
for (const d of diagnostics) {
|
|
113
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) {
|
|
114
|
+
kept.push(d);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const key = fingerprint(d);
|
|
118
|
+
const left = remaining.get(key);
|
|
119
|
+
if (left !== void 0 && left > 0) {
|
|
120
|
+
remaining.set(key, left - 1);
|
|
121
|
+
hidden++;
|
|
122
|
+
} else {
|
|
123
|
+
kept.push(d);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { kept, hidden };
|
|
127
|
+
}
|
|
128
|
+
function totalCount(baseline) {
|
|
129
|
+
let total = 0;
|
|
130
|
+
for (const byRule of Object.values(baseline.entries)) {
|
|
131
|
+
for (const byMessage of Object.values(byRule)) {
|
|
132
|
+
for (const count of Object.values(byMessage)) total += count;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return total;
|
|
136
|
+
}
|
|
137
|
+
function baselineWriteSummary(baseline) {
|
|
138
|
+
const violations = totalCount(baseline);
|
|
139
|
+
const files = Object.keys(baseline.entries).length;
|
|
140
|
+
const v = `${violations} ${violations === 1 ? "violation" : "violations"}`;
|
|
141
|
+
const f = `${files} ${files === 1 ? "file" : "files"}`;
|
|
142
|
+
return `Wrote ${v} across ${f} to ${BASELINE_DISPLAY_PATH}`;
|
|
143
|
+
}
|
|
144
|
+
function baselineMalformedWarning() {
|
|
145
|
+
return `madr-lint: warning: ${BASELINE_DISPLAY_PATH} is malformed; ignoring it (run --update-baseline to regenerate)`;
|
|
146
|
+
}
|
|
147
|
+
function baselineHiddenSummary(hidden) {
|
|
148
|
+
return `${hidden} ${hidden === 1 ? "problem" : "problems"} hidden by baseline (${BASELINE_DISPLAY_PATH})`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/core/cache.ts
|
|
152
|
+
import { createHash } from "crypto";
|
|
153
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
154
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
17
155
|
function computeContentHash(content) {
|
|
18
156
|
return createHash("sha1").update(content, "utf8").digest("hex");
|
|
19
157
|
}
|
|
@@ -33,12 +171,12 @@ function stableStringify(value) {
|
|
|
33
171
|
return "{" + entries.map(([k, v]) => JSON.stringify(k) + ":" + stableStringify(v)).join(",") + "}";
|
|
34
172
|
}
|
|
35
173
|
function manifestPath(cacheDir) {
|
|
36
|
-
return
|
|
174
|
+
return join2(cacheDir, "manifest.json");
|
|
37
175
|
}
|
|
38
176
|
function loadManifest(path) {
|
|
39
|
-
if (!
|
|
177
|
+
if (!existsSync2(path)) return null;
|
|
40
178
|
try {
|
|
41
|
-
const data = JSON.parse(
|
|
179
|
+
const data = JSON.parse(readFileSync2(path, "utf8"));
|
|
42
180
|
if (typeof data.version !== "string" || typeof data.configHash !== "string" || typeof data.files !== "object" || data.files === null) {
|
|
43
181
|
return null;
|
|
44
182
|
}
|
|
@@ -48,12 +186,12 @@ function loadManifest(path) {
|
|
|
48
186
|
}
|
|
49
187
|
}
|
|
50
188
|
function saveManifest(path, manifest) {
|
|
51
|
-
|
|
52
|
-
|
|
189
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
190
|
+
writeFileSync2(path, JSON.stringify(manifest), "utf8");
|
|
53
191
|
}
|
|
54
192
|
|
|
55
193
|
// src/core/config.ts
|
|
56
|
-
import { existsSync as
|
|
194
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
57
195
|
import { resolve } from "path";
|
|
58
196
|
import { createJiti } from "jiti";
|
|
59
197
|
|
|
@@ -64,7 +202,7 @@ var recommended = {
|
|
|
64
202
|
"madr/no-broken-links": "error",
|
|
65
203
|
"madr/no-duplicate-numbering": "error",
|
|
66
204
|
// Convention-only — opt in if your team treats ADR numbering as a
|
|
67
|
-
// contiguous sequence. See
|
|
205
|
+
// contiguous sequence. See https://knktkc.github.io/madr-lint/rules/no-numbering-gap/.
|
|
68
206
|
"madr/no-numbering-gap": "off",
|
|
69
207
|
"madr/required-sections": "error",
|
|
70
208
|
"madr/status-enum": "error",
|
|
@@ -85,12 +223,43 @@ var CONFIG_FILES = [
|
|
|
85
223
|
"madr-lint.config.mjs",
|
|
86
224
|
"madr-lint.config.cjs"
|
|
87
225
|
];
|
|
226
|
+
var ConfigFileNotFoundError = class extends Error {
|
|
227
|
+
constructor(message) {
|
|
228
|
+
super(message);
|
|
229
|
+
this.name = "ConfigFileNotFoundError";
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
function loadConfigFromPath(filePath) {
|
|
233
|
+
let raw;
|
|
234
|
+
try {
|
|
235
|
+
raw = readFileSync3(filePath, "utf8");
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const code = err.code;
|
|
238
|
+
if (code === "ENOENT") {
|
|
239
|
+
throw new ConfigFileNotFoundError(`Config file not found: ${filePath}`);
|
|
240
|
+
}
|
|
241
|
+
if (code === "EISDIR") {
|
|
242
|
+
throw new ConfigFileNotFoundError(
|
|
243
|
+
`Config path is a directory, not a file: ${filePath}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
if (filePath.endsWith(".json")) {
|
|
249
|
+
const parsed = JSON.parse(raw);
|
|
250
|
+
return resolveExtends(parsed);
|
|
251
|
+
}
|
|
252
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
253
|
+
const loaded = jiti(filePath);
|
|
254
|
+
const config = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : loaded;
|
|
255
|
+
return resolveExtends(config);
|
|
256
|
+
}
|
|
88
257
|
function loadConfig(cwd) {
|
|
89
258
|
for (const name of CONFIG_FILES) {
|
|
90
259
|
const path = resolve(cwd, name);
|
|
91
|
-
if (!
|
|
260
|
+
if (!existsSync3(path)) continue;
|
|
92
261
|
if (name.endsWith(".json")) {
|
|
93
|
-
const parsed = JSON.parse(
|
|
262
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
94
263
|
return resolveExtends(parsed);
|
|
95
264
|
}
|
|
96
265
|
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
@@ -115,17 +284,17 @@ function resolveExtends(config) {
|
|
|
115
284
|
}
|
|
116
285
|
|
|
117
286
|
// src/core/discover.ts
|
|
118
|
-
import { existsSync as
|
|
119
|
-
import { join as
|
|
287
|
+
import { existsSync as existsSync4, readdirSync, statSync } from "fs";
|
|
288
|
+
import { join as join3 } from "path";
|
|
120
289
|
function findAdrFiles(target) {
|
|
121
|
-
if (!
|
|
290
|
+
if (!existsSync4(target)) return [];
|
|
122
291
|
const stat = statSync(target);
|
|
123
292
|
if (stat.isFile()) {
|
|
124
293
|
return target.endsWith(".md") ? [target] : [];
|
|
125
294
|
}
|
|
126
295
|
if (!stat.isDirectory()) return [];
|
|
127
296
|
const entries = readdirSync(target, { recursive: true, withFileTypes: true });
|
|
128
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) =>
|
|
297
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => join3(e.parentPath, e.name)).toSorted();
|
|
129
298
|
}
|
|
130
299
|
|
|
131
300
|
// src/core/ignore.ts
|
|
@@ -152,7 +321,7 @@ function matchesPattern(relativePath, base, pattern) {
|
|
|
152
321
|
}
|
|
153
322
|
|
|
154
323
|
// src/core/lint.ts
|
|
155
|
-
import { readFileSync as
|
|
324
|
+
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
156
325
|
import { isAbsolute, relative, resolve as resolve2, sep } from "path";
|
|
157
326
|
|
|
158
327
|
// src/core/runner.ts
|
|
@@ -167,20 +336,33 @@ function parseFile(content) {
|
|
|
167
336
|
const data = matter.data;
|
|
168
337
|
const frontmatter = Object.keys(data).length > 0 ? data : null;
|
|
169
338
|
const ast = fromMarkdown(matter.content);
|
|
170
|
-
const
|
|
339
|
+
const listResult = extractListMetadataWithLoc(ast);
|
|
340
|
+
const listMetadata = listResult?.values ?? null;
|
|
171
341
|
let metadata = null;
|
|
342
|
+
let metadataLoc = listResult && Object.keys(listResult.loc).length > 0 ? { ...listResult.loc } : null;
|
|
172
343
|
if (frontmatter && listMetadata) {
|
|
173
344
|
const frontmatterDefined = {};
|
|
174
345
|
for (const [k, v] of Object.entries(frontmatter)) {
|
|
175
346
|
if (v !== null && v !== void 0) frontmatterDefined[k] = v;
|
|
176
347
|
}
|
|
177
348
|
metadata = { ...listMetadata, ...frontmatterDefined };
|
|
349
|
+
if (metadataLoc) {
|
|
350
|
+
for (const k of Object.keys(frontmatterDefined)) delete metadataLoc[k];
|
|
351
|
+
if (Object.keys(metadataLoc).length === 0) metadataLoc = null;
|
|
352
|
+
}
|
|
178
353
|
} else if (frontmatter) {
|
|
179
354
|
metadata = frontmatter;
|
|
180
355
|
} else if (listMetadata) {
|
|
181
356
|
metadata = listMetadata;
|
|
182
357
|
}
|
|
183
|
-
return {
|
|
358
|
+
return {
|
|
359
|
+
frontmatter,
|
|
360
|
+
listMetadata,
|
|
361
|
+
metadata,
|
|
362
|
+
metadataLoc,
|
|
363
|
+
ast,
|
|
364
|
+
body: matter.content
|
|
365
|
+
};
|
|
184
366
|
}
|
|
185
367
|
var RECOGNIZED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
186
368
|
"status",
|
|
@@ -190,7 +372,7 @@ var RECOGNIZED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
190
372
|
"consulted",
|
|
191
373
|
"informed"
|
|
192
374
|
]);
|
|
193
|
-
function
|
|
375
|
+
function extractListMetadataWithLoc(ast) {
|
|
194
376
|
let firstList = null;
|
|
195
377
|
for (const child of ast.children) {
|
|
196
378
|
if (child.type === "heading") {
|
|
@@ -205,17 +387,20 @@ function extractListMetadata(ast) {
|
|
|
205
387
|
break;
|
|
206
388
|
}
|
|
207
389
|
if (!firstList) return null;
|
|
208
|
-
const
|
|
390
|
+
const values = {};
|
|
391
|
+
const loc = {};
|
|
209
392
|
for (const item of firstList.children) {
|
|
210
393
|
const pair = extractListItemKV(item);
|
|
211
|
-
if (pair && !(pair.key in
|
|
212
|
-
|
|
394
|
+
if (pair && !(pair.key in values)) {
|
|
395
|
+
values[pair.key] = pair.value;
|
|
396
|
+
const start = item.position?.start;
|
|
397
|
+
if (start) loc[pair.key] = { line: start.line, column: start.column };
|
|
213
398
|
}
|
|
214
399
|
}
|
|
215
|
-
const hasRecognizedKey = Object.keys(
|
|
400
|
+
const hasRecognizedKey = Object.keys(values).some(
|
|
216
401
|
(k) => RECOGNIZED_METADATA_KEYS.has(k)
|
|
217
402
|
);
|
|
218
|
-
return hasRecognizedKey ?
|
|
403
|
+
return hasRecognizedKey ? { values, loc } : null;
|
|
219
404
|
}
|
|
220
405
|
var KEY_PATTERN = /^[A-Za-z][A-Za-z0-9 \-_]*$/;
|
|
221
406
|
var SEPARATOR_PATTERN = /^\s*:\s*/;
|
|
@@ -255,6 +440,118 @@ function extractListItemKV(item) {
|
|
|
255
440
|
return null;
|
|
256
441
|
}
|
|
257
442
|
|
|
443
|
+
// src/core/suppression.ts
|
|
444
|
+
var DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
|
|
445
|
+
"disable-file",
|
|
446
|
+
"disable",
|
|
447
|
+
"enable",
|
|
448
|
+
"disable-next-line"
|
|
449
|
+
]);
|
|
450
|
+
var DIRECTIVE_PREFIX = "madr-lint-";
|
|
451
|
+
function parseDirective(value, line) {
|
|
452
|
+
const trimmed = value.trim();
|
|
453
|
+
if (!trimmed.startsWith("<!--") || !trimmed.endsWith("-->")) return null;
|
|
454
|
+
const inner = trimmed.slice(4, -3).trim();
|
|
455
|
+
if (inner.includes("<!--") || inner.includes("-->")) return null;
|
|
456
|
+
if (!inner.startsWith(DIRECTIVE_PREFIX)) return null;
|
|
457
|
+
const afterPrefix = inner.slice(DIRECTIVE_PREFIX.length);
|
|
458
|
+
const wsIndex = afterPrefix.search(/\s/);
|
|
459
|
+
const keyword = wsIndex === -1 ? afterPrefix : afterPrefix.slice(0, wsIndex);
|
|
460
|
+
if (!DIRECTIVE_KINDS.has(keyword)) return null;
|
|
461
|
+
const rest = wsIndex === -1 ? "" : afterPrefix.slice(wsIndex);
|
|
462
|
+
return { kind: keyword, rules: parseRuleList(rest), line };
|
|
463
|
+
}
|
|
464
|
+
function parseRuleList(rest) {
|
|
465
|
+
const trimmed = rest.trim();
|
|
466
|
+
if (trimmed === "") return null;
|
|
467
|
+
const rules = trimmed.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
468
|
+
return rules.length > 0 ? rules : null;
|
|
469
|
+
}
|
|
470
|
+
function walkHtml(node, out) {
|
|
471
|
+
if (node.type === "html") {
|
|
472
|
+
const line = node.position?.start.line;
|
|
473
|
+
if (typeof line === "number") {
|
|
474
|
+
const directive = parseDirective(node.value, line);
|
|
475
|
+
if (directive) out.push(directive);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
479
|
+
for (const child of node.children) walkHtml(child, out);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function collectDirectives(ast, body) {
|
|
483
|
+
const all = [];
|
|
484
|
+
walkHtml(ast, all);
|
|
485
|
+
if (all.length === 0) return null;
|
|
486
|
+
let lines = null;
|
|
487
|
+
const nextNonBlankLine = (after) => {
|
|
488
|
+
lines ??= body.split("\n");
|
|
489
|
+
for (let ln = after + 1; ln <= lines.length; ln++) {
|
|
490
|
+
if ((lines[ln - 1] ?? "").trim() !== "") return ln;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
};
|
|
494
|
+
const index = {
|
|
495
|
+
fileDisableAll: false,
|
|
496
|
+
fileDisableRules: /* @__PURE__ */ new Set(),
|
|
497
|
+
ranged: [],
|
|
498
|
+
nextLine: /* @__PURE__ */ new Map()
|
|
499
|
+
};
|
|
500
|
+
for (const directive of all) {
|
|
501
|
+
switch (directive.kind) {
|
|
502
|
+
case "disable-file": {
|
|
503
|
+
if (directive.rules === null) index.fileDisableAll = true;
|
|
504
|
+
else for (const rule9 of directive.rules) index.fileDisableRules.add(rule9);
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case "disable":
|
|
508
|
+
case "enable": {
|
|
509
|
+
index.ranged.push(directive);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
case "disable-next-line": {
|
|
513
|
+
const target = nextNonBlankLine(directive.line);
|
|
514
|
+
if (target === null) break;
|
|
515
|
+
let entry = index.nextLine.get(target);
|
|
516
|
+
if (!entry) {
|
|
517
|
+
entry = { all: false, rules: /* @__PURE__ */ new Set() };
|
|
518
|
+
index.nextLine.set(target, entry);
|
|
519
|
+
}
|
|
520
|
+
if (directive.rules === null) entry.all = true;
|
|
521
|
+
else for (const rule9 of directive.rules) entry.rules.add(rule9);
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
index.ranged.sort((a, b) => a.line - b.line);
|
|
527
|
+
return index;
|
|
528
|
+
}
|
|
529
|
+
function directiveApplies(directive, ruleName) {
|
|
530
|
+
return directive.rules === null || directive.rules.includes(ruleName);
|
|
531
|
+
}
|
|
532
|
+
function isSuppressed(index, ruleName, line) {
|
|
533
|
+
if (index.fileDisableAll || index.fileDisableRules.has(ruleName)) return true;
|
|
534
|
+
if (line !== void 0) {
|
|
535
|
+
const next = index.nextLine.get(line);
|
|
536
|
+
if (next && (next.all || next.rules.has(ruleName))) return true;
|
|
537
|
+
}
|
|
538
|
+
const ceiling = line ?? Number.POSITIVE_INFINITY;
|
|
539
|
+
let disabled = false;
|
|
540
|
+
for (const directive of index.ranged) {
|
|
541
|
+
if (directive.line > ceiling) break;
|
|
542
|
+
if (directiveApplies(directive, ruleName)) {
|
|
543
|
+
disabled = directive.kind === "disable";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return disabled;
|
|
547
|
+
}
|
|
548
|
+
function filterSuppressed(diagnostics, index) {
|
|
549
|
+
return diagnostics.filter((d) => {
|
|
550
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) return true;
|
|
551
|
+
return !isSuppressed(index, d.ruleName, d.loc?.line);
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
258
555
|
// src/core/runner.ts
|
|
259
556
|
var ajv = new Ajv({ strict: true, allErrors: true });
|
|
260
557
|
var validatorCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -266,7 +563,6 @@ function getValidator(schema) {
|
|
|
266
563
|
}
|
|
267
564
|
return validator;
|
|
268
565
|
}
|
|
269
|
-
var INTERNAL_ERROR_RULE_NAME = "core/internal-error";
|
|
270
566
|
var RuleOptionsError = class extends Error {
|
|
271
567
|
constructor(ruleName, ajvErrors) {
|
|
272
568
|
super(`Invalid options for rule ${ruleName}: ${ajvErrors}`);
|
|
@@ -277,6 +573,27 @@ var RuleOptionsError = class extends Error {
|
|
|
277
573
|
ruleName;
|
|
278
574
|
ajvErrors;
|
|
279
575
|
};
|
|
576
|
+
var PerFileRuleContext = class {
|
|
577
|
+
file;
|
|
578
|
+
options;
|
|
579
|
+
report;
|
|
580
|
+
ensureParsed;
|
|
581
|
+
constructor(file, options, ensureParsed, report) {
|
|
582
|
+
this.file = file;
|
|
583
|
+
this.options = options;
|
|
584
|
+
this.ensureParsed = ensureParsed;
|
|
585
|
+
this.report = report;
|
|
586
|
+
}
|
|
587
|
+
get frontmatter() {
|
|
588
|
+
return this.ensureParsed().frontmatter;
|
|
589
|
+
}
|
|
590
|
+
get metadata() {
|
|
591
|
+
return this.ensureParsed().metadata;
|
|
592
|
+
}
|
|
593
|
+
get metadataLoc() {
|
|
594
|
+
return this.ensureParsed().metadataLoc;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
280
597
|
function runRulesOnFile(rules, file, runtime = {}) {
|
|
281
598
|
const diagnostics = [];
|
|
282
599
|
const severity = runtime.severity ?? "error";
|
|
@@ -300,16 +617,11 @@ function runRulesOnFile(rules, file, runtime = {}) {
|
|
|
300
617
|
throw new RuleOptionsError(rule9.meta.name, ajv.errorsText(validator.errors));
|
|
301
618
|
}
|
|
302
619
|
}
|
|
303
|
-
const context =
|
|
620
|
+
const context = new PerFileRuleContext(
|
|
304
621
|
file,
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
get metadata() {
|
|
309
|
-
return ensureParsed().metadata;
|
|
310
|
-
},
|
|
311
|
-
options: mergedOptions,
|
|
312
|
-
report(d) {
|
|
622
|
+
mergedOptions,
|
|
623
|
+
ensureParsed,
|
|
624
|
+
(d) => {
|
|
313
625
|
diagnostics.push({
|
|
314
626
|
ruleName: rule9.meta.name,
|
|
315
627
|
severity,
|
|
@@ -317,7 +629,7 @@ function runRulesOnFile(rules, file, runtime = {}) {
|
|
|
317
629
|
...d
|
|
318
630
|
});
|
|
319
631
|
}
|
|
320
|
-
|
|
632
|
+
);
|
|
321
633
|
let listeners;
|
|
322
634
|
try {
|
|
323
635
|
listeners = rule9.create(context);
|
|
@@ -336,7 +648,11 @@ function runRulesOnFile(rules, file, runtime = {}) {
|
|
|
336
648
|
if (hasASTRule) {
|
|
337
649
|
walk(ensureParsed().ast, enterMap, exitMap);
|
|
338
650
|
}
|
|
339
|
-
return diagnostics;
|
|
651
|
+
if (diagnostics.length === 0) return diagnostics;
|
|
652
|
+
if (!file.content.includes(DIRECTIVE_PREFIX)) return diagnostics;
|
|
653
|
+
const { ast, body } = ensureParsed();
|
|
654
|
+
const directives = collectDirectives(ast, body);
|
|
655
|
+
return directives ? filterSuppressed(diagnostics, directives) : diagnostics;
|
|
340
656
|
}
|
|
341
657
|
function internalErrorDiagnostic(ruleName, operation, err, path) {
|
|
342
658
|
return {
|
|
@@ -394,6 +710,7 @@ function buildProjectFile(file) {
|
|
|
394
710
|
return {
|
|
395
711
|
path: file.path,
|
|
396
712
|
content: file.content,
|
|
713
|
+
body: parsed.body,
|
|
397
714
|
frontmatter: parsed.frontmatter,
|
|
398
715
|
metadata: parsed.metadata,
|
|
399
716
|
ast: parsed.ast
|
|
@@ -440,11 +757,6 @@ function runRulesOnProject(rules, files, runtime = {}) {
|
|
|
440
757
|
return diagnostics;
|
|
441
758
|
}
|
|
442
759
|
|
|
443
|
-
// src/core/types.ts
|
|
444
|
-
function isProjectRule(rule9) {
|
|
445
|
-
return "check" in rule9;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
760
|
// src/core/lint.ts
|
|
449
761
|
function toPosix(p) {
|
|
450
762
|
if (sep === "/") return p;
|
|
@@ -454,7 +766,7 @@ function lintFiles(opts) {
|
|
|
454
766
|
const allDiagnostics = [];
|
|
455
767
|
const fileEntries = opts.files.map((absolutePath) => ({
|
|
456
768
|
relativePath: toPosix(relative(opts.cwd, absolutePath)),
|
|
457
|
-
content:
|
|
769
|
+
content: readFileSync4(absolutePath, "utf8")
|
|
458
770
|
}));
|
|
459
771
|
const perFileRules = [];
|
|
460
772
|
const projectRules = [];
|
|
@@ -545,8 +857,9 @@ function lintFiles(opts) {
|
|
|
545
857
|
if (sev === "error") errorRules.push(rule9);
|
|
546
858
|
else if (sev === "warn") warnRules.push(rule9);
|
|
547
859
|
}
|
|
860
|
+
const projectDiagnostics = [];
|
|
548
861
|
if (errorRules.length > 0) {
|
|
549
|
-
|
|
862
|
+
projectDiagnostics.push(
|
|
550
863
|
...runRulesOnProject(errorRules, projectFiles, {
|
|
551
864
|
severity: "error",
|
|
552
865
|
fileExists,
|
|
@@ -555,7 +868,7 @@ function lintFiles(opts) {
|
|
|
555
868
|
);
|
|
556
869
|
}
|
|
557
870
|
if (warnRules.length > 0) {
|
|
558
|
-
|
|
871
|
+
projectDiagnostics.push(
|
|
559
872
|
...runRulesOnProject(warnRules, projectFiles, {
|
|
560
873
|
severity: "warn",
|
|
561
874
|
fileExists,
|
|
@@ -563,14 +876,27 @@ function lintFiles(opts) {
|
|
|
563
876
|
})
|
|
564
877
|
);
|
|
565
878
|
}
|
|
879
|
+
allDiagnostics.push(
|
|
880
|
+
...filterSuppressedProjectDiagnostics(projectDiagnostics, projectFiles)
|
|
881
|
+
);
|
|
566
882
|
}
|
|
567
883
|
if (manifest && opts.cache) {
|
|
568
884
|
saveManifest(manifestPath(opts.cache.dir), manifest);
|
|
569
885
|
}
|
|
886
|
+
if (!opts.baseline) {
|
|
887
|
+
return {
|
|
888
|
+
filesChecked: opts.files.length,
|
|
889
|
+
filesFromCache,
|
|
890
|
+
diagnostics: allDiagnostics,
|
|
891
|
+
baselineHidden: 0
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const applied = applyBaseline(allDiagnostics, opts.baseline);
|
|
570
895
|
return {
|
|
571
896
|
filesChecked: opts.files.length,
|
|
572
897
|
filesFromCache,
|
|
573
|
-
diagnostics:
|
|
898
|
+
diagnostics: applied.kept,
|
|
899
|
+
baselineHidden: applied.hidden
|
|
574
900
|
};
|
|
575
901
|
}
|
|
576
902
|
function runPerFileRulesForFile(perFileRules, ruleSeverity, optionsByRule, file) {
|
|
@@ -605,6 +931,25 @@ function runPerFileRulesForFile(perFileRules, ruleSeverity, optionsByRule, file)
|
|
|
605
931
|
}
|
|
606
932
|
return diagnostics;
|
|
607
933
|
}
|
|
934
|
+
function filterSuppressedProjectDiagnostics(diagnostics, projectFiles) {
|
|
935
|
+
if (diagnostics.length === 0) return [];
|
|
936
|
+
const fileByPath = new Map(projectFiles.map((f) => [f.path, f]));
|
|
937
|
+
const indexByPath = /* @__PURE__ */ new Map();
|
|
938
|
+
const indexFor = (path) => {
|
|
939
|
+
const cached = indexByPath.get(path);
|
|
940
|
+
if (cached !== void 0) return cached;
|
|
941
|
+
const file = fileByPath.get(path);
|
|
942
|
+
const index = file && file.content.includes(DIRECTIVE_PREFIX) ? collectDirectives(file.ast, file.body) : null;
|
|
943
|
+
indexByPath.set(path, index);
|
|
944
|
+
return index;
|
|
945
|
+
};
|
|
946
|
+
return diagnostics.filter((d) => {
|
|
947
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) return true;
|
|
948
|
+
const index = indexFor(d.path);
|
|
949
|
+
if (!index) return true;
|
|
950
|
+
return !isSuppressed(index, d.ruleName, d.loc?.line);
|
|
951
|
+
});
|
|
952
|
+
}
|
|
608
953
|
function resolveSeverity(config) {
|
|
609
954
|
if (config === void 0) return "off";
|
|
610
955
|
if (typeof config === "string") return config;
|
|
@@ -621,6 +966,8 @@ function extractOptionsByRule(ruleSeverity) {
|
|
|
621
966
|
// src/core/reporter.ts
|
|
622
967
|
import pc from "picocolors";
|
|
623
968
|
var textReporter = {
|
|
969
|
+
// Deliberately 2-param (meta unused): the CLI prints baselineHiddenSummary()
|
|
970
|
+
// itself for text format, so the footer can outlive a suppressed banner.
|
|
624
971
|
format(diagnostics, rulesByName) {
|
|
625
972
|
if (diagnostics.length === 0) {
|
|
626
973
|
return pc.green("\u2713 All clear.");
|
|
@@ -669,7 +1016,7 @@ function formatSummary(errors, warnings) {
|
|
|
669
1016
|
return parts.join(", ");
|
|
670
1017
|
}
|
|
671
1018
|
var jsonReporter = {
|
|
672
|
-
format(diagnostics, rulesByName) {
|
|
1019
|
+
format(diagnostics, rulesByName, meta) {
|
|
673
1020
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
674
1021
|
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
675
1022
|
const payload = {
|
|
@@ -677,7 +1024,10 @@ var jsonReporter = {
|
|
|
677
1024
|
summary: {
|
|
678
1025
|
total: diagnostics.length,
|
|
679
1026
|
errors,
|
|
680
|
-
warnings
|
|
1027
|
+
warnings,
|
|
1028
|
+
// Always present (0 when no baseline) so CI consumers can detect an
|
|
1029
|
+
// active baseline without probing for the key. SARIF stays untouched.
|
|
1030
|
+
baselineHidden: meta?.baselineHidden ?? 0
|
|
681
1031
|
},
|
|
682
1032
|
results: diagnostics.map((d) => ({
|
|
683
1033
|
path: d.path,
|
|
@@ -737,10 +1087,40 @@ var sarifReporter = {
|
|
|
737
1087
|
return JSON.stringify(sarif, null, 2);
|
|
738
1088
|
}
|
|
739
1089
|
};
|
|
1090
|
+
function escapeGhMessage(s) {
|
|
1091
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
1092
|
+
}
|
|
1093
|
+
function escapeGhProperty(s) {
|
|
1094
|
+
return escapeGhMessage(s).replace(/,/g, "%2C").replace(/:/g, "%3A");
|
|
1095
|
+
}
|
|
1096
|
+
var githubReporter = {
|
|
1097
|
+
format(diagnostics, rulesByName, meta) {
|
|
1098
|
+
const lines = [];
|
|
1099
|
+
for (const d of diagnostics) {
|
|
1100
|
+
const level = d.severity === "error" ? "error" : "warning";
|
|
1101
|
+
const file = escapeGhProperty(d.path);
|
|
1102
|
+
const title = escapeGhProperty(d.ruleName);
|
|
1103
|
+
const message = escapeGhMessage(renderMessage(d, rulesByName));
|
|
1104
|
+
const linePart = d.loc ? `,line=${d.loc.line}` : "";
|
|
1105
|
+
lines.push(`::${level} file=${file}${linePart},title=${title}::${message}`);
|
|
1106
|
+
}
|
|
1107
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
1108
|
+
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
1109
|
+
const summaryParts = [];
|
|
1110
|
+
if (errors > 0) summaryParts.push(`${errors} ${errors === 1 ? "error" : "errors"}`);
|
|
1111
|
+
if (warnings > 0) summaryParts.push(`${warnings} ${warnings === 1 ? "warning" : "warnings"}`);
|
|
1112
|
+
if (summaryParts.length === 0) summaryParts.push("0 errors, 0 warnings");
|
|
1113
|
+
const baselineHidden = meta?.baselineHidden ?? 0;
|
|
1114
|
+
if (baselineHidden > 0) summaryParts.push(`${baselineHidden} hidden by baseline`);
|
|
1115
|
+
lines.push(summaryParts.join(", "));
|
|
1116
|
+
return lines.join("\n");
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
740
1119
|
var reporters = {
|
|
741
1120
|
text: textReporter,
|
|
742
1121
|
json: jsonReporter,
|
|
743
|
-
sarif: sarifReporter
|
|
1122
|
+
sarif: sarifReporter,
|
|
1123
|
+
github: githubReporter
|
|
744
1124
|
};
|
|
745
1125
|
|
|
746
1126
|
// src/rules/index.ts
|
|
@@ -793,7 +1173,7 @@ var rule = {
|
|
|
793
1173
|
versionCompat: ["v2", "v3", "v4"],
|
|
794
1174
|
docs: {
|
|
795
1175
|
description: "Validate that ADR `date` (frontmatter or v2 bold-list) is a valid ISO 8601 calendar date (YYYY-MM-DD)",
|
|
796
|
-
url: "https://github.
|
|
1176
|
+
url: "https://knktkc.github.io/madr-lint/rules/date-iso8601/",
|
|
797
1177
|
recommended: true
|
|
798
1178
|
},
|
|
799
1179
|
messages: {
|
|
@@ -825,8 +1205,10 @@ var rule = {
|
|
|
825
1205
|
return;
|
|
826
1206
|
}
|
|
827
1207
|
if (!isValidIso8601Date(dateStr)) {
|
|
1208
|
+
const loc = context.metadataLoc?.[fieldName];
|
|
828
1209
|
context.report({
|
|
829
1210
|
messageId: "invalidDate",
|
|
1211
|
+
...loc ? { loc } : {},
|
|
830
1212
|
data: { date: dateStr }
|
|
831
1213
|
});
|
|
832
1214
|
}
|
|
@@ -884,7 +1266,7 @@ var rule2 = {
|
|
|
884
1266
|
versionCompat: ["v2", "v3", "v4"],
|
|
885
1267
|
docs: {
|
|
886
1268
|
description: "Enforce ADR filename convention NNNN-kebab-case.md",
|
|
887
|
-
url: "https://github.
|
|
1269
|
+
url: "https://knktkc.github.io/madr-lint/rules/filename-format/",
|
|
888
1270
|
recommended: true
|
|
889
1271
|
},
|
|
890
1272
|
messages: {
|
|
@@ -910,7 +1292,7 @@ var rule2 = {
|
|
|
910
1292
|
var filename_format_default = rule2;
|
|
911
1293
|
|
|
912
1294
|
// src/rules/no-broken-links/index.ts
|
|
913
|
-
import { dirname as
|
|
1295
|
+
import { dirname as dirname3, posix } from "path";
|
|
914
1296
|
|
|
915
1297
|
// src/rules/no-broken-links/schema.json
|
|
916
1298
|
var schema_default3 = {
|
|
@@ -963,7 +1345,7 @@ var rule3 = {
|
|
|
963
1345
|
versionCompat: ["v2", "v3", "v4"],
|
|
964
1346
|
docs: {
|
|
965
1347
|
description: "Relative-path Markdown links must resolve to an existing file in the project",
|
|
966
|
-
url: "https://github.
|
|
1348
|
+
url: "https://knktkc.github.io/madr-lint/rules/no-broken-links/",
|
|
967
1349
|
recommended: true
|
|
968
1350
|
},
|
|
969
1351
|
messages: {
|
|
@@ -985,13 +1367,15 @@ var rule3 = {
|
|
|
985
1367
|
if (isExternalOrAnchor(url)) continue;
|
|
986
1368
|
const pathOnly = safeDecode(stripAnchorAndQuery(url));
|
|
987
1369
|
if (pathOnly === "") continue;
|
|
988
|
-
const fileDir =
|
|
1370
|
+
const fileDir = dirname3(file.path);
|
|
989
1371
|
const resolvedPath = resolveRelative(fileDir, pathOnly);
|
|
990
1372
|
const exists = knownPaths.has(resolvedPath) || (context.fileExists?.(resolvedPath) ?? false);
|
|
991
1373
|
if (!exists) {
|
|
1374
|
+
const start = link.position?.start;
|
|
992
1375
|
context.report({
|
|
993
1376
|
messageId: "brokenLink",
|
|
994
1377
|
path: file.path,
|
|
1378
|
+
...start ? { loc: { line: start.line, column: start.column } } : {},
|
|
995
1379
|
data: { url, resolvedPath }
|
|
996
1380
|
});
|
|
997
1381
|
}
|
|
@@ -1021,7 +1405,7 @@ var rule4 = {
|
|
|
1021
1405
|
versionCompat: ["v2", "v3", "v4"],
|
|
1022
1406
|
docs: {
|
|
1023
1407
|
description: "No two ADRs share the same NNNN- number prefix",
|
|
1024
|
-
url: "https://github.
|
|
1408
|
+
url: "https://knktkc.github.io/madr-lint/rules/no-duplicate-numbering/",
|
|
1025
1409
|
recommended: true
|
|
1026
1410
|
},
|
|
1027
1411
|
messages: {
|
|
@@ -1080,7 +1464,7 @@ var rule5 = {
|
|
|
1080
1464
|
versionCompat: ["v2", "v3", "v4"],
|
|
1081
1465
|
docs: {
|
|
1082
1466
|
description: "Detect gaps in ADR numbering (e.g. 0001 and 0003 exist but 0002 is missing). Convention-only; default off.",
|
|
1083
|
-
url: "https://github.
|
|
1467
|
+
url: "https://knktkc.github.io/madr-lint/rules/no-numbering-gap/",
|
|
1084
1468
|
recommended: false
|
|
1085
1469
|
},
|
|
1086
1470
|
messages: {
|
|
@@ -1156,7 +1540,7 @@ var rule6 = {
|
|
|
1156
1540
|
versionCompat: ["v2", "v3", "v4"],
|
|
1157
1541
|
docs: {
|
|
1158
1542
|
description: "Enforce that ADRs contain required heading sections",
|
|
1159
|
-
url: "https://github.
|
|
1543
|
+
url: "https://knktkc.github.io/madr-lint/rules/required-sections/",
|
|
1160
1544
|
recommended: true
|
|
1161
1545
|
},
|
|
1162
1546
|
messages: {
|
|
@@ -1233,7 +1617,7 @@ var rule7 = {
|
|
|
1233
1617
|
versionCompat: ["v2", "v3", "v4"],
|
|
1234
1618
|
docs: {
|
|
1235
1619
|
description: "Validate that ADR `status` (frontmatter or v2 bold-list) is in the allowed enum",
|
|
1236
|
-
url: "https://github.
|
|
1620
|
+
url: "https://knktkc.github.io/madr-lint/rules/status-enum/",
|
|
1237
1621
|
recommended: true
|
|
1238
1622
|
},
|
|
1239
1623
|
messages: {
|
|
@@ -1267,8 +1651,10 @@ var rule7 = {
|
|
|
1267
1651
|
...values,
|
|
1268
1652
|
...prefixValues.map((p) => `${p} ...`)
|
|
1269
1653
|
];
|
|
1654
|
+
const loc = context.metadataLoc?.status;
|
|
1270
1655
|
context.report({
|
|
1271
1656
|
messageId: "invalidStatus",
|
|
1657
|
+
...loc ? { loc } : {},
|
|
1272
1658
|
data: { status, allowed }
|
|
1273
1659
|
});
|
|
1274
1660
|
}
|
|
@@ -1309,7 +1695,7 @@ var rule8 = {
|
|
|
1309
1695
|
versionCompat: ["v3", "v4"],
|
|
1310
1696
|
docs: {
|
|
1311
1697
|
description: "Frontmatter `supersedes` and `superseded-by` must reference each other bidirectionally",
|
|
1312
|
-
url: "https://github.
|
|
1698
|
+
url: "https://knktkc.github.io/madr-lint/rules/supersedes-bidirectional/",
|
|
1313
1699
|
recommended: true
|
|
1314
1700
|
},
|
|
1315
1701
|
messages: {
|
|
@@ -1392,7 +1778,7 @@ function toPosix2(p) {
|
|
|
1392
1778
|
return p.split(sep2).join("/");
|
|
1393
1779
|
}
|
|
1394
1780
|
var pkg = JSON.parse(
|
|
1395
|
-
|
|
1781
|
+
readFileSync5(new URL("../package.json", import.meta.url), "utf8")
|
|
1396
1782
|
);
|
|
1397
1783
|
var main = defineCommand({
|
|
1398
1784
|
meta: {
|
|
@@ -1418,14 +1804,71 @@ var main = defineCommand({
|
|
|
1418
1804
|
},
|
|
1419
1805
|
format: {
|
|
1420
1806
|
type: "string",
|
|
1421
|
-
description: "Reporter format: text (default), json, or
|
|
1807
|
+
description: "Reporter format: text (default), json, sarif, or github",
|
|
1422
1808
|
default: "text"
|
|
1809
|
+
},
|
|
1810
|
+
baseline: {
|
|
1811
|
+
type: "boolean",
|
|
1812
|
+
description: "Subtract .madr-lint/baseline.json when present (use --no-baseline to ignore it)",
|
|
1813
|
+
default: true
|
|
1814
|
+
},
|
|
1815
|
+
"update-baseline": {
|
|
1816
|
+
type: "boolean",
|
|
1817
|
+
description: "Rewrite .madr-lint/baseline.json from a full lint, then exit 0",
|
|
1818
|
+
default: false
|
|
1819
|
+
},
|
|
1820
|
+
quiet: {
|
|
1821
|
+
type: "boolean",
|
|
1822
|
+
description: "Report errors only; suppress warnings from output",
|
|
1823
|
+
default: false
|
|
1824
|
+
},
|
|
1825
|
+
"max-warnings": {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
description: "Exit 1 when warning count exceeds n. n=0 means any warning fails. Negative = no limit.",
|
|
1828
|
+
required: false
|
|
1829
|
+
},
|
|
1830
|
+
config: {
|
|
1831
|
+
type: "string",
|
|
1832
|
+
description: "Path to config file, bypassing auto-discovery",
|
|
1833
|
+
required: false
|
|
1423
1834
|
}
|
|
1424
1835
|
},
|
|
1425
1836
|
run({ args }) {
|
|
1426
1837
|
const cwd = process.cwd();
|
|
1427
|
-
let
|
|
1428
|
-
|
|
1838
|
+
let maxWarnings = -1;
|
|
1839
|
+
const rawMaxWarnings = args["max-warnings"];
|
|
1840
|
+
if (rawMaxWarnings !== void 0) {
|
|
1841
|
+
const parsed = Number(rawMaxWarnings);
|
|
1842
|
+
if (rawMaxWarnings.trim() === "" || !Number.isInteger(parsed)) {
|
|
1843
|
+
console.error(
|
|
1844
|
+
`Invalid --max-warnings "${rawMaxWarnings}": must be an integer.`
|
|
1845
|
+
);
|
|
1846
|
+
process.exit(2);
|
|
1847
|
+
}
|
|
1848
|
+
maxWarnings = parsed;
|
|
1849
|
+
}
|
|
1850
|
+
let config;
|
|
1851
|
+
let configExplicit = false;
|
|
1852
|
+
const configArg = args.config;
|
|
1853
|
+
if (configArg) {
|
|
1854
|
+
const absConfigPath = resolve3(cwd, configArg);
|
|
1855
|
+
try {
|
|
1856
|
+
config = loadConfigFromPath(absConfigPath);
|
|
1857
|
+
configExplicit = true;
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
if (err instanceof ConfigFileNotFoundError) {
|
|
1860
|
+
console.error(err.message);
|
|
1861
|
+
} else {
|
|
1862
|
+
console.error(
|
|
1863
|
+
`Failed to load config "${configArg}": ${err instanceof Error ? err.message : String(err)}`
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
process.exit(2);
|
|
1867
|
+
}
|
|
1868
|
+
} else {
|
|
1869
|
+
config = loadConfig(cwd);
|
|
1870
|
+
}
|
|
1871
|
+
if (!configExplicit && Object.keys(config.rules).length === 0) {
|
|
1429
1872
|
config = resolveExtends({ extends: ["madr-lint:recommended"] });
|
|
1430
1873
|
}
|
|
1431
1874
|
const positionals = args._ ?? [];
|
|
@@ -1461,6 +1904,13 @@ var main = defineCommand({
|
|
|
1461
1904
|
}),
|
|
1462
1905
|
pkgVersion: pkg.version
|
|
1463
1906
|
} : null;
|
|
1907
|
+
const baselineFile = baselinePath(cwd);
|
|
1908
|
+
const updateBaseline = args["update-baseline"] === true;
|
|
1909
|
+
const useBaseline = !updateBaseline && args.baseline !== false;
|
|
1910
|
+
const baseline = useBaseline ? loadBaseline(baselineFile) : null;
|
|
1911
|
+
if (useBaseline && baseline === null && existsSync5(baselineFile)) {
|
|
1912
|
+
console.error(baselineMalformedWarning());
|
|
1913
|
+
}
|
|
1464
1914
|
const allRulesArray = Object.values(rules_exports);
|
|
1465
1915
|
let result;
|
|
1466
1916
|
try {
|
|
@@ -1469,7 +1919,8 @@ var main = defineCommand({
|
|
|
1469
1919
|
ruleSeverity: config.rules,
|
|
1470
1920
|
files,
|
|
1471
1921
|
cwd,
|
|
1472
|
-
cache: cacheConfig
|
|
1922
|
+
cache: cacheConfig,
|
|
1923
|
+
baseline
|
|
1473
1924
|
});
|
|
1474
1925
|
} catch (err) {
|
|
1475
1926
|
if (err instanceof RuleOptionsError) {
|
|
@@ -1478,6 +1929,12 @@ var main = defineCommand({
|
|
|
1478
1929
|
}
|
|
1479
1930
|
throw err;
|
|
1480
1931
|
}
|
|
1932
|
+
if (updateBaseline) {
|
|
1933
|
+
const built = buildBaseline(result.diagnostics);
|
|
1934
|
+
writeBaseline(baselineFile, built);
|
|
1935
|
+
console.log(baselineWriteSummary(built));
|
|
1936
|
+
process.exit(0);
|
|
1937
|
+
}
|
|
1481
1938
|
const rulesByName = new Map(
|
|
1482
1939
|
allRulesArray.map((r) => [r.meta.name, r])
|
|
1483
1940
|
);
|
|
@@ -1489,11 +1946,28 @@ var main = defineCommand({
|
|
|
1489
1946
|
);
|
|
1490
1947
|
process.exit(2);
|
|
1491
1948
|
}
|
|
1492
|
-
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1949
|
+
const allDiagnostics = result.diagnostics;
|
|
1950
|
+
const warningCount = allDiagnostics.filter((d) => d.severity === "warn").length;
|
|
1951
|
+
const errorCount = allDiagnostics.filter((d) => d.severity === "error").length;
|
|
1952
|
+
const overWarningLimit = maxWarnings >= 0 && warningCount > maxWarnings;
|
|
1953
|
+
const reported = args.quiet ? allDiagnostics.filter((d) => d.severity !== "warn") : allDiagnostics;
|
|
1954
|
+
const suppressAllClear = format === "text" && reported.length === 0 && overWarningLimit;
|
|
1955
|
+
if (!suppressAllClear) {
|
|
1956
|
+
console.log(
|
|
1957
|
+
reporter.format(reported, rulesByName, {
|
|
1958
|
+
baselineHidden: result.baselineHidden
|
|
1959
|
+
})
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
if (format === "text" && result.baselineHidden > 0) {
|
|
1963
|
+
console.log(baselineHiddenSummary(result.baselineHidden));
|
|
1964
|
+
}
|
|
1965
|
+
if (overWarningLimit) {
|
|
1966
|
+
console.error(
|
|
1967
|
+
`madr-lint: ${warningCount} warning(s) found, exceeds --max-warnings ${maxWarnings}`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
process.exit(errorCount > 0 || overWarningLimit ? 1 : 0);
|
|
1497
1971
|
}
|
|
1498
1972
|
});
|
|
1499
1973
|
await runMain(main);
|