unguard 0.7.1 → 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 +96 -33
- package/dist/chunk-I4RZLVE2.js +1718 -0
- package/dist/chunk-I4RZLVE2.js.map +1 -0
- package/dist/cli.js +214 -77
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -21
- package/dist/index.js +5 -1
- package/package.json +9 -8
- package/dist/chunk-LEYOVUJE.js +0 -1222
- package/dist/chunk-LEYOVUJE.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,46 +1,89 @@
|
|
|
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
|
|
6
|
-
import {
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { basename, relative, resolve } from "path";
|
|
12
|
+
import { parseArgs } from "util";
|
|
7
13
|
import pc from "picocolors";
|
|
8
|
-
var SEVERITY_RANK = { info: 0, warning: 1, error: 2 };
|
|
9
|
-
function isSeverity(value) {
|
|
10
|
-
return value === "info" || value === "warning" || value === "error";
|
|
11
|
-
}
|
|
12
14
|
async function main(argv) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const rawArgs = argv.slice(2);
|
|
16
|
+
const userArgs = rawArgs[0] === "scan" ? rawArgs.slice(1) : rawArgs;
|
|
17
|
+
const { values, positionals: cliPaths } = parseArgs({
|
|
18
|
+
args: userArgs,
|
|
19
|
+
options: {
|
|
20
|
+
strict: { type: "boolean", default: false },
|
|
21
|
+
filter: { type: "string" },
|
|
22
|
+
format: { type: "string", default: "grouped" },
|
|
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" },
|
|
28
|
+
help: { type: "boolean", short: "h", default: false }
|
|
29
|
+
},
|
|
30
|
+
allowPositionals: true
|
|
31
|
+
});
|
|
32
|
+
if (values.help) {
|
|
16
33
|
printHelp();
|
|
17
34
|
return 0;
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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({
|
|
26
67
|
paths,
|
|
27
|
-
strict,
|
|
28
|
-
rules: filter ? [filter] : void 0
|
|
68
|
+
strict: values.strict,
|
|
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
|
|
29
74
|
});
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
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;
|
|
35
79
|
}
|
|
36
|
-
if (format === "flat") {
|
|
37
|
-
printDiagnosticsFlat(
|
|
80
|
+
if (values.format === "flat") {
|
|
81
|
+
printDiagnosticsFlat(diagnostics);
|
|
38
82
|
} else {
|
|
39
|
-
printDiagnostics(
|
|
83
|
+
printDiagnostics(diagnostics);
|
|
40
84
|
}
|
|
41
|
-
printSummary(
|
|
42
|
-
|
|
43
|
-
return hasError ? 2 : 1;
|
|
85
|
+
printSummary(diagnostics, execution.fileCount);
|
|
86
|
+
return execution.exitCode;
|
|
44
87
|
}
|
|
45
88
|
function printHelp() {
|
|
46
89
|
console.log(`unguard: data-shape static analyzer
|
|
@@ -50,62 +93,50 @@ Usage:
|
|
|
50
93
|
unguard [paths...] [options]
|
|
51
94
|
|
|
52
95
|
Options:
|
|
53
|
-
--
|
|
96
|
+
--config <path> Path to unguard config file (defaults to ./unguard.config.json)
|
|
97
|
+
--strict Treat all diagnostics as errors
|
|
54
98
|
--filter <rule> Run only the specified rule
|
|
55
|
-
--
|
|
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)
|
|
56
103
|
--format <mode> Output format: grouped (default) or flat
|
|
57
104
|
-h, --help Show this help
|
|
58
105
|
|
|
59
106
|
Exit codes:
|
|
60
|
-
0
|
|
61
|
-
1
|
|
62
|
-
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
|
|
63
110
|
|
|
64
111
|
Examples:
|
|
65
112
|
unguard scan src
|
|
66
|
-
unguard scan src --
|
|
67
|
-
unguard scan src --
|
|
68
|
-
unguard scan src --
|
|
69
|
-
unguard scan src --
|
|
70
|
-
|
|
71
|
-
function extractFlag(args, flag, fallback) {
|
|
72
|
-
for (let i = 0; i < args.length; i++) {
|
|
73
|
-
const arg = args[i];
|
|
74
|
-
if (arg === void 0) continue;
|
|
75
|
-
if (arg === flag) {
|
|
76
|
-
const value = args[i + 1];
|
|
77
|
-
args.splice(i, 2);
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
if (arg.startsWith(flag + "=")) {
|
|
81
|
-
const value = arg.slice(flag.length + 1);
|
|
82
|
-
args.splice(i, 1);
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return fallback;
|
|
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`);
|
|
87
118
|
}
|
|
88
119
|
function printDiagnostics(diagnostics) {
|
|
89
120
|
const byFile = /* @__PURE__ */ new Map();
|
|
90
|
-
for (const
|
|
91
|
-
let list = byFile.get(
|
|
121
|
+
for (const diagnostic of diagnostics) {
|
|
122
|
+
let list = byFile.get(diagnostic.file);
|
|
92
123
|
if (list === void 0) {
|
|
93
124
|
list = [];
|
|
94
|
-
byFile.set(
|
|
125
|
+
byFile.set(diagnostic.file, list);
|
|
95
126
|
}
|
|
96
|
-
list.push(
|
|
127
|
+
list.push(diagnostic);
|
|
97
128
|
}
|
|
98
|
-
const cwd = process.cwd()
|
|
129
|
+
const cwd = `${process.cwd()}/`;
|
|
99
130
|
const cwdBase = process.cwd();
|
|
100
|
-
for (const [file,
|
|
131
|
+
for (const [file, fileDiagnostics] of byFile) {
|
|
101
132
|
const rel = relative(cwdBase, file);
|
|
102
133
|
console.log(pc.underline(rel));
|
|
103
|
-
for (const
|
|
104
|
-
const loc = `${
|
|
105
|
-
const sev =
|
|
106
|
-
const msg =
|
|
107
|
-
const rule = pc.dim(
|
|
108
|
-
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)}` : "";
|
|
109
140
|
console.log(` ${pc.dim(loc.padEnd(10))} ${sev} ${msg} ${rule}${annotation}`);
|
|
110
141
|
}
|
|
111
142
|
console.log();
|
|
@@ -113,21 +144,21 @@ function printDiagnostics(diagnostics) {
|
|
|
113
144
|
}
|
|
114
145
|
function printDiagnosticsFlat(diagnostics) {
|
|
115
146
|
const cwdBase = process.cwd();
|
|
116
|
-
const cwd = cwdBase
|
|
117
|
-
for (const
|
|
118
|
-
const rel = relative(cwdBase,
|
|
119
|
-
const msg =
|
|
120
|
-
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}`);
|
|
121
152
|
}
|
|
122
153
|
}
|
|
123
154
|
function printSummary(diagnostics, fileCount) {
|
|
124
155
|
const counts = /* @__PURE__ */ new Map();
|
|
125
|
-
for (const
|
|
126
|
-
const prev = counts.get(
|
|
127
|
-
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);
|
|
128
159
|
}
|
|
129
160
|
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
130
|
-
const maxRule = Math.max(...sorted.map(([
|
|
161
|
+
const maxRule = Math.max(...sorted.map(([rule]) => rule.length));
|
|
131
162
|
console.log(pc.bold("Summary"));
|
|
132
163
|
for (const [rule, count] of sorted) {
|
|
133
164
|
console.log(` ${rule.padEnd(maxRule)} ${pc.bold(String(count))}`);
|
|
@@ -137,6 +168,112 @@ function printSummary(diagnostics, fileCount) {
|
|
|
137
168
|
${pc.bold(String(diagnostics.length))} issue${diagnostics.length === 1 ? "" : "s"} in ${fileCount} files.`
|
|
138
169
|
);
|
|
139
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
|
+
}
|
|
140
277
|
export {
|
|
141
278
|
main
|
|
142
279
|
};
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { 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\";\nconst SEVERITY_RANK: Record<Severity, number> = { info: 0, warning: 1, error: 2 };\n\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 args = argv.slice(2);\n const first = args[0];\n\n if (first === \"-h\" || first === \"--help\") {\n printHelp();\n return 0;\n }\n\n const userArgs = first === \"scan\" ? args.slice(1) : args;\n const strict = userArgs.includes(\"--strict\");\n const filter = extractFlag(userArgs, \"--filter\");\n const format = extractFlag(userArgs, \"--format\", \"grouped\");\n const severityFilter = extractFlag(userArgs, \"--severity\");\n const paths = userArgs.filter((a) => !a.startsWith(\"--\"));\n\n const result = await scan({\n paths,\n strict,\n rules: filter ? [filter] : undefined,\n });\n\n const minRank = severityFilter !== undefined && isSeverity(severityFilter) ? SEVERITY_RANK[severityFilter] : 0;\n const filtered = minRank > 0 ? result.diagnostics.filter((d) => SEVERITY_RANK[d.severity] >= minRank) : 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 (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> Show only diagnostics at this level or above (error, warning, info)\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 --format=flat | grep error`);\n}\n\nfunction extractFlag(args: string[], flag: string, fallback?: string): string | undefined {\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === undefined) continue;\n if (arg === flag) {\n const value = args[i + 1];\n args.splice(i, 2);\n return value;\n }\n if (arg.startsWith(flag + \"=\")) {\n const value = arg.slice(flag.length + 1);\n args.splice(i, 1);\n return value;\n }\n }\n return fallback;\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
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
2
|
|
|
3
3
|
interface TypeEntry {
|
|
4
4
|
name: string;
|
|
5
5
|
file: string;
|
|
6
6
|
line: number;
|
|
7
7
|
hash: string;
|
|
8
|
-
node: Node;
|
|
8
|
+
node: ts.Node;
|
|
9
9
|
exported: boolean;
|
|
10
10
|
}
|
|
11
11
|
declare class TypeRegistry {
|
|
12
12
|
private entries;
|
|
13
13
|
private byHash;
|
|
14
|
-
add(name: string, file: string, line: number, typeNode: Node,
|
|
14
|
+
add(name: string, file: string, line: number, typeNode: ts.Node, sourceFile: ts.SourceFile, exported: boolean): void;
|
|
15
15
|
getDuplicateGroups(): TypeEntry[][];
|
|
16
16
|
getAll(): TypeEntry[];
|
|
17
17
|
getNameCollisionGroups(): TypeEntry[][];
|
|
@@ -29,8 +29,9 @@ interface FunctionEntry {
|
|
|
29
29
|
line: number;
|
|
30
30
|
hash: string;
|
|
31
31
|
params: ParamInfo[];
|
|
32
|
-
node: Node;
|
|
32
|
+
node: ts.Node;
|
|
33
33
|
exported: boolean;
|
|
34
|
+
symbol?: ts.Symbol;
|
|
34
35
|
}
|
|
35
36
|
declare class FunctionRegistry {
|
|
36
37
|
private entries;
|
|
@@ -42,6 +43,12 @@ declare class FunctionRegistry {
|
|
|
42
43
|
getNameCollisionGroups(): FunctionEntry[][];
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
interface CommentInfo {
|
|
47
|
+
type: "Line" | "Block";
|
|
48
|
+
value: string;
|
|
49
|
+
start: number;
|
|
50
|
+
end: number;
|
|
51
|
+
}
|
|
45
52
|
interface ProjectIndex {
|
|
46
53
|
types: TypeRegistry;
|
|
47
54
|
functions: FunctionRegistry;
|
|
@@ -49,8 +56,8 @@ interface ProjectIndex {
|
|
|
49
56
|
imports: ImportEntry[];
|
|
50
57
|
files: Map<string, {
|
|
51
58
|
source: string;
|
|
52
|
-
|
|
53
|
-
comments:
|
|
59
|
+
sourceFile: ts.SourceFile;
|
|
60
|
+
comments: CommentInfo[];
|
|
54
61
|
}>;
|
|
55
62
|
}
|
|
56
63
|
interface CallSite {
|
|
@@ -58,7 +65,8 @@ interface CallSite {
|
|
|
58
65
|
file: string;
|
|
59
66
|
line: number;
|
|
60
67
|
argCount: number;
|
|
61
|
-
node:
|
|
68
|
+
node: ts.CallExpression;
|
|
69
|
+
symbol?: ts.Symbol;
|
|
62
70
|
}
|
|
63
71
|
interface ImportEntry {
|
|
64
72
|
file: string;
|
|
@@ -76,22 +84,22 @@ interface Diagnostic {
|
|
|
76
84
|
column: number;
|
|
77
85
|
annotation?: string;
|
|
78
86
|
}
|
|
79
|
-
interface
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
interface VisitContext {
|
|
84
|
-
report(span: Span, message?: string): void;
|
|
87
|
+
interface TSVisitContext {
|
|
88
|
+
report(node: ts.Node, message?: string): void;
|
|
89
|
+
reportAtOffset(offset: number, message?: string): void;
|
|
85
90
|
filename: string;
|
|
86
91
|
source: string;
|
|
87
|
-
|
|
92
|
+
sourceFile: ts.SourceFile;
|
|
93
|
+
checker: ts.TypeChecker;
|
|
94
|
+
isNullable(node: ts.Node): boolean;
|
|
95
|
+
isExternal(node: ts.Node): boolean;
|
|
88
96
|
}
|
|
89
|
-
interface
|
|
97
|
+
interface TSRule {
|
|
98
|
+
kind: "ts";
|
|
90
99
|
id: string;
|
|
91
100
|
severity: "info" | "warning" | "error";
|
|
92
101
|
message: string;
|
|
93
|
-
visit(node: Node,
|
|
94
|
-
visitComment?(comment: Comment, ctx: VisitContext): void;
|
|
102
|
+
visit(node: ts.Node, ctx: TSVisitContext): void;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
interface CrossFileRule {
|
|
@@ -100,19 +108,44 @@ interface CrossFileRule {
|
|
|
100
108
|
message: string;
|
|
101
109
|
analyze(project: ProjectIndex): Diagnostic[];
|
|
102
110
|
}
|
|
103
|
-
type Rule =
|
|
111
|
+
type Rule = CrossFileRule | TSRule;
|
|
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;
|
|
104
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[];
|
|
105
129
|
interface ScanOptions {
|
|
106
130
|
paths: string[];
|
|
107
131
|
strict?: boolean;
|
|
108
132
|
rules?: string[];
|
|
133
|
+
ignore?: string[];
|
|
134
|
+
rulePolicy?: RulePolicy;
|
|
135
|
+
showSeverities?: Severity[];
|
|
136
|
+
failOn?: FailOn;
|
|
137
|
+
useGitIgnore?: boolean;
|
|
109
138
|
}
|
|
110
139
|
interface ScanResult {
|
|
111
140
|
diagnostics: Diagnostic[];
|
|
112
141
|
fileCount: number;
|
|
113
142
|
}
|
|
114
|
-
|
|
143
|
+
interface ScanExecutionResult extends ScanResult {
|
|
144
|
+
visibleDiagnostics: Diagnostic[];
|
|
145
|
+
exitCode: number;
|
|
146
|
+
}
|
|
115
147
|
|
|
116
|
-
declare
|
|
148
|
+
declare function executeScan(options: ScanOptions): Promise<ScanExecutionResult>;
|
|
149
|
+
declare function scan(options: ScanOptions): Promise<ScanResult>;
|
|
117
150
|
|
|
118
|
-
export { type CrossFileRule, type Diagnostic, type Rule, type ScanOptions, type ScanResult, type
|
|
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,15 +49,14 @@
|
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"fast-glob": "^3.3.3",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
52
|
+
"ignore": "^7.0.5",
|
|
53
|
+
"picocolors": "^1.1.1",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
|
-
"@
|
|
57
|
+
"@biomejs/biome": "^1.9.4",
|
|
56
58
|
"@types/node": "^25.3.0",
|
|
57
59
|
"tsup": "^8.5.1",
|
|
58
|
-
"typescript": "^5.9.3",
|
|
59
60
|
"vitest": "^4.0.18"
|
|
60
61
|
}
|
|
61
62
|
}
|