unguard 0.8.0 → 0.9.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/README.md +73 -19
- package/dist/{chunk-YYX4R25B.js → chunk-I4RZLVE2.js} +262 -63
- package/dist/chunk-I4RZLVE2.js.map +1 -0
- package/dist/cli.js +198 -50
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -3
- package/dist/index.js +5 -1
- package/package.json +7 -3
- package/dist/chunk-YYX4R25B.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
executeScan,
|
|
3
|
+
isFailOn,
|
|
4
|
+
isRulePolicySeverity,
|
|
5
|
+
isSeverity,
|
|
6
|
+
toRulePolicyEntries
|
|
7
|
+
} from "./chunk-I4RZLVE2.js";
|
|
4
8
|
|
|
5
9
|
// src/cli.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { basename, relative, resolve } from "path";
|
|
6
12
|
import { parseArgs } from "util";
|
|
7
|
-
import { relative } from "path";
|
|
8
13
|
import pc from "picocolors";
|
|
9
|
-
function isSeverity(value) {
|
|
10
|
-
return value === "info" || value === "warning" || value === "error";
|
|
11
|
-
}
|
|
12
14
|
async function main(argv) {
|
|
13
15
|
const rawArgs = argv.slice(2);
|
|
14
16
|
const userArgs = rawArgs[0] === "scan" ? rawArgs.slice(1) : rawArgs;
|
|
15
|
-
const { values, positionals:
|
|
17
|
+
const { values, positionals: cliPaths } = parseArgs({
|
|
16
18
|
args: userArgs,
|
|
17
19
|
options: {
|
|
18
20
|
strict: { type: "boolean", default: false },
|
|
19
21
|
filter: { type: "string" },
|
|
20
22
|
format: { type: "string", default: "grouped" },
|
|
21
23
|
severity: { type: "string", multiple: true, default: [] },
|
|
24
|
+
ignore: { type: "string", multiple: true, default: [] },
|
|
25
|
+
rule: { type: "string", multiple: true, default: [] },
|
|
26
|
+
config: { type: "string" },
|
|
27
|
+
"fail-on": { type: "string" },
|
|
22
28
|
help: { type: "boolean", short: "h", default: false }
|
|
23
29
|
},
|
|
24
30
|
allowPositionals: true
|
|
@@ -27,25 +33,57 @@ async function main(argv) {
|
|
|
27
33
|
printHelp();
|
|
28
34
|
return 0;
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
let config = null;
|
|
37
|
+
try {
|
|
38
|
+
const configPath = findConfigPath(values.config);
|
|
39
|
+
config = configPath ? loadConfig(configPath) : null;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
42
|
+
console.error(pc.red(message));
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
const rulePolicyResult = parseRulePolicyArgs(values.rule);
|
|
46
|
+
if (!rulePolicyResult.ok) {
|
|
47
|
+
console.error(pc.red(rulePolicyResult.message));
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const severityFiltersResult = parseSeverityFilters(values.severity);
|
|
51
|
+
if (!severityFiltersResult.ok) {
|
|
52
|
+
console.error(pc.red(severityFiltersResult.message));
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const failOnResult = resolveFailOn(values["fail-on"], config?.failOn);
|
|
56
|
+
if (!failOnResult.ok) {
|
|
57
|
+
console.error(pc.red(failOnResult.message));
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const paths = cliPaths.length > 0 ? cliPaths : config?.paths ?? [];
|
|
61
|
+
const ignore = [...config?.ignore ?? [], ...values.ignore];
|
|
62
|
+
const rulePolicy = [
|
|
63
|
+
...toRulePolicyEntries(config?.rules),
|
|
64
|
+
...rulePolicyResult.value
|
|
65
|
+
];
|
|
66
|
+
const execution = await executeScan({
|
|
31
67
|
paths,
|
|
32
68
|
strict: values.strict,
|
|
33
|
-
rules: values.filter ? [values.filter] : void 0
|
|
69
|
+
rules: values.filter ? [values.filter] : void 0,
|
|
70
|
+
ignore: ignore.length > 0 ? ignore : void 0,
|
|
71
|
+
rulePolicy: rulePolicy.length > 0 ? rulePolicy : void 0,
|
|
72
|
+
showSeverities: severityFiltersResult.value.length > 0 ? severityFiltersResult.value : void 0,
|
|
73
|
+
failOn: failOnResult.value
|
|
34
74
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return 0;
|
|
75
|
+
const diagnostics = execution.visibleDiagnostics;
|
|
76
|
+
if (diagnostics.length === 0) {
|
|
77
|
+
console.log(pc.green(`No issues found in ${execution.fileCount} files.`));
|
|
78
|
+
return execution.exitCode;
|
|
40
79
|
}
|
|
41
80
|
if (values.format === "flat") {
|
|
42
|
-
printDiagnosticsFlat(
|
|
81
|
+
printDiagnosticsFlat(diagnostics);
|
|
43
82
|
} else {
|
|
44
|
-
printDiagnostics(
|
|
83
|
+
printDiagnostics(diagnostics);
|
|
45
84
|
}
|
|
46
|
-
printSummary(
|
|
47
|
-
|
|
48
|
-
return hasError ? 2 : 1;
|
|
85
|
+
printSummary(diagnostics, execution.fileCount);
|
|
86
|
+
return execution.exitCode;
|
|
49
87
|
}
|
|
50
88
|
function printHelp() {
|
|
51
89
|
console.log(`unguard: data-shape static analyzer
|
|
@@ -55,46 +93,50 @@ Usage:
|
|
|
55
93
|
unguard [paths...] [options]
|
|
56
94
|
|
|
57
95
|
Options:
|
|
58
|
-
--
|
|
96
|
+
--config <path> Path to unguard config file (defaults to ./unguard.config.json)
|
|
97
|
+
--strict Treat all diagnostics as errors
|
|
59
98
|
--filter <rule> Run only the specified rule
|
|
60
|
-
--
|
|
99
|
+
--rule <sel=sev> Override rule severity (supports id, *, category:<name>, tag:<name>)
|
|
100
|
+
--ignore <glob> Ignore path glob. Repeatable.
|
|
101
|
+
--severity <levels> Filter output by severity (comma-separated and/or repeatable)
|
|
102
|
+
--fail-on <level> Exit threshold: none, error, warning, info (default: info)
|
|
61
103
|
--format <mode> Output format: grouped (default) or flat
|
|
62
104
|
-h, --help Show this help
|
|
63
105
|
|
|
64
106
|
Exit codes:
|
|
65
|
-
0
|
|
66
|
-
1
|
|
67
|
-
2
|
|
107
|
+
0 Clean, or does not meet --fail-on threshold
|
|
108
|
+
1 Meets threshold without errors
|
|
109
|
+
2 Meets threshold with at least one error
|
|
68
110
|
|
|
69
111
|
Examples:
|
|
70
112
|
unguard scan src
|
|
71
|
-
unguard scan src --
|
|
72
|
-
unguard scan src --
|
|
73
|
-
unguard scan src --
|
|
74
|
-
unguard scan src --severity=error
|
|
75
|
-
unguard scan src --
|
|
113
|
+
unguard scan src --rule duplicate-*=warning
|
|
114
|
+
unguard scan src --rule category:cross-file=warning
|
|
115
|
+
unguard scan src --rule tag:safety=error
|
|
116
|
+
unguard scan src --severity=error,warning
|
|
117
|
+
unguard scan src --fail-on=error`);
|
|
76
118
|
}
|
|
77
119
|
function printDiagnostics(diagnostics) {
|
|
78
120
|
const byFile = /* @__PURE__ */ new Map();
|
|
79
|
-
for (const
|
|
80
|
-
let list = byFile.get(
|
|
121
|
+
for (const diagnostic of diagnostics) {
|
|
122
|
+
let list = byFile.get(diagnostic.file);
|
|
81
123
|
if (list === void 0) {
|
|
82
124
|
list = [];
|
|
83
|
-
byFile.set(
|
|
125
|
+
byFile.set(diagnostic.file, list);
|
|
84
126
|
}
|
|
85
|
-
list.push(
|
|
127
|
+
list.push(diagnostic);
|
|
86
128
|
}
|
|
87
|
-
const cwd = process.cwd()
|
|
129
|
+
const cwd = `${process.cwd()}/`;
|
|
88
130
|
const cwdBase = process.cwd();
|
|
89
|
-
for (const [file,
|
|
131
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
90
132
|
const rel = relative(cwdBase, file);
|
|
91
133
|
console.log(pc.underline(rel));
|
|
92
|
-
for (const
|
|
93
|
-
const loc = `${
|
|
94
|
-
const sev =
|
|
95
|
-
const msg =
|
|
96
|
-
const rule = pc.dim(
|
|
97
|
-
const annotation =
|
|
134
|
+
for (const diagnostic of fileDiagnostics) {
|
|
135
|
+
const loc = `${diagnostic.line}:${diagnostic.column}`;
|
|
136
|
+
const sev = diagnostic.severity === "error" ? pc.red("error") : diagnostic.severity === "warning" ? pc.yellow("warning") : pc.blue("info");
|
|
137
|
+
const msg = diagnostic.message.replaceAll(cwd, "");
|
|
138
|
+
const rule = pc.dim(diagnostic.ruleId);
|
|
139
|
+
const annotation = diagnostic.annotation !== void 0 ? ` ${pc.cyan(diagnostic.annotation)}` : "";
|
|
98
140
|
console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);
|
|
99
141
|
}
|
|
100
142
|
console.log();
|
|
@@ -102,21 +144,21 @@ function printDiagnostics(diagnostics) {
|
|
|
102
144
|
}
|
|
103
145
|
function printDiagnosticsFlat(diagnostics) {
|
|
104
146
|
const cwdBase = process.cwd();
|
|
105
|
-
const cwd = cwdBase
|
|
106
|
-
for (const
|
|
107
|
-
const rel = relative(cwdBase,
|
|
108
|
-
const msg =
|
|
109
|
-
console.log(`${rel}:${
|
|
147
|
+
const cwd = `${cwdBase}/`;
|
|
148
|
+
for (const diagnostic of diagnostics) {
|
|
149
|
+
const rel = relative(cwdBase, diagnostic.file);
|
|
150
|
+
const msg = diagnostic.message.replaceAll(cwd, "");
|
|
151
|
+
console.log(`${rel}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} [${diagnostic.ruleId}] ${msg}`);
|
|
110
152
|
}
|
|
111
153
|
}
|
|
112
154
|
function printSummary(diagnostics, fileCount) {
|
|
113
155
|
const counts = /* @__PURE__ */ new Map();
|
|
114
|
-
for (const
|
|
115
|
-
const prev = counts.get(
|
|
116
|
-
counts.set(
|
|
156
|
+
for (const diagnostic of diagnostics) {
|
|
157
|
+
const prev = counts.get(diagnostic.ruleId);
|
|
158
|
+
counts.set(diagnostic.ruleId, (prev === void 0 ? 0 : prev) + 1);
|
|
117
159
|
}
|
|
118
160
|
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
119
|
-
const maxRule = Math.max(...sorted.map(([
|
|
161
|
+
const maxRule = Math.max(...sorted.map(([rule]) => rule.length));
|
|
120
162
|
console.log(pc.bold("Summary"));
|
|
121
163
|
for (const [rule, count] of sorted) {
|
|
122
164
|
console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);
|
|
@@ -126,6 +168,112 @@ function printSummary(diagnostics, fileCount) {
|
|
|
126
168
|
${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${fileCount} files.`
|
|
127
169
|
);
|
|
128
170
|
}
|
|
171
|
+
function parseSeverityFilters(values) {
|
|
172
|
+
const levels = splitCsv(values);
|
|
173
|
+
const parsed = [];
|
|
174
|
+
for (const value of levels) {
|
|
175
|
+
if (!isSeverity(value)) {
|
|
176
|
+
return { ok: false, message: `Invalid --severity value "${value}". Use error, warning, info.` };
|
|
177
|
+
}
|
|
178
|
+
parsed.push(value);
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, value: parsed };
|
|
181
|
+
}
|
|
182
|
+
function parseRulePolicyArgs(values) {
|
|
183
|
+
const entries = [];
|
|
184
|
+
for (const arg of values) {
|
|
185
|
+
const separator = resolveRuleSeparator(arg);
|
|
186
|
+
if (separator === null) {
|
|
187
|
+
return { ok: false, message: `Invalid --rule value "${arg}". Use <selector>=<off|info|warning|error>.` };
|
|
188
|
+
}
|
|
189
|
+
const selector = arg.slice(0, separator).trim();
|
|
190
|
+
const severity = arg.slice(separator + 1).trim();
|
|
191
|
+
if (!selector) {
|
|
192
|
+
return { ok: false, message: `Invalid --rule value "${arg}". Rule selector is required.` };
|
|
193
|
+
}
|
|
194
|
+
if (!isRulePolicySeverity(severity)) {
|
|
195
|
+
return { ok: false, message: `Invalid --rule value "${arg}". Severity must be off, info, warning, or error.` };
|
|
196
|
+
}
|
|
197
|
+
entries.push({ selector, severity });
|
|
198
|
+
}
|
|
199
|
+
return { ok: true, value: entries };
|
|
200
|
+
}
|
|
201
|
+
function resolveRuleSeparator(value) {
|
|
202
|
+
const equalIndex = value.indexOf("=");
|
|
203
|
+
if (equalIndex >= 0) return equalIndex;
|
|
204
|
+
const colonIndex = value.lastIndexOf(":");
|
|
205
|
+
if (colonIndex <= 0) return null;
|
|
206
|
+
return colonIndex;
|
|
207
|
+
}
|
|
208
|
+
function splitCsv(values) {
|
|
209
|
+
return values.flatMap((value) => value.split(",")).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
210
|
+
}
|
|
211
|
+
function findConfigPath(cliValue) {
|
|
212
|
+
if (cliValue) {
|
|
213
|
+
const explicitPath = resolve(process.cwd(), cliValue);
|
|
214
|
+
if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${cliValue}`);
|
|
215
|
+
return explicitPath;
|
|
216
|
+
}
|
|
217
|
+
const candidates = ["unguard.config.json", ".unguardrc.json"];
|
|
218
|
+
for (const candidate of candidates) {
|
|
219
|
+
const candidatePath = resolve(process.cwd(), candidate);
|
|
220
|
+
if (existsSync(candidatePath)) return candidatePath;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function loadConfig(path) {
|
|
225
|
+
const text = readFileSync(path, "utf8");
|
|
226
|
+
let parsed;
|
|
227
|
+
try {
|
|
228
|
+
parsed = JSON.parse(text);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
231
|
+
throw new Error(`Invalid JSON in ${basename(path)}: ${message}`);
|
|
232
|
+
}
|
|
233
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
234
|
+
throw new Error(`Invalid config in ${basename(path)}: expected a JSON object.`);
|
|
235
|
+
}
|
|
236
|
+
const raw = parsed;
|
|
237
|
+
const config = {};
|
|
238
|
+
if (raw.paths !== void 0) {
|
|
239
|
+
if (!isStringArray(raw.paths)) throw new Error(`Invalid config in ${basename(path)}: "paths" must be a string array.`);
|
|
240
|
+
config.paths = raw.paths;
|
|
241
|
+
}
|
|
242
|
+
if (raw.ignore !== void 0) {
|
|
243
|
+
if (!isStringArray(raw.ignore)) throw new Error(`Invalid config in ${basename(path)}: "ignore" must be a string array.`);
|
|
244
|
+
config.ignore = raw.ignore;
|
|
245
|
+
}
|
|
246
|
+
if (raw.failOn !== void 0) {
|
|
247
|
+
if (typeof raw.failOn !== "string" || !isFailOn(raw.failOn)) {
|
|
248
|
+
throw new Error(`Invalid config in ${basename(path)}: "failOn" must be one of none, error, warning, info.`);
|
|
249
|
+
}
|
|
250
|
+
config.failOn = raw.failOn;
|
|
251
|
+
}
|
|
252
|
+
if (raw.rules !== void 0) {
|
|
253
|
+
if (typeof raw.rules !== "object" || raw.rules === null || Array.isArray(raw.rules)) {
|
|
254
|
+
throw new Error(`Invalid config in ${basename(path)}: "rules" must be an object.`);
|
|
255
|
+
}
|
|
256
|
+
const rules = {};
|
|
257
|
+
for (const [selector, severity] of Object.entries(raw.rules)) {
|
|
258
|
+
if (typeof severity !== "string" || !isRulePolicySeverity(severity)) {
|
|
259
|
+
throw new Error(`Invalid config in ${basename(path)}: rule "${selector}" must be off, info, warning, or error.`);
|
|
260
|
+
}
|
|
261
|
+
rules[selector] = severity;
|
|
262
|
+
}
|
|
263
|
+
config.rules = rules;
|
|
264
|
+
}
|
|
265
|
+
return config;
|
|
266
|
+
}
|
|
267
|
+
function isStringArray(value) {
|
|
268
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
269
|
+
}
|
|
270
|
+
function resolveFailOn(cliValue, configValue) {
|
|
271
|
+
const selected = cliValue ?? configValue ?? "info";
|
|
272
|
+
if (!isFailOn(selected)) {
|
|
273
|
+
return { ok: false, message: `Invalid --fail-on value "${selected}". Use none, error, warning, or info.` };
|
|
274
|
+
}
|
|
275
|
+
return { ok: true, value: selected };
|
|
276
|
+
}
|
|
129
277
|
export {
|
|
130
278
|
main
|
|
131
279
|
};
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { relative } from \"node:path\";\nimport pc from \"picocolors\";\nimport { scan } from \"./engine.ts\";\nimport type { Diagnostic } from \"./rules/types.ts\";\n\ntype Severity = \"info\" | \"warning\" | \"error\";\nfunction isSeverity(value: string): value is Severity {\n return value === \"info\" || value === \"warning\" || value === \"error\";\n}\n\nexport async function main(argv: string[]): Promise<number> {\n const rawArgs = argv.slice(2);\n const userArgs = rawArgs[0] === \"scan\" ? rawArgs.slice(1) : rawArgs;\n\n const { values, positionals: paths } = parseArgs({\n args: userArgs,\n options: {\n strict: { type: \"boolean\", default: false },\n filter: { type: \"string\" },\n format: { type: \"string\", default: \"grouped\" },\n severity: { type: \"string\", multiple: true, default: [] },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n });\n\n if (values.help) {\n printHelp();\n return 0;\n }\n\n const result = await scan({\n paths,\n strict: values.strict,\n rules: values.filter ? [values.filter] : undefined,\n });\n\n const severitySet = values.severity.length > 0\n ? new Set(values.severity.filter(isSeverity))\n : null;\n const filtered = severitySet\n ? result.diagnostics.filter((d) => severitySet.has(d.severity))\n : result.diagnostics;\n\n if (filtered.length === 0) {\n console.log(pc.green(`No issues found in ${result.fileCount} files.`));\n return 0;\n }\n\n if (values.format === \"flat\") {\n printDiagnosticsFlat(filtered);\n } else {\n printDiagnostics(filtered);\n }\n printSummary(filtered, result.fileCount);\n\n const hasError = filtered.some((d) => d.severity === \"error\");\n return hasError ? 2 : 1;\n}\n\nfunction printHelp() {\n console.log(`unguard: data-shape static analyzer\n\nUsage:\n unguard scan [paths...] [options]\n unguard [paths...] [options]\n\nOptions:\n --strict Treat all warnings as errors\n --filter <rule> Run only the specified rule\n --severity <level> Filter by severity (error, warning, info). Repeatable.\n --format <mode> Output format: grouped (default) or flat\n -h, --help Show this help\n\nExit codes:\n 0 No issues\n 1 Warnings or info only\n 2 At least one error\n\nExamples:\n unguard scan src\n unguard scan src --strict\n unguard scan src --filter no-empty-catch\n unguard scan src --severity=error\n unguard scan src --severity=error --severity=warning\n unguard scan src --format=flat | grep error`);\n}\n\n\nfunction printDiagnostics(diagnostics: Diagnostic[]) {\n const byFile = new Map<string, Diagnostic[]>();\n for (const d of diagnostics) {\n let list = byFile.get(d.file);\n if (list === undefined) {\n list = [];\n byFile.set(d.file, list);\n }\n list.push(d);\n }\n\n const cwd = process.cwd() + \"/\";\n const cwdBase = process.cwd();\n\n for (const [file, diags] of byFile) {\n const rel = relative(cwdBase, file);\n console.log(pc.underline(rel));\n\n for (const d of diags) {\n const loc = `${d.line}:${d.column}`;\n const sev = d.severity === \"error\" ? pc.red(\"error\") : d.severity === \"warning\" ? pc.yellow(\"warning\") : pc.blue(\"info\");\n const msg = d.message.replaceAll(cwd, \"\");\n const rule = pc.dim(d.ruleId);\n const annotation = d.annotation !== undefined ? ` ${pc.cyan(d.annotation)}` : \"\";\n console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);\n }\n\n console.log();\n }\n}\n\nfunction printDiagnosticsFlat(diagnostics: Diagnostic[]) {\n const cwdBase = process.cwd();\n const cwd = cwdBase + \"/\";\n\n for (const d of diagnostics) {\n const rel = relative(cwdBase, d.file);\n const msg = d.message.replaceAll(cwd, \"\");\n console.log(`${rel}:${d.line}:${d.column} ${d.severity} [${d.ruleId}] ${msg}`);\n }\n}\n\nfunction printSummary(diagnostics: Diagnostic[], fileCount: number) {\n const counts = new Map<string, number>();\n for (const d of diagnostics) {\n const prev = counts.get(d.ruleId);\n counts.set(d.ruleId, (prev === undefined ? 0 : prev) + 1);\n }\n\n const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);\n const maxRule = Math.max(...sorted.map(([r]) => r.length));\n\n console.log(pc.bold(\"Summary\"));\n for (const [rule, count] of sorted) {\n console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);\n }\n\n console.log(\n `\\n${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? \"\" : \"s\"} in ${fileCount} files.`,\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { basename, relative, resolve } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport pc from \"picocolors\";\nimport { executeScan, type FailOn, type RulePolicyEntry, type RulePolicySeverity, type Severity } from \"./engine.ts\";\nimport { isFailOn, isRulePolicySeverity, isSeverity, toRulePolicyEntries } from \"./scan/config.ts\";\nimport type { RulePolicy } from \"./scan/types.ts\";\nimport type { Diagnostic } from \"./rules/types.ts\";\n\ninterface UnguardConfig {\n paths?: string[];\n ignore?: string[];\n rules?: RulePolicy;\n failOn?: FailOn;\n}\n\nexport async function main(argv: string[]): Promise<number> {\n const rawArgs = argv.slice(2);\n const userArgs = rawArgs[0] === \"scan\" ? rawArgs.slice(1) : rawArgs;\n\n const { values, positionals: cliPaths } = parseArgs({\n args: userArgs,\n options: {\n strict: { type: \"boolean\", default: false },\n filter: { type: \"string\" },\n format: { type: \"string\", default: \"grouped\" },\n severity: { type: \"string\", multiple: true, default: [] },\n ignore: { type: \"string\", multiple: true, default: [] },\n rule: { type: \"string\", multiple: true, default: [] },\n config: { type: \"string\" },\n \"fail-on\": { type: \"string\" },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n });\n\n if (values.help) {\n printHelp();\n return 0;\n }\n\n let config: UnguardConfig | null = null;\n try {\n const configPath = findConfigPath(values.config);\n config = configPath ? loadConfig(configPath) : null;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(pc.red(message));\n return 1;\n }\n\n const rulePolicyResult = parseRulePolicyArgs(values.rule);\n if (!rulePolicyResult.ok) {\n console.error(pc.red(rulePolicyResult.message));\n return 1;\n }\n\n const severityFiltersResult = parseSeverityFilters(values.severity);\n if (!severityFiltersResult.ok) {\n console.error(pc.red(severityFiltersResult.message));\n return 1;\n }\n\n const failOnResult = resolveFailOn(values[\"fail-on\"], config?.failOn);\n if (!failOnResult.ok) {\n console.error(pc.red(failOnResult.message));\n return 1;\n }\n\n const paths = cliPaths.length > 0 ? cliPaths : config?.paths ?? [];\n const ignore = [...(config?.ignore ?? []), ...values.ignore];\n const rulePolicy = [\n ...toRulePolicyEntries(config?.rules),\n ...rulePolicyResult.value,\n ];\n\n const execution = await executeScan({\n paths,\n strict: values.strict,\n rules: values.filter ? [values.filter] : undefined,\n ignore: ignore.length > 0 ? ignore : undefined,\n rulePolicy: rulePolicy.length > 0 ? rulePolicy : undefined,\n showSeverities: severityFiltersResult.value.length > 0 ? severityFiltersResult.value : undefined,\n failOn: failOnResult.value,\n });\n\n const diagnostics = execution.visibleDiagnostics;\n if (diagnostics.length === 0) {\n console.log(pc.green(`No issues found in ${execution.fileCount} files.`));\n return execution.exitCode;\n }\n\n if (values.format === \"flat\") {\n printDiagnosticsFlat(diagnostics);\n } else {\n printDiagnostics(diagnostics);\n }\n printSummary(diagnostics, execution.fileCount);\n\n return execution.exitCode;\n}\n\nfunction printHelp() {\n console.log(`unguard: data-shape static analyzer\n\nUsage:\n unguard scan [paths...] [options]\n unguard [paths...] [options]\n\nOptions:\n --config <path> Path to unguard config file (defaults to ./unguard.config.json)\n --strict Treat all diagnostics as errors\n --filter <rule> Run only the specified rule\n --rule <sel=sev> Override rule severity (supports id, *, category:<name>, tag:<name>)\n --ignore <glob> Ignore path glob. Repeatable.\n --severity <levels> Filter output by severity (comma-separated and/or repeatable)\n --fail-on <level> Exit threshold: none, error, warning, info (default: info)\n --format <mode> Output format: grouped (default) or flat\n -h, --help Show this help\n\nExit codes:\n 0 Clean, or does not meet --fail-on threshold\n 1 Meets threshold without errors\n 2 Meets threshold with at least one error\n\nExamples:\n unguard scan src\n unguard scan src --rule duplicate-*=warning\n unguard scan src --rule category:cross-file=warning\n unguard scan src --rule tag:safety=error\n unguard scan src --severity=error,warning\n unguard scan src --fail-on=error`);\n}\n\nfunction printDiagnostics(diagnostics: Diagnostic[]) {\n const byFile = new Map<string, Diagnostic[]>();\n for (const diagnostic of diagnostics) {\n let list = byFile.get(diagnostic.file);\n if (list === undefined) {\n list = [];\n byFile.set(diagnostic.file, list);\n }\n list.push(diagnostic);\n }\n\n const cwd = `${process.cwd()}/`;\n const cwdBase = process.cwd();\n\n for (const [file, fileDiagnostics] of byFile) {\n const rel = relative(cwdBase, file);\n console.log(pc.underline(rel));\n\n for (const diagnostic of fileDiagnostics) {\n const loc = `${diagnostic.line}:${diagnostic.column}`;\n const sev = diagnostic.severity === \"error\"\n ? pc.red(\"error\")\n : diagnostic.severity === \"warning\"\n ? pc.yellow(\"warning\")\n : pc.blue(\"info\");\n const msg = diagnostic.message.replaceAll(cwd, \"\");\n const rule = pc.dim(diagnostic.ruleId);\n const annotation = diagnostic.annotation !== undefined ? ` ${pc.cyan(diagnostic.annotation)}` : \"\";\n console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);\n }\n\n console.log();\n }\n}\n\nfunction printDiagnosticsFlat(diagnostics: Diagnostic[]) {\n const cwdBase = process.cwd();\n const cwd = `${cwdBase}/`;\n\n for (const diagnostic of diagnostics) {\n const rel = relative(cwdBase, diagnostic.file);\n const msg = diagnostic.message.replaceAll(cwd, \"\");\n console.log(`${rel}:${diagnostic.line}:${diagnostic.column} ${diagnostic.severity} [${diagnostic.ruleId}] ${msg}`);\n }\n}\n\nfunction printSummary(diagnostics: Diagnostic[], fileCount: number) {\n const counts = new Map<string, number>();\n for (const diagnostic of diagnostics) {\n const prev = counts.get(diagnostic.ruleId);\n counts.set(diagnostic.ruleId, (prev === undefined ? 0 : prev) + 1);\n }\n\n const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);\n const maxRule = Math.max(...sorted.map(([rule]) => rule.length));\n\n console.log(pc.bold(\"Summary\"));\n for (const [rule, count] of sorted) {\n console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);\n }\n\n console.log(\n `\\n${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? \"\" : \"s\"} in ${fileCount} files.`,\n );\n}\n\nfunction parseSeverityFilters(values: string[]): { ok: true; value: Severity[] } | { ok: false; message: string } {\n const levels = splitCsv(values);\n const parsed: Severity[] = [];\n for (const value of levels) {\n if (!isSeverity(value)) {\n return { ok: false, message: `Invalid --severity value \"${value}\". Use error, warning, info.` };\n }\n parsed.push(value);\n }\n return { ok: true, value: parsed };\n}\n\nfunction parseRulePolicyArgs(values: string[]): { ok: true; value: RulePolicyEntry[] } | { ok: false; message: string } {\n const entries: RulePolicyEntry[] = [];\n\n for (const arg of values) {\n const separator = resolveRuleSeparator(arg);\n if (separator === null) {\n return { ok: false, message: `Invalid --rule value \"${arg}\". Use <selector>=<off|info|warning|error>.` };\n }\n\n const selector = arg.slice(0, separator).trim();\n const severity = arg.slice(separator + 1).trim();\n if (!selector) {\n return { ok: false, message: `Invalid --rule value \"${arg}\". Rule selector is required.` };\n }\n if (!isRulePolicySeverity(severity)) {\n return { ok: false, message: `Invalid --rule value \"${arg}\". Severity must be off, info, warning, or error.` };\n }\n\n entries.push({ selector, severity });\n }\n\n return { ok: true, value: entries };\n}\n\nfunction resolveRuleSeparator(value: string): number | null {\n const equalIndex = value.indexOf(\"=\");\n if (equalIndex >= 0) return equalIndex;\n const colonIndex = value.lastIndexOf(\":\");\n if (colonIndex <= 0) return null;\n return colonIndex;\n}\n\nfunction splitCsv(values: string[]): string[] {\n return values\n .flatMap((value) => value.split(\",\"))\n .map((value) => value.trim())\n .filter((value) => value.length > 0);\n}\n\nfunction findConfigPath(cliValue: string | undefined): string | null {\n if (cliValue) {\n const explicitPath = resolve(process.cwd(), cliValue);\n if (!existsSync(explicitPath)) throw new Error(`Config file not found: ${cliValue}`);\n return explicitPath;\n }\n\n const candidates = [\"unguard.config.json\", \".unguardrc.json\"];\n for (const candidate of candidates) {\n const candidatePath = resolve(process.cwd(), candidate);\n if (existsSync(candidatePath)) return candidatePath;\n }\n return null;\n}\n\nfunction loadConfig(path: string): UnguardConfig {\n const text = readFileSync(path, \"utf8\");\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(`Invalid JSON in ${basename(path)}: ${message}`);\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(`Invalid config in ${basename(path)}: expected a JSON object.`);\n }\n\n const raw = parsed as Record<string, unknown>;\n const config: UnguardConfig = {};\n\n if (raw.paths !== undefined) {\n if (!isStringArray(raw.paths)) throw new Error(`Invalid config in ${basename(path)}: \"paths\" must be a string array.`);\n config.paths = raw.paths;\n }\n\n if (raw.ignore !== undefined) {\n if (!isStringArray(raw.ignore)) throw new Error(`Invalid config in ${basename(path)}: \"ignore\" must be a string array.`);\n config.ignore = raw.ignore;\n }\n\n if (raw.failOn !== undefined) {\n if (typeof raw.failOn !== \"string\" || !isFailOn(raw.failOn)) {\n throw new Error(`Invalid config in ${basename(path)}: \"failOn\" must be one of none, error, warning, info.`);\n }\n config.failOn = raw.failOn;\n }\n\n if (raw.rules !== undefined) {\n if (typeof raw.rules !== \"object\" || raw.rules === null || Array.isArray(raw.rules)) {\n throw new Error(`Invalid config in ${basename(path)}: \"rules\" must be an object.`);\n }\n\n const rules: Record<string, RulePolicySeverity> = {};\n for (const [selector, severity] of Object.entries(raw.rules as Record<string, unknown>)) {\n if (typeof severity !== \"string\" || !isRulePolicySeverity(severity)) {\n throw new Error(`Invalid config in ${basename(path)}: rule \"${selector}\" must be off, info, warning, or error.`);\n }\n rules[selector] = severity;\n }\n config.rules = rules;\n }\n\n return config;\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((entry) => typeof entry === \"string\");\n}\n\nfunction resolveFailOn(\n cliValue: string | undefined,\n configValue: FailOn | undefined,\n): { ok: true; value: FailOn } | { ok: false; message: string } {\n const selected = cliValue ?? configValue ?? \"info\";\n if (!isFailOn(selected)) {\n return { ok: false, message: `Invalid --fail-on value \"${selected}\". Use none, error, warning, or info.` };\n }\n return { ok: true, value: selected };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -110,17 +110,42 @@ interface CrossFileRule {
|
|
|
110
110
|
}
|
|
111
111
|
type Rule = CrossFileRule | TSRule;
|
|
112
112
|
|
|
113
|
+
type RuleCategory = "type-evasion" | "defensive-code" | "error-handling" | "interface-design" | "cross-file" | "imports";
|
|
114
|
+
interface RuleMetadata {
|
|
115
|
+
category: RuleCategory;
|
|
116
|
+
tags: string[];
|
|
117
|
+
}
|
|
118
|
+
declare const allRules: Rule[];
|
|
119
|
+
declare function getRuleMetadata(ruleId: string): RuleMetadata;
|
|
120
|
+
|
|
121
|
+
type Severity = Diagnostic["severity"];
|
|
122
|
+
type RulePolicySeverity = Severity | "off";
|
|
123
|
+
type FailOn = "none" | Severity;
|
|
124
|
+
interface RulePolicyEntry {
|
|
125
|
+
selector: string;
|
|
126
|
+
severity: RulePolicySeverity;
|
|
127
|
+
}
|
|
128
|
+
type RulePolicy = Record<string, RulePolicySeverity> | RulePolicyEntry[];
|
|
113
129
|
interface ScanOptions {
|
|
114
130
|
paths: string[];
|
|
115
131
|
strict?: boolean;
|
|
116
132
|
rules?: string[];
|
|
133
|
+
ignore?: string[];
|
|
134
|
+
rulePolicy?: RulePolicy;
|
|
135
|
+
showSeverities?: Severity[];
|
|
136
|
+
failOn?: FailOn;
|
|
137
|
+
useGitIgnore?: boolean;
|
|
117
138
|
}
|
|
118
139
|
interface ScanResult {
|
|
119
140
|
diagnostics: Diagnostic[];
|
|
120
141
|
fileCount: number;
|
|
121
142
|
}
|
|
122
|
-
|
|
143
|
+
interface ScanExecutionResult extends ScanResult {
|
|
144
|
+
visibleDiagnostics: Diagnostic[];
|
|
145
|
+
exitCode: number;
|
|
146
|
+
}
|
|
123
147
|
|
|
124
|
-
declare
|
|
148
|
+
declare function executeScan(options: ScanOptions): Promise<ScanExecutionResult>;
|
|
149
|
+
declare function scan(options: ScanOptions): Promise<ScanResult>;
|
|
125
150
|
|
|
126
|
-
export { type CrossFileRule, type Diagnostic, type Rule, type ScanOptions, type ScanResult, type TSRule, type TSVisitContext, allRules, scan };
|
|
151
|
+
export { type CrossFileRule, type Diagnostic, type Rule, type RuleCategory, type RuleMetadata, type ScanExecutionResult, type ScanOptions, type ScanResult, type TSRule, type TSVisitContext, allRules, executeScan, getRuleMetadata, scan };
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unguard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Detect overdefensive AI-generated code",
|
|
5
5
|
"author": "Seva Maltsev <me@seva.dev> (https://seva.dev)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsup",
|
|
38
38
|
"typecheck": "tsc",
|
|
39
|
+
"lint:biome": "biome lint src tests/**/*.test.ts tests/harness.ts",
|
|
40
|
+
"lint": "npm run lint:biome",
|
|
39
41
|
"test": "vitest run",
|
|
40
42
|
"test:watch": "vitest",
|
|
41
|
-
"scan": "node ./bin/unguard.mjs scan .",
|
|
42
|
-
"scan:strict": "node ./bin/unguard.mjs scan . --strict",
|
|
43
|
+
"scan": "node ./bin/unguard.mjs scan src \"tests/**/*.test.ts\" tests/harness.ts --fail-on=error",
|
|
44
|
+
"scan:strict": "node ./bin/unguard.mjs scan src \"tests/**/*.test.ts\" tests/harness.ts --strict --fail-on=error",
|
|
43
45
|
"prepublishOnly": "npm run build && npm run test"
|
|
44
46
|
},
|
|
45
47
|
"engines": {
|
|
@@ -47,10 +49,12 @@
|
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"fast-glob": "^3.3.3",
|
|
52
|
+
"ignore": "^7.0.5",
|
|
50
53
|
"picocolors": "^1.1.1",
|
|
51
54
|
"typescript": "^5.9.3"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
57
|
+
"@biomejs/biome": "^1.9.4",
|
|
54
58
|
"@types/node": "^25.3.0",
|
|
55
59
|
"tsup": "^8.5.1",
|
|
56
60
|
"vitest": "^4.0.18"
|