unguard 0.15.2 → 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 +232 -81
- package/dist/{chunk-WXUUG77Y.js → chunk-3Y76J2K2.js} +215 -100
- 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 +211 -40
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +134 -9
- package/dist/index.js +2 -2
- package/dist/worker.js +8 -3
- package/dist/worker.js.map +1 -1
- package/package.json +8 -2
- package/schema.json +23 -1
- package/dist/chunk-BLCVXVLX.js +0 -3850
- package/dist/chunk-BLCVXVLX.js.map +0 -1
- package/dist/chunk-WXUUG77Y.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,45 +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
|
|
39
|
-
unguard
|
|
40
|
-
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
|
|
41
129
|
```
|
|
42
130
|
|
|
43
|
-
|
|
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.
|
|
44
139
|
|
|
45
140
|
### Config
|
|
46
141
|
|
|
@@ -58,6 +153,15 @@ Add `unguard` to your lint check, especially if code is written by AI.
|
|
|
58
153
|
"no-ts-ignore": "error",
|
|
59
154
|
"prefer-*": "off"
|
|
60
155
|
},
|
|
156
|
+
"overrides": [
|
|
157
|
+
{
|
|
158
|
+
"files": ["tests/**", "**/*.test.ts"],
|
|
159
|
+
"rules": {
|
|
160
|
+
"no-non-null-assertion": "off",
|
|
161
|
+
"duplicate-*": "off"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
],
|
|
61
165
|
"failOn": "error",
|
|
62
166
|
"concurrency": 4,
|
|
63
167
|
"cache": true
|
|
@@ -71,6 +175,9 @@ Selectors support:
|
|
|
71
175
|
- wildcard: `duplicate-*`
|
|
72
176
|
- category: `category:cross-file`
|
|
73
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.
|
|
74
181
|
|
|
75
182
|
### Ignore behavior
|
|
76
183
|
|
|
@@ -97,6 +204,8 @@ unguard src --fail-on=error
|
|
|
97
204
|
|
|
98
205
|
`--severity` filters display only. `--fail-on` evaluates all diagnostics after rule policy.
|
|
99
206
|
|
|
207
|
+
`unguard audit` defaults to `--fail-on=none` and exits 0 regardless of findings. Pass `--fail-on` explicitly to make audit gate.
|
|
208
|
+
|
|
100
209
|
### Output formats
|
|
101
210
|
|
|
102
211
|
**Grouped** (default) -- diagnostics grouped by file:
|
|
@@ -112,84 +221,124 @@ src/lib/probe.ts
|
|
|
112
221
|
src/lib/probe.ts:37:4 warning [no-swallowed-catch] Catch swallows the error...
|
|
113
222
|
```
|
|
114
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
|
+
|
|
115
252
|
## Current Rules
|
|
116
253
|
|
|
254
|
+
The **Tier** column says which command runs the rule: `scan` (proven) or `audit` (heuristic).
|
|
255
|
+
|
|
117
256
|
### Type system evasion
|
|
118
257
|
|
|
119
|
-
| Rule | Severity | What it catches |
|
|
120
|
-
| ---- | -------- | --------------- |
|
|
121
|
-
| `no-any-cast` | error | `x as any` |
|
|
122
|
-
| `no-explicit-any-annotation` | error | `param: any`, `const x: any` |
|
|
123
|
-
| `no-inline-type-assertion` | error | `x as { ... }`, `<{ ... }>x` |
|
|
124
|
-
| `no-type-assertion` | error | `x as unknown as T` |
|
|
125
|
-
| `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 |
|
|
126
271
|
|
|
127
272
|
### Defensive code (type-aware)
|
|
128
273
|
|
|
129
274
|
These rules use the TypeScript type checker. Non-nullable types suppress the diagnostic; nullable types are flagged.
|
|
130
275
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
| `no-optional-
|
|
136
|
-
| `no-
|
|
137
|
-
| `no-
|
|
138
|
-
| `no-
|
|
139
|
-
| `no-
|
|
140
|
-
| `no-
|
|
141
|
-
| `no-
|
|
142
|
-
| `no-
|
|
143
|
-
| `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 |
|
|
144
296
|
|
|
145
297
|
### Error handling
|
|
146
298
|
|
|
147
|
-
| Rule | Severity | What it catches |
|
|
148
|
-
| ---- | -------- | --------------- |
|
|
149
|
-
| `no-swallowed-catch` | warning |
|
|
150
|
-
| `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 }` |
|
|
151
303
|
|
|
152
304
|
### Interface design
|
|
153
305
|
|
|
154
|
-
| Rule | Severity | What it catches |
|
|
155
|
-
| ---- | -------- | --------------- |
|
|
156
|
-
| `no-inline-param-type` | warning | `(params: { id: string; ... })`
|
|
157
|
-
| `prefer-
|
|
158
|
-
| `
|
|
159
|
-
| `no-defaulted-required-port-arg` | warning | `class C implements I { method(arg = x) }` where `I.method(arg)` is required -- the
|
|
160
|
-
| `repeated-literal-property` | warning | Same literal value repeated across object properties -- likely a missed constant |
|
|
161
|
-
| `repeated-return-shape` | warning | Multiple functions return object literals with the same property names -- extract a shared return type |
|
|
162
|
-
|
|
163
|
-
### State management
|
|
164
|
-
|
|
165
|
-
| Rule | Severity | What it catches |
|
|
166
|
-
| ---- | -------- | --------------- |
|
|
167
|
-
| `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) |
|
|
168
315
|
|
|
169
316
|
### Cross-file analysis
|
|
170
317
|
|
|
171
|
-
| Rule | Severity | What it catches |
|
|
172
|
-
| ---- | -------- | --------------- |
|
|
173
|
-
| `duplicate-type-declaration` | warning | Same type shape in multiple files |
|
|
174
|
-
| `duplicate-type-name` | warning | Same exported type name, different shapes |
|
|
175
|
-
| `duplicate-function-declaration` | warning | Same function body in multiple files |
|
|
176
|
-
| `duplicate-function-name` | warning | Same exported function name, different bodies |
|
|
177
|
-
| `duplicate-constant-declaration` |
|
|
178
|
-
| `duplicate-inline-type-in-params` | warning | Same inline `{ ... }` param type repeated across signatures |
|
|
179
|
-
| `duplicate-file` | warning | File with identical content to another file |
|
|
180
|
-
| `near-duplicate-function` | warning | Function bodies that match after renaming params and literals -- likely a copy-paste |
|
|
181
|
-
| `duplicate-statement-sequence` |
|
|
182
|
-
| `trivial-wrapper` |
|
|
183
|
-
| `unused-export` |
|
|
184
|
-
| `optional-arg-always-used` | warning | Optional param provided at every call site |
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
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 |
|
|
187
336
|
|
|
188
337
|
### Imports
|
|
189
338
|
|
|
190
|
-
| Rule | Severity | What it catches |
|
|
191
|
-
| ---- | -------- | --------------- |
|
|
192
|
-
| `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) |
|
|
193
342
|
|
|
194
343
|
## Annotations
|
|
195
344
|
|
|
@@ -207,8 +356,8 @@ file.ts:2:31 error Explicit `any` annotation ... (intentional escape hatch for u
|
|
|
207
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:
|
|
208
357
|
|
|
209
358
|
```typescript
|
|
210
|
-
// @unguard no-
|
|
211
|
-
|
|
359
|
+
// @unguard no-nullish-coalescing intentional default for legacy callers
|
|
360
|
+
const port = config.port ?? 3000;
|
|
212
361
|
```
|
|
213
362
|
|
|
214
363
|
`@unguard` never suppresses `error` diagnostics.
|
|
@@ -221,6 +370,7 @@ import { executeScan, scan } from "unguard";
|
|
|
221
370
|
const result = await scan({ paths: ["src/"] }); // raw diagnostics
|
|
222
371
|
const execution = await executeScan({
|
|
223
372
|
paths: ["src"],
|
|
373
|
+
mode: "scan", // or "audit" for the heuristic tier
|
|
224
374
|
ignore: ["**/*.gen.ts"],
|
|
225
375
|
rulePolicy: {
|
|
226
376
|
"duplicate-*": "warning",
|
|
@@ -228,6 +378,7 @@ const execution = await executeScan({
|
|
|
228
378
|
"tag:safety": "error",
|
|
229
379
|
"prefer-*": "off",
|
|
230
380
|
},
|
|
381
|
+
overrides: [{ files: ["tests/**"], rules: { "no-non-null-assertion": "off" } }],
|
|
231
382
|
showSeverities: ["error", "warning"],
|
|
232
383
|
failOn: "error",
|
|
233
384
|
});
|
|
@@ -245,7 +396,7 @@ run, if every file's content hash and the active rule set are unchanged,
|
|
|
245
396
|
unguard returns cached diagnostics without building a TypeScript program.
|
|
246
397
|
The cache invalidates automatically on:
|
|
247
398
|
|
|
248
|
-
- file content changes (mtime-only changes are ignored
|
|
399
|
+
- file content changes (mtime-only changes are ignored - `git checkout` and `git stash` stay cache hits)
|
|
249
400
|
- changes to active rules or rule severities
|
|
250
401
|
- changes to scan paths, ignore globs, or `failOn`
|
|
251
402
|
- unguard version upgrades
|