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 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 API.
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
- ## Install
13
+ ![unguard scanning a file and citing the types that prove each guard dead](https://raw.githubusercontent.com/TwoAbove/unguard/main/demo/demo.gif)
14
+
15
+ ## Quick start
14
16
 
15
17
  ```bash
16
- npm install -g unguard
18
+ npx unguard
17
19
  ```
18
20
 
19
- or simply
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
- ```bash
22
- npx unguard
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 src # scan files/directories
29
- unguard src --config ./unguard.config.json # load config
30
- unguard src --ignore '**/*.gen.ts' # add ignore globs
31
- unguard src --filter no-any-cast # run a single rule
32
- unguard src --rule duplicate-*=warning # override rule severity/policy
33
- unguard src --rule category:cross-file=warning
34
- unguard src --rule tag:safety=error
35
- unguard src --severity=error,warning # show errors+warnings
36
- unguard src --fail-on=error # fail only on errors
37
- unguard src --format=flat # one-line-per-diagnostic, grepable
38
- unguard src --format=flat | grep error
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
- Add `unguard` to your lint check, especially if code is written by AI.
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
- "failOn": "error"
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` / `@ts-expect-error` |
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
- | Rule | Severity | What it catches |
126
- | ---- | -------- | --------------- |
127
- | `no-optional-property-access` | warning | `obj?.prop` on a non-nullable type |
128
- | `no-optional-element-access` | warning | `obj?.[key]` on a non-nullable type |
129
- | `no-optional-call` | warning | `fn?.()` on a non-nullable type |
130
- | `no-nullish-coalescing` | warning | `x ?? fallback` on a non-nullable type |
131
- | `no-logical-or-fallback` | warning | `map.get(k) \|\| fallback`, `count \|\| 1` -- `\|\|` swallows `0` and `""`; use `??` |
132
- | `no-null-ternary-normalization` | warning | `x == null ? fallback : x` |
133
- | `no-coalesce-then-guard` | warning | `const x = a ?? null; if (x == null)` -- guard re-checks the same partition the `??` just created |
134
- | `no-await-coalesce` | warning | `await fn() ?? fallback` -- fuses the call's failure mode into the fallback (skips `Map.get`, `Array.find`, and structural optionals) |
135
- | `no-non-null-assertion` | warning | `x!` on a nullable type without a local narrowing guard |
136
- | `no-double-negation-coercion` | info | `!!value` |
137
- | `no-redundant-existence-guard` | warning | `obj && obj.prop` on a non-nullable type |
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 | `catch (e) {}`, `catch (e) { log(e); return fallback }`, `.catch(() => fallback)` -- error neither rethrown nor surfaced in the return value |
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; ... })` inline object type on parameter |
151
- | `prefer-default-param-value` | info | Optional param reassigned with `??` in the body |
152
- | `prefer-required-param-with-guard` | info | `arg?: T` followed by `if (!arg) throw` |
153
- | `no-defaulted-required-port-arg` | warning | `class C implements I { method(arg = x) }` where `I.method(arg)` is required -- the default widens the interface contract |
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` | warning | Same constant value in multiple files |
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` | info | Repeated block of statements across functions or files |
176
- | `trivial-wrapper` | info | Function that delegates to another without transformation |
177
- | `unused-export` | info | Exported function with no usages in the project |
178
- | `optional-arg-always-used` | warning | Optional param provided at every call site |
179
- | `explicit-null-arg` | warning | `fn(null)` / `fn(undefined)` to project functions |
180
- | `dead-overload` | warning | Overload signature with zero matching project call sites |
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` | error | `import("./module")` |
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-module-state-write module cache is intentional in this adapter
205
- cache.set(user.id, user);
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