madr-lint 0.2.0 → 0.4.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 +51 -0
- package/README.md +4 -2
- package/dist/cli.js +1534 -627
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +189 -5
- package/dist/index.js +460 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -6,192 +6,17 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import { existsSync as
|
|
10
|
-
import { relative as relative2, resolve as
|
|
11
|
-
import { defineCommand, runMain } from "citty";
|
|
9
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
10
|
+
import { relative as relative2, resolve as resolve5, sep as sep2 } from "path";
|
|
11
|
+
import { defineCommand as defineCommand2, runMain } from "citty";
|
|
12
12
|
|
|
13
|
-
// src/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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";
|
|
155
|
-
function computeContentHash(content) {
|
|
156
|
-
return createHash("sha1").update(content, "utf8").digest("hex");
|
|
157
|
-
}
|
|
158
|
-
function computeConfigHash(config) {
|
|
159
|
-
return createHash("sha1").update(stableStringify(config), "utf8").digest("hex");
|
|
160
|
-
}
|
|
161
|
-
function stableStringify(value) {
|
|
162
|
-
if (value === null || typeof value !== "object") {
|
|
163
|
-
return JSON.stringify(value);
|
|
164
|
-
}
|
|
165
|
-
if (Array.isArray(value)) {
|
|
166
|
-
return "[" + value.map(stableStringify).join(",") + "]";
|
|
167
|
-
}
|
|
168
|
-
const entries = Object.entries(value).toSorted(
|
|
169
|
-
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
170
|
-
);
|
|
171
|
-
return "{" + entries.map(([k, v]) => JSON.stringify(k) + ":" + stableStringify(v)).join(",") + "}";
|
|
172
|
-
}
|
|
173
|
-
function manifestPath(cacheDir) {
|
|
174
|
-
return join2(cacheDir, "manifest.json");
|
|
175
|
-
}
|
|
176
|
-
function loadManifest(path) {
|
|
177
|
-
if (!existsSync2(path)) return null;
|
|
178
|
-
try {
|
|
179
|
-
const data = JSON.parse(readFileSync2(path, "utf8"));
|
|
180
|
-
if (typeof data.version !== "string" || typeof data.configHash !== "string" || typeof data.files !== "object" || data.files === null) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
return data;
|
|
184
|
-
} catch {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
function saveManifest(path, manifest) {
|
|
189
|
-
mkdirSync2(dirname2(path), { recursive: true });
|
|
190
|
-
writeFileSync2(path, JSON.stringify(manifest), "utf8");
|
|
191
|
-
}
|
|
13
|
+
// src/commands/init.ts
|
|
14
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
15
|
+
import { resolve as resolve4 } from "path";
|
|
16
|
+
import { defineCommand } from "citty";
|
|
192
17
|
|
|
193
18
|
// src/core/config.ts
|
|
194
|
-
import { existsSync
|
|
19
|
+
import { existsSync, readFileSync } from "fs";
|
|
195
20
|
import { resolve } from "path";
|
|
196
21
|
import { createJiti } from "jiti";
|
|
197
22
|
|
|
@@ -232,7 +57,7 @@ var ConfigFileNotFoundError = class extends Error {
|
|
|
232
57
|
function loadConfigFromPath(filePath) {
|
|
233
58
|
let raw;
|
|
234
59
|
try {
|
|
235
|
-
raw =
|
|
60
|
+
raw = readFileSync(filePath, "utf8");
|
|
236
61
|
} catch (err) {
|
|
237
62
|
const code = err.code;
|
|
238
63
|
if (code === "ENOENT") {
|
|
@@ -257,9 +82,9 @@ function loadConfigFromPath(filePath) {
|
|
|
257
82
|
function loadConfig(cwd) {
|
|
258
83
|
for (const name of CONFIG_FILES) {
|
|
259
84
|
const path = resolve(cwd, name);
|
|
260
|
-
if (!
|
|
85
|
+
if (!existsSync(path)) continue;
|
|
261
86
|
if (name.endsWith(".json")) {
|
|
262
|
-
const parsed = JSON.parse(
|
|
87
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
263
88
|
return resolveExtends(parsed);
|
|
264
89
|
}
|
|
265
90
|
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
@@ -284,62 +109,40 @@ function resolveExtends(config) {
|
|
|
284
109
|
}
|
|
285
110
|
|
|
286
111
|
// src/core/discover.ts
|
|
287
|
-
import { existsSync as
|
|
288
|
-
import { join
|
|
112
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
113
|
+
import { join } from "path";
|
|
289
114
|
function findAdrFiles(target) {
|
|
290
|
-
if (!
|
|
115
|
+
if (!existsSync2(target)) return [];
|
|
291
116
|
const stat = statSync(target);
|
|
292
117
|
if (stat.isFile()) {
|
|
293
118
|
return target.endsWith(".md") ? [target] : [];
|
|
294
119
|
}
|
|
295
120
|
if (!stat.isDirectory()) return [];
|
|
296
121
|
const entries = readdirSync(target, { recursive: true, withFileTypes: true });
|
|
297
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) =>
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// src/core/ignore.ts
|
|
301
|
-
import { basename } from "path";
|
|
302
|
-
import picomatch from "picomatch";
|
|
303
|
-
function shouldIgnore(relativePath, patterns) {
|
|
304
|
-
if (patterns.length === 0) return false;
|
|
305
|
-
const base = basename(relativePath);
|
|
306
|
-
for (const pattern of patterns) {
|
|
307
|
-
if (matchesPattern(relativePath, base, pattern)) return true;
|
|
308
|
-
}
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
function matchesPattern(relativePath, base, pattern) {
|
|
312
|
-
if (relativePath === pattern) return true;
|
|
313
|
-
const matcher = picomatch(pattern, { dot: true });
|
|
314
|
-
if (matcher(relativePath)) return true;
|
|
315
|
-
if (matcher(base)) return true;
|
|
316
|
-
if (pattern.includes("/") && !pattern.startsWith("**/")) {
|
|
317
|
-
const suffixMatcher = picomatch(`**/${pattern}`, { dot: true });
|
|
318
|
-
if (suffixMatcher(relativePath)) return true;
|
|
319
|
-
}
|
|
320
|
-
return false;
|
|
122
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => join(e.parentPath, e.name)).toSorted();
|
|
321
123
|
}
|
|
322
124
|
|
|
323
|
-
// src/core/
|
|
324
|
-
import { readFileSync as
|
|
325
|
-
import {
|
|
326
|
-
|
|
327
|
-
// src/core/runner.ts
|
|
328
|
-
import Ajv from "ajv";
|
|
125
|
+
// src/core/init.ts
|
|
126
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
127
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
329
128
|
|
|
330
129
|
// src/core/parser.ts
|
|
331
130
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
332
131
|
import { toString } from "mdast-util-to-string";
|
|
333
132
|
import grayMatter from "gray-matter";
|
|
133
|
+
function frontmatterOffset(content) {
|
|
134
|
+
return content.length - grayMatter(content).content.length;
|
|
135
|
+
}
|
|
334
136
|
function parseFile(content) {
|
|
335
137
|
const matter = grayMatter(content);
|
|
336
138
|
const data = matter.data;
|
|
337
139
|
const frontmatter = Object.keys(data).length > 0 ? data : null;
|
|
338
140
|
const ast = fromMarkdown(matter.content);
|
|
339
|
-
const listResult = extractListMetadataWithLoc(ast);
|
|
141
|
+
const listResult = extractListMetadataWithLoc(ast, matter.content);
|
|
340
142
|
const listMetadata = listResult?.values ?? null;
|
|
341
143
|
let metadata = null;
|
|
342
144
|
let metadataLoc = listResult && Object.keys(listResult.loc).length > 0 ? { ...listResult.loc } : null;
|
|
145
|
+
let metadataValueLoc = listResult && Object.keys(listResult.valueOffsets).length > 0 ? { ...listResult.valueOffsets } : null;
|
|
343
146
|
if (frontmatter && listMetadata) {
|
|
344
147
|
const frontmatterDefined = {};
|
|
345
148
|
for (const [k, v] of Object.entries(frontmatter)) {
|
|
@@ -350,6 +153,10 @@ function parseFile(content) {
|
|
|
350
153
|
for (const k of Object.keys(frontmatterDefined)) delete metadataLoc[k];
|
|
351
154
|
if (Object.keys(metadataLoc).length === 0) metadataLoc = null;
|
|
352
155
|
}
|
|
156
|
+
if (metadataValueLoc) {
|
|
157
|
+
for (const k of Object.keys(frontmatterDefined)) delete metadataValueLoc[k];
|
|
158
|
+
if (Object.keys(metadataValueLoc).length === 0) metadataValueLoc = null;
|
|
159
|
+
}
|
|
353
160
|
} else if (frontmatter) {
|
|
354
161
|
metadata = frontmatter;
|
|
355
162
|
} else if (listMetadata) {
|
|
@@ -360,6 +167,7 @@ function parseFile(content) {
|
|
|
360
167
|
listMetadata,
|
|
361
168
|
metadata,
|
|
362
169
|
metadataLoc,
|
|
170
|
+
metadataValueLoc,
|
|
363
171
|
ast,
|
|
364
172
|
body: matter.content
|
|
365
173
|
};
|
|
@@ -372,7 +180,7 @@ var RECOGNIZED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
372
180
|
"consulted",
|
|
373
181
|
"informed"
|
|
374
182
|
]);
|
|
375
|
-
function extractListMetadataWithLoc(ast) {
|
|
183
|
+
function extractListMetadataWithLoc(ast, body) {
|
|
376
184
|
let firstList = null;
|
|
377
185
|
for (const child of ast.children) {
|
|
378
186
|
if (child.type === "heading") {
|
|
@@ -389,18 +197,22 @@ function extractListMetadataWithLoc(ast) {
|
|
|
389
197
|
if (!firstList) return null;
|
|
390
198
|
const values = {};
|
|
391
199
|
const loc = {};
|
|
200
|
+
const valueOffsets = {};
|
|
392
201
|
for (const item of firstList.children) {
|
|
393
202
|
const pair = extractListItemKV(item);
|
|
394
203
|
if (pair && !(pair.key in values)) {
|
|
395
204
|
values[pair.key] = pair.value;
|
|
396
205
|
const start = item.position?.start;
|
|
397
206
|
if (start) loc[pair.key] = { line: start.line, column: start.column };
|
|
207
|
+
if (body !== void 0 && pair.valueOffset && body.slice(pair.valueOffset.start, pair.valueOffset.end) === pair.value) {
|
|
208
|
+
valueOffsets[pair.key] = pair.valueOffset;
|
|
209
|
+
}
|
|
398
210
|
}
|
|
399
211
|
}
|
|
400
212
|
const hasRecognizedKey = Object.keys(values).some(
|
|
401
213
|
(k) => RECOGNIZED_METADATA_KEYS.has(k)
|
|
402
214
|
);
|
|
403
|
-
return hasRecognizedKey ? { values, loc } : null;
|
|
215
|
+
return hasRecognizedKey ? { values, loc, valueOffsets } : null;
|
|
404
216
|
}
|
|
405
217
|
var KEY_PATTERN = /^[A-Za-z][A-Za-z0-9 \-_]*$/;
|
|
406
218
|
var SEPARATOR_PATTERN = /^\s*:\s*/;
|
|
@@ -424,59 +236,585 @@ function extractListItemKV(item) {
|
|
|
424
236
|
const valueTail = paragraph.children.slice(2).map((n) => toString(n)).join("");
|
|
425
237
|
const value = (valueHead + valueTail).trim();
|
|
426
238
|
if (value === "") return null;
|
|
427
|
-
|
|
239
|
+
let valueOffset;
|
|
240
|
+
const sepStart = sep3.position?.start.offset;
|
|
241
|
+
if (paragraph.children.length === 2 && typeof sepStart === "number" && valueHead !== "") {
|
|
242
|
+
const sepMatch = SEPARATOR_PATTERN.exec(sep3.value);
|
|
243
|
+
const vStart = sepStart + (sepMatch ? sepMatch[0].length : 0);
|
|
244
|
+
valueOffset = { start: vStart, end: vStart + value.length };
|
|
245
|
+
}
|
|
246
|
+
return { key: normalizeKey(keyNode.value), value, valueOffset };
|
|
428
247
|
}
|
|
429
248
|
if (first.type === "text") {
|
|
430
249
|
const colonIdx = first.value.indexOf(":");
|
|
431
250
|
if (colonIdx === -1) return null;
|
|
432
251
|
const rawKey = first.value.slice(0, colonIdx);
|
|
433
252
|
if (!KEY_PATTERN.test(rawKey)) return null;
|
|
434
|
-
const
|
|
253
|
+
const afterColon = first.value.slice(colonIdx + 1);
|
|
254
|
+
const valueHead = afterColon.replace(/^\s+/, "");
|
|
435
255
|
const valueTail = paragraph.children.slice(1).map((n) => toString(n)).join("");
|
|
436
256
|
const value = (valueHead + valueTail).trim();
|
|
437
257
|
if (value === "") return null;
|
|
438
|
-
|
|
258
|
+
let valueOffset;
|
|
259
|
+
const nodeStart = first.position?.start.offset;
|
|
260
|
+
if (paragraph.children.length === 1 && typeof nodeStart === "number" && valueHead !== "") {
|
|
261
|
+
const leadingWs = afterColon.length - valueHead.length;
|
|
262
|
+
const vStart = nodeStart + colonIdx + 1 + leadingWs;
|
|
263
|
+
valueOffset = { start: vStart, end: vStart + value.length };
|
|
264
|
+
}
|
|
265
|
+
return { key: normalizeKey(rawKey), value, valueOffset };
|
|
439
266
|
}
|
|
440
267
|
return null;
|
|
441
268
|
}
|
|
442
269
|
|
|
443
|
-
// src/core/
|
|
444
|
-
var
|
|
445
|
-
|
|
446
|
-
"
|
|
447
|
-
"
|
|
448
|
-
"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
270
|
+
// src/core/init.ts
|
|
271
|
+
var GETTING_STARTED_URL = "https://knktkc.github.io/madr-lint/guides/getting-started/";
|
|
272
|
+
var ADR_DIR_CANDIDATES = [
|
|
273
|
+
"docs/adr",
|
|
274
|
+
"docs/decisions",
|
|
275
|
+
"doc/adr",
|
|
276
|
+
"adr",
|
|
277
|
+
"docs/architecture/decisions"
|
|
278
|
+
];
|
|
279
|
+
var ADR_FILE_PATTERN = /^\d{4}-.*\.md$/;
|
|
280
|
+
function detectAdrDir(cwd) {
|
|
281
|
+
for (const candidate of ADR_DIR_CANDIDATES) {
|
|
282
|
+
let entries;
|
|
283
|
+
try {
|
|
284
|
+
entries = readdirSync2(resolve2(cwd, candidate), { withFileTypes: true });
|
|
285
|
+
} catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (entries.some((e) => e.isFile() && ADR_FILE_PATTERN.test(e.name))) {
|
|
289
|
+
return { adrDir: candidate, source: "detected" };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { adrDir: "docs/adr", source: "fallback" };
|
|
463
293
|
}
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
294
|
+
function detectFileVersion(content) {
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = parseFile(content);
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
if (parsed.frontmatter) {
|
|
302
|
+
return "decision-makers" in parsed.frontmatter ? "v4" : "v3";
|
|
303
|
+
}
|
|
304
|
+
if (parsed.listMetadata) return "v2";
|
|
305
|
+
return null;
|
|
469
306
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
307
|
+
function detectMadrVersion(cwd, adrDir) {
|
|
308
|
+
const dir = resolve2(cwd, adrDir);
|
|
309
|
+
let entries;
|
|
310
|
+
try {
|
|
311
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
312
|
+
} catch {
|
|
313
|
+
return "auto";
|
|
314
|
+
}
|
|
315
|
+
const sample = entries.filter((e) => e.isFile() && ADR_FILE_PATTERN.test(e.name)).map((e) => e.name).toSorted().slice(0, 20);
|
|
316
|
+
const votes = { v2: 0, v3: 0, v4: 0 };
|
|
317
|
+
for (const name of sample) {
|
|
318
|
+
let content;
|
|
319
|
+
try {
|
|
320
|
+
content = readFileSync2(join2(dir, name), "utf8");
|
|
321
|
+
} catch {
|
|
322
|
+
continue;
|
|
476
323
|
}
|
|
324
|
+
const version = detectFileVersion(content);
|
|
325
|
+
if (version) votes[version]++;
|
|
477
326
|
}
|
|
478
|
-
|
|
479
|
-
|
|
327
|
+
const max = Math.max(votes.v2, votes.v3, votes.v4);
|
|
328
|
+
if (max === 0) return "auto";
|
|
329
|
+
const winners = ["v2", "v3", "v4"].filter(
|
|
330
|
+
(v) => votes[v] === max
|
|
331
|
+
);
|
|
332
|
+
return winners.length === 1 ? winners[0] : "auto";
|
|
333
|
+
}
|
|
334
|
+
var CONFIG_FILENAME_BY_FORMAT = {
|
|
335
|
+
ts: "madr-lint.config.ts",
|
|
336
|
+
json: ".madrlintrc.json"
|
|
337
|
+
};
|
|
338
|
+
function detectConfigFormat(cwd) {
|
|
339
|
+
if (existsSync3(resolve2(cwd, "tsconfig.json"))) return "ts";
|
|
340
|
+
try {
|
|
341
|
+
const pkg2 = JSON.parse(
|
|
342
|
+
readFileSync2(resolve2(cwd, "package.json"), "utf8")
|
|
343
|
+
);
|
|
344
|
+
if (pkg2.dependencies?.["typescript"] !== void 0 || pkg2.devDependencies?.["typescript"] !== void 0) {
|
|
345
|
+
return "ts";
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
return "json";
|
|
350
|
+
}
|
|
351
|
+
function findExistingConfigFile(cwd) {
|
|
352
|
+
for (const name of CONFIG_FILES) {
|
|
353
|
+
if (existsSync3(resolve2(cwd, name))) return name;
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
function tsQuote(value) {
|
|
358
|
+
return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
359
|
+
}
|
|
360
|
+
function renderConfig(format, options) {
|
|
361
|
+
if (format === "json") {
|
|
362
|
+
const config = {
|
|
363
|
+
extends: ["madr-lint:recommended"],
|
|
364
|
+
adrDir: options.adrDir
|
|
365
|
+
};
|
|
366
|
+
if (options.madrVersion !== "auto") config.madrVersion = options.madrVersion;
|
|
367
|
+
return `${JSON.stringify(config, null, 2)}
|
|
368
|
+
`;
|
|
369
|
+
}
|
|
370
|
+
const versionLine = options.madrVersion !== "auto" ? `
|
|
371
|
+
madrVersion: ${tsQuote(options.madrVersion)},` : "";
|
|
372
|
+
return `import { defineConfig } from 'madr-lint';
|
|
373
|
+
|
|
374
|
+
export default defineConfig({
|
|
375
|
+
extends: ['madr-lint:recommended'],
|
|
376
|
+
adrDir: ${tsQuote(options.adrDir)},${versionLine}
|
|
377
|
+
});
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
function buildEpilogue(input) {
|
|
381
|
+
const dirNote = input.adrDirSource === "detected" ? "(detected)" : input.adrDirSource === "override" ? "(from --dir)" : input.filesChecked === 0 ? "(default \u2014 no existing ADRs found, so init created nothing yet; add your first ADR here)" : "(default \u2014 no NNNN-*.md at its top level, though the initial lint found Markdown files nested inside)";
|
|
382
|
+
const lines = [
|
|
383
|
+
`Wrote ${input.configPath}`,
|
|
384
|
+
"",
|
|
385
|
+
` ADR directory: ${input.adrDir} ${dirNote}`,
|
|
386
|
+
"",
|
|
387
|
+
"Next steps:",
|
|
388
|
+
" - Lint your ADRs: npx madr-lint"
|
|
389
|
+
];
|
|
390
|
+
const total = input.errors + input.warnings;
|
|
391
|
+
if (input.filesChecked > 0 && total > 0) {
|
|
392
|
+
lines.push(
|
|
393
|
+
` - The initial lint found ${input.errors} error(s) and ${input.warnings} warning(s) across ${input.filesChecked} file(s).`,
|
|
394
|
+
" Adopting on a legacy repo? Snapshot the debt so only new violations fail the build:",
|
|
395
|
+
" npx madr-lint --update-baseline"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
lines.push(` - Docs: ${GETTING_STARTED_URL}`);
|
|
399
|
+
return lines.join("\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/core/lint.ts
|
|
403
|
+
import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
404
|
+
import { isAbsolute, relative, resolve as resolve3, sep } from "path";
|
|
405
|
+
|
|
406
|
+
// src/core/baseline.ts
|
|
407
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
408
|
+
import { dirname, join as join3 } from "path";
|
|
409
|
+
|
|
410
|
+
// src/core/types.ts
|
|
411
|
+
var INTERNAL_ERROR_RULE_NAME = "core/internal-error";
|
|
412
|
+
function isProjectRule(rule9) {
|
|
413
|
+
return "check" in rule9;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/core/baseline.ts
|
|
417
|
+
var BASELINE_VERSION = 1;
|
|
418
|
+
var BASELINE_DISPLAY_PATH = ".madr-lint/baseline.json";
|
|
419
|
+
function baselinePath(cwd) {
|
|
420
|
+
return join3(cwd, ".madr-lint", "baseline.json");
|
|
421
|
+
}
|
|
422
|
+
function nullProtoMap() {
|
|
423
|
+
return /* @__PURE__ */ Object.create(null);
|
|
424
|
+
}
|
|
425
|
+
function buildBaseline(diagnostics) {
|
|
426
|
+
const entries = nullProtoMap();
|
|
427
|
+
for (const d of diagnostics) {
|
|
428
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) continue;
|
|
429
|
+
const byRule = entries[d.path] ??= nullProtoMap();
|
|
430
|
+
const byMessage = byRule[d.ruleName] ??= nullProtoMap();
|
|
431
|
+
byMessage[d.messageId] = (byMessage[d.messageId] ?? 0) + 1;
|
|
432
|
+
}
|
|
433
|
+
return { version: BASELINE_VERSION, entries };
|
|
434
|
+
}
|
|
435
|
+
function serializeBaseline(baseline) {
|
|
436
|
+
const sortedEntries = nullProtoMap();
|
|
437
|
+
for (const path of Object.keys(baseline.entries).toSorted()) {
|
|
438
|
+
const byRule = baseline.entries[path] ?? {};
|
|
439
|
+
const sortedByRule = nullProtoMap();
|
|
440
|
+
for (const rule9 of Object.keys(byRule).toSorted()) {
|
|
441
|
+
const byMessage = byRule[rule9] ?? {};
|
|
442
|
+
const sortedByMessage = nullProtoMap();
|
|
443
|
+
for (const messageId of Object.keys(byMessage).toSorted()) {
|
|
444
|
+
sortedByMessage[messageId] = byMessage[messageId] ?? 0;
|
|
445
|
+
}
|
|
446
|
+
sortedByRule[rule9] = sortedByMessage;
|
|
447
|
+
}
|
|
448
|
+
sortedEntries[path] = sortedByRule;
|
|
449
|
+
}
|
|
450
|
+
const ordered = { version: baseline.version, entries: sortedEntries };
|
|
451
|
+
return JSON.stringify(ordered, null, 2) + "\n";
|
|
452
|
+
}
|
|
453
|
+
function loadBaseline(path) {
|
|
454
|
+
if (!existsSync4(path)) return null;
|
|
455
|
+
try {
|
|
456
|
+
const data = JSON.parse(readFileSync3(path, "utf8"));
|
|
457
|
+
if (typeof data.entries !== "object" || data.entries === null) return null;
|
|
458
|
+
return {
|
|
459
|
+
version: typeof data.version === "number" ? data.version : BASELINE_VERSION,
|
|
460
|
+
entries: rebuildEntries(data.entries)
|
|
461
|
+
};
|
|
462
|
+
} catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function rebuildEntries(raw) {
|
|
467
|
+
const entries = nullProtoMap();
|
|
468
|
+
for (const [path, rawByRule] of Object.entries(raw)) {
|
|
469
|
+
if (typeof rawByRule !== "object" || rawByRule === null) continue;
|
|
470
|
+
const byRule = nullProtoMap();
|
|
471
|
+
for (const [rule9, rawByMessage] of Object.entries(rawByRule)) {
|
|
472
|
+
if (typeof rawByMessage !== "object" || rawByMessage === null) continue;
|
|
473
|
+
const byMessage = nullProtoMap();
|
|
474
|
+
for (const [messageId, count] of Object.entries(
|
|
475
|
+
rawByMessage
|
|
476
|
+
)) {
|
|
477
|
+
if (typeof count === "number") byMessage[messageId] = count;
|
|
478
|
+
}
|
|
479
|
+
byRule[rule9] = byMessage;
|
|
480
|
+
}
|
|
481
|
+
entries[path] = byRule;
|
|
482
|
+
}
|
|
483
|
+
return entries;
|
|
484
|
+
}
|
|
485
|
+
function writeBaseline(path, baseline) {
|
|
486
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
487
|
+
writeFileSync(path, serializeBaseline(baseline), "utf8");
|
|
488
|
+
}
|
|
489
|
+
var KEY_SEP = "\0";
|
|
490
|
+
function fingerprint(d) {
|
|
491
|
+
return d.path + KEY_SEP + d.ruleName + KEY_SEP + d.messageId;
|
|
492
|
+
}
|
|
493
|
+
function applyBaseline(diagnostics, baseline) {
|
|
494
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
495
|
+
for (const [path, byRule] of Object.entries(baseline.entries)) {
|
|
496
|
+
for (const [rule9, byMessage] of Object.entries(byRule)) {
|
|
497
|
+
if (rule9 === INTERNAL_ERROR_RULE_NAME) continue;
|
|
498
|
+
for (const [messageId, count] of Object.entries(byMessage)) {
|
|
499
|
+
remaining.set(path + KEY_SEP + rule9 + KEY_SEP + messageId, count);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const kept = [];
|
|
504
|
+
let hidden = 0;
|
|
505
|
+
for (const d of diagnostics) {
|
|
506
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) {
|
|
507
|
+
kept.push(d);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const key = fingerprint(d);
|
|
511
|
+
const left = remaining.get(key);
|
|
512
|
+
if (left !== void 0 && left > 0) {
|
|
513
|
+
remaining.set(key, left - 1);
|
|
514
|
+
hidden++;
|
|
515
|
+
} else {
|
|
516
|
+
kept.push(d);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { kept, hidden };
|
|
520
|
+
}
|
|
521
|
+
function totalCount(baseline) {
|
|
522
|
+
let total = 0;
|
|
523
|
+
for (const byRule of Object.values(baseline.entries)) {
|
|
524
|
+
for (const byMessage of Object.values(byRule)) {
|
|
525
|
+
for (const count of Object.values(byMessage)) total += count;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return total;
|
|
529
|
+
}
|
|
530
|
+
function baselineWriteSummary(baseline) {
|
|
531
|
+
const violations = totalCount(baseline);
|
|
532
|
+
const files = Object.keys(baseline.entries).length;
|
|
533
|
+
const v = `${violations} ${violations === 1 ? "violation" : "violations"}`;
|
|
534
|
+
const f = `${files} ${files === 1 ? "file" : "files"}`;
|
|
535
|
+
return `Wrote ${v} across ${f} to ${BASELINE_DISPLAY_PATH}`;
|
|
536
|
+
}
|
|
537
|
+
function baselineMalformedWarning() {
|
|
538
|
+
return `madr-lint: warning: ${BASELINE_DISPLAY_PATH} is malformed; ignoring it (run --update-baseline to regenerate)`;
|
|
539
|
+
}
|
|
540
|
+
function baselineHiddenSummary(hidden) {
|
|
541
|
+
return `${hidden} ${hidden === 1 ? "problem" : "problems"} hidden by baseline (${BASELINE_DISPLAY_PATH})`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/core/cache.ts
|
|
545
|
+
import { createHash } from "crypto";
|
|
546
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
547
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
548
|
+
var CACHE_SCHEMA_VERSION = 3;
|
|
549
|
+
function computeContentHash(content) {
|
|
550
|
+
return createHash("sha1").update(content, "utf8").digest("hex");
|
|
551
|
+
}
|
|
552
|
+
function computeConfigHash(config) {
|
|
553
|
+
return createHash("sha1").update(stableStringify(config), "utf8").digest("hex");
|
|
554
|
+
}
|
|
555
|
+
function stableStringify(value) {
|
|
556
|
+
if (value === null || typeof value !== "object") {
|
|
557
|
+
return JSON.stringify(value);
|
|
558
|
+
}
|
|
559
|
+
if (Array.isArray(value)) {
|
|
560
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
561
|
+
}
|
|
562
|
+
const entries = Object.entries(value).toSorted(
|
|
563
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
564
|
+
);
|
|
565
|
+
return "{" + entries.map(([k, v]) => JSON.stringify(k) + ":" + stableStringify(v)).join(",") + "}";
|
|
566
|
+
}
|
|
567
|
+
function manifestPath(cacheDir) {
|
|
568
|
+
return join4(cacheDir, "manifest.json");
|
|
569
|
+
}
|
|
570
|
+
function loadManifest(path) {
|
|
571
|
+
if (!existsSync5(path)) return null;
|
|
572
|
+
try {
|
|
573
|
+
const data = JSON.parse(readFileSync4(path, "utf8"));
|
|
574
|
+
if (
|
|
575
|
+
// Single invalidation point for schema drift: an unknown (or absent)
|
|
576
|
+
// schema version nulls here, so every consumer sees a cold cache — no
|
|
577
|
+
// scattered re-hydration of stale-shape entries downstream.
|
|
578
|
+
data.schemaVersion !== CACHE_SCHEMA_VERSION || typeof data.version !== "string" || typeof data.configHash !== "string" || typeof data.files !== "object" || data.files === null
|
|
579
|
+
) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
return data;
|
|
583
|
+
} catch {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function saveManifest(path, manifest) {
|
|
588
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
589
|
+
writeFileSync2(path, JSON.stringify(manifest), "utf8");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/core/fix.ts
|
|
593
|
+
var MAX_FIX_PASSES = 10;
|
|
594
|
+
function makeFixer(base) {
|
|
595
|
+
const shift = (n) => n + base;
|
|
596
|
+
return {
|
|
597
|
+
replaceRange(range, text) {
|
|
598
|
+
return { range: [shift(range[0]), shift(range[1])], text };
|
|
599
|
+
},
|
|
600
|
+
insertAt(offset, text) {
|
|
601
|
+
const at = shift(offset);
|
|
602
|
+
return { range: [at, at], text };
|
|
603
|
+
},
|
|
604
|
+
remove(range) {
|
|
605
|
+
return { range: [shift(range[0]), shift(range[1])], text: "" };
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function applyEditsCounted(content, edits) {
|
|
610
|
+
const valid = edits.filter((e) => {
|
|
611
|
+
const [start, end] = e.range;
|
|
612
|
+
return Number.isInteger(start) && Number.isInteger(end) && start >= 0 && end >= start && end <= content.length;
|
|
613
|
+
});
|
|
614
|
+
const sorted = valid.toSorted(
|
|
615
|
+
(a, b) => a.range[0] - b.range[0] || a.range[1] - b.range[1]
|
|
616
|
+
);
|
|
617
|
+
let out = "";
|
|
618
|
+
let cursor = 0;
|
|
619
|
+
let applied = 0;
|
|
620
|
+
for (const edit of sorted) {
|
|
621
|
+
const [start, end] = edit.range;
|
|
622
|
+
if (start < cursor) continue;
|
|
623
|
+
out += content.slice(cursor, start) + edit.text;
|
|
624
|
+
cursor = end;
|
|
625
|
+
applied++;
|
|
626
|
+
}
|
|
627
|
+
return { text: out + content.slice(cursor), applied };
|
|
628
|
+
}
|
|
629
|
+
function collectFixes(diagnostics, content) {
|
|
630
|
+
const fixer = makeFixer(frontmatterOffset(content));
|
|
631
|
+
const edits = [];
|
|
632
|
+
for (const d of diagnostics) {
|
|
633
|
+
if (!d.fix) continue;
|
|
634
|
+
const result = d.fix(fixer);
|
|
635
|
+
if (result === null) continue;
|
|
636
|
+
if (Array.isArray(result)) edits.push(...result);
|
|
637
|
+
else edits.push(result);
|
|
638
|
+
}
|
|
639
|
+
return edits;
|
|
640
|
+
}
|
|
641
|
+
function collectProjectFixes(diagnostics, contentByPath) {
|
|
642
|
+
const fixer = makeFixer(0);
|
|
643
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
644
|
+
for (const d of diagnostics) {
|
|
645
|
+
if (!d.fix) continue;
|
|
646
|
+
if (byPath.has(d.path)) continue;
|
|
647
|
+
if (contentByPath(d.path) === void 0) continue;
|
|
648
|
+
const result = d.fix(fixer);
|
|
649
|
+
if (result === null) continue;
|
|
650
|
+
byPath.set(d.path, Array.isArray(result) ? [...result] : [result]);
|
|
651
|
+
}
|
|
652
|
+
return byPath;
|
|
653
|
+
}
|
|
654
|
+
function fixFileContent(originalContent, lint, maxPasses = MAX_FIX_PASSES) {
|
|
655
|
+
let content = originalContent;
|
|
656
|
+
let passes = 0;
|
|
657
|
+
let applied = 0;
|
|
658
|
+
for (; ; ) {
|
|
659
|
+
const diagnostics = lint(content);
|
|
660
|
+
const done = () => ({
|
|
661
|
+
fixedContent: content,
|
|
662
|
+
remaining: diagnostics,
|
|
663
|
+
changed: content !== originalContent,
|
|
664
|
+
passes,
|
|
665
|
+
applied
|
|
666
|
+
});
|
|
667
|
+
if (passes >= maxPasses) return done();
|
|
668
|
+
const edits = collectFixes(diagnostics, content);
|
|
669
|
+
if (edits.length === 0) return done();
|
|
670
|
+
const { text: next, applied: landed } = applyEditsCounted(content, edits);
|
|
671
|
+
if (next === content) return done();
|
|
672
|
+
applied += landed;
|
|
673
|
+
content = next;
|
|
674
|
+
passes++;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
var CONTEXT = 3;
|
|
678
|
+
function unifiedDiff(path, before, after) {
|
|
679
|
+
if (before === after) return "";
|
|
680
|
+
const a = splitLines(before);
|
|
681
|
+
const b = splitLines(after);
|
|
682
|
+
const ops = diffLines(a, b);
|
|
683
|
+
const hunks = groupHunks(ops);
|
|
684
|
+
if (hunks.length === 0) return "";
|
|
685
|
+
const lines = [`--- a/${path}`, `+++ b/${path}`];
|
|
686
|
+
for (const hunk of hunks) lines.push(...renderHunk(hunk));
|
|
687
|
+
return lines.join("\n") + "\n";
|
|
688
|
+
}
|
|
689
|
+
function splitLines(text) {
|
|
690
|
+
if (text === "") return [];
|
|
691
|
+
const parts = text.split("\n");
|
|
692
|
+
if (parts[parts.length - 1] === "") parts.pop();
|
|
693
|
+
return parts;
|
|
694
|
+
}
|
|
695
|
+
function diffLines(a, b) {
|
|
696
|
+
const n = a.length;
|
|
697
|
+
const m = b.length;
|
|
698
|
+
const lcs = Array.from(
|
|
699
|
+
{ length: n + 1 },
|
|
700
|
+
() => Array.from({ length: m + 1 }).fill(0)
|
|
701
|
+
);
|
|
702
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
703
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
704
|
+
lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const ops = [];
|
|
708
|
+
let i = 0;
|
|
709
|
+
let j = 0;
|
|
710
|
+
while (i < n && j < m) {
|
|
711
|
+
if (a[i] === b[j]) {
|
|
712
|
+
ops.push({ kind: "eq", text: a[i] });
|
|
713
|
+
i++;
|
|
714
|
+
j++;
|
|
715
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
716
|
+
ops.push({ kind: "del", text: a[i] });
|
|
717
|
+
i++;
|
|
718
|
+
} else {
|
|
719
|
+
ops.push({ kind: "ins", text: b[j] });
|
|
720
|
+
j++;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
while (i < n) ops.push({ kind: "del", text: a[i++] });
|
|
724
|
+
while (j < m) ops.push({ kind: "ins", text: b[j++] });
|
|
725
|
+
return ops;
|
|
726
|
+
}
|
|
727
|
+
function groupHunks(ops) {
|
|
728
|
+
const changeIdx = [];
|
|
729
|
+
ops.forEach((op, idx) => {
|
|
730
|
+
if (op.kind !== "eq") changeIdx.push(idx);
|
|
731
|
+
});
|
|
732
|
+
if (changeIdx.length === 0) return [];
|
|
733
|
+
const windows = [];
|
|
734
|
+
for (const idx of changeIdx) {
|
|
735
|
+
const start = Math.max(0, idx - CONTEXT);
|
|
736
|
+
const end = Math.min(ops.length - 1, idx + CONTEXT);
|
|
737
|
+
const last = windows[windows.length - 1];
|
|
738
|
+
if (last && start <= last[1] + 1) last[1] = Math.max(last[1], end);
|
|
739
|
+
else windows.push([start, end]);
|
|
740
|
+
}
|
|
741
|
+
const hunks = [];
|
|
742
|
+
for (const [start, end] of windows) {
|
|
743
|
+
let aLine = 1;
|
|
744
|
+
let bLine = 1;
|
|
745
|
+
for (let k = 0; k < start; k++) {
|
|
746
|
+
if (ops[k].kind !== "ins") aLine++;
|
|
747
|
+
if (ops[k].kind !== "del") bLine++;
|
|
748
|
+
}
|
|
749
|
+
const hunkOps = ops.slice(start, end + 1);
|
|
750
|
+
let aCount = 0;
|
|
751
|
+
let bCount = 0;
|
|
752
|
+
for (const op of hunkOps) {
|
|
753
|
+
if (op.kind !== "ins") aCount++;
|
|
754
|
+
if (op.kind !== "del") bCount++;
|
|
755
|
+
}
|
|
756
|
+
hunks.push({ aStart: aLine, aCount, bStart: bLine, bCount, ops: hunkOps });
|
|
757
|
+
}
|
|
758
|
+
return hunks;
|
|
759
|
+
}
|
|
760
|
+
function renderHunk(hunk) {
|
|
761
|
+
const header = `@@ -${hunk.aStart},${hunk.aCount} +${hunk.bStart},${hunk.bCount} @@`;
|
|
762
|
+
const body = hunk.ops.map((op) => {
|
|
763
|
+
const prefix = op.kind === "eq" ? " " : op.kind === "del" ? "-" : "+";
|
|
764
|
+
return `${prefix}${op.text}`;
|
|
765
|
+
});
|
|
766
|
+
return [header, ...body];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/core/runner.ts
|
|
770
|
+
import Ajv from "ajv";
|
|
771
|
+
|
|
772
|
+
// src/core/interpolate.ts
|
|
773
|
+
function interpolate(template, data) {
|
|
774
|
+
if (!template.includes("{{")) return template;
|
|
775
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
776
|
+
const v = data[key];
|
|
777
|
+
return v !== void 0 ? String(v) : `{{${key}}}`;
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/core/suppression.ts
|
|
782
|
+
var DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
|
|
783
|
+
"disable-file",
|
|
784
|
+
"disable",
|
|
785
|
+
"enable",
|
|
786
|
+
"disable-next-line"
|
|
787
|
+
]);
|
|
788
|
+
var DIRECTIVE_PREFIX = "madr-lint-";
|
|
789
|
+
function parseDirective(value, line) {
|
|
790
|
+
const trimmed = value.trim();
|
|
791
|
+
if (!trimmed.startsWith("<!--") || !trimmed.endsWith("-->")) return null;
|
|
792
|
+
const inner = trimmed.slice(4, -3).trim();
|
|
793
|
+
if (inner.includes("<!--") || inner.includes("-->")) return null;
|
|
794
|
+
if (!inner.startsWith(DIRECTIVE_PREFIX)) return null;
|
|
795
|
+
const afterPrefix = inner.slice(DIRECTIVE_PREFIX.length);
|
|
796
|
+
const wsIndex = afterPrefix.search(/\s/);
|
|
797
|
+
const keyword = wsIndex === -1 ? afterPrefix : afterPrefix.slice(0, wsIndex);
|
|
798
|
+
if (!DIRECTIVE_KINDS.has(keyword)) return null;
|
|
799
|
+
const rest = wsIndex === -1 ? "" : afterPrefix.slice(wsIndex);
|
|
800
|
+
return { kind: keyword, rules: parseRuleList(rest), line };
|
|
801
|
+
}
|
|
802
|
+
function parseRuleList(rest) {
|
|
803
|
+
const trimmed = rest.trim();
|
|
804
|
+
if (trimmed === "") return null;
|
|
805
|
+
const rules = trimmed.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
806
|
+
return rules.length > 0 ? rules : null;
|
|
807
|
+
}
|
|
808
|
+
function walkHtml(node, out) {
|
|
809
|
+
if (node.type === "html") {
|
|
810
|
+
const line = node.position?.start.line;
|
|
811
|
+
if (typeof line === "number") {
|
|
812
|
+
const directive = parseDirective(node.value, line);
|
|
813
|
+
if (directive) out.push(directive);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if ("children" in node && Array.isArray(node.children)) {
|
|
817
|
+
for (const child of node.children) walkHtml(child, out);
|
|
480
818
|
}
|
|
481
819
|
}
|
|
482
820
|
function collectDirectives(ast, body) {
|
|
@@ -563,6 +901,7 @@ function getValidator(schema) {
|
|
|
563
901
|
}
|
|
564
902
|
return validator;
|
|
565
903
|
}
|
|
904
|
+
var INTERNAL_ERROR_DOCS_URL = "https://github.com/knktkc/madr-lint";
|
|
566
905
|
var RuleOptionsError = class extends Error {
|
|
567
906
|
constructor(ruleName, ajvErrors) {
|
|
568
907
|
super(`Invalid options for rule ${ruleName}: ${ajvErrors}`);
|
|
@@ -593,6 +932,9 @@ var PerFileRuleContext = class {
|
|
|
593
932
|
get metadataLoc() {
|
|
594
933
|
return this.ensureParsed().metadataLoc;
|
|
595
934
|
}
|
|
935
|
+
get metadataValueLoc() {
|
|
936
|
+
return this.ensureParsed().metadataValueLoc;
|
|
937
|
+
}
|
|
596
938
|
};
|
|
597
939
|
function runRulesOnFile(rules, file, runtime = {}) {
|
|
598
940
|
const diagnostics = [];
|
|
@@ -617,15 +959,29 @@ function runRulesOnFile(rules, file, runtime = {}) {
|
|
|
617
959
|
throw new RuleOptionsError(rule9.meta.name, ajv.errorsText(validator.errors));
|
|
618
960
|
}
|
|
619
961
|
}
|
|
962
|
+
const ruleName = rule9.meta.name;
|
|
963
|
+
const suggestions = rule9.meta.suggestions;
|
|
964
|
+
const docsUrl = rule9.meta.docs.url ?? "";
|
|
965
|
+
const fixableRule = rule9.meta.fixable === "code";
|
|
620
966
|
const context = new PerFileRuleContext(
|
|
621
967
|
file,
|
|
622
968
|
mergedOptions,
|
|
623
969
|
ensureParsed,
|
|
624
970
|
(d) => {
|
|
971
|
+
const template = suggestions?.[d.messageId];
|
|
972
|
+
const hasFix = d.fix !== void 0;
|
|
973
|
+
if (hasFix && !fixableRule) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
`Rule "${ruleName}" attached a fix but meta.fixable is not 'code'`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
625
978
|
diagnostics.push({
|
|
626
|
-
ruleName
|
|
979
|
+
ruleName,
|
|
627
980
|
severity,
|
|
628
981
|
path: file.path,
|
|
982
|
+
suggestion: template === void 0 ? null : interpolate(template, d.data ?? {}),
|
|
983
|
+
docsUrl,
|
|
984
|
+
fixable: hasFix,
|
|
629
985
|
...d
|
|
630
986
|
});
|
|
631
987
|
}
|
|
@@ -660,6 +1016,9 @@ function internalErrorDiagnostic(ruleName, operation, err, path) {
|
|
|
660
1016
|
messageId: "ruleThrew",
|
|
661
1017
|
severity: "error",
|
|
662
1018
|
path,
|
|
1019
|
+
suggestion: null,
|
|
1020
|
+
docsUrl: INTERNAL_ERROR_DOCS_URL,
|
|
1021
|
+
fixable: false,
|
|
663
1022
|
data: {
|
|
664
1023
|
rule: ruleName,
|
|
665
1024
|
operation,
|
|
@@ -734,14 +1093,28 @@ function runRulesOnProject(rules, files, runtime = {}) {
|
|
|
734
1093
|
);
|
|
735
1094
|
}
|
|
736
1095
|
}
|
|
1096
|
+
const ruleName = rule9.meta.name;
|
|
1097
|
+
const suggestions = rule9.meta.suggestions;
|
|
1098
|
+
const docsUrl = rule9.meta.docs.url ?? "";
|
|
1099
|
+
const fixableRule = rule9.meta.fixable === "code";
|
|
737
1100
|
const context = {
|
|
738
1101
|
files,
|
|
739
1102
|
options: mergedOptions,
|
|
740
1103
|
fileExists: runtime.fileExists,
|
|
741
1104
|
report(d) {
|
|
1105
|
+
const template = suggestions?.[d.messageId];
|
|
1106
|
+
const hasFix = d.fix !== void 0;
|
|
1107
|
+
if (hasFix && !fixableRule) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`Rule "${ruleName}" attached a fix but meta.fixable is not 'code'`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
742
1112
|
diagnostics.push({
|
|
743
|
-
ruleName
|
|
1113
|
+
ruleName,
|
|
744
1114
|
severity,
|
|
1115
|
+
suggestion: template === void 0 ? null : interpolate(template, d.data ?? {}),
|
|
1116
|
+
docsUrl,
|
|
1117
|
+
fixable: hasFix,
|
|
745
1118
|
...d
|
|
746
1119
|
});
|
|
747
1120
|
}
|
|
@@ -766,7 +1139,7 @@ function lintFiles(opts) {
|
|
|
766
1139
|
const allDiagnostics = [];
|
|
767
1140
|
const fileEntries = opts.files.map((absolutePath) => ({
|
|
768
1141
|
relativePath: toPosix(relative(opts.cwd, absolutePath)),
|
|
769
|
-
content:
|
|
1142
|
+
content: readFileSync5(absolutePath, "utf8")
|
|
770
1143
|
}));
|
|
771
1144
|
const perFileRules = [];
|
|
772
1145
|
const projectRules = [];
|
|
@@ -784,6 +1157,7 @@ function lintFiles(opts) {
|
|
|
784
1157
|
manifest = loaded;
|
|
785
1158
|
} else {
|
|
786
1159
|
manifest = {
|
|
1160
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
787
1161
|
version: opts.cache.pkgVersion,
|
|
788
1162
|
configHash: opts.cache.configHash,
|
|
789
1163
|
files: {}
|
|
@@ -829,55 +1203,14 @@ function lintFiles(opts) {
|
|
|
829
1203
|
const projectFiles = fileEntries.map(
|
|
830
1204
|
(f) => buildProjectFile({ path: f.relativePath, content: f.content })
|
|
831
1205
|
);
|
|
832
|
-
const projectRoot = resolve2(opts.cwd);
|
|
833
|
-
const existsCache = /* @__PURE__ */ new Map();
|
|
834
|
-
const fileExists = (resolvedPath) => {
|
|
835
|
-
const cached = existsCache.get(resolvedPath);
|
|
836
|
-
if (cached !== void 0) return cached;
|
|
837
|
-
const abs = resolve2(projectRoot, resolvedPath);
|
|
838
|
-
const rel = relative(projectRoot, abs);
|
|
839
|
-
const escapesRoot = rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
840
|
-
let result;
|
|
841
|
-
if (escapesRoot) {
|
|
842
|
-
result = false;
|
|
843
|
-
} else {
|
|
844
|
-
try {
|
|
845
|
-
result = statSync2(abs, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
846
|
-
} catch {
|
|
847
|
-
result = false;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
existsCache.set(resolvedPath, result);
|
|
851
|
-
return result;
|
|
852
|
-
};
|
|
853
|
-
const errorRules = [];
|
|
854
|
-
const warnRules = [];
|
|
855
|
-
for (const rule9 of enabledProjectRules) {
|
|
856
|
-
const sev = resolveSeverity(opts.ruleSeverity[rule9.meta.name]);
|
|
857
|
-
if (sev === "error") errorRules.push(rule9);
|
|
858
|
-
else if (sev === "warn") warnRules.push(rule9);
|
|
859
|
-
}
|
|
860
|
-
const projectDiagnostics = [];
|
|
861
|
-
if (errorRules.length > 0) {
|
|
862
|
-
projectDiagnostics.push(
|
|
863
|
-
...runRulesOnProject(errorRules, projectFiles, {
|
|
864
|
-
severity: "error",
|
|
865
|
-
fileExists,
|
|
866
|
-
optionsByRule
|
|
867
|
-
})
|
|
868
|
-
);
|
|
869
|
-
}
|
|
870
|
-
if (warnRules.length > 0) {
|
|
871
|
-
projectDiagnostics.push(
|
|
872
|
-
...runRulesOnProject(warnRules, projectFiles, {
|
|
873
|
-
severity: "warn",
|
|
874
|
-
fileExists,
|
|
875
|
-
optionsByRule
|
|
876
|
-
})
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
1206
|
allDiagnostics.push(
|
|
880
|
-
...
|
|
1207
|
+
...runProjectPass(
|
|
1208
|
+
projectFiles,
|
|
1209
|
+
enabledProjectRules,
|
|
1210
|
+
opts.ruleSeverity,
|
|
1211
|
+
optionsByRule,
|
|
1212
|
+
opts.cwd
|
|
1213
|
+
)
|
|
881
1214
|
);
|
|
882
1215
|
}
|
|
883
1216
|
if (manifest && opts.cache) {
|
|
@@ -899,6 +1232,109 @@ function lintFiles(opts) {
|
|
|
899
1232
|
baselineHidden: applied.hidden
|
|
900
1233
|
};
|
|
901
1234
|
}
|
|
1235
|
+
function lintAndFix(opts) {
|
|
1236
|
+
const perFileRules = [];
|
|
1237
|
+
const projectRules = [];
|
|
1238
|
+
for (const rule9 of opts.rules) {
|
|
1239
|
+
if (isProjectRule(rule9)) projectRules.push(rule9);
|
|
1240
|
+
else perFileRules.push(rule9);
|
|
1241
|
+
}
|
|
1242
|
+
const optionsByRule = extractOptionsByRule(opts.ruleSeverity);
|
|
1243
|
+
const baseline = opts.baseline ?? null;
|
|
1244
|
+
const files = [];
|
|
1245
|
+
const remaining = [];
|
|
1246
|
+
let fixed = 0;
|
|
1247
|
+
let baselineHidden = 0;
|
|
1248
|
+
for (const absPath of opts.files) {
|
|
1249
|
+
const relativePath = toPosix(relative(opts.cwd, absPath));
|
|
1250
|
+
const original = readFileSync5(absPath, "utf8");
|
|
1251
|
+
let lastHidden = 0;
|
|
1252
|
+
const lint = (content) => {
|
|
1253
|
+
const raw = runPerFileRulesForFile(
|
|
1254
|
+
perFileRules,
|
|
1255
|
+
opts.ruleSeverity,
|
|
1256
|
+
optionsByRule,
|
|
1257
|
+
{ relativePath, content }
|
|
1258
|
+
);
|
|
1259
|
+
if (!baseline) {
|
|
1260
|
+
lastHidden = 0;
|
|
1261
|
+
return raw;
|
|
1262
|
+
}
|
|
1263
|
+
const sub = applyBaseline(raw, baseline);
|
|
1264
|
+
lastHidden = sub.hidden;
|
|
1265
|
+
return sub.kept;
|
|
1266
|
+
};
|
|
1267
|
+
const res = fixFileContent(original, lint, opts.maxPasses);
|
|
1268
|
+
files.push({
|
|
1269
|
+
path: relativePath,
|
|
1270
|
+
absPath,
|
|
1271
|
+
original,
|
|
1272
|
+
fixed: res.fixedContent,
|
|
1273
|
+
changed: res.changed
|
|
1274
|
+
});
|
|
1275
|
+
remaining.push(...res.remaining);
|
|
1276
|
+
fixed += res.applied;
|
|
1277
|
+
baselineHidden += lastHidden;
|
|
1278
|
+
}
|
|
1279
|
+
const enabledProjectRules = projectRules.filter(
|
|
1280
|
+
(rule9) => resolveSeverity(opts.ruleSeverity[rule9.meta.name]) !== "off"
|
|
1281
|
+
);
|
|
1282
|
+
if (enabledProjectRules.length > 0) {
|
|
1283
|
+
const contentByPath = new Map(files.map((f) => [f.path, f.fixed]));
|
|
1284
|
+
const maxPasses = opts.maxPasses ?? MAX_FIX_PASSES;
|
|
1285
|
+
let projectDiags = [];
|
|
1286
|
+
let passes = 0;
|
|
1287
|
+
for (; ; ) {
|
|
1288
|
+
const projectFiles = files.map(
|
|
1289
|
+
(f) => buildProjectFile({ path: f.path, content: contentByPath.get(f.path) ?? f.fixed })
|
|
1290
|
+
);
|
|
1291
|
+
let diags = runProjectPass(
|
|
1292
|
+
projectFiles,
|
|
1293
|
+
enabledProjectRules,
|
|
1294
|
+
opts.ruleSeverity,
|
|
1295
|
+
optionsByRule,
|
|
1296
|
+
opts.cwd
|
|
1297
|
+
);
|
|
1298
|
+
let hiddenThisPass = 0;
|
|
1299
|
+
if (baseline) {
|
|
1300
|
+
const sub = applyBaseline(diags, baseline);
|
|
1301
|
+
diags = sub.kept;
|
|
1302
|
+
hiddenThisPass = sub.hidden;
|
|
1303
|
+
}
|
|
1304
|
+
projectDiags = diags;
|
|
1305
|
+
if (passes >= maxPasses) {
|
|
1306
|
+
baselineHidden += hiddenThisPass;
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
const fixesByPath = collectProjectFixes(diags, (p) => contentByPath.get(p));
|
|
1310
|
+
let progressed = false;
|
|
1311
|
+
for (const [path, edits] of fixesByPath) {
|
|
1312
|
+
const before = contentByPath.get(path);
|
|
1313
|
+
if (before === void 0) continue;
|
|
1314
|
+
const { text, applied } = applyEditsCounted(before, edits);
|
|
1315
|
+
if (text !== before) {
|
|
1316
|
+
contentByPath.set(path, text);
|
|
1317
|
+
fixed += applied;
|
|
1318
|
+
progressed = true;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (!progressed) {
|
|
1322
|
+
baselineHidden += hiddenThisPass;
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
passes++;
|
|
1326
|
+
}
|
|
1327
|
+
for (const f of files) {
|
|
1328
|
+
const c = contentByPath.get(f.path);
|
|
1329
|
+
if (c !== void 0 && c !== f.fixed) {
|
|
1330
|
+
f.fixed = c;
|
|
1331
|
+
f.changed = f.fixed !== f.original;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
remaining.push(...projectDiags);
|
|
1335
|
+
}
|
|
1336
|
+
return { files, diagnostics: remaining, fixed, baselineHidden };
|
|
1337
|
+
}
|
|
902
1338
|
function runPerFileRulesForFile(perFileRules, ruleSeverity, optionsByRule, file) {
|
|
903
1339
|
const fileContext = {
|
|
904
1340
|
content: file.content,
|
|
@@ -907,221 +1343,114 @@ function runPerFileRulesForFile(perFileRules, ruleSeverity, optionsByRule, file)
|
|
|
907
1343
|
const errorRules = [];
|
|
908
1344
|
const warnRules = [];
|
|
909
1345
|
for (const rule9 of perFileRules) {
|
|
910
|
-
const sev = resolveSeverity(ruleSeverity[rule9.meta.name]);
|
|
911
|
-
if (sev === "off") continue;
|
|
912
|
-
if (sev === "error") errorRules.push(rule9);
|
|
913
|
-
else warnRules.push(rule9);
|
|
914
|
-
}
|
|
915
|
-
const diagnostics = [];
|
|
916
|
-
if (errorRules.length > 0) {
|
|
917
|
-
diagnostics.push(
|
|
918
|
-
...runRulesOnFile(errorRules, fileContext, {
|
|
919
|
-
severity: "error",
|
|
920
|
-
optionsByRule
|
|
921
|
-
})
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
if (warnRules.length > 0) {
|
|
925
|
-
diagnostics.push(
|
|
926
|
-
...runRulesOnFile(warnRules, fileContext, {
|
|
927
|
-
severity: "warn",
|
|
928
|
-
optionsByRule
|
|
929
|
-
})
|
|
930
|
-
);
|
|
931
|
-
}
|
|
932
|
-
return diagnostics;
|
|
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
|
-
}
|
|
953
|
-
function resolveSeverity(config) {
|
|
954
|
-
if (config === void 0) return "off";
|
|
955
|
-
if (typeof config === "string") return config;
|
|
956
|
-
return config[0];
|
|
957
|
-
}
|
|
958
|
-
function extractOptionsByRule(ruleSeverity) {
|
|
959
|
-
const map = {};
|
|
960
|
-
for (const [name, config] of Object.entries(ruleSeverity)) {
|
|
961
|
-
if (Array.isArray(config) && config[1]) map[name] = config[1];
|
|
962
|
-
}
|
|
963
|
-
return map;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
// src/core/reporter.ts
|
|
967
|
-
import pc from "picocolors";
|
|
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.
|
|
971
|
-
format(diagnostics, rulesByName) {
|
|
972
|
-
if (diagnostics.length === 0) {
|
|
973
|
-
return pc.green("\u2713 All clear.");
|
|
974
|
-
}
|
|
975
|
-
const byFile = groupByFile(diagnostics);
|
|
976
|
-
const lines = [];
|
|
977
|
-
for (const [file, fileDiagnostics] of byFile) {
|
|
978
|
-
lines.push(pc.underline(file));
|
|
979
|
-
for (const d of fileDiagnostics) {
|
|
980
|
-
const sev = d.severity === "error" ? pc.red(" error ") : pc.yellow(" warn ");
|
|
981
|
-
const ruleId = pc.dim(d.ruleName.padEnd(28));
|
|
982
|
-
const message = renderMessage(d, rulesByName);
|
|
983
|
-
lines.push(`${sev}${ruleId} ${message}`);
|
|
984
|
-
}
|
|
985
|
-
lines.push("");
|
|
986
|
-
}
|
|
987
|
-
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
988
|
-
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
989
|
-
lines.push(pc.bold(formatSummary(errors, warnings)));
|
|
990
|
-
return lines.join("\n");
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
|
-
function groupByFile(diagnostics) {
|
|
994
|
-
const map = /* @__PURE__ */ new Map();
|
|
995
|
-
for (const d of diagnostics) {
|
|
996
|
-
if (!map.has(d.path)) map.set(d.path, []);
|
|
997
|
-
map.get(d.path)?.push(d);
|
|
998
|
-
}
|
|
999
|
-
return map;
|
|
1000
|
-
}
|
|
1001
|
-
function renderMessage(d, rulesByName) {
|
|
1002
|
-
const rule9 = rulesByName.get(d.ruleName);
|
|
1003
|
-
const template = rule9?.meta.messages[d.messageId] ?? d.messageId;
|
|
1004
|
-
return interpolate(template, d.data ?? {});
|
|
1005
|
-
}
|
|
1006
|
-
function interpolate(template, data) {
|
|
1007
|
-
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
1008
|
-
const v = data[key];
|
|
1009
|
-
return v !== void 0 ? String(v) : `{{${key}}}`;
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
function formatSummary(errors, warnings) {
|
|
1013
|
-
const parts = [];
|
|
1014
|
-
if (errors > 0) parts.push(`${errors} ${errors === 1 ? "error" : "errors"}`);
|
|
1015
|
-
if (warnings > 0) parts.push(`${warnings} ${warnings === 1 ? "warning" : "warnings"}`);
|
|
1016
|
-
return parts.join(", ");
|
|
1017
|
-
}
|
|
1018
|
-
var jsonReporter = {
|
|
1019
|
-
format(diagnostics, rulesByName, meta) {
|
|
1020
|
-
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
1021
|
-
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
1022
|
-
const payload = {
|
|
1023
|
-
version: 1,
|
|
1024
|
-
summary: {
|
|
1025
|
-
total: diagnostics.length,
|
|
1026
|
-
errors,
|
|
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
|
|
1031
|
-
},
|
|
1032
|
-
results: diagnostics.map((d) => ({
|
|
1033
|
-
path: d.path,
|
|
1034
|
-
ruleName: d.ruleName,
|
|
1035
|
-
messageId: d.messageId,
|
|
1036
|
-
severity: d.severity,
|
|
1037
|
-
message: renderMessage(d, rulesByName),
|
|
1038
|
-
data: d.data ?? {}
|
|
1039
|
-
}))
|
|
1040
|
-
};
|
|
1041
|
-
return JSON.stringify(payload, null, 2);
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
var sarifReporter = {
|
|
1045
|
-
format(diagnostics, rulesByName) {
|
|
1046
|
-
const ruleNames = [...new Set(diagnostics.map((d) => d.ruleName))];
|
|
1047
|
-
const ruleIndex = new Map(ruleNames.map((name, i) => [name, i]));
|
|
1048
|
-
const sarifRules = ruleNames.map((name) => {
|
|
1049
|
-
const rule9 = rulesByName.get(name);
|
|
1050
|
-
return {
|
|
1051
|
-
id: name,
|
|
1052
|
-
name: name.replace(/[^A-Za-z0-9]/g, "_"),
|
|
1053
|
-
shortDescription: { text: rule9?.meta.docs.description ?? name },
|
|
1054
|
-
helpUri: rule9?.meta.docs.url
|
|
1055
|
-
};
|
|
1056
|
-
});
|
|
1057
|
-
const sarifResults = diagnostics.map((d) => ({
|
|
1058
|
-
ruleId: d.ruleName,
|
|
1059
|
-
ruleIndex: ruleIndex.get(d.ruleName) ?? 0,
|
|
1060
|
-
level: d.severity === "error" ? "error" : "warning",
|
|
1061
|
-
message: { text: renderMessage(d, rulesByName) },
|
|
1062
|
-
locations: [
|
|
1063
|
-
{
|
|
1064
|
-
physicalLocation: {
|
|
1065
|
-
artifactLocation: { uri: d.path, uriBaseId: "%SRCROOT%" },
|
|
1066
|
-
region: { startLine: 1 }
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
]
|
|
1070
|
-
}));
|
|
1071
|
-
const sarif = {
|
|
1072
|
-
$schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
|
|
1073
|
-
version: "2.1.0",
|
|
1074
|
-
runs: [
|
|
1075
|
-
{
|
|
1076
|
-
tool: {
|
|
1077
|
-
driver: {
|
|
1078
|
-
name: "madr-lint",
|
|
1079
|
-
informationUri: "https://github.com/knktkc/madr-lint",
|
|
1080
|
-
rules: sarifRules
|
|
1081
|
-
}
|
|
1082
|
-
},
|
|
1083
|
-
results: sarifResults
|
|
1084
|
-
}
|
|
1085
|
-
]
|
|
1086
|
-
};
|
|
1087
|
-
return JSON.stringify(sarif, null, 2);
|
|
1346
|
+
const sev = resolveSeverity(ruleSeverity[rule9.meta.name]);
|
|
1347
|
+
if (sev === "off") continue;
|
|
1348
|
+
if (sev === "error") errorRules.push(rule9);
|
|
1349
|
+
else warnRules.push(rule9);
|
|
1088
1350
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1351
|
+
const diagnostics = [];
|
|
1352
|
+
if (errorRules.length > 0) {
|
|
1353
|
+
diagnostics.push(
|
|
1354
|
+
...runRulesOnFile(errorRules, fileContext, {
|
|
1355
|
+
severity: "error",
|
|
1356
|
+
optionsByRule
|
|
1357
|
+
})
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
if (warnRules.length > 0) {
|
|
1361
|
+
diagnostics.push(
|
|
1362
|
+
...runRulesOnFile(warnRules, fileContext, {
|
|
1363
|
+
severity: "warn",
|
|
1364
|
+
optionsByRule
|
|
1365
|
+
})
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
return diagnostics;
|
|
1095
1369
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1370
|
+
function makeFileExists(cwd) {
|
|
1371
|
+
const projectRoot = resolve3(cwd);
|
|
1372
|
+
const existsCache = /* @__PURE__ */ new Map();
|
|
1373
|
+
return (resolvedPath) => {
|
|
1374
|
+
const cached = existsCache.get(resolvedPath);
|
|
1375
|
+
if (cached !== void 0) return cached;
|
|
1376
|
+
const abs = resolve3(projectRoot, resolvedPath);
|
|
1377
|
+
const rel = relative(projectRoot, abs);
|
|
1378
|
+
const escapesRoot = rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
1379
|
+
let result;
|
|
1380
|
+
if (escapesRoot) {
|
|
1381
|
+
result = false;
|
|
1382
|
+
} else {
|
|
1383
|
+
try {
|
|
1384
|
+
result = statSync2(abs, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
1385
|
+
} catch {
|
|
1386
|
+
result = false;
|
|
1387
|
+
}
|
|
1106
1388
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1389
|
+
existsCache.set(resolvedPath, result);
|
|
1390
|
+
return result;
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function runProjectPass(projectFiles, enabledProjectRules, ruleSeverity, optionsByRule, cwd) {
|
|
1394
|
+
const fileExists = makeFileExists(cwd);
|
|
1395
|
+
const errorRules = [];
|
|
1396
|
+
const warnRules = [];
|
|
1397
|
+
for (const rule9 of enabledProjectRules) {
|
|
1398
|
+
const sev = resolveSeverity(ruleSeverity[rule9.meta.name]);
|
|
1399
|
+
if (sev === "error") errorRules.push(rule9);
|
|
1400
|
+
else if (sev === "warn") warnRules.push(rule9);
|
|
1117
1401
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1402
|
+
const projectDiagnostics = [];
|
|
1403
|
+
if (errorRules.length > 0) {
|
|
1404
|
+
projectDiagnostics.push(
|
|
1405
|
+
...runRulesOnProject(errorRules, projectFiles, {
|
|
1406
|
+
severity: "error",
|
|
1407
|
+
fileExists,
|
|
1408
|
+
optionsByRule
|
|
1409
|
+
})
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
if (warnRules.length > 0) {
|
|
1413
|
+
projectDiagnostics.push(
|
|
1414
|
+
...runRulesOnProject(warnRules, projectFiles, {
|
|
1415
|
+
severity: "warn",
|
|
1416
|
+
fileExists,
|
|
1417
|
+
optionsByRule
|
|
1418
|
+
})
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
return filterSuppressedProjectDiagnostics(projectDiagnostics, projectFiles);
|
|
1422
|
+
}
|
|
1423
|
+
function filterSuppressedProjectDiagnostics(diagnostics, projectFiles) {
|
|
1424
|
+
if (diagnostics.length === 0) return [];
|
|
1425
|
+
const fileByPath = new Map(projectFiles.map((f) => [f.path, f]));
|
|
1426
|
+
const indexByPath = /* @__PURE__ */ new Map();
|
|
1427
|
+
const indexFor = (path) => {
|
|
1428
|
+
const cached = indexByPath.get(path);
|
|
1429
|
+
if (cached !== void 0) return cached;
|
|
1430
|
+
const file = fileByPath.get(path);
|
|
1431
|
+
const index = file && file.content.includes(DIRECTIVE_PREFIX) ? collectDirectives(file.ast, file.body) : null;
|
|
1432
|
+
indexByPath.set(path, index);
|
|
1433
|
+
return index;
|
|
1434
|
+
};
|
|
1435
|
+
return diagnostics.filter((d) => {
|
|
1436
|
+
if (d.ruleName === INTERNAL_ERROR_RULE_NAME) return true;
|
|
1437
|
+
const index = indexFor(d.path);
|
|
1438
|
+
if (!index) return true;
|
|
1439
|
+
return !isSuppressed(index, d.ruleName, d.loc?.line);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
function resolveSeverity(config) {
|
|
1443
|
+
if (config === void 0) return "off";
|
|
1444
|
+
if (typeof config === "string") return config;
|
|
1445
|
+
return config[0];
|
|
1446
|
+
}
|
|
1447
|
+
function extractOptionsByRule(ruleSeverity) {
|
|
1448
|
+
const map = {};
|
|
1449
|
+
for (const [name, config] of Object.entries(ruleSeverity)) {
|
|
1450
|
+
if (Array.isArray(config) && config[1]) map[name] = config[1];
|
|
1451
|
+
}
|
|
1452
|
+
return map;
|
|
1453
|
+
}
|
|
1125
1454
|
|
|
1126
1455
|
// src/rules/index.ts
|
|
1127
1456
|
var rules_exports = {};
|
|
@@ -1166,6 +1495,58 @@ function isValidIso8601Date(s) {
|
|
|
1166
1495
|
const d = new Date(Date.UTC(year, month - 1, day));
|
|
1167
1496
|
return d.getUTCFullYear() === year && d.getUTCMonth() === month - 1 && d.getUTCDate() === day;
|
|
1168
1497
|
}
|
|
1498
|
+
var MONTHS = {
|
|
1499
|
+
jan: 1,
|
|
1500
|
+
january: 1,
|
|
1501
|
+
feb: 2,
|
|
1502
|
+
february: 2,
|
|
1503
|
+
mar: 3,
|
|
1504
|
+
march: 3,
|
|
1505
|
+
apr: 4,
|
|
1506
|
+
april: 4,
|
|
1507
|
+
may: 5,
|
|
1508
|
+
jun: 6,
|
|
1509
|
+
june: 6,
|
|
1510
|
+
jul: 7,
|
|
1511
|
+
july: 7,
|
|
1512
|
+
aug: 8,
|
|
1513
|
+
august: 8,
|
|
1514
|
+
sep: 9,
|
|
1515
|
+
sept: 9,
|
|
1516
|
+
september: 9,
|
|
1517
|
+
oct: 10,
|
|
1518
|
+
october: 10,
|
|
1519
|
+
nov: 11,
|
|
1520
|
+
november: 11,
|
|
1521
|
+
dec: 12,
|
|
1522
|
+
december: 12
|
|
1523
|
+
};
|
|
1524
|
+
function pad(n, width) {
|
|
1525
|
+
return String(n).padStart(width, "0");
|
|
1526
|
+
}
|
|
1527
|
+
function buildIso(year, month, day) {
|
|
1528
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
1529
|
+
const iso = `${pad(year, 4)}-${pad(month, 2)}-${pad(day, 2)}`;
|
|
1530
|
+
return isValidIso8601Date(iso) ? iso : null;
|
|
1531
|
+
}
|
|
1532
|
+
function normalizeToIso(raw) {
|
|
1533
|
+
const s = raw.trim();
|
|
1534
|
+
const numeric = /^(\d{4})([-/.])(\d{1,2})\2(\d{1,2})$/.exec(s);
|
|
1535
|
+
if (numeric) {
|
|
1536
|
+
return buildIso(Number(numeric[1]), Number(numeric[3]), Number(numeric[4]));
|
|
1537
|
+
}
|
|
1538
|
+
const dMonY = /^(\d{1,2})\s+([A-Za-z]+)\.?\s+(\d{4})$/.exec(s);
|
|
1539
|
+
if (dMonY) {
|
|
1540
|
+
const month = MONTHS[dMonY[2].toLowerCase()];
|
|
1541
|
+
return month ? buildIso(Number(dMonY[3]), month, Number(dMonY[1])) : null;
|
|
1542
|
+
}
|
|
1543
|
+
const monDY = /^([A-Za-z]+)\.?\s+(\d{1,2}),?\s+(\d{4})$/.exec(s);
|
|
1544
|
+
if (monDY) {
|
|
1545
|
+
const month = MONTHS[monDY[1].toLowerCase()];
|
|
1546
|
+
return month ? buildIso(Number(monDY[3]), month, Number(monDY[2])) : null;
|
|
1547
|
+
}
|
|
1548
|
+
return null;
|
|
1549
|
+
}
|
|
1169
1550
|
var rule = {
|
|
1170
1551
|
meta: {
|
|
1171
1552
|
name: "madr/date-iso8601",
|
|
@@ -1180,6 +1561,14 @@ var rule = {
|
|
|
1180
1561
|
missingDate: 'Metadata does not contain a "{{field}}" field (checked frontmatter and v2 bold-list)',
|
|
1181
1562
|
invalidDate: 'Date "{{date}}" is not a valid ISO 8601 calendar date (YYYY-MM-DD)'
|
|
1182
1563
|
},
|
|
1564
|
+
suggestions: {
|
|
1565
|
+
missingDate: 'add a "{{field}}" field in YYYY-MM-DD format to the frontmatter (for MADR v2, a "* Date: ..." list item)',
|
|
1566
|
+
invalidDate: "use the YYYY-MM-DD calendar-date format, e.g. 2025-03-14"
|
|
1567
|
+
},
|
|
1568
|
+
// Autofix (#29): normalize UNAMBIGUOUS shapes of a v2 list-sourced value —
|
|
1569
|
+
// year-first numeric and English named-month forms. Ambiguous day/month
|
|
1570
|
+
// order, invalid calendar dates, and frontmatter-sourced values get no fix.
|
|
1571
|
+
fixable: "code",
|
|
1183
1572
|
defaultOptions: {
|
|
1184
1573
|
field: "date"
|
|
1185
1574
|
},
|
|
@@ -1206,10 +1595,14 @@ var rule = {
|
|
|
1206
1595
|
}
|
|
1207
1596
|
if (!isValidIso8601Date(dateStr)) {
|
|
1208
1597
|
const loc = context.metadataLoc?.[fieldName];
|
|
1598
|
+
const valueRange = context.metadataValueLoc?.[fieldName];
|
|
1599
|
+
const normalized = valueRange ? normalizeToIso(dateStr) : null;
|
|
1600
|
+
const fix = normalized !== null && normalized !== dateStr && valueRange ? (fixer) => fixer.replaceRange([valueRange.start, valueRange.end], normalized) : void 0;
|
|
1209
1601
|
context.report({
|
|
1210
1602
|
messageId: "invalidDate",
|
|
1211
1603
|
...loc ? { loc } : {},
|
|
1212
|
-
data: { date: dateStr }
|
|
1604
|
+
data: { date: dateStr },
|
|
1605
|
+
...fix ? { fix } : {}
|
|
1213
1606
|
});
|
|
1214
1607
|
}
|
|
1215
1608
|
}
|
|
@@ -1217,7 +1610,7 @@ var rule = {
|
|
|
1217
1610
|
var date_iso8601_default = rule;
|
|
1218
1611
|
|
|
1219
1612
|
// src/rules/filename-format/index.ts
|
|
1220
|
-
import { basename
|
|
1613
|
+
import { basename } from "path";
|
|
1221
1614
|
|
|
1222
1615
|
// src/core/regex-safety.ts
|
|
1223
1616
|
import safeRegex from "safe-regex2";
|
|
@@ -1272,6 +1665,11 @@ var rule2 = {
|
|
|
1272
1665
|
messages: {
|
|
1273
1666
|
invalidFilename: 'Filename "{{filename}}" does not match expected pattern "{{expected}}"'
|
|
1274
1667
|
},
|
|
1668
|
+
suggestions: {
|
|
1669
|
+
// The message already states the expected pattern; the suggestion adds a
|
|
1670
|
+
// concrete, conforming example of the NNNN-kebab-case.md convention.
|
|
1671
|
+
invalidFilename: "rename the file to the NNNN-kebab-case.md convention, e.g. 0001-record-architecture-decisions.md"
|
|
1672
|
+
},
|
|
1275
1673
|
defaultOptions: {
|
|
1276
1674
|
pattern: "^[0-9]{4}-[a-z0-9-]+\\.md$"
|
|
1277
1675
|
},
|
|
@@ -1280,7 +1678,7 @@ var rule2 = {
|
|
|
1280
1678
|
create(context) {
|
|
1281
1679
|
const { pattern } = context.options;
|
|
1282
1680
|
const regex = assertSafeRegex(pattern, "madr/filename-format", "pattern");
|
|
1283
|
-
const filename =
|
|
1681
|
+
const filename = basename(context.file.path);
|
|
1284
1682
|
if (!regex.test(filename)) {
|
|
1285
1683
|
context.report({
|
|
1286
1684
|
messageId: "invalidFilename",
|
|
@@ -1351,6 +1749,9 @@ var rule3 = {
|
|
|
1351
1749
|
messages: {
|
|
1352
1750
|
brokenLink: 'Link to "{{url}}" resolves to "{{resolvedPath}}", which does not exist in the project'
|
|
1353
1751
|
},
|
|
1752
|
+
suggestions: {
|
|
1753
|
+
brokenLink: "fix the link target or remove the link; relative paths resolve from the ADR file's own directory"
|
|
1754
|
+
},
|
|
1354
1755
|
defaultOptions: {},
|
|
1355
1756
|
schema: schema_default3
|
|
1356
1757
|
},
|
|
@@ -1386,7 +1787,7 @@ var rule3 = {
|
|
|
1386
1787
|
var no_broken_links_default = rule3;
|
|
1387
1788
|
|
|
1388
1789
|
// src/rules/no-duplicate-numbering/index.ts
|
|
1389
|
-
import { basename as
|
|
1790
|
+
import { basename as basename2 } from "path";
|
|
1390
1791
|
|
|
1391
1792
|
// src/rules/no-duplicate-numbering/schema.json
|
|
1392
1793
|
var schema_default4 = {
|
|
@@ -1411,13 +1812,16 @@ var rule4 = {
|
|
|
1411
1812
|
messages: {
|
|
1412
1813
|
duplicateNumber: "ADR number {{number}} is used by multiple files: {{paths}}"
|
|
1413
1814
|
},
|
|
1815
|
+
suggestions: {
|
|
1816
|
+
duplicateNumber: "renumber all but one of these files so each ADR has a unique NNNN- prefix"
|
|
1817
|
+
},
|
|
1414
1818
|
defaultOptions: {},
|
|
1415
1819
|
schema: schema_default4
|
|
1416
1820
|
},
|
|
1417
1821
|
check(context) {
|
|
1418
1822
|
const byNumber = /* @__PURE__ */ new Map();
|
|
1419
1823
|
for (const file of context.files) {
|
|
1420
|
-
const match = NUMBER_REGEX.exec(
|
|
1824
|
+
const match = NUMBER_REGEX.exec(basename2(file.path));
|
|
1421
1825
|
if (!match) continue;
|
|
1422
1826
|
const number = match[1];
|
|
1423
1827
|
if (number === void 0) continue;
|
|
@@ -1442,7 +1846,7 @@ var rule4 = {
|
|
|
1442
1846
|
var no_duplicate_numbering_default = rule4;
|
|
1443
1847
|
|
|
1444
1848
|
// src/rules/no-numbering-gap/index.ts
|
|
1445
|
-
import { basename as
|
|
1849
|
+
import { basename as basename3 } from "path";
|
|
1446
1850
|
|
|
1447
1851
|
// src/rules/no-numbering-gap/schema.json
|
|
1448
1852
|
var schema_default5 = {
|
|
@@ -1470,13 +1874,16 @@ var rule5 = {
|
|
|
1470
1874
|
messages: {
|
|
1471
1875
|
numberingGap: "Numbering gap: missing {{missing}} between ADR-{{from}} and ADR-{{to}}"
|
|
1472
1876
|
},
|
|
1877
|
+
// No `suggestions`: a gap can be intentional (a rejected or deleted ADR),
|
|
1878
|
+
// and closing it may mean either adding the missing ADR OR renumbering —
|
|
1879
|
+
// the right remediation is contextual, so we omit a suggestion. See #67.
|
|
1473
1880
|
defaultOptions: {},
|
|
1474
1881
|
schema: schema_default5
|
|
1475
1882
|
},
|
|
1476
1883
|
check(context) {
|
|
1477
1884
|
const numberToPath = /* @__PURE__ */ new Map();
|
|
1478
1885
|
for (const file of context.files) {
|
|
1479
|
-
const match = NUMBER_REGEX2.exec(
|
|
1886
|
+
const match = NUMBER_REGEX2.exec(basename3(file.path));
|
|
1480
1887
|
if (!match?.[1]) continue;
|
|
1481
1888
|
const n = Number.parseInt(match[1], 10);
|
|
1482
1889
|
if (!Number.isNaN(n)) numberToPath.set(n, file.path);
|
|
@@ -1546,6 +1953,9 @@ var rule6 = {
|
|
|
1546
1953
|
messages: {
|
|
1547
1954
|
missingSection: 'Missing required section: "{{section}}"'
|
|
1548
1955
|
},
|
|
1956
|
+
suggestions: {
|
|
1957
|
+
missingSection: 'add a "## {{section}}" heading to the document body'
|
|
1958
|
+
},
|
|
1549
1959
|
defaultOptions: {
|
|
1550
1960
|
sections: [
|
|
1551
1961
|
"Context and Problem Statement",
|
|
@@ -1610,6 +2020,43 @@ var schema_default7 = {
|
|
|
1610
2020
|
};
|
|
1611
2021
|
|
|
1612
2022
|
// src/rules/status-enum/index.ts
|
|
2023
|
+
var TYPO_CORRECTIONS = {
|
|
2024
|
+
superceded: "superseded",
|
|
2025
|
+
supercedes: "supersedes",
|
|
2026
|
+
depricated: "deprecated"
|
|
2027
|
+
};
|
|
2028
|
+
function repairTypos(lower) {
|
|
2029
|
+
return lower.split(" ").map((tok) => TYPO_CORRECTIONS[tok] ?? tok).join(" ");
|
|
2030
|
+
}
|
|
2031
|
+
function tailAfterWords(s, n) {
|
|
2032
|
+
let i = 0;
|
|
2033
|
+
for (let w = 0; w < n; w++) {
|
|
2034
|
+
while (i < s.length && /\s/.test(s[i])) i++;
|
|
2035
|
+
while (i < s.length && !/\s/.test(s[i])) i++;
|
|
2036
|
+
}
|
|
2037
|
+
return s.slice(i);
|
|
2038
|
+
}
|
|
2039
|
+
function correctStatus(status, values, prefixValues) {
|
|
2040
|
+
const repaired = repairTypos(status.toLowerCase());
|
|
2041
|
+
const exact = [...new Set(values.filter((v) => v.toLowerCase() === repaired))];
|
|
2042
|
+
if (exact.length === 1) {
|
|
2043
|
+
return exact[0] !== status ? exact[0] : void 0;
|
|
2044
|
+
}
|
|
2045
|
+
if (exact.length > 1) return void 0;
|
|
2046
|
+
const prefixes = [
|
|
2047
|
+
...new Set(
|
|
2048
|
+
prefixValues.filter((p) => {
|
|
2049
|
+
const pl = p.toLowerCase();
|
|
2050
|
+
return repaired === pl || repaired.startsWith(`${pl} `);
|
|
2051
|
+
})
|
|
2052
|
+
)
|
|
2053
|
+
];
|
|
2054
|
+
if (prefixes.length !== 1) return void 0;
|
|
2055
|
+
const prefix = prefixes[0];
|
|
2056
|
+
const tail = tailAfterWords(status, prefix.split(" ").length);
|
|
2057
|
+
const replacement = `${prefix}${tail}`;
|
|
2058
|
+
return replacement !== status ? replacement : void 0;
|
|
2059
|
+
}
|
|
1613
2060
|
var rule7 = {
|
|
1614
2061
|
meta: {
|
|
1615
2062
|
name: "madr/status-enum",
|
|
@@ -1624,6 +2071,17 @@ var rule7 = {
|
|
|
1624
2071
|
missingStatus: 'Metadata does not contain a "status" field (checked frontmatter and v2 bold-list)',
|
|
1625
2072
|
invalidStatus: 'Status "{{status}}" is not one of: {{allowed}}'
|
|
1626
2073
|
},
|
|
2074
|
+
// No suggestion for invalidStatus: the message already lists the allowed
|
|
2075
|
+
// values (the expected-vs-actual is self-contained), so a suggestion would
|
|
2076
|
+
// only restate it. See issue #67.
|
|
2077
|
+
suggestions: {
|
|
2078
|
+
missingStatus: 'add a "status" field to the frontmatter (for MADR v2, a "* Status: ..." list item) \u2014 allowed values: {{allowed}}'
|
|
2079
|
+
},
|
|
2080
|
+
// Autofix (#28/#29): normalize a v2 list-sourced value to the unique
|
|
2081
|
+
// canonical enum entry — case difference, a curated misspelling, or a prefix
|
|
2082
|
+
// case/typo — preserving any tail (see create()). Ambiguous or
|
|
2083
|
+
// frontmatter-sourced values get no fix.
|
|
2084
|
+
fixable: "code",
|
|
1627
2085
|
defaultOptions: {
|
|
1628
2086
|
values: ["proposed", "rejected", "accepted", "deprecated"],
|
|
1629
2087
|
prefixValues: ["superseded by"],
|
|
@@ -1632,12 +2090,13 @@ var rule7 = {
|
|
|
1632
2090
|
schema: schema_default7
|
|
1633
2091
|
},
|
|
1634
2092
|
create(context) {
|
|
2093
|
+
const { values, prefixValues, caseSensitive } = context.options;
|
|
2094
|
+
const allowed = [...values, ...prefixValues.map((p) => `${p} ...`)];
|
|
1635
2095
|
const meta = context.metadata;
|
|
1636
2096
|
if (!meta || typeof meta.status !== "string") {
|
|
1637
|
-
context.report({ messageId: "missingStatus", data: {} });
|
|
2097
|
+
context.report({ messageId: "missingStatus", data: { allowed } });
|
|
1638
2098
|
return;
|
|
1639
2099
|
}
|
|
1640
|
-
const { values, prefixValues, caseSensitive } = context.options;
|
|
1641
2100
|
const status = meta.status;
|
|
1642
2101
|
const compareStatus = caseSensitive ? status : status.toLowerCase();
|
|
1643
2102
|
const compareValues = caseSensitive ? values : values.map((v) => v.toLowerCase());
|
|
@@ -1647,15 +2106,15 @@ var rule7 = {
|
|
|
1647
2106
|
(p) => compareStatus.startsWith(p)
|
|
1648
2107
|
);
|
|
1649
2108
|
if (!exactMatch && !prefixMatch) {
|
|
1650
|
-
const allowed = [
|
|
1651
|
-
...values,
|
|
1652
|
-
...prefixValues.map((p) => `${p} ...`)
|
|
1653
|
-
];
|
|
1654
2109
|
const loc = context.metadataLoc?.status;
|
|
2110
|
+
const canonical = correctStatus(status, values, prefixValues);
|
|
2111
|
+
const valueRange = context.metadataValueLoc?.status;
|
|
2112
|
+
const fix = canonical !== void 0 && canonical !== status && valueRange ? (fixer) => fixer.replaceRange([valueRange.start, valueRange.end], canonical) : void 0;
|
|
1655
2113
|
context.report({
|
|
1656
2114
|
messageId: "invalidStatus",
|
|
1657
2115
|
...loc ? { loc } : {},
|
|
1658
|
-
data: { status, allowed }
|
|
2116
|
+
data: { status, allowed },
|
|
2117
|
+
...fix ? { fix } : {}
|
|
1659
2118
|
});
|
|
1660
2119
|
}
|
|
1661
2120
|
}
|
|
@@ -1663,7 +2122,7 @@ var rule7 = {
|
|
|
1663
2122
|
var status_enum_default = rule7;
|
|
1664
2123
|
|
|
1665
2124
|
// src/rules/supersedes-bidirectional/index.ts
|
|
1666
|
-
import { basename as
|
|
2125
|
+
import { basename as basename4 } from "path";
|
|
1667
2126
|
|
|
1668
2127
|
// src/rules/supersedes-bidirectional/schema.json
|
|
1669
2128
|
var schema_default8 = {
|
|
@@ -1678,7 +2137,7 @@ var ADR_NUMBER_REGEX = /^(\d{4})-/;
|
|
|
1678
2137
|
var FORWARD_FIELD = "supersedes";
|
|
1679
2138
|
var BACKWARD_FIELD = "superseded-by";
|
|
1680
2139
|
function extractAdrNumber(path) {
|
|
1681
|
-
const match = ADR_NUMBER_REGEX.exec(
|
|
2140
|
+
const match = ADR_NUMBER_REGEX.exec(basename4(path));
|
|
1682
2141
|
return match?.[1] ?? null;
|
|
1683
2142
|
}
|
|
1684
2143
|
function normalizeRefs(value) {
|
|
@@ -1688,6 +2147,28 @@ function normalizeRefs(value) {
|
|
|
1688
2147
|
}
|
|
1689
2148
|
return [];
|
|
1690
2149
|
}
|
|
2150
|
+
function escapeRegExp(s) {
|
|
2151
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2152
|
+
}
|
|
2153
|
+
function frontmatterInsertEdit(content, key, value) {
|
|
2154
|
+
const fmEnd = frontmatterOffset(content);
|
|
2155
|
+
if (fmEnd === 0) return null;
|
|
2156
|
+
const fmBlock = content.slice(0, fmEnd);
|
|
2157
|
+
const closeMatch = /\r?\n(---[ \t]*\r?\n)$/.exec(fmBlock);
|
|
2158
|
+
if (!closeMatch) return null;
|
|
2159
|
+
const fenceLine = closeMatch[1];
|
|
2160
|
+
const offset = closeMatch.index + (closeMatch[0].length - fenceLine.length);
|
|
2161
|
+
if (content.slice(offset, offset + 3) !== "---") return null;
|
|
2162
|
+
const keyPattern = new RegExp(`(^|\\r?\\n)${escapeRegExp(key)}[ \\t]*:`, "i");
|
|
2163
|
+
if (keyPattern.test(content.slice(0, offset))) return null;
|
|
2164
|
+
const eol = fenceLine.endsWith("\r\n") ? "\r\n" : "\n";
|
|
2165
|
+
return { offset, text: `${key}: ${value}${eol}` };
|
|
2166
|
+
}
|
|
2167
|
+
function backReferenceFix(target, key, value) {
|
|
2168
|
+
const edit = frontmatterInsertEdit(target.content, key, value);
|
|
2169
|
+
if (!edit) return void 0;
|
|
2170
|
+
return (fixer) => fixer.insertAt(edit.offset, edit.text);
|
|
2171
|
+
}
|
|
1691
2172
|
var rule8 = {
|
|
1692
2173
|
meta: {
|
|
1693
2174
|
name: "madr/supersedes-bidirectional",
|
|
@@ -1700,8 +2181,21 @@ var rule8 = {
|
|
|
1700
2181
|
},
|
|
1701
2182
|
messages: {
|
|
1702
2183
|
unknownReference: "Frontmatter `{{direction}}: {{ref}}` references an ADR that does not exist",
|
|
1703
|
-
|
|
2184
|
+
// `declared` is the field the SOURCE file actually declares;
|
|
2185
|
+
// `direction` is the field THIS file (the diagnostic's path) must add.
|
|
2186
|
+
// They are opposites — rendering `direction` as the declared field
|
|
2187
|
+
// would mislabel the source's frontmatter in both directions.
|
|
2188
|
+
missingBackReference: "`{{source}}` declares `{{declared}}: {{ref}}`, but `{{ref}}` (this file) does not back-reference it via `{{direction}}: {{expected}}`"
|
|
1704
2189
|
},
|
|
2190
|
+
suggestions: {
|
|
2191
|
+
unknownReference: 'correct "{{ref}}" to an existing ADR number, or remove "{{direction}}: {{ref}}" from the frontmatter',
|
|
2192
|
+
missingBackReference: 'add "{{direction}}: {{expected}}" to the frontmatter of this file'
|
|
2193
|
+
},
|
|
2194
|
+
// Cross-file autofix (#29): `missingBackReference` inserts the reciprocal
|
|
2195
|
+
// `<direction>: <expected>` line into the target's EXISTING frontmatter.
|
|
2196
|
+
// Targets without frontmatter, or where the key already exists, decline;
|
|
2197
|
+
// `unknownReference` is contextual and never fixable.
|
|
2198
|
+
fixable: "code",
|
|
1705
2199
|
defaultOptions: {},
|
|
1706
2200
|
schema: schema_default8
|
|
1707
2201
|
},
|
|
@@ -1711,66 +2205,399 @@ var rule8 = {
|
|
|
1711
2205
|
const num = extractAdrNumber(file.path);
|
|
1712
2206
|
if (num) adrIndex.set(`ADR-${num}`, file);
|
|
1713
2207
|
}
|
|
1714
|
-
for (const file of context.files) {
|
|
1715
|
-
const fm = file.frontmatter;
|
|
1716
|
-
if (!fm) continue;
|
|
1717
|
-
const fileNumber = extractAdrNumber(file.path);
|
|
1718
|
-
if (!fileNumber) continue;
|
|
1719
|
-
const fileRef = `ADR-${fileNumber}`;
|
|
1720
|
-
const forwardRefs = normalizeRefs(fm[FORWARD_FIELD]);
|
|
1721
|
-
const backwardRefs = normalizeRefs(fm[BACKWARD_FIELD]);
|
|
1722
|
-
for (const ref of forwardRefs) {
|
|
1723
|
-
const target = adrIndex.get(ref);
|
|
1724
|
-
if (!target) {
|
|
1725
|
-
context.report({
|
|
1726
|
-
messageId: "unknownReference",
|
|
1727
|
-
path: file.path,
|
|
1728
|
-
data: { ref, direction: FORWARD_FIELD }
|
|
1729
|
-
});
|
|
1730
|
-
continue;
|
|
2208
|
+
for (const file of context.files) {
|
|
2209
|
+
const fm = file.frontmatter;
|
|
2210
|
+
if (!fm) continue;
|
|
2211
|
+
const fileNumber = extractAdrNumber(file.path);
|
|
2212
|
+
if (!fileNumber) continue;
|
|
2213
|
+
const fileRef = `ADR-${fileNumber}`;
|
|
2214
|
+
const forwardRefs = normalizeRefs(fm[FORWARD_FIELD]);
|
|
2215
|
+
const backwardRefs = normalizeRefs(fm[BACKWARD_FIELD]);
|
|
2216
|
+
for (const ref of forwardRefs) {
|
|
2217
|
+
const target = adrIndex.get(ref);
|
|
2218
|
+
if (!target) {
|
|
2219
|
+
context.report({
|
|
2220
|
+
messageId: "unknownReference",
|
|
2221
|
+
path: file.path,
|
|
2222
|
+
data: { ref, direction: FORWARD_FIELD }
|
|
2223
|
+
});
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
const targetBack = normalizeRefs(target.frontmatter?.[BACKWARD_FIELD]);
|
|
2227
|
+
if (!targetBack.includes(fileRef)) {
|
|
2228
|
+
const fix = backReferenceFix(target, BACKWARD_FIELD, fileRef);
|
|
2229
|
+
context.report({
|
|
2230
|
+
messageId: "missingBackReference",
|
|
2231
|
+
path: target.path,
|
|
2232
|
+
data: {
|
|
2233
|
+
ref,
|
|
2234
|
+
source: file.path,
|
|
2235
|
+
declared: FORWARD_FIELD,
|
|
2236
|
+
direction: BACKWARD_FIELD,
|
|
2237
|
+
expected: fileRef
|
|
2238
|
+
},
|
|
2239
|
+
...fix ? { fix } : {}
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
for (const ref of backwardRefs) {
|
|
2244
|
+
const target = adrIndex.get(ref);
|
|
2245
|
+
if (!target) {
|
|
2246
|
+
context.report({
|
|
2247
|
+
messageId: "unknownReference",
|
|
2248
|
+
path: file.path,
|
|
2249
|
+
data: { ref, direction: BACKWARD_FIELD }
|
|
2250
|
+
});
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
const targetFwd = normalizeRefs(target.frontmatter?.[FORWARD_FIELD]);
|
|
2254
|
+
if (!targetFwd.includes(fileRef)) {
|
|
2255
|
+
const fix = backReferenceFix(target, FORWARD_FIELD, fileRef);
|
|
2256
|
+
context.report({
|
|
2257
|
+
messageId: "missingBackReference",
|
|
2258
|
+
path: target.path,
|
|
2259
|
+
data: {
|
|
2260
|
+
ref,
|
|
2261
|
+
source: file.path,
|
|
2262
|
+
declared: BACKWARD_FIELD,
|
|
2263
|
+
direction: FORWARD_FIELD,
|
|
2264
|
+
expected: fileRef
|
|
2265
|
+
},
|
|
2266
|
+
...fix ? { fix } : {}
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
var supersedes_bidirectional_default = rule8;
|
|
2274
|
+
|
|
2275
|
+
// src/commands/init.ts
|
|
2276
|
+
var initCommand = defineCommand({
|
|
2277
|
+
meta: {
|
|
2278
|
+
name: "init",
|
|
2279
|
+
description: "Scaffold a madr-lint config file (detects ADR directory, MADR version, and TS vs JSON)"
|
|
2280
|
+
},
|
|
2281
|
+
args: {
|
|
2282
|
+
force: {
|
|
2283
|
+
type: "boolean",
|
|
2284
|
+
description: "Overwrite an existing config file",
|
|
2285
|
+
default: false
|
|
2286
|
+
},
|
|
2287
|
+
dir: {
|
|
2288
|
+
type: "string",
|
|
2289
|
+
description: "ADR directory to write into the config, overriding detection",
|
|
2290
|
+
required: false
|
|
2291
|
+
},
|
|
2292
|
+
json: {
|
|
2293
|
+
type: "boolean",
|
|
2294
|
+
description: "Emit a machine-readable JSON summary instead of text",
|
|
2295
|
+
default: false
|
|
2296
|
+
}
|
|
2297
|
+
},
|
|
2298
|
+
run({ args }) {
|
|
2299
|
+
const cwd = process.cwd();
|
|
2300
|
+
const rawDir = args.dir;
|
|
2301
|
+
if (rawDir !== void 0 && rawDir.trim() === "") {
|
|
2302
|
+
console.error(`Invalid --dir "${rawDir}": must be a non-empty path.`);
|
|
2303
|
+
process.exit(2);
|
|
2304
|
+
}
|
|
2305
|
+
const existing = findExistingConfigFile(cwd);
|
|
2306
|
+
if (existing && args.force !== true) {
|
|
2307
|
+
console.error(
|
|
2308
|
+
`A madr-lint config already exists: ${existing}. Re-run with --force to overwrite.`
|
|
2309
|
+
);
|
|
2310
|
+
process.exit(2);
|
|
2311
|
+
}
|
|
2312
|
+
let adrDir;
|
|
2313
|
+
let adrDirSource;
|
|
2314
|
+
if (rawDir !== void 0) {
|
|
2315
|
+
adrDir = rawDir;
|
|
2316
|
+
adrDirSource = "override";
|
|
2317
|
+
} else {
|
|
2318
|
+
const detected = detectAdrDir(cwd);
|
|
2319
|
+
adrDir = detected.adrDir;
|
|
2320
|
+
adrDirSource = detected.source;
|
|
2321
|
+
}
|
|
2322
|
+
const madrVersion = detectMadrVersion(cwd, adrDir);
|
|
2323
|
+
const configFormat = detectConfigFormat(cwd);
|
|
2324
|
+
const configPath = CONFIG_FILENAME_BY_FORMAT[configFormat];
|
|
2325
|
+
writeFileSync3(
|
|
2326
|
+
resolve4(cwd, configPath),
|
|
2327
|
+
renderConfig(configFormat, { adrDir, madrVersion })
|
|
2328
|
+
);
|
|
2329
|
+
if (existing && existing !== configPath) {
|
|
2330
|
+
const oldWins = CONFIG_FILES.indexOf(existing) < CONFIG_FILES.indexOf(configPath);
|
|
2331
|
+
console.error(
|
|
2332
|
+
oldWins ? `Note: ${existing} still exists and precedes ${configPath} in config discovery \u2014 remove it to make the new config take effect.` : `Note: the leftover ${existing} is now shadowed by ${configPath}, which precedes it in config discovery \u2014 you can delete it.`
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
const files = findAdrFiles(resolve4(cwd, adrDir));
|
|
2336
|
+
let errors = 0;
|
|
2337
|
+
let warnings = 0;
|
|
2338
|
+
if (files.length > 0) {
|
|
2339
|
+
try {
|
|
2340
|
+
const config = resolveExtends({ extends: ["madr-lint:recommended"] });
|
|
2341
|
+
const allRules = Object.values(rules_exports);
|
|
2342
|
+
const result = lintFiles({
|
|
2343
|
+
rules: allRules,
|
|
2344
|
+
ruleSeverity: config.rules,
|
|
2345
|
+
files,
|
|
2346
|
+
cwd,
|
|
2347
|
+
cache: null,
|
|
2348
|
+
baseline: null
|
|
2349
|
+
});
|
|
2350
|
+
for (const d of result.diagnostics) {
|
|
2351
|
+
if (d.severity === "error") errors++;
|
|
2352
|
+
else warnings++;
|
|
2353
|
+
}
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
console.error(
|
|
2356
|
+
`Note: the initial lint failed (${err instanceof Error ? err.message : String(err)}); the config was still written.`
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const filesChecked = files.length;
|
|
2361
|
+
const suggestUpdateBaseline = filesChecked > 0 && errors + warnings > 0;
|
|
2362
|
+
if (args.json === true) {
|
|
2363
|
+
console.log(
|
|
2364
|
+
JSON.stringify(
|
|
2365
|
+
{
|
|
2366
|
+
written: true,
|
|
2367
|
+
configPath,
|
|
2368
|
+
configFormat,
|
|
2369
|
+
adrDir,
|
|
2370
|
+
adrDirSource,
|
|
2371
|
+
madrVersion,
|
|
2372
|
+
filesChecked,
|
|
2373
|
+
errors,
|
|
2374
|
+
warnings,
|
|
2375
|
+
suggestUpdateBaseline,
|
|
2376
|
+
docsUrl: GETTING_STARTED_URL
|
|
2377
|
+
},
|
|
2378
|
+
null,
|
|
2379
|
+
2
|
|
2380
|
+
)
|
|
2381
|
+
);
|
|
2382
|
+
} else {
|
|
2383
|
+
console.log(
|
|
2384
|
+
buildEpilogue({
|
|
2385
|
+
adrDir,
|
|
2386
|
+
adrDirSource,
|
|
2387
|
+
configPath,
|
|
2388
|
+
filesChecked,
|
|
2389
|
+
errors,
|
|
2390
|
+
warnings
|
|
2391
|
+
})
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
process.exit(0);
|
|
2395
|
+
}
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
// src/core/ignore.ts
|
|
2399
|
+
import { basename as basename5 } from "path";
|
|
2400
|
+
import picomatch from "picomatch";
|
|
2401
|
+
function shouldIgnore(relativePath, patterns) {
|
|
2402
|
+
if (patterns.length === 0) return false;
|
|
2403
|
+
const base = basename5(relativePath);
|
|
2404
|
+
for (const pattern of patterns) {
|
|
2405
|
+
if (matchesPattern(relativePath, base, pattern)) return true;
|
|
2406
|
+
}
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
function matchesPattern(relativePath, base, pattern) {
|
|
2410
|
+
if (relativePath === pattern) return true;
|
|
2411
|
+
const matcher = picomatch(pattern, { dot: true });
|
|
2412
|
+
if (matcher(relativePath)) return true;
|
|
2413
|
+
if (matcher(base)) return true;
|
|
2414
|
+
if (pattern.includes("/") && !pattern.startsWith("**/")) {
|
|
2415
|
+
const suffixMatcher = picomatch(`**/${pattern}`, { dot: true });
|
|
2416
|
+
if (suffixMatcher(relativePath)) return true;
|
|
2417
|
+
}
|
|
2418
|
+
return false;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// src/core/reporter.ts
|
|
2422
|
+
import pc from "picocolors";
|
|
2423
|
+
var textReporter = {
|
|
2424
|
+
// Deliberately 2-param (meta unused): the CLI prints baselineHiddenSummary()
|
|
2425
|
+
// itself for text format, so the footer can outlive a suppressed banner.
|
|
2426
|
+
format(diagnostics, rulesByName) {
|
|
2427
|
+
if (diagnostics.length === 0) {
|
|
2428
|
+
return pc.green("\u2713 All clear.");
|
|
2429
|
+
}
|
|
2430
|
+
const byFile = groupByFile(diagnostics);
|
|
2431
|
+
const lines = [];
|
|
2432
|
+
const gutter = " ".repeat(9 + 28 + 2);
|
|
2433
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
2434
|
+
lines.push(pc.underline(file));
|
|
2435
|
+
const docsByRule = /* @__PURE__ */ new Map();
|
|
2436
|
+
for (const d of fileDiagnostics) {
|
|
2437
|
+
const sev = d.severity === "error" ? pc.red(" error ") : pc.yellow(" warn ");
|
|
2438
|
+
const ruleId = pc.dim(d.ruleName.padEnd(28));
|
|
2439
|
+
const message = renderMessage(d, rulesByName);
|
|
2440
|
+
const fixTag = d.fixable ? pc.dim(" \u{1F527} fixable") : "";
|
|
2441
|
+
lines.push(`${sev}${ruleId} ${message}${fixTag}`);
|
|
2442
|
+
if (d.suggestion) {
|
|
2443
|
+
lines.push(`${gutter}${pc.cyan(`\u2192 ${d.suggestion}`)}`);
|
|
1731
2444
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
context.report({
|
|
1735
|
-
messageId: "missingBackReference",
|
|
1736
|
-
path: target.path,
|
|
1737
|
-
data: {
|
|
1738
|
-
ref,
|
|
1739
|
-
source: file.path,
|
|
1740
|
-
direction: BACKWARD_FIELD,
|
|
1741
|
-
expected: fileRef
|
|
1742
|
-
}
|
|
1743
|
-
});
|
|
2445
|
+
if (d.docsUrl && !docsByRule.has(d.ruleName)) {
|
|
2446
|
+
docsByRule.set(d.ruleName, d.docsUrl);
|
|
1744
2447
|
}
|
|
1745
2448
|
}
|
|
1746
|
-
for (const
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2449
|
+
for (const [ruleName, url] of docsByRule) {
|
|
2450
|
+
lines.push(pc.dim(` ${ruleName} ${url}`));
|
|
2451
|
+
}
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
}
|
|
2454
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
2455
|
+
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
2456
|
+
lines.push(pc.bold(formatSummary(errors, warnings)));
|
|
2457
|
+
return lines.join("\n");
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
function groupByFile(diagnostics) {
|
|
2461
|
+
const map = /* @__PURE__ */ new Map();
|
|
2462
|
+
for (const d of diagnostics) {
|
|
2463
|
+
if (!map.has(d.path)) map.set(d.path, []);
|
|
2464
|
+
map.get(d.path)?.push(d);
|
|
2465
|
+
}
|
|
2466
|
+
return map;
|
|
2467
|
+
}
|
|
2468
|
+
function renderMessage(d, rulesByName) {
|
|
2469
|
+
const rule9 = rulesByName.get(d.ruleName);
|
|
2470
|
+
const template = rule9?.meta.messages[d.messageId] ?? d.messageId;
|
|
2471
|
+
return interpolate(template, d.data ?? {});
|
|
2472
|
+
}
|
|
2473
|
+
function formatSummary(errors, warnings) {
|
|
2474
|
+
const parts = [];
|
|
2475
|
+
if (errors > 0) parts.push(`${errors} ${errors === 1 ? "error" : "errors"}`);
|
|
2476
|
+
if (warnings > 0) parts.push(`${warnings} ${warnings === 1 ? "warning" : "warnings"}`);
|
|
2477
|
+
return parts.join(", ");
|
|
2478
|
+
}
|
|
2479
|
+
var jsonReporter = {
|
|
2480
|
+
format(diagnostics, rulesByName, meta) {
|
|
2481
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
2482
|
+
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
2483
|
+
const payload = {
|
|
2484
|
+
version: 1,
|
|
2485
|
+
summary: {
|
|
2486
|
+
total: diagnostics.length,
|
|
2487
|
+
errors,
|
|
2488
|
+
warnings,
|
|
2489
|
+
// Always present (0 when no baseline) so CI consumers can detect an
|
|
2490
|
+
// active baseline without probing for the key. SARIF stays untouched.
|
|
2491
|
+
baselineHidden: meta?.baselineHidden ?? 0,
|
|
2492
|
+
// `fixed` appears ONLY when a fix pass ran (#28) — a plain lint keeps
|
|
2493
|
+
// the summary shape it always had.
|
|
2494
|
+
...meta?.fixed !== void 0 ? { fixed: meta.fixed } : {}
|
|
2495
|
+
},
|
|
2496
|
+
// Dry-run diffs (#28): embedded so `--fix-dry-run --format json` keeps
|
|
2497
|
+
// stdout pure JSON while still delivering the diff. Absent otherwise.
|
|
2498
|
+
...meta?.fixDiffs !== void 0 ? { diffs: meta.fixDiffs } : {},
|
|
2499
|
+
results: diagnostics.map((d) => ({
|
|
2500
|
+
path: d.path,
|
|
2501
|
+
ruleName: d.ruleName,
|
|
2502
|
+
messageId: d.messageId,
|
|
2503
|
+
severity: d.severity,
|
|
2504
|
+
message: renderMessage(d, rulesByName),
|
|
2505
|
+
// Self-contained diagnostics (#67): always present so consumers can
|
|
2506
|
+
// rely on the keys — `suggestion` is null when the rule declares none.
|
|
2507
|
+
suggestion: d.suggestion,
|
|
2508
|
+
docsUrl: d.docsUrl,
|
|
2509
|
+
// Autofix (#28): whether `--fix` can repair this diagnostic.
|
|
2510
|
+
fixable: d.fixable,
|
|
2511
|
+
data: d.data ?? {}
|
|
2512
|
+
}))
|
|
2513
|
+
};
|
|
2514
|
+
return JSON.stringify(payload, null, 2);
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2517
|
+
var sarifReporter = {
|
|
2518
|
+
format(diagnostics, rulesByName) {
|
|
2519
|
+
const ruleNames = [...new Set(diagnostics.map((d) => d.ruleName))];
|
|
2520
|
+
const ruleIndex = new Map(ruleNames.map((name, i) => [name, i]));
|
|
2521
|
+
const sarifRules = ruleNames.map((name) => {
|
|
2522
|
+
const rule9 = rulesByName.get(name);
|
|
2523
|
+
return {
|
|
2524
|
+
id: name,
|
|
2525
|
+
name: name.replace(/[^A-Za-z0-9]/g, "_"),
|
|
2526
|
+
shortDescription: { text: rule9?.meta.docs.description ?? name },
|
|
2527
|
+
helpUri: rule9?.meta.docs.url
|
|
2528
|
+
};
|
|
2529
|
+
});
|
|
2530
|
+
const sarifResults = diagnostics.map((d) => ({
|
|
2531
|
+
ruleId: d.ruleName,
|
|
2532
|
+
ruleIndex: ruleIndex.get(d.ruleName) ?? 0,
|
|
2533
|
+
level: d.severity === "error" ? "error" : "warning",
|
|
2534
|
+
message: { text: renderMessage(d, rulesByName) },
|
|
2535
|
+
locations: [
|
|
2536
|
+
{
|
|
2537
|
+
physicalLocation: {
|
|
2538
|
+
artifactLocation: { uri: d.path, uriBaseId: "%SRCROOT%" },
|
|
2539
|
+
region: { startLine: 1 }
|
|
2540
|
+
}
|
|
1755
2541
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2542
|
+
]
|
|
2543
|
+
}));
|
|
2544
|
+
const sarif = {
|
|
2545
|
+
$schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
|
|
2546
|
+
version: "2.1.0",
|
|
2547
|
+
runs: [
|
|
2548
|
+
{
|
|
2549
|
+
tool: {
|
|
2550
|
+
driver: {
|
|
2551
|
+
name: "madr-lint",
|
|
2552
|
+
informationUri: "https://github.com/knktkc/madr-lint",
|
|
2553
|
+
rules: sarifRules
|
|
1766
2554
|
}
|
|
1767
|
-
}
|
|
2555
|
+
},
|
|
2556
|
+
results: sarifResults
|
|
1768
2557
|
}
|
|
1769
|
-
|
|
2558
|
+
]
|
|
2559
|
+
};
|
|
2560
|
+
return JSON.stringify(sarif, null, 2);
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
function escapeGhMessage(s) {
|
|
2564
|
+
return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
2565
|
+
}
|
|
2566
|
+
function escapeGhProperty(s) {
|
|
2567
|
+
return escapeGhMessage(s).replace(/,/g, "%2C").replace(/:/g, "%3A");
|
|
2568
|
+
}
|
|
2569
|
+
var githubReporter = {
|
|
2570
|
+
format(diagnostics, rulesByName, meta) {
|
|
2571
|
+
const lines = [];
|
|
2572
|
+
for (const d of diagnostics) {
|
|
2573
|
+
const level = d.severity === "error" ? "error" : "warning";
|
|
2574
|
+
const file = escapeGhProperty(d.path);
|
|
2575
|
+
const title = escapeGhProperty(d.ruleName);
|
|
2576
|
+
const rendered = renderMessage(d, rulesByName);
|
|
2577
|
+
const message = escapeGhMessage(
|
|
2578
|
+
d.suggestion ? `${rendered} \u2014 ${d.suggestion}` : rendered
|
|
2579
|
+
);
|
|
2580
|
+
const linePart = d.loc ? `,line=${d.loc.line}` : "";
|
|
2581
|
+
lines.push(`::${level} file=${file}${linePart},title=${title}::${message}`);
|
|
1770
2582
|
}
|
|
2583
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
2584
|
+
const warnings = diagnostics.filter((d) => d.severity === "warn").length;
|
|
2585
|
+
const summaryParts = [];
|
|
2586
|
+
if (errors > 0) summaryParts.push(`${errors} ${errors === 1 ? "error" : "errors"}`);
|
|
2587
|
+
if (warnings > 0) summaryParts.push(`${warnings} ${warnings === 1 ? "warning" : "warnings"}`);
|
|
2588
|
+
if (summaryParts.length === 0) summaryParts.push("0 errors, 0 warnings");
|
|
2589
|
+
const baselineHidden = meta?.baselineHidden ?? 0;
|
|
2590
|
+
if (baselineHidden > 0) summaryParts.push(`${baselineHidden} hidden by baseline`);
|
|
2591
|
+
lines.push(summaryParts.join(", "));
|
|
2592
|
+
return lines.join("\n");
|
|
1771
2593
|
}
|
|
1772
2594
|
};
|
|
1773
|
-
var
|
|
2595
|
+
var reporters = {
|
|
2596
|
+
text: textReporter,
|
|
2597
|
+
json: jsonReporter,
|
|
2598
|
+
sarif: sarifReporter,
|
|
2599
|
+
github: githubReporter
|
|
2600
|
+
};
|
|
1774
2601
|
|
|
1775
2602
|
// src/cli.ts
|
|
1776
2603
|
function toPosix2(p) {
|
|
@@ -1778,9 +2605,9 @@ function toPosix2(p) {
|
|
|
1778
2605
|
return p.split(sep2).join("/");
|
|
1779
2606
|
}
|
|
1780
2607
|
var pkg = JSON.parse(
|
|
1781
|
-
|
|
2608
|
+
readFileSync6(new URL("../package.json", import.meta.url), "utf8")
|
|
1782
2609
|
);
|
|
1783
|
-
var main =
|
|
2610
|
+
var main = defineCommand2({
|
|
1784
2611
|
meta: {
|
|
1785
2612
|
name: "madr-lint",
|
|
1786
2613
|
version: pkg.version,
|
|
@@ -1831,6 +2658,16 @@ var main = defineCommand({
|
|
|
1831
2658
|
type: "string",
|
|
1832
2659
|
description: "Path to config file, bypassing auto-discovery",
|
|
1833
2660
|
required: false
|
|
2661
|
+
},
|
|
2662
|
+
fix: {
|
|
2663
|
+
type: "boolean",
|
|
2664
|
+
description: "Apply autofixes in place; exit code reflects remaining problems",
|
|
2665
|
+
default: false
|
|
2666
|
+
},
|
|
2667
|
+
"fix-dry-run": {
|
|
2668
|
+
type: "boolean",
|
|
2669
|
+
description: "Show a diff of the fixes that --fix would apply; write nothing",
|
|
2670
|
+
default: false
|
|
1834
2671
|
}
|
|
1835
2672
|
},
|
|
1836
2673
|
run({ args }) {
|
|
@@ -1851,7 +2688,7 @@ var main = defineCommand({
|
|
|
1851
2688
|
let configExplicit = false;
|
|
1852
2689
|
const configArg = args.config;
|
|
1853
2690
|
if (configArg) {
|
|
1854
|
-
const absConfigPath =
|
|
2691
|
+
const absConfigPath = resolve5(cwd, configArg);
|
|
1855
2692
|
try {
|
|
1856
2693
|
config = loadConfigFromPath(absConfigPath);
|
|
1857
2694
|
configExplicit = true;
|
|
@@ -1896,7 +2733,7 @@ var main = defineCommand({
|
|
|
1896
2733
|
const cacheEnabled = (args.cache ?? config.cache) === true;
|
|
1897
2734
|
const cacheDir = args["cache-dir"] ?? config.cacheLocation;
|
|
1898
2735
|
const cacheConfig = cacheEnabled ? {
|
|
1899
|
-
dir:
|
|
2736
|
+
dir: resolve5(cwd, cacheDir),
|
|
1900
2737
|
configHash: computeConfigHash({
|
|
1901
2738
|
rules: config.rules,
|
|
1902
2739
|
ignorePatterns: config.ignorePatterns,
|
|
@@ -1908,10 +2745,107 @@ var main = defineCommand({
|
|
|
1908
2745
|
const updateBaseline = args["update-baseline"] === true;
|
|
1909
2746
|
const useBaseline = !updateBaseline && args.baseline !== false;
|
|
1910
2747
|
const baseline = useBaseline ? loadBaseline(baselineFile) : null;
|
|
1911
|
-
if (useBaseline && baseline === null &&
|
|
2748
|
+
if (useBaseline && baseline === null && existsSync6(baselineFile)) {
|
|
1912
2749
|
console.error(baselineMalformedWarning());
|
|
1913
2750
|
}
|
|
1914
2751
|
const allRulesArray = Object.values(rules_exports);
|
|
2752
|
+
const format = args.format ?? "text";
|
|
2753
|
+
const reportAndExit = (diagnostics, baselineHidden, fixedCount, dryRun, fixDiffs) => {
|
|
2754
|
+
const rulesByName = new Map(
|
|
2755
|
+
allRulesArray.map((r) => [r.meta.name, r])
|
|
2756
|
+
);
|
|
2757
|
+
const reporter = reporters[format];
|
|
2758
|
+
if (!reporter) {
|
|
2759
|
+
console.error(
|
|
2760
|
+
`Unknown --format "${format}". Available: ${Object.keys(reporters).join(", ")}`
|
|
2761
|
+
);
|
|
2762
|
+
process.exit(2);
|
|
2763
|
+
}
|
|
2764
|
+
const warningCount = diagnostics.filter((d) => d.severity === "warn").length;
|
|
2765
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
2766
|
+
const overWarningLimit = maxWarnings >= 0 && warningCount > maxWarnings;
|
|
2767
|
+
const reported = args.quiet ? diagnostics.filter((d) => d.severity !== "warn") : diagnostics;
|
|
2768
|
+
const suppressAllClear = format === "text" && reported.length === 0 && overWarningLimit;
|
|
2769
|
+
if (!suppressAllClear) {
|
|
2770
|
+
console.log(
|
|
2771
|
+
reporter.format(reported, rulesByName, {
|
|
2772
|
+
baselineHidden,
|
|
2773
|
+
...fixedCount !== void 0 ? { fixed: fixedCount } : {},
|
|
2774
|
+
...fixDiffs !== void 0 ? { fixDiffs } : {}
|
|
2775
|
+
})
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
if (format === "text" && baselineHidden > 0) {
|
|
2779
|
+
console.log(baselineHiddenSummary(baselineHidden));
|
|
2780
|
+
}
|
|
2781
|
+
if (format === "text" && fixedCount !== void 0 && fixedCount > 0) {
|
|
2782
|
+
const noun = fixedCount === 1 ? "problem" : "problems";
|
|
2783
|
+
console.log(
|
|
2784
|
+
dryRun ? `${fixedCount} ${noun} fixable (dry run; no files written)` : `Fixed ${fixedCount} ${noun}`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
if (overWarningLimit) {
|
|
2788
|
+
console.error(
|
|
2789
|
+
`madr-lint: ${warningCount} warning(s) found, exceeds --max-warnings ${maxWarnings}`
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
process.exit(errorCount > 0 || overWarningLimit ? 1 : 0);
|
|
2793
|
+
};
|
|
2794
|
+
const fixMode = args.fix === true || args["fix-dry-run"] === true;
|
|
2795
|
+
if (fixMode && updateBaseline) {
|
|
2796
|
+
console.error(
|
|
2797
|
+
"madr-lint: --update-baseline cannot be combined with --fix or --fix-dry-run"
|
|
2798
|
+
);
|
|
2799
|
+
process.exit(2);
|
|
2800
|
+
}
|
|
2801
|
+
if (fixMode) {
|
|
2802
|
+
const dryRun = args["fix-dry-run"] === true;
|
|
2803
|
+
let fixResult;
|
|
2804
|
+
try {
|
|
2805
|
+
fixResult = lintAndFix({
|
|
2806
|
+
rules: allRulesArray,
|
|
2807
|
+
ruleSeverity: config.rules,
|
|
2808
|
+
files,
|
|
2809
|
+
cwd,
|
|
2810
|
+
baseline
|
|
2811
|
+
});
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
if (err instanceof RuleOptionsError) {
|
|
2814
|
+
console.error(`Invalid rule options in config: ${err.message}`);
|
|
2815
|
+
process.exit(2);
|
|
2816
|
+
}
|
|
2817
|
+
throw err;
|
|
2818
|
+
}
|
|
2819
|
+
let fixDiffs;
|
|
2820
|
+
if (dryRun) {
|
|
2821
|
+
const changed = fixResult.files.filter((f) => f.changed);
|
|
2822
|
+
if (format === "text") {
|
|
2823
|
+
for (const f of changed) {
|
|
2824
|
+
process.stdout.write(unifiedDiff(f.path, f.original, f.fixed));
|
|
2825
|
+
}
|
|
2826
|
+
} else if (format === "json") {
|
|
2827
|
+
fixDiffs = changed.map((f) => ({
|
|
2828
|
+
path: f.path,
|
|
2829
|
+
diff: unifiedDiff(f.path, f.original, f.fixed)
|
|
2830
|
+
}));
|
|
2831
|
+
} else {
|
|
2832
|
+
for (const f of changed) {
|
|
2833
|
+
process.stderr.write(unifiedDiff(f.path, f.original, f.fixed));
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
} else {
|
|
2837
|
+
for (const f of fixResult.files) {
|
|
2838
|
+
if (f.changed) writeFileSync4(f.absPath, f.fixed, "utf8");
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
reportAndExit(
|
|
2842
|
+
fixResult.diagnostics,
|
|
2843
|
+
fixResult.baselineHidden,
|
|
2844
|
+
fixResult.fixed,
|
|
2845
|
+
dryRun,
|
|
2846
|
+
fixDiffs
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
1915
2849
|
let result;
|
|
1916
2850
|
try {
|
|
1917
2851
|
result = lintFiles({
|
|
@@ -1935,40 +2869,13 @@ var main = defineCommand({
|
|
|
1935
2869
|
console.log(baselineWriteSummary(built));
|
|
1936
2870
|
process.exit(0);
|
|
1937
2871
|
}
|
|
1938
|
-
|
|
1939
|
-
allRulesArray.map((r) => [r.meta.name, r])
|
|
1940
|
-
);
|
|
1941
|
-
const format = args.format ?? "text";
|
|
1942
|
-
const reporter = reporters[format];
|
|
1943
|
-
if (!reporter) {
|
|
1944
|
-
console.error(
|
|
1945
|
-
`Unknown --format "${format}". Available: ${Object.keys(reporters).join(", ")}`
|
|
1946
|
-
);
|
|
1947
|
-
process.exit(2);
|
|
1948
|
-
}
|
|
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);
|
|
2872
|
+
reportAndExit(result.diagnostics, result.baselineHidden, void 0, false);
|
|
1971
2873
|
}
|
|
1972
2874
|
});
|
|
1973
|
-
|
|
2875
|
+
var rawArgs = process.argv.slice(2);
|
|
2876
|
+
if (rawArgs[0] === "init") {
|
|
2877
|
+
await runMain(initCommand, { rawArgs: rawArgs.slice(1) });
|
|
2878
|
+
} else {
|
|
2879
|
+
await runMain(main);
|
|
2880
|
+
}
|
|
1974
2881
|
//# sourceMappingURL=cli.js.map
|