unguard 0.15.1 → 0.16.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 +251 -79
- package/dist/chunk-3Y76J2K2.js +511 -0
- package/dist/chunk-3Y76J2K2.js.map +1 -0
- package/dist/chunk-Z34GRD3S.js +5352 -0
- package/dist/chunk-Z34GRD3S.js.map +1 -0
- package/dist/cli.js +251 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +139 -10
- package/dist/index.js +5 -3
- package/dist/worker.js +49 -0
- package/dist/worker.js.map +1 -0
- package/package.json +8 -2
- package/schema.json +32 -1
- package/dist/chunk-VHRD75ET.js +0 -3935
- package/dist/chunk-VHRD75ET.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,43 +2,140 @@
|
|
|
2
2
|
|
|
3
3
|
Unguard your code. Defend against overdefensive AI-generated code.
|
|
4
4
|
|
|
5
|
-
Type-aware static analysis powered by the TypeScript compiler
|
|
5
|
+
Type-aware static analysis powered by the TypeScript compiler itself.
|
|
6
6
|
|
|
7
7
|
If `??` is on a non-nullable type, you don't need it.
|
|
8
8
|
|
|
9
9
|
If `?.` is on a guaranteed object, it's noise.
|
|
10
10
|
|
|
11
|
-
unguard proves it with types.
|
|
11
|
+
unguard proves it with types, and complains.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
18
|
+
npx unguard
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
No config, no setup. unguard finds your tsconfig, builds a real TypeScript program, and reports only what the types prove. (`npm install -g unguard` if you prefer a global install.)
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
## What it catches
|
|
24
|
+
|
|
25
|
+
AI assistants pad code with guards against states the type system already rules out. Each one reads like caution, and in aggregate, they bury the real contracts. unguard reads the types and proves which guards need to be unguarded.
|
|
26
|
+
|
|
27
|
+
**Dead fallbacks** - the type says the value is always there:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
const attempts = job.attempts ?? 1; // attempts: number
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```txt
|
|
34
|
+
10:20 warning Nullish coalescing (??) fallback on a non-nullable type is dead
|
|
35
|
+
code; remove the fallback or fix the type upstream
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Dead narrowing** - the diagnostic cites the type that decides the condition:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
if (typeof job.id !== "string") { // id: string
|
|
42
|
+
throw new Error("job is missing an id");
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```txt
|
|
47
|
+
12:7 warning typeof comparison is always false: `job.id` is `string`, and
|
|
48
|
+
typeof always yields "string" for that type
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Fabricated types** - external data asserted into shape instead of validated:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
const payload = JSON.parse(job.payload) as Payload;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```txt
|
|
58
|
+
17:21 error Casting `any`/`unknown` to a concrete type without runtime
|
|
59
|
+
validation fabricates structure; validate first or narrow with
|
|
60
|
+
a type guard
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Swallowed errors** - failure silently becomes a normal-looking value:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
try {
|
|
67
|
+
return { id: job.id, attempts, payload };
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```txt
|
|
74
|
+
19:5 warning Catch swallows the error: it neither throws nor returns a value
|
|
75
|
+
referencing the caught error. Propagate via throw, or model
|
|
76
|
+
failure into the return type carrying the original error
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Plus more type-system evasions (`as any`, `x as unknown as T`, `@ts-ignore`), cross-file analysis (exports nobody imports, optional parameters no call site ever passes, arguments that are the same literal everywhere, dead overloads, copy-pasted functions), and more - the full catalog is in [Current Rules](#current-rules).
|
|
80
|
+
|
|
81
|
+
## Why not just typescript-eslint?
|
|
82
|
+
|
|
83
|
+
Keep typescript-eslint - unguard is a complement to it, not a replacement. Its `no-unnecessary-condition` overlaps with a slice of `no-dead-narrowing`, and that's roughly where the overlap ends. Unguard, on the other hand, provides more:
|
|
84
|
+
|
|
85
|
+
- **Whole-project analysis.** unguard indexes every call site, import, and export across your tsconfig groups (and is monorepo-aware). Lint rules see one file at a time while unguard can prove an optional parameter is never passed, an argument is always the same literal, an export is never imported.
|
|
86
|
+
- **Defensive-pattern focus.** Swallowed catches detected structurally (does any expression in the catch reference the error?), `throw new Error(e.message)` losing the cause, unvalidated casts of external data, defaults that widen interface contracts.
|
|
87
|
+
- **Tiers split by proof.** `scan` runs only rules whose findings the checker demonstrates - it's designed to gate CI without false-positive fatigue. Everything debatable lives in `audit`.
|
|
88
|
+
- **Zero config.** `npx unguard` on any TypeScript repo.
|
|
89
|
+
|
|
90
|
+
## For AI coding agents
|
|
91
|
+
|
|
92
|
+
unguard is built for the verify loop. Findings are proofs with the evidence in the message, which is exactly what an agent needs to fix code instead of arguing with a style warning. Add to your `CLAUDE.md` / `AGENTS.md` / rules file:
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
After changing TypeScript, run `npx unguard` and fix every finding.
|
|
96
|
+
Diagnostics cite the type that proves the defect - fix the code (or the type),
|
|
97
|
+
don't add suppressions. If a guard is genuinely intentional, annotate it:
|
|
98
|
+
`// @unguard <rule-id> <reason>`.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
And gate CI:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
- name: unguard
|
|
105
|
+
run: npx unguard --fail-on=error
|
|
23
106
|
```
|
|
24
107
|
|
|
25
108
|
## Usage
|
|
26
109
|
|
|
27
110
|
```bash
|
|
28
|
-
unguard
|
|
29
|
-
unguard
|
|
30
|
-
unguard
|
|
31
|
-
unguard
|
|
32
|
-
unguard
|
|
33
|
-
unguard
|
|
34
|
-
unguard
|
|
35
|
-
unguard
|
|
36
|
-
unguard
|
|
37
|
-
unguard
|
|
38
|
-
unguard
|
|
111
|
+
unguard # scan files/directories
|
|
112
|
+
unguard audit # run heuristic rules as review prompts
|
|
113
|
+
unguard --config ./unguard.config.json # load config
|
|
114
|
+
unguard --ignore '**/*.gen.ts' # add ignore globs
|
|
115
|
+
unguard --filter no-any-cast # run a single rule
|
|
116
|
+
unguard --rule duplicate-*=warning # override rule severity/policy
|
|
117
|
+
unguard --rule category:cross-file=warning
|
|
118
|
+
unguard --rule tag:safety=error
|
|
119
|
+
unguard --severity=error,warning # show errors+warnings
|
|
120
|
+
unguard --fail-on=error # fail only on errors
|
|
121
|
+
unguard --format=flat # one-line-per-diagnostic, grepable
|
|
122
|
+
unguard --format=flat | grep error
|
|
123
|
+
unguard --format=json # machine-readable report
|
|
124
|
+
unguard --fix # apply auto-fixes, report what remains
|
|
125
|
+
unguard baseline # record current issues as the baseline
|
|
126
|
+
unguard --no-baseline # ignore unguard.baseline.json for this scan
|
|
127
|
+
unguard --concurrency 1 # disable worker-thread parallelism, can be slow for large codebases
|
|
128
|
+
unguard --no-cache # bypass on-disk diagnostic cache
|
|
39
129
|
```
|
|
40
130
|
|
|
41
|
-
|
|
131
|
+
### Two tiers: `scan` and `audit`
|
|
132
|
+
|
|
133
|
+
Every rule is classified by confidence:
|
|
134
|
+
|
|
135
|
+
- **proven** - the checker or AST demonstrates the defect; every finding demands a fix (or an explicit `@unguard` annotation). `unguard scan` runs these and is meant to gate CI or commits.
|
|
136
|
+
- **heuristic** - strong pattern evidence, but a correct alternative reading exists: deliberate duplication, API surface for external consumers, intentionally optional params. `unguard audit` runs these and exits 0 unless `--fail-on` is passed explicitly, so findings surface for review without blocking anything.
|
|
137
|
+
|
|
138
|
+
`--filter <rule>` and explicit `rules` selections bypass the tiers - a rule requested by name runs in either command.
|
|
42
139
|
|
|
43
140
|
### Config
|
|
44
141
|
|
|
@@ -56,20 +153,36 @@ Add `unguard` to your lint check, especially if code is written by AI.
|
|
|
56
153
|
"no-ts-ignore": "error",
|
|
57
154
|
"prefer-*": "off"
|
|
58
155
|
},
|
|
59
|
-
"
|
|
156
|
+
"overrides": [
|
|
157
|
+
{
|
|
158
|
+
"files": ["tests/**", "**/*.test.ts"],
|
|
159
|
+
"rules": {
|
|
160
|
+
"no-non-null-assertion": "off",
|
|
161
|
+
"duplicate-*": "off"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"failOn": "error",
|
|
166
|
+
"concurrency": 4,
|
|
167
|
+
"cache": true
|
|
60
168
|
}
|
|
61
169
|
```
|
|
62
170
|
|
|
63
171
|
`rules` values can be `off`, `info`, `warning`, or `error`.
|
|
64
172
|
Selectors support:
|
|
173
|
+
|
|
65
174
|
- exact rule id: `no-ts-ignore`
|
|
66
175
|
- wildcard: `duplicate-*`
|
|
67
176
|
- category: `category:cross-file`
|
|
68
177
|
- tag: `tag:safety`
|
|
178
|
+
- confidence tier: `confidence:proven`, `confidence:heuristic`
|
|
179
|
+
|
|
180
|
+
`overrides` entries apply a rule policy only to diagnostics in files matching the listed globs (gitignore syntax, relative to the working directory). Later entries win; `off` drops the diagnostic. Typical use: relaxing rules in test directories, where non-null assertions and duplication are often deliberate.
|
|
69
181
|
|
|
70
182
|
### Ignore behavior
|
|
71
183
|
|
|
72
184
|
`unguard` ignores:
|
|
185
|
+
|
|
73
186
|
- built-in: `node_modules`, `dist`, `.git`, declaration files (`*.d.ts`, `*.d.cts`, `*.d.mts`)
|
|
74
187
|
- generated files: `*.gen.*`, `*.generated.*`
|
|
75
188
|
- project `.gitignore`
|
|
@@ -91,6 +204,8 @@ unguard src --fail-on=error
|
|
|
91
204
|
|
|
92
205
|
`--severity` filters display only. `--fail-on` evaluates all diagnostics after rule policy.
|
|
93
206
|
|
|
207
|
+
`unguard audit` defaults to `--fail-on=none` and exits 0 regardless of findings. Pass `--fail-on` explicitly to make audit gate.
|
|
208
|
+
|
|
94
209
|
### Output formats
|
|
95
210
|
|
|
96
211
|
**Grouped** (default) -- diagnostics grouped by file:
|
|
@@ -106,84 +221,124 @@ src/lib/probe.ts
|
|
|
106
221
|
src/lib/probe.ts:37:4 warning [no-swallowed-catch] Catch swallows the error...
|
|
107
222
|
```
|
|
108
223
|
|
|
224
|
+
**JSON** (`--format=json`) -- machine-readable, for CI integrations:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"diagnostics": [
|
|
229
|
+
{
|
|
230
|
+
"file": "src/lib/probe.ts",
|
|
231
|
+
"line": 37,
|
|
232
|
+
"column": 4,
|
|
233
|
+
"severity": "warning",
|
|
234
|
+
"ruleId": "no-swallowed-catch",
|
|
235
|
+
"message": "Catch swallows the error...",
|
|
236
|
+
"fixable": false
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
"fileCount": 1,
|
|
240
|
+
"exitCode": 1
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Auto-fix
|
|
245
|
+
|
|
246
|
+
`--fix` applies fixes for diagnostics whose replacement is provably semantics-preserving (dead `??` fallbacks, dead `?.`, no-op `await`, redundant casts, `cond ? true : false`, and similar), then reports what remains. The exit code reflects only the remaining diagnostics.
|
|
247
|
+
|
|
248
|
+
### Baseline
|
|
249
|
+
|
|
250
|
+
Adopting unguard in an existing codebase? `unguard baseline src` records every current issue in `unguard.baseline.json`. Subsequent scans suppress a `(file, rule)` group as long as its count stays at or below the recorded number - new issues anywhere still fail, and fixing old ones ratchets the allowance down on the next `unguard baseline`. Use `--no-baseline` to see the full picture.
|
|
251
|
+
|
|
109
252
|
## Current Rules
|
|
110
253
|
|
|
254
|
+
The **Tier** column says which command runs the rule: `scan` (proven) or `audit` (heuristic).
|
|
255
|
+
|
|
111
256
|
### Type system evasion
|
|
112
257
|
|
|
113
|
-
| Rule | Severity | What it catches |
|
|
114
|
-
| ---- | -------- | --------------- |
|
|
115
|
-
| `no-any-cast` | error | `x as any` |
|
|
116
|
-
| `no-explicit-any-annotation` | error | `param: any`, `const x: any` |
|
|
117
|
-
| `no-inline-type-assertion` | error | `x as { ... }`, `<{ ... }>x` |
|
|
118
|
-
| `no-type-assertion` | error | `x as unknown as T` |
|
|
119
|
-
| `no-ts-ignore` | error | `@ts-ignore`
|
|
258
|
+
| Rule | Severity | Tier | What it catches |
|
|
259
|
+
| ---- | -------- | ---- | --------------- |
|
|
260
|
+
| `no-any-cast` | error | scan | `x as any` -- erases type safety for everything downstream of the cast |
|
|
261
|
+
| `no-explicit-any-annotation` | error | scan | `param: any`, `const x: any` -- an `any` annotation where a real type (or `unknown`) belongs |
|
|
262
|
+
| `no-inline-type-assertion` | error | scan | `x as { ... }`, `<{ ... }>x` -- asserting into an anonymous inline shape; name the type or fix it upstream |
|
|
263
|
+
| `no-type-assertion` | error | scan | `x as unknown as T` -- the double assertion that can connect any two types |
|
|
264
|
+
| `no-ts-ignore` | error | scan | `@ts-ignore` -- silences the checker unconditionally |
|
|
265
|
+
| `no-ts-expect-error` | warning | scan | `@ts-expect-error` -- self-expiring, but still an evasion |
|
|
266
|
+
| `no-never-cast` | warning | scan | `x as never` -- silences the checker completely, usually to force past an exhaustiveness error |
|
|
267
|
+
| `no-redundant-cast` | error | scan | `x as T` where `x` already has type `T` |
|
|
268
|
+
| `no-unvalidated-cast` | error | scan | `JSON.parse(...) as T`, `await fetch(...).json() as T` -- casting external data into a shape nothing has checked |
|
|
269
|
+
| `redundant-narrowing-then-cast` | warning | scan | `if (typeof x === "string") { (x as string).length }` -- the `if` already narrowed `x`, so the cast adds nothing |
|
|
270
|
+
| `return-type-widens-via-destructure` | warning | scan | `const [x] = await db...returning(); return x;` in a function declared to return `T` -- an array element is really `T \| undefined`, so the return type hides the empty case |
|
|
120
271
|
|
|
121
272
|
### Defensive code (type-aware)
|
|
122
273
|
|
|
123
274
|
These rules use the TypeScript type checker. Non-nullable types suppress the diagnostic; nullable types are flagged.
|
|
124
275
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
|
128
|
-
|
|
|
129
|
-
| `no-optional-
|
|
130
|
-
| `no-
|
|
131
|
-
| `no-
|
|
132
|
-
| `no-
|
|
133
|
-
| `no-
|
|
134
|
-
| `no-
|
|
135
|
-
| `no-
|
|
136
|
-
| `no-
|
|
137
|
-
| `no-
|
|
276
|
+
Nullability-driven rules require `strictNullChecks` (or `strict`). Without it the checker erases `null`/`undefined` from every type, so these rules are skipped with a warning rather than reporting every load-bearing guard as dead code.
|
|
277
|
+
|
|
278
|
+
| Rule | Severity | Tier | What it catches |
|
|
279
|
+
| ---- | -------- | ---- | --------------- |
|
|
280
|
+
| `no-optional-property-access` | warning | scan | `obj?.prop` on a non-nullable type |
|
|
281
|
+
| `no-optional-element-access` | warning | scan | `obj?.[key]` on a non-nullable type |
|
|
282
|
+
| `no-optional-call` | warning | scan | `fn?.()` on a non-nullable type |
|
|
283
|
+
| `no-nullish-coalescing` | warning | scan | `x ?? fallback` on a non-nullable type |
|
|
284
|
+
| `no-logical-or-fallback` | warning | scan | `map.get(k) \|\| fallback`, `count \|\| 1` -- `\|\|` swallows `0` and `""`; use `??` |
|
|
285
|
+
| `no-null-ternary-normalization` | warning | scan | `x == null ? fallback : x` -- a hand-rolled `??`: dead if the type is non-nullable, and a sign the type needs fixing if it isn't |
|
|
286
|
+
| `no-coalesce-then-guard` | warning | scan | `const x = a ?? null; if (x == null)` -- the `if` re-asks the question the `??` just answered |
|
|
287
|
+
| `no-await-coalesce` | warning | audit | `await fn() ?? fallback` -- defaulting away a call's nullable result hides why it can be empty; check it and branch instead (built-in lookups like `Map.get` and `Array.find` are exempt) |
|
|
288
|
+
| `no-non-null-assertion` | warning | scan | `x!` on a nullable type without a local narrowing guard |
|
|
289
|
+
| `no-double-negation-coercion` | warning | scan | `!!value` inside an `if`/`while`/ternary test -- the construct already coerces (`const flag = !!x` and other places that need an actual boolean are fine) |
|
|
290
|
+
| `no-redundant-existence-guard` | warning | scan | `obj && obj.prop` on a non-nullable type |
|
|
291
|
+
| `no-dead-narrowing` | warning | scan | Conditions statically decided by the operand's declared type: truthiness checks that can never fail, `typeof` comparisons that can never (or must always) match, always-true `instanceof`, type-predicate calls whose argument already has the asserted type. Exempt: ambient globals (`typeof document === "undefined"` probes the environment, not the type) and, without `noUncheckedIndexedAccess`, truthiness checks (index reads erase `undefined`, so the guard may be load-bearing) |
|
|
292
|
+
| `no-coalesce-undefined` | warning | scan | `x ?? undefined` where `x` can be `undefined` but never `null` -- an identity no-op |
|
|
293
|
+
| `redundant-boolean-branch` | warning | scan | `cond ? true : false`, `if (cond) return true; return false;` -- the condition is already boolean. The inverted form is flagged only when the replacement is a readable `!cond`; compound conditions are never rewritten into `!(a && b)` |
|
|
294
|
+
| `no-useless-await` | warning | scan | `await x` where `x`'s type has no `then` -- a no-op that suggests asynchrony that isn't there |
|
|
295
|
+
| `redundant-destructure-default` | warning | scan | `const { a = fallback } = obj` where `obj.a` is required and non-undefined -- the default can never apply |
|
|
138
296
|
|
|
139
297
|
### Error handling
|
|
140
298
|
|
|
141
|
-
| Rule | Severity | What it catches |
|
|
142
|
-
| ---- | -------- | --------------- |
|
|
143
|
-
| `no-swallowed-catch` | warning |
|
|
144
|
-
| `no-error-rewrap` | error | `throw new Error(e.message)` without `{ cause: e }` |
|
|
299
|
+
| Rule | Severity | Tier | What it catches |
|
|
300
|
+
| ---- | -------- | ---- | --------------- |
|
|
301
|
+
| `no-swallowed-catch` | warning | scan | `catch (e) {}`, `.catch(() => fallback)` -- error is neither rethrown, returned, nor passed into a handling call |
|
|
302
|
+
| `no-error-rewrap` | error | scan | `throw new Error(e.message)` without `{ cause: e }` |
|
|
145
303
|
|
|
146
304
|
### Interface design
|
|
147
305
|
|
|
148
|
-
| Rule | Severity | What it catches |
|
|
149
|
-
| ---- | -------- | --------------- |
|
|
150
|
-
| `no-inline-param-type` | warning | `(params: { id: string; ... })`
|
|
151
|
-
| `prefer-
|
|
152
|
-
| `
|
|
153
|
-
| `no-defaulted-required-port-arg` | warning | `class C implements I { method(arg = x) }` where `I.method(arg)` is required -- the
|
|
154
|
-
| `repeated-literal-property` | warning | Same literal value repeated across object properties -- likely a missed constant |
|
|
155
|
-
| `repeated-return-shape` | warning | Multiple functions return object literals with the same property names -- extract a shared return type |
|
|
156
|
-
|
|
157
|
-
### State management
|
|
158
|
-
|
|
159
|
-
| Rule | Severity | What it catches |
|
|
160
|
-
| ---- | -------- | --------------- |
|
|
161
|
-
| `no-module-state-write` | warning | Function mutates a module-scope binding (`count++`, `state.ready = ...`, `cache.set(...)`) |
|
|
306
|
+
| Rule | Severity | Tier | What it catches |
|
|
307
|
+
| ---- | -------- | ---- | --------------- |
|
|
308
|
+
| `no-inline-param-type` | warning | audit | `(params: { id: string; ... })` -- an object type spelled out in the signature; name it so call sites and other signatures can share it |
|
|
309
|
+
| `prefer-type-predicate` | warning | audit | `(x: unknown): boolean` whose body is `typeof`/`instanceof`/`in` checks -- returning `x is T` would let callers narrow |
|
|
310
|
+
| `optional-param-coerced-in-body` | warning | scan | Optional param forced non-optional in the body (`x = x ?? def`, `x ??= def`, or `if (!x) throw`) |
|
|
311
|
+
| `no-defaulted-required-port-arg` | warning | scan | `class C implements I { method(arg = x) }` where `I.method(arg)` is required -- the implementation quietly makes a required argument optional |
|
|
312
|
+
| `repeated-literal-property` | warning | audit | Same literal value repeated across object properties -- likely a missed constant |
|
|
313
|
+
| `repeated-return-shape` | warning | audit | Multiple functions return object literals with the same property names -- extract a shared return type |
|
|
314
|
+
| `trivial-type-alias` | info | audit | `type Foo = Bar;` -- a second name for an existing type with no change (info: an alias marking a domain boundary is often deliberate) |
|
|
162
315
|
|
|
163
316
|
### Cross-file analysis
|
|
164
317
|
|
|
165
|
-
| Rule | Severity | What it catches |
|
|
166
|
-
| ---- | -------- | --------------- |
|
|
167
|
-
| `duplicate-type-declaration` | warning | Same type shape in multiple files |
|
|
168
|
-
| `duplicate-type-name` | warning | Same exported type name, different shapes |
|
|
169
|
-
| `duplicate-function-declaration` | warning | Same function body in multiple files |
|
|
170
|
-
| `duplicate-function-name` | warning | Same exported function name, different bodies |
|
|
171
|
-
| `duplicate-constant-declaration` |
|
|
172
|
-
| `duplicate-inline-type-in-params` | warning | Same inline `{ ... }` param type repeated across signatures |
|
|
173
|
-
| `duplicate-file` | warning | File with identical content to another file |
|
|
174
|
-
| `near-duplicate-function` | warning | Function bodies that match after renaming params and literals -- likely a copy-paste |
|
|
175
|
-
| `duplicate-statement-sequence` |
|
|
176
|
-
| `trivial-wrapper` |
|
|
177
|
-
| `unused-export` |
|
|
178
|
-
| `optional-arg-always-used` | warning | Optional param provided at every call site |
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
318
|
+
| Rule | Severity | Tier | What it catches |
|
|
319
|
+
| ---- | -------- | ---- | --------------- |
|
|
320
|
+
| `duplicate-type-declaration` | warning | audit | Same type shape in multiple files |
|
|
321
|
+
| `duplicate-type-name` | warning | audit | Same exported type name, different shapes |
|
|
322
|
+
| `duplicate-function-declaration` | warning | audit | Same function body in multiple files |
|
|
323
|
+
| `duplicate-function-name` | warning | audit | Same exported function name, different bodies |
|
|
324
|
+
| `duplicate-constant-declaration` | info | audit | Same constant value in multiple files (info: coincidental value equality is common) |
|
|
325
|
+
| `duplicate-inline-type-in-params` | warning | audit | Same inline `{ ... }` param type repeated across signatures |
|
|
326
|
+
| `duplicate-file` | warning | audit | File with identical content to another file |
|
|
327
|
+
| `near-duplicate-function` | warning | audit | Function bodies that match after renaming params and literals -- likely a copy-paste |
|
|
328
|
+
| `duplicate-statement-sequence` | warning | audit | Repeated block of statements across functions or files (identical text; lookup tables that vary only by literal data are not flagged) |
|
|
329
|
+
| `trivial-wrapper` | warning | audit | Function that delegates to another without transformation (skipped when the wrapper specializes a type predicate, introduces generics, reorders args, or partially applies) |
|
|
330
|
+
| `unused-export` | warning | audit | Exported function, type, or constant with no usages in the project. Imports resolve through the checker (path aliases, workspace packages) and usage is merged across tsconfig groups, so an export consumed by a sibling monorepo package counts as used. Consumers outside the scanned tree, such as a published package's API surface, are invisible to the analysis - hence audit tier |
|
|
331
|
+
| `optional-arg-always-used` | warning | audit | Optional param provided at every call site -- make it required |
|
|
332
|
+
| `optional-arg-never-used` | warning | audit | Optional param never provided at any call site -- remove it, inline the default |
|
|
333
|
+
| `constant-argument` | warning | audit | Parameter receives the same literal at every call site -- inline the value |
|
|
334
|
+
| `explicit-null-arg` | warning | audit | `fn(null)` / `fn(undefined)` passed to a project function -- the parameter invites nullish values; redesign it so callers can omit the argument |
|
|
335
|
+
| `dead-overload` | warning | audit | Overload signature with zero matching project call sites |
|
|
181
336
|
|
|
182
337
|
### Imports
|
|
183
338
|
|
|
184
|
-
| Rule | Severity | What it catches |
|
|
185
|
-
| ---- | -------- | --------------- |
|
|
186
|
-
| `no-dynamic-import` |
|
|
339
|
+
| Rule | Severity | Tier | What it catches |
|
|
340
|
+
| ---- | -------- | ---- | --------------- |
|
|
341
|
+
| `no-dynamic-import` | warning | audit | `import("./module")` -- breaks static analysis (warning, not error: code-splitting and lazy loading are legitimate) |
|
|
187
342
|
|
|
188
343
|
## Annotations
|
|
189
344
|
|
|
@@ -201,8 +356,8 @@ file.ts:2:31 error Explicit `any` annotation ... (intentional escape hatch for u
|
|
|
201
356
|
For `warning` and `info` diagnostics, you can explicitly mark a finding as intentional with `@unguard <rule-id>` on the same line or immediately above:
|
|
202
357
|
|
|
203
358
|
```typescript
|
|
204
|
-
// @unguard no-
|
|
205
|
-
|
|
359
|
+
// @unguard no-nullish-coalescing intentional default for legacy callers
|
|
360
|
+
const port = config.port ?? 3000;
|
|
206
361
|
```
|
|
207
362
|
|
|
208
363
|
`@unguard` never suppresses `error` diagnostics.
|
|
@@ -215,6 +370,7 @@ import { executeScan, scan } from "unguard";
|
|
|
215
370
|
const result = await scan({ paths: ["src/"] }); // raw diagnostics
|
|
216
371
|
const execution = await executeScan({
|
|
217
372
|
paths: ["src"],
|
|
373
|
+
mode: "scan", // or "audit" for the heuristic tier
|
|
218
374
|
ignore: ["**/*.gen.ts"],
|
|
219
375
|
rulePolicy: {
|
|
220
376
|
"duplicate-*": "warning",
|
|
@@ -222,6 +378,7 @@ const execution = await executeScan({
|
|
|
222
378
|
"tag:safety": "error",
|
|
223
379
|
"prefer-*": "off",
|
|
224
380
|
},
|
|
381
|
+
overrides: [{ files: ["tests/**"], rules: { "no-non-null-assertion": "off" } }],
|
|
225
382
|
showSeverities: ["error", "warning"],
|
|
226
383
|
failOn: "error",
|
|
227
384
|
});
|
|
@@ -232,6 +389,21 @@ for (const d of execution.visibleDiagnostics) {
|
|
|
232
389
|
}
|
|
233
390
|
```
|
|
234
391
|
|
|
392
|
+
### Caching
|
|
393
|
+
|
|
394
|
+
unguard caches scan results under `node_modules/.cache/unguard/`. On a warm
|
|
395
|
+
run, if every file's content hash and the active rule set are unchanged,
|
|
396
|
+
unguard returns cached diagnostics without building a TypeScript program.
|
|
397
|
+
The cache invalidates automatically on:
|
|
398
|
+
|
|
399
|
+
- file content changes (mtime-only changes are ignored - `git checkout` and `git stash` stay cache hits)
|
|
400
|
+
- changes to active rules or rule severities
|
|
401
|
+
- changes to scan paths, ignore globs, or `failOn`
|
|
402
|
+
- unguard version upgrades
|
|
403
|
+
|
|
404
|
+
Disable with `--no-cache` (CLI), `cache: false` (config), or pass
|
|
405
|
+
`cache: false` to `executeScan({ cache: false })` programmatically.
|
|
406
|
+
|
|
235
407
|
## License
|
|
236
408
|
|
|
237
409
|
MIT
|