unguard 0.16.1 → 0.16.2
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/dist/analyze-Cccs9TDD.js +5454 -0
- package/dist/analyze-Cccs9TDD.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +430 -415
- package/dist/cli.js.map +1 -1
- package/dist/engine-C4KGqeYw.js +479 -0
- package/dist/engine-C4KGqeYw.js.map +1 -0
- package/dist/index.d.ts +236 -226
- package/dist/index.js +3 -15
- package/dist/worker.js +46 -44
- package/dist/worker.js.map +1 -1
- package/package.json +8 -7
- package/dist/chunk-3Y76J2K2.js +0 -511
- package/dist/chunk-3Y76J2K2.js.map +0 -1
- package/dist/chunk-Z34GRD3S.js +0 -5352
- package/dist/chunk-Z34GRD3S.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,217 +1,236 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
executeScan,
|
|
6
|
-
isFailOn,
|
|
7
|
-
isRulePolicySeverity,
|
|
8
|
-
isSeverity,
|
|
9
|
-
loadBaseline,
|
|
10
|
-
toRulePolicyEntries,
|
|
11
|
-
writeBaseline
|
|
12
|
-
} from "./chunk-3Y76J2K2.js";
|
|
13
|
-
import "./chunk-Z34GRD3S.js";
|
|
14
|
-
|
|
15
|
-
// src/cli.ts
|
|
16
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
17
|
-
import { basename, relative, resolve } from "path";
|
|
18
|
-
import { parseArgs } from "util";
|
|
1
|
+
import { a as buildBaseline, c as isFailOn, d as toRulePolicyEntries, i as BASELINE_FILENAME, l as isRulePolicySeverity, o as loadBaseline, r as computeExitCode, s as writeBaseline, t as executeScan, u as isSeverity } from "./engine-C4KGqeYw.js";
|
|
2
|
+
import { basename, relative, resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
19
5
|
import pc from "picocolors";
|
|
20
|
-
|
|
6
|
+
//#region src/cli.ts
|
|
7
|
+
const COMMANDS = /* @__PURE__ */ new Set([
|
|
8
|
+
"scan",
|
|
9
|
+
"audit",
|
|
10
|
+
"baseline"
|
|
11
|
+
]);
|
|
21
12
|
async function main(argv) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
13
|
+
const rawArgs = argv.slice(2);
|
|
14
|
+
const first = rawArgs[0];
|
|
15
|
+
const command = first !== void 0 && COMMANDS.has(first) ? first : "scan";
|
|
16
|
+
const { values, positionals: cliPaths } = parseArgs({
|
|
17
|
+
args: first !== void 0 && COMMANDS.has(first) ? rawArgs.slice(1) : rawArgs,
|
|
18
|
+
options: {
|
|
19
|
+
strict: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
default: false
|
|
22
|
+
},
|
|
23
|
+
filter: { type: "string" },
|
|
24
|
+
fix: {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
default: false
|
|
27
|
+
},
|
|
28
|
+
format: {
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "grouped"
|
|
31
|
+
},
|
|
32
|
+
severity: {
|
|
33
|
+
type: "string",
|
|
34
|
+
multiple: true,
|
|
35
|
+
default: []
|
|
36
|
+
},
|
|
37
|
+
ignore: {
|
|
38
|
+
type: "string",
|
|
39
|
+
multiple: true,
|
|
40
|
+
default: []
|
|
41
|
+
},
|
|
42
|
+
rule: {
|
|
43
|
+
type: "string",
|
|
44
|
+
multiple: true,
|
|
45
|
+
default: []
|
|
46
|
+
},
|
|
47
|
+
config: { type: "string" },
|
|
48
|
+
"fail-on": { type: "string" },
|
|
49
|
+
concurrency: { type: "string" },
|
|
50
|
+
"no-baseline": {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
default: false
|
|
53
|
+
},
|
|
54
|
+
"no-cache": {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
default: false
|
|
57
|
+
},
|
|
58
|
+
version: {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
short: "v",
|
|
61
|
+
default: false
|
|
62
|
+
},
|
|
63
|
+
help: {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
short: "h",
|
|
66
|
+
default: false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
allowPositionals: true
|
|
70
|
+
});
|
|
71
|
+
if (values.version) {
|
|
72
|
+
console.log(readVersion());
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (values.help) {
|
|
76
|
+
printHelp();
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (values.format !== "grouped" && values.format !== "flat" && values.format !== "json") {
|
|
80
|
+
console.error(pc.red(`Invalid --format value "${values.format}". Use grouped, flat, or json.`));
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
let config = null;
|
|
84
|
+
try {
|
|
85
|
+
const configPath = findConfigPath(values.config);
|
|
86
|
+
config = configPath ? loadConfig(configPath) : null;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return printErrorAndFail(err);
|
|
89
|
+
}
|
|
90
|
+
const rulePolicyResult = parseRulePolicyArgs(values.rule);
|
|
91
|
+
if (!rulePolicyResult.ok) {
|
|
92
|
+
console.error(pc.red(rulePolicyResult.message));
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const severityFiltersResult = parseSeverityFilters(values.severity);
|
|
96
|
+
if (!severityFiltersResult.ok) {
|
|
97
|
+
console.error(pc.red(severityFiltersResult.message));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const failOnResult = command === "audit" ? resolveFailOn(values["fail-on"], "none") : resolveFailOn(values["fail-on"], "info", config?.failOn);
|
|
101
|
+
if (!failOnResult.ok) {
|
|
102
|
+
console.error(pc.red(failOnResult.message));
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
const concurrencyResult = resolveConcurrency(values.concurrency, config?.concurrency);
|
|
106
|
+
if (!concurrencyResult.ok) {
|
|
107
|
+
console.error(pc.red(concurrencyResult.message));
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
const paths = cliPaths.length > 0 ? cliPaths : config?.paths ?? [];
|
|
111
|
+
const ignore = [...config?.ignore ?? [], ...values.ignore];
|
|
112
|
+
const rulePolicy = [...toRulePolicyEntries(config?.rules), ...rulePolicyResult.value];
|
|
113
|
+
const cacheEnabled = values["no-cache"] ? false : config?.cache ?? true;
|
|
114
|
+
let baseline = null;
|
|
115
|
+
if (command === "scan" && !values["no-baseline"]) try {
|
|
116
|
+
baseline = loadBaseline(process.cwd());
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return printErrorAndFail(err);
|
|
119
|
+
}
|
|
120
|
+
const execution = await executeScan({
|
|
121
|
+
paths,
|
|
122
|
+
mode: command === "audit" ? "audit" : "scan",
|
|
123
|
+
strict: values.strict,
|
|
124
|
+
rules: values.filter ? [values.filter] : void 0,
|
|
125
|
+
ignore: ignore.length > 0 ? ignore : void 0,
|
|
126
|
+
rulePolicy: rulePolicy.length > 0 ? rulePolicy : void 0,
|
|
127
|
+
overrides: config?.overrides,
|
|
128
|
+
showSeverities: severityFiltersResult.value.length > 0 ? severityFiltersResult.value : config?.severity,
|
|
129
|
+
failOn: failOnResult.value,
|
|
130
|
+
concurrency: concurrencyResult.value,
|
|
131
|
+
cache: cacheEnabled,
|
|
132
|
+
baseline: baseline ?? void 0
|
|
133
|
+
});
|
|
134
|
+
if (command === "baseline") {
|
|
135
|
+
const data = buildBaseline(execution.visibleDiagnostics, process.cwd());
|
|
136
|
+
writeBaseline(data, process.cwd());
|
|
137
|
+
console.log(`Baseline written to ${BASELINE_FILENAME}: ${plural(execution.visibleDiagnostics.length, "known issue")} across ${plural(Object.keys(data.rules).length, "file")}.`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
let diagnostics = execution.visibleDiagnostics;
|
|
141
|
+
let exitCode = execution.exitCode;
|
|
142
|
+
let fixedCount = 0;
|
|
143
|
+
let fixedFileCount = 0;
|
|
144
|
+
if (values.fix) {
|
|
145
|
+
const result = applyFixes(diagnostics);
|
|
146
|
+
fixedCount = result.applied;
|
|
147
|
+
fixedFileCount = result.fileCount;
|
|
148
|
+
diagnostics = diagnostics.filter((d) => !result.fixed.has(d));
|
|
149
|
+
exitCode = computeExitCode(diagnostics, failOnResult.value);
|
|
150
|
+
}
|
|
151
|
+
if (values.format === "json") {
|
|
152
|
+
console.log(JSON.stringify(buildJsonReport(diagnostics, execution.fileCount, exitCode, values.fix ? fixedCount : null), null, 2));
|
|
153
|
+
return exitCode;
|
|
154
|
+
}
|
|
155
|
+
if (baseline !== null) console.log(pc.dim(`Using ${BASELINE_FILENAME} (run "unguard baseline" to regenerate, --no-baseline to ignore)`));
|
|
156
|
+
if (values.fix && fixedCount > 0) console.log(pc.green(`Applied ${fixedCount} fix${fixedCount === 1 ? "" : "es"} in ${fixedFileCount} file${fixedFileCount === 1 ? "" : "s"}. Re-run unguard to verify.`));
|
|
157
|
+
if (diagnostics.length === 0) {
|
|
158
|
+
console.log(pc.green(`No issues found in ${plural(execution.fileCount, "file")}.`));
|
|
159
|
+
return exitCode;
|
|
160
|
+
}
|
|
161
|
+
if (values.format === "flat") printDiagnosticsFlat(diagnostics);
|
|
162
|
+
else printDiagnostics(diagnostics);
|
|
163
|
+
printSummary(diagnostics, execution.fileCount);
|
|
164
|
+
return exitCode;
|
|
154
165
|
}
|
|
155
166
|
function buildJsonReport(diagnostics, fileCount, exitCode, fixedCount) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
return {
|
|
168
|
+
diagnostics: diagnostics.map((d) => ({
|
|
169
|
+
file: d.file,
|
|
170
|
+
line: d.line,
|
|
171
|
+
column: d.column,
|
|
172
|
+
severity: d.severity,
|
|
173
|
+
ruleId: d.ruleId,
|
|
174
|
+
message: d.message,
|
|
175
|
+
...d.annotation !== void 0 ? { annotation: d.annotation } : {},
|
|
176
|
+
fixable: d.fix !== void 0
|
|
177
|
+
})),
|
|
178
|
+
fileCount,
|
|
179
|
+
exitCode,
|
|
180
|
+
...fixedCount !== null ? { fixedCount } : {}
|
|
181
|
+
};
|
|
171
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Apply fix edits, last-to-first per file so earlier offsets stay valid.
|
|
185
|
+
* Overlapping edits in one pass are skipped — the re-run picks them up.
|
|
186
|
+
*/
|
|
172
187
|
function applyFixes(diagnostics) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
const byFile = groupByFile(diagnostics.filter((d) => d.fix !== void 0));
|
|
189
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
190
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
191
|
+
const ordered = [...fileDiagnostics].sort((a, b) => (b.fix?.start ?? 0) - (a.fix?.start ?? 0));
|
|
192
|
+
let content = readFileSync(file, "utf8");
|
|
193
|
+
let lastAppliedStart = Number.POSITIVE_INFINITY;
|
|
194
|
+
for (const diagnostic of ordered) {
|
|
195
|
+
const fix = diagnostic.fix;
|
|
196
|
+
if (fix === void 0) continue;
|
|
197
|
+
if (fix.end > lastAppliedStart) continue;
|
|
198
|
+
content = content.slice(0, fix.start) + fix.text + content.slice(fix.end);
|
|
199
|
+
lastAppliedStart = fix.start;
|
|
200
|
+
fixed.add(diagnostic);
|
|
201
|
+
}
|
|
202
|
+
writeFileSync(file, content, "utf8");
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
applied: fixed.size,
|
|
206
|
+
fileCount: byFile.size,
|
|
207
|
+
fixed
|
|
208
|
+
};
|
|
190
209
|
}
|
|
191
210
|
function groupByFile(diagnostics) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const diagnostic of diagnostics) {
|
|
213
|
+
let list = byFile.get(diagnostic.file);
|
|
214
|
+
if (list === void 0) {
|
|
215
|
+
list = [];
|
|
216
|
+
byFile.set(diagnostic.file, list);
|
|
217
|
+
}
|
|
218
|
+
list.push(diagnostic);
|
|
219
|
+
}
|
|
220
|
+
return byFile;
|
|
202
221
|
}
|
|
222
|
+
/** Surface a failure as output + exit code 1 instead of a thrown error. */
|
|
203
223
|
function printErrorAndFail(err) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
224
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.error(pc.red(message));
|
|
226
|
+
return 1;
|
|
207
227
|
}
|
|
208
228
|
function readVersion() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return typeof version === "string" ? version : "unknown";
|
|
229
|
+
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
230
|
+
return typeof version === "string" ? version : "unknown";
|
|
212
231
|
}
|
|
213
232
|
function printHelp() {
|
|
214
|
-
|
|
233
|
+
console.log(`unguard: data-shape static analyzer
|
|
215
234
|
|
|
216
235
|
Usage:
|
|
217
236
|
unguard scan [paths...] [options]
|
|
@@ -262,248 +281,244 @@ Examples:
|
|
|
262
281
|
unguard scan src --fail-on=error`);
|
|
263
282
|
}
|
|
264
283
|
function printDiagnostics(diagnostics) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
284
|
+
const byFile = groupByFile(diagnostics);
|
|
285
|
+
const cwd = `${process.cwd()}/`;
|
|
286
|
+
const cwdBase = process.cwd();
|
|
287
|
+
const prefixWidth = 22;
|
|
288
|
+
const columns = process.stdout.columns;
|
|
289
|
+
const wrapWidth = columns !== void 0 && columns > 42 ? columns - prefixWidth : null;
|
|
290
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
291
|
+
const rel = relative(cwdBase, file);
|
|
292
|
+
console.log(pc.underline(rel));
|
|
293
|
+
for (const diagnostic of fileDiagnostics) {
|
|
294
|
+
const loc = `${diagnostic.line}:${diagnostic.column}`;
|
|
295
|
+
const sev = diagnostic.severity === "error" ? pc.red("error".padEnd(7)) : diagnostic.severity === "warning" ? pc.yellow("warning") : pc.blue("info".padEnd(7));
|
|
296
|
+
const msg = diagnostic.message.replaceAll(cwd, "");
|
|
297
|
+
const head = ` ${pc.dim(loc.padEnd(10))} ${sev} `;
|
|
298
|
+
const rule = pc.dim(diagnostic.ruleId);
|
|
299
|
+
const annotation = diagnostic.annotation !== void 0 ? ` ${pc.cyan(diagnostic.annotation)}` : "";
|
|
300
|
+
if (wrapWidth === null) {
|
|
301
|
+
console.log(`${head}${msg} ${rule}${annotation}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const lines = wrapWords(msg, wrapWidth);
|
|
305
|
+
const tailWidth = diagnostic.ruleId.length + (diagnostic.annotation !== void 0 ? 2 + diagnostic.annotation.length : 0);
|
|
306
|
+
const last = lines.length - 1;
|
|
307
|
+
const tailFits = (lines[last]?.length ?? 0) + 2 + tailWidth <= wrapWidth;
|
|
308
|
+
const indent = " ".repeat(prefixWidth);
|
|
309
|
+
for (let i = 0; i < lines.length; i++) {
|
|
310
|
+
const start = i === 0 ? head : indent;
|
|
311
|
+
const tail = i === last && tailFits ? ` ${rule}${annotation}` : "";
|
|
312
|
+
console.log(`${start}${lines[i]}${tail}`);
|
|
313
|
+
}
|
|
314
|
+
if (!tailFits) console.log(`${indent}${rule}${annotation}`);
|
|
315
|
+
}
|
|
316
|
+
console.log();
|
|
317
|
+
}
|
|
299
318
|
}
|
|
319
|
+
/** Greedy word wrap; words longer than the width get their own line unbroken. */
|
|
300
320
|
function wrapWords(text, width) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
line += ` ${word}`;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (line !== "") lines.push(line);
|
|
314
|
-
return lines;
|
|
321
|
+
const lines = [];
|
|
322
|
+
let line = "";
|
|
323
|
+
for (const word of text.split(" ")) if (line === "") line = word;
|
|
324
|
+
else if (line.length + 1 + word.length > width) {
|
|
325
|
+
lines.push(line);
|
|
326
|
+
line = word;
|
|
327
|
+
} else line += ` ${word}`;
|
|
328
|
+
if (line !== "") lines.push(line);
|
|
329
|
+
return lines;
|
|
315
330
|
}
|
|
316
331
|
function printDiagnosticsFlat(diagnostics) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
const cwdBase = process.cwd();
|
|
333
|
+
const cwd = `${cwdBase}/`;
|
|
334
|
+
for (const diagnostic of diagnostics) {
|
|
335
|
+
const rel = relative(cwdBase, diagnostic.file);
|
|
336
|
+
const msg = diagnostic.message.replaceAll(cwd, "");
|
|
337
|
+
console.log(`${rel}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} [${diagnostic.ruleId}] ${msg}`);
|
|
338
|
+
}
|
|
324
339
|
}
|
|
325
340
|
function printSummary(diagnostics, fileCount) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
console.log(
|
|
338
|
-
`
|
|
339
|
-
${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${plural(fileCount, "file")}.`
|
|
340
|
-
);
|
|
341
|
+
const counts = /* @__PURE__ */ new Map();
|
|
342
|
+
for (const diagnostic of diagnostics) {
|
|
343
|
+
const prev = counts.get(diagnostic.ruleId);
|
|
344
|
+
counts.set(diagnostic.ruleId, (prev === void 0 ? 0 : prev) + 1);
|
|
345
|
+
}
|
|
346
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
347
|
+
const maxRule = Math.max(...sorted.map(([rule]) => rule.length));
|
|
348
|
+
console.log(pc.bold("Summary"));
|
|
349
|
+
for (const [rule, count] of sorted) console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);
|
|
350
|
+
console.log(`\n${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${plural(fileCount, "file")}.`);
|
|
341
351
|
}
|
|
342
352
|
function plural(count, noun) {
|
|
343
|
-
|
|
353
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
344
354
|
}
|
|
345
355
|
function parseSeverityFilters(values) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
356
|
+
const levels = splitCsv(values);
|
|
357
|
+
const parsed = [];
|
|
358
|
+
for (const value of levels) {
|
|
359
|
+
if (!isSeverity(value)) return {
|
|
360
|
+
ok: false,
|
|
361
|
+
message: `Invalid --severity value "${value}". Use error, warning, info.`
|
|
362
|
+
};
|
|
363
|
+
parsed.push(value);
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
value: parsed
|
|
368
|
+
};
|
|
355
369
|
}
|
|
356
370
|
function parseRulePolicyArgs(values) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
const entries = [];
|
|
372
|
+
for (const arg of values) {
|
|
373
|
+
const separator = resolveRuleSeparator(arg);
|
|
374
|
+
if (separator === null) return {
|
|
375
|
+
ok: false,
|
|
376
|
+
message: `Invalid --rule value "${arg}". Use <selector>=<off|info|warning|error>.`
|
|
377
|
+
};
|
|
378
|
+
const selector = arg.slice(0, separator).trim();
|
|
379
|
+
const severity = arg.slice(separator + 1).trim();
|
|
380
|
+
if (!selector) return {
|
|
381
|
+
ok: false,
|
|
382
|
+
message: `Invalid --rule value "${arg}". Rule selector is required.`
|
|
383
|
+
};
|
|
384
|
+
if (!isRulePolicySeverity(severity)) return {
|
|
385
|
+
ok: false,
|
|
386
|
+
message: `Invalid --rule value "${arg}". Severity must be off, info, warning, or error.`
|
|
387
|
+
};
|
|
388
|
+
entries.push({
|
|
389
|
+
selector,
|
|
390
|
+
severity
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
value: entries
|
|
396
|
+
};
|
|
374
397
|
}
|
|
375
398
|
function resolveRuleSeparator(value) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
399
|
+
const equalIndex = value.indexOf("=");
|
|
400
|
+
if (equalIndex >= 0) return equalIndex;
|
|
401
|
+
const colonIndex = value.lastIndexOf(":");
|
|
402
|
+
if (colonIndex <= 0) return null;
|
|
403
|
+
return colonIndex;
|
|
381
404
|
}
|
|
382
405
|
function splitCsv(values) {
|
|
383
|
-
|
|
406
|
+
return values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
384
407
|
}
|
|
385
408
|
function findConfigPath(cliValue) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
return null;
|
|
409
|
+
if (cliValue) {
|
|
410
|
+
const explicitPath = resolve(process.cwd(), cliValue);
|
|
411
|
+
if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${cliValue}`);
|
|
412
|
+
return explicitPath;
|
|
413
|
+
}
|
|
414
|
+
for (const candidate of ["unguard.config.json", ".unguardrc.json"]) {
|
|
415
|
+
const candidatePath = resolve(process.cwd(), candidate);
|
|
416
|
+
if (existsSync(candidatePath)) return candidatePath;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
397
419
|
}
|
|
398
420
|
function loadConfig(path) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (!Array.isArray(raw.overrides)) {
|
|
449
|
-
throw new Error(`Invalid config in ${basename(path)}: "overrides" must be an array.`);
|
|
450
|
-
}
|
|
451
|
-
config.overrides = raw.overrides.map((entry, index) => {
|
|
452
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
453
|
-
throw new Error(`Invalid config in ${basename(path)}: overrides[${index}] must be an object.`);
|
|
454
|
-
}
|
|
455
|
-
const override = entry;
|
|
456
|
-
if (!isStringArray(override.files) || override.files.length === 0) {
|
|
457
|
-
throw new Error(`Invalid config in ${basename(path)}: overrides[${index}].files must be a non-empty string array.`);
|
|
458
|
-
}
|
|
459
|
-
return {
|
|
460
|
-
files: override.files,
|
|
461
|
-
rules: parseRulesObject(override.rules, `overrides[${index}].rules`, basename(path))
|
|
462
|
-
};
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
return config;
|
|
421
|
+
const text = readFileSync(path, "utf8");
|
|
422
|
+
let parsed;
|
|
423
|
+
try {
|
|
424
|
+
parsed = JSON.parse(text);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
427
|
+
throw new Error(`Invalid JSON in ${basename(path)}: ${message}`);
|
|
428
|
+
}
|
|
429
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid config in ${basename(path)}: expected a JSON object.`);
|
|
430
|
+
const raw = parsed;
|
|
431
|
+
const config = {};
|
|
432
|
+
if (raw.paths !== void 0) {
|
|
433
|
+
if (!isStringArray(raw.paths)) throw new Error(`Invalid config in ${basename(path)}: "paths" must be a string array.`);
|
|
434
|
+
config.paths = raw.paths;
|
|
435
|
+
}
|
|
436
|
+
if (raw.ignore !== void 0) {
|
|
437
|
+
if (!isStringArray(raw.ignore)) throw new Error(`Invalid config in ${basename(path)}: "ignore" must be a string array.`);
|
|
438
|
+
config.ignore = raw.ignore;
|
|
439
|
+
}
|
|
440
|
+
if (raw.failOn !== void 0) {
|
|
441
|
+
if (typeof raw.failOn !== "string" || !isFailOn(raw.failOn)) throw new Error(`Invalid config in ${basename(path)}: "failOn" must be one of none, error, warning, info.`);
|
|
442
|
+
config.failOn = raw.failOn;
|
|
443
|
+
}
|
|
444
|
+
if (raw.severity !== void 0) {
|
|
445
|
+
if (!isStringArray(raw.severity) || !raw.severity.every(isSeverity)) throw new Error(`Invalid config in ${basename(path)}: "severity" must be an array of error, warning, info.`);
|
|
446
|
+
config.severity = raw.severity;
|
|
447
|
+
}
|
|
448
|
+
if (raw.concurrency !== void 0) {
|
|
449
|
+
if (typeof raw.concurrency !== "number" || !Number.isInteger(raw.concurrency) || raw.concurrency < 1) throw new Error(`Invalid config in ${basename(path)}: "concurrency" must be a positive integer.`);
|
|
450
|
+
config.concurrency = raw.concurrency;
|
|
451
|
+
}
|
|
452
|
+
if (raw.cache !== void 0) {
|
|
453
|
+
if (typeof raw.cache !== "boolean") throw new Error(`Invalid config in ${basename(path)}: "cache" must be a boolean.`);
|
|
454
|
+
config.cache = raw.cache;
|
|
455
|
+
}
|
|
456
|
+
if (raw.rules !== void 0) config.rules = parseRulesObject(raw.rules, `"rules"`, basename(path));
|
|
457
|
+
if (raw.overrides !== void 0) {
|
|
458
|
+
if (!Array.isArray(raw.overrides)) throw new Error(`Invalid config in ${basename(path)}: "overrides" must be an array.`);
|
|
459
|
+
config.overrides = raw.overrides.map((entry, index) => {
|
|
460
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`Invalid config in ${basename(path)}: overrides[${index}] must be an object.`);
|
|
461
|
+
const override = entry;
|
|
462
|
+
if (!isStringArray(override.files) || override.files.length === 0) throw new Error(`Invalid config in ${basename(path)}: overrides[${index}].files must be a non-empty string array.`);
|
|
463
|
+
return {
|
|
464
|
+
files: override.files,
|
|
465
|
+
rules: parseRulesObject(override.rules, `overrides[${index}].rules`, basename(path))
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return config;
|
|
466
470
|
}
|
|
467
471
|
function parseRulesObject(value, label, configName) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
rules[selector] = severity;
|
|
477
|
-
}
|
|
478
|
-
return rules;
|
|
472
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`Invalid config in ${configName}: ${label} must be an object.`);
|
|
473
|
+
const rules = {};
|
|
474
|
+
for (const [selector, severity] of Object.entries(value)) {
|
|
475
|
+
if (typeof severity !== "string" || !isRulePolicySeverity(severity)) throw new Error(`Invalid config in ${configName}: rule "${selector}" in ${label} must be off, info, warning, or error.`);
|
|
476
|
+
rules[selector] = severity;
|
|
477
|
+
}
|
|
478
|
+
return rules;
|
|
479
479
|
}
|
|
480
480
|
function isStringArray(value) {
|
|
481
|
-
|
|
481
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
482
482
|
}
|
|
483
483
|
function resolveFailOn(cliValue, fallback, configValue) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
484
|
+
const selected = cliValue ?? configValue ?? fallback;
|
|
485
|
+
if (!isFailOn(selected)) return {
|
|
486
|
+
ok: false,
|
|
487
|
+
message: `Invalid --fail-on value "${selected}". Use none, error, warning, or info.`
|
|
488
|
+
};
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
value: selected
|
|
492
|
+
};
|
|
489
493
|
}
|
|
490
494
|
function resolveConcurrency(cliValue, configValue) {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
495
|
+
if (cliValue !== void 0) {
|
|
496
|
+
const parsed = Number(cliValue);
|
|
497
|
+
if (!Number.isInteger(parsed) || parsed < 1) return {
|
|
498
|
+
ok: false,
|
|
499
|
+
message: `Invalid --concurrency value "${cliValue}". Use a positive integer.`
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
ok: true,
|
|
503
|
+
value: parsed
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (configValue !== void 0) {
|
|
507
|
+
if (!Number.isInteger(configValue) || configValue < 1) return {
|
|
508
|
+
ok: false,
|
|
509
|
+
message: "Invalid concurrency in config: must be a positive integer."
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
value: configValue
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
ok: true,
|
|
518
|
+
value: void 0
|
|
519
|
+
};
|
|
505
520
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
521
|
+
//#endregion
|
|
522
|
+
export { main };
|
|
523
|
+
|
|
509
524
|
//# sourceMappingURL=cli.js.map
|