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/dist/cli.js CHANGED
@@ -1,46 +1,89 @@
1
1
  import {
2
- scan
3
- } from "./chunk-LEYOVUJE.js";
2
+ executeScan,
3
+ isFailOn,
4
+ isRulePolicySeverity,
5
+ isSeverity,
6
+ toRulePolicyEntries
7
+ } from "./chunk-I4RZLVE2.js";
4
8
 
5
9
  // src/cli.ts
6
- import { relative } from "path";
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 args = argv.slice(2);
14
- const first = args[0];
15
- if (first === "-h" || first === "--help") {
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
- const userArgs = first === "scan" ? args.slice(1) : args;
20
- const strict = userArgs.includes("--strict");
21
- const filter = extractFlag(userArgs, "--filter");
22
- const format = extractFlag(userArgs, "--format", "grouped");
23
- const severityFilter = extractFlag(userArgs, "--severity");
24
- const paths = userArgs.filter((a) => !a.startsWith("--"));
25
- const result = await scan({
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 minRank = severityFilter !== void 0 && isSeverity(severityFilter) ? SEVERITY_RANK[severityFilter] : 0;
31
- const filtered = minRank > 0 ? result.diagnostics.filter((d) => SEVERITY_RANK[d.severity] >= minRank) : result.diagnostics;
32
- if (filtered.length === 0) {
33
- console.log(pc.green(`No issues found in ${result.fileCount} files.`));
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(filtered);
80
+ if (values.format === "flat") {
81
+ printDiagnosticsFlat(diagnostics);
38
82
  } else {
39
- printDiagnostics(filtered);
83
+ printDiagnostics(diagnostics);
40
84
  }
41
- printSummary(filtered, result.fileCount);
42
- const hasError = filtered.some((d) => d.severity === "error");
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
- --strict Treat all warnings as errors
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
- --severity <level> Show only diagnostics at this level or above (error, warning, info)
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 No issues
61
- 1 Warnings or info only
62
- 2 At least one error
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 --strict
67
- unguard scan src --filter no-empty-catch
68
- unguard scan src --severity=error
69
- unguard scan src --format=flat | grep error`);
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 d of diagnostics) {
91
- let list = byFile.get(d.file);
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(d.file, list);
125
+ byFile.set(diagnostic.file, list);
95
126
  }
96
- list.push(d);
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, diags] of byFile) {
131
+ for (const [file, fileDiagnostics] of byFile) {
101
132
  const rel = relative(cwdBase, file);
102
133
  console.log(pc.underline(rel));
103
- for (const d of diags) {
104
- const loc = `${d.line}:${d.column}`;
105
- const sev = d.severity === "error" ? pc.red("error") : d.severity === "warning" ? pc.yellow("warning") : pc.blue("info");
106
- const msg = d.message.replaceAll(cwd, "");
107
- const rule = pc.dim(d.ruleId);
108
- const annotation = d.annotation !== void 0 ? ` ${pc.cyan(d.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 d of diagnostics) {
118
- const rel = relative(cwdBase, d.file);
119
- const msg = d.message.replaceAll(cwd, "");
120
- console.log(`${rel}:${d.line}:${d.column} ${d.severity} [${d.ruleId}] ${msg}`);
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 d of diagnostics) {
126
- const prev = counts.get(d.ruleId);
127
- counts.set(d.ruleId, (prev === void 0 ? 0 : prev) + 1);
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(([r]) => r.length));
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 { Node, Comment } from 'oxc-parser';
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, source: string, exported: boolean): void;
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
- program: Node;
53
- comments: Comment[];
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: 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 Span {
80
- start: number;
81
- end: number;
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
- comments: Comment[];
92
+ sourceFile: ts.SourceFile;
93
+ checker: ts.TypeChecker;
94
+ isNullable(node: ts.Node): boolean;
95
+ isExternal(node: ts.Node): boolean;
88
96
  }
89
- interface SingleFileRule {
97
+ interface TSRule {
98
+ kind: "ts";
90
99
  id: string;
91
100
  severity: "info" | "warning" | "error";
92
101
  message: string;
93
- visit(node: Node, parent: Node | null, ctx: VisitContext): void;
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 = SingleFileRule | CrossFileRule;
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
- declare function scan(options: ScanOptions): Promise<ScanResult>;
143
+ interface ScanExecutionResult extends ScanResult {
144
+ visibleDiagnostics: Diagnostic[];
145
+ exitCode: number;
146
+ }
115
147
 
116
- declare const allRules: Rule[];
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 SingleFileRule, 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
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  allRules,
3
+ executeScan,
4
+ getRuleMetadata,
3
5
  scan
4
- } from "./chunk-LEYOVUJE.js";
6
+ } from "./chunk-I4RZLVE2.js";
5
7
  export {
6
8
  allRules,
9
+ executeScan,
10
+ getRuleMetadata,
7
11
  scan
8
12
  };
9
13
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unguard",
3
- "version": "0.7.1",
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
- "oxc-parser": "^0.114.0",
51
- "oxc-walker": "^0.7.0",
52
- "picocolors": "^1.1.1"
52
+ "ignore": "^7.0.5",
53
+ "picocolors": "^1.1.1",
54
+ "typescript": "^5.9.3"
53
55
  },
54
56
  "devDependencies": {
55
- "@oxc-project/types": "^0.114.0",
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
  }