vigiles 2.5.0 → 3.0.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/.claude-plugin/plugin.json +9 -0
- package/README.md +73 -434
- package/action.yml +143 -7
- package/dist/action-gate.d.ts +1 -1
- package/dist/action-gate.js +1 -1
- package/dist/adapter-conformance.d.ts +30 -0
- package/dist/adapter-conformance.js +153 -0
- package/dist/adapter-registry.d.ts +42 -0
- package/dist/adapter-registry.js +55 -0
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.js +16 -0
- package/dist/adapters/claude-code/adapter.d.ts +3 -0
- package/dist/adapters/claude-code/adapter.js +46 -0
- package/dist/{agent-result.d.ts → adapters/claude-code/agent-result.d.ts} +1 -1
- package/dist/adapters/claude-code/dialect.d.ts +13 -0
- package/dist/adapters/claude-code/dialect.js +51 -0
- package/dist/adapters/claude-code/egress-entry.d.ts +2 -0
- package/dist/adapters/claude-code/egress-entry.js +115 -0
- package/dist/adapters/claude-code/egress-proxy.d.ts +2 -0
- package/dist/adapters/claude-code/egress-proxy.js +60 -0
- package/dist/adapters/claude-code/egress.d.ts +114 -0
- package/dist/adapters/claude-code/egress.js +276 -0
- package/dist/adapters/claude-code/eval-baseline.d.ts +68 -0
- package/dist/adapters/claude-code/eval-baseline.js +173 -0
- package/dist/{eval-cache.d.ts → adapters/claude-code/eval-cache.d.ts} +1 -1
- package/dist/{eval-cache.js → adapters/claude-code/eval-cache.js} +1 -1
- package/dist/{eval.d.ts → adapters/claude-code/eval.d.ts} +28 -4
- package/dist/{eval.js → adapters/claude-code/eval.js} +61 -25
- package/dist/{harness-test.d.ts → adapters/claude-code/harness-test.d.ts} +33 -37
- package/dist/{harness-test.js → adapters/claude-code/harness-test.js} +124 -43
- package/dist/adapters/claude-code/hook-protocol.d.ts +10 -0
- package/dist/adapters/claude-code/hook-protocol.js +10 -0
- package/dist/adapters/claude-code/layout.d.ts +8 -0
- package/dist/adapters/claude-code/layout.js +18 -0
- package/dist/{mock-model.d.ts → adapters/claude-code/mock-model.d.ts} +2 -24
- package/dist/adapters/claude-code/model-mock.d.ts +11 -0
- package/dist/adapters/claude-code/model-mock.js +10 -0
- package/dist/adapters/claude-code/plugin-loader.d.ts +25 -0
- package/dist/adapters/claude-code/plugin-loader.js +19 -0
- package/dist/adapters/claude-code/run-hook.d.ts +204 -0
- package/dist/adapters/claude-code/run-hook.js +418 -0
- package/dist/adapters/claude-code/run-scripts.d.ts +52 -0
- package/dist/adapters/claude-code/run-scripts.js +150 -0
- package/dist/adapters/claude-code/runtime.d.ts +16 -0
- package/dist/adapters/claude-code/runtime.js +39 -0
- package/dist/{sandbox.d.ts → adapters/claude-code/sandbox.d.ts} +45 -4
- package/dist/{sandbox.js → adapters/claude-code/sandbox.js} +76 -8
- package/dist/{skill-driver.d.ts → adapters/claude-code/skill-driver.d.ts} +1 -1
- package/dist/adapters/codex/adapter.d.ts +3 -0
- package/dist/adapters/codex/adapter.js +49 -0
- package/dist/adapters/codex/dialect.d.ts +10 -0
- package/dist/adapters/codex/dialect.js +30 -0
- package/dist/adapters/codex/driver.d.ts +20 -0
- package/dist/adapters/codex/driver.js +89 -0
- package/dist/adapters/codex/hook-protocol.d.ts +10 -0
- package/dist/adapters/codex/hook-protocol.js +18 -0
- package/dist/adapters/codex/layout.d.ts +16 -0
- package/dist/adapters/codex/layout.js +18 -0
- package/dist/adapters/codex/mock-model.d.ts +52 -0
- package/dist/adapters/codex/mock-model.js +210 -0
- package/dist/adapters/codex/model-mock.d.ts +11 -0
- package/dist/adapters/codex/model-mock.js +10 -0
- package/dist/adapters/codex/runtime.d.ts +38 -0
- package/dist/adapters/codex/runtime.js +62 -0
- package/dist/adapters/opencode/adapter.d.ts +3 -0
- package/dist/adapters/opencode/adapter.js +48 -0
- package/dist/adapters/opencode/dialect.d.ts +10 -0
- package/dist/adapters/opencode/dialect.js +36 -0
- package/dist/adapters/opencode/layout.d.ts +9 -0
- package/dist/adapters/opencode/layout.js +25 -0
- package/dist/adapters/opencode/model-mock.d.ts +10 -0
- package/dist/adapters/opencode/model-mock.js +10 -0
- package/dist/adapters/opencode/runtime.d.ts +9 -0
- package/dist/adapters/opencode/runtime.js +21 -0
- package/dist/claude-code.d.ts +16 -0
- package/dist/claude-code.js +32 -0
- package/dist/cli-flags.d.ts +22 -0
- package/dist/cli-flags.js +38 -0
- package/dist/cli.js +406 -206
- package/dist/codex.d.ts +20 -0
- package/dist/codex.js +36 -0
- package/dist/community-skills.d.ts +1 -1
- package/dist/community-skills.js +2 -2
- package/dist/core/adapter.d.ts +88 -0
- package/dist/core/adapter.js +3 -0
- package/dist/{compile.d.ts → core/compile.d.ts} +11 -2
- package/dist/{compile.js → core/compile.js} +68 -53
- package/dist/core/compose.d.ts +79 -0
- package/dist/core/compose.js +145 -0
- package/dist/core/dialect.d.ts +51 -0
- package/dist/core/dialect.js +3 -0
- package/dist/core/harness-driver.d.ts +134 -0
- package/dist/core/harness-driver.js +3 -0
- package/dist/core/hook-protocol.d.ts +28 -0
- package/dist/core/hook-protocol.js +3 -0
- package/dist/core/layout.d.ts +43 -0
- package/dist/core/layout.js +3 -0
- package/dist/core/model-mock.d.ts +27 -0
- package/dist/core/model-mock.js +3 -0
- package/dist/core/refs.d.ts +62 -0
- package/dist/{refs.js → core/refs.js} +49 -23
- package/dist/core/runtime.d.ts +38 -0
- package/dist/core/runtime.js +3 -0
- package/dist/{types.d.ts → core/types.d.ts} +29 -0
- package/dist/{validate.d.ts → core/validate.d.ts} +1 -1
- package/dist/{validate.js → core/validate.js} +41 -4
- package/dist/e2e.d.ts +19 -0
- package/dist/e2e.js +39 -0
- package/dist/harness-assert.d.ts +60 -11
- package/dist/harness-assert.js +111 -9
- package/dist/integration.d.ts +16 -0
- package/dist/integration.js +32 -0
- package/dist/leaderboard.d.ts +33 -0
- package/dist/leaderboard.js +107 -0
- package/dist/linting.d.ts +9 -0
- package/dist/linting.js +25 -0
- package/dist/plugin-loader.d.ts +6 -3
- package/dist/plugin-loader.js +101 -49
- package/dist/scan.d.ts +49 -0
- package/dist/scan.js +177 -0
- package/dist/setup-plan.d.ts +51 -0
- package/dist/setup-plan.js +85 -0
- package/dist/skill-test.d.ts +1 -1
- package/dist/skill-test.js +1 -1
- package/dist/test-coverage.d.ts +71 -0
- package/dist/test-coverage.js +228 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +28 -0
- package/dist/unit.d.ts +17 -0
- package/dist/unit.js +36 -0
- package/hooks/refs-nudge.sh +24 -0
- package/package.json +35 -16
- package/skills/audit-feedback-loop/SKILL.md +76 -0
- package/skills/edit-spec/SKILL.md +131 -0
- package/skills/enforce-rules-format/SKILL.md +71 -0
- package/skills/generate-logo/SKILL.md +103 -0
- package/skills/generate-rule/SKILL.md +64 -0
- package/skills/linter-docs/clippy.md +241 -0
- package/skills/linter-docs/eslint.md +384 -0
- package/skills/linter-docs/pylint.md +288 -0
- package/skills/linter-docs/rubocop.md +277 -0
- package/skills/linter-docs/ruff.md +187 -0
- package/skills/linter-docs/stylelint.md +247 -0
- package/skills/migrate-to-spec/SKILL.md +126 -0
- package/skills/pr-to-lint-rule/SKILL.md +97 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/skills/test-harness/SKILL.md +157 -0
- package/dist/action.d.ts +0 -7
- package/dist/action.js +0 -180
- package/dist/refs.d.ts +0 -44
- package/dist/run-hook.d.ts +0 -77
- package/dist/run-hook.js +0 -80
- package/dist/run-scripts.d.ts +0 -20
- package/dist/run-scripts.js +0 -70
- /package/dist/{agent-result.js → adapters/claude-code/agent-result.js} +0 -0
- /package/dist/{agent-runtime.d.ts → adapters/claude-code/agent-runtime.d.ts} +0 -0
- /package/dist/{agent-runtime.js → adapters/claude-code/agent-runtime.js} +0 -0
- /package/dist/{judge.d.ts → adapters/claude-code/judge.d.ts} +0 -0
- /package/dist/{judge.js → adapters/claude-code/judge.js} +0 -0
- /package/dist/{mock-entry.d.ts → adapters/claude-code/mock-entry.d.ts} +0 -0
- /package/dist/{mock-entry.js → adapters/claude-code/mock-entry.js} +0 -0
- /package/dist/{mock-model.js → adapters/claude-code/mock-model.js} +0 -0
- /package/dist/{skill-driver.js → adapters/claude-code/skill-driver.js} +0 -0
- /package/dist/{skill-runtime.d.ts → adapters/claude-code/skill-runtime.d.ts} +0 -0
- /package/dist/{skill-runtime.js → adapters/claude-code/skill-runtime.js} +0 -0
- /package/dist/{stats.d.ts → adapters/claude-code/stats.d.ts} +0 -0
- /package/dist/{stats.js → adapters/claude-code/stats.js} +0 -0
- /package/dist/{compile-generator.d.ts → core/compile-generator.d.ts} +0 -0
- /package/dist/{compile-generator.js → core/compile-generator.js} +0 -0
- /package/dist/{coverage.d.ts → core/coverage.d.ts} +0 -0
- /package/dist/{coverage.js → core/coverage.js} +0 -0
- /package/dist/{doc-refs.d.ts → core/doc-refs.d.ts} +0 -0
- /package/dist/{doc-refs.js → core/doc-refs.js} +0 -0
- /package/dist/{evolve.d.ts → core/evolve.d.ts} +0 -0
- /package/dist/{evolve.js → core/evolve.js} +0 -0
- /package/dist/{frontmatter.d.ts → core/frontmatter.d.ts} +0 -0
- /package/dist/{frontmatter.js → core/frontmatter.js} +0 -0
- /package/dist/{generate-schema.d.ts → core/generate-schema.d.ts} +0 -0
- /package/dist/{generate-schema.js → core/generate-schema.js} +0 -0
- /package/dist/{generate-types.d.ts → core/generate-types.d.ts} +0 -0
- /package/dist/{generate-types.js → core/generate-types.js} +0 -0
- /package/dist/{hash.d.ts → core/hash.d.ts} +0 -0
- /package/dist/{hash.js → core/hash.js} +0 -0
- /package/dist/{inline.d.ts → core/inline.d.ts} +0 -0
- /package/dist/{inline.js → core/inline.js} +0 -0
- /package/dist/{integrity.d.ts → core/integrity.d.ts} +0 -0
- /package/dist/{integrity.js → core/integrity.js} +0 -0
- /package/dist/{linters.d.ts → core/linters.d.ts} +0 -0
- /package/dist/{linters.js → core/linters.js} +0 -0
- /package/dist/{mcp.d.ts → core/mcp.d.ts} +0 -0
- /package/dist/{mcp.js → core/mcp.js} +0 -0
- /package/dist/{orphans.d.ts → core/orphans.d.ts} +0 -0
- /package/dist/{orphans.js → core/orphans.js} +0 -0
- /package/dist/{proofs.d.ts → core/proofs.d.ts} +0 -0
- /package/dist/{proofs.js → core/proofs.js} +0 -0
- /package/dist/{session.d.ts → core/session.d.ts} +0 -0
- /package/dist/{session.js → core/session.js} +0 -0
- /package/dist/{sidecar.d.ts → core/sidecar.d.ts} +0 -0
- /package/dist/{sidecar.js → core/sidecar.js} +0 -0
- /package/dist/{spec.d.ts → core/spec.d.ts} +0 -0
- /package/dist/{spec.js → core/spec.js} +0 -0
- /package/dist/{symbols.d.ts → core/symbols.d.ts} +0 -0
- /package/dist/{symbols.js → core/symbols.js} +0 -0
- /package/dist/{test-utils.d.ts → core/test-utils.d.ts} +0 -0
- /package/dist/{test-utils.js → core/test-utils.js} +0 -0
- /package/dist/{types.js → core/types.js} +0 -0
- /package/{.claude-plugin/hooks → hooks}/post-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/pre-edit.sh +0 -0
- /package/{.claude-plugin/hooks → hooks}/session-start.sh +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Clippy — Reference
|
|
2
|
+
|
|
3
|
+
Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom lints).
|
|
4
|
+
|
|
5
|
+
## Check Existing Lints First
|
|
6
|
+
|
|
7
|
+
Before writing a custom lint, search these lint groups — the pattern may already be covered:
|
|
8
|
+
|
|
9
|
+
| Lint group | Scope | Key lints to know |
|
|
10
|
+
| ------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| `correctness` | Code that is outright wrong or will panic | `uninit_assumed_init`, `wrong_self_convention`, `nonsensical_open_options`, `mistyped_literal_suffixes`, `transmuting_null`, `undropped_manually_drops` |
|
|
12
|
+
| `suspicious` | Code that is very likely a bug | `suspicious_else_formatting`, `suspicious_unary_op_formatting`, `suspicious_op_assign_impl`, `blanket_clippy_restriction_lints`, `almost_complete_range` |
|
|
13
|
+
| `style` | Idiomatic Rust style violations | `needless_return`, `redundant_closure`, `match_bool`, `single_match`, `collapsible_if`, `len_zero`, `manual_map`, `if_let_mutex` |
|
|
14
|
+
| `complexity` | Code that can be simplified | `unnecessary_cast`, `needless_borrow`, `redundant_clone`, `type_complexity`, `manual_strip`, `option_map_unit_fn`, `useless_conversion` |
|
|
15
|
+
| `perf` | Code that can be made faster | `large_enum_variant`, `box_collection`, `redundant_allocation`, `manual_memcpy`, `iter_nth`, `unnecessary_to_owned`, `needless_collect` |
|
|
16
|
+
| `pedantic` | Stricter lints, off by default | `unwrap_used`, `expect_used`, `cast_possible_truncation`, `cast_sign_loss`, `missing_errors_doc`, `missing_panics_doc`, `doc_markdown`, `must_use_candidate` |
|
|
17
|
+
| `restriction` | Lints for specific project policies, off by default | `print_stdout`, `print_stderr`, `dbg_macro`, `unimplemented`, `todo`, `unwrap_used`, `expect_used`, `mem_forget`, `shadow_reuse`, `float_arithmetic` |
|
|
18
|
+
| `nursery` | Experimental lints, may have false positives | `missing_const_for_fn`, `cognitive_complexity`, `use_self`, `option_if_let_else`, `redundant_pub_crate`, `significant_drop_tightening` |
|
|
19
|
+
| `cargo` | Cargo manifest issues | `multiple_crate_versions`, `wildcard_dependencies`, `negative_feature_names`, `redundant_feature_names` |
|
|
20
|
+
|
|
21
|
+
**Tip:** Run `cargo clippy --warn clippy::pedantic` on your codebase to see what pedantic catches before manually hunting for rules. The full lint list is at <https://rust-lang.github.io/rust-clippy/stable/>.
|
|
22
|
+
|
|
23
|
+
## Lint Configuration
|
|
24
|
+
|
|
25
|
+
### Cargo.toml `[lints.clippy]` section (recommended)
|
|
26
|
+
|
|
27
|
+
The modern way to configure Clippy project-wide. Checked into version control, applies to every `cargo clippy` invocation:
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
# Cargo.toml
|
|
31
|
+
[lints.clippy]
|
|
32
|
+
# Enable entire groups
|
|
33
|
+
pedantic = "warn"
|
|
34
|
+
nursery = "warn"
|
|
35
|
+
|
|
36
|
+
# Override individual lints
|
|
37
|
+
unwrap_used = "deny"
|
|
38
|
+
expect_used = "warn"
|
|
39
|
+
cast_possible_truncation = "allow"
|
|
40
|
+
|
|
41
|
+
# Restriction lints — opt-in individually
|
|
42
|
+
print_stdout = "warn"
|
|
43
|
+
dbg_macro = "deny"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This replaces the older `#![warn(clippy::pedantic)]` crate-level attribute approach. vigiles reads `Cargo.toml` for `enforce()` verification.
|
|
47
|
+
|
|
48
|
+
### Inline attributes
|
|
49
|
+
|
|
50
|
+
Use `#[allow]`, `#[warn]`, and `#[deny]` for per-item overrides:
|
|
51
|
+
|
|
52
|
+
```rust
|
|
53
|
+
// Suppress a lint on a single function
|
|
54
|
+
#[allow(clippy::too_many_arguments)]
|
|
55
|
+
fn create_widget(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32, g: u32) { /* ... */ }
|
|
56
|
+
|
|
57
|
+
// Escalate a lint to a hard error for a module
|
|
58
|
+
#[deny(clippy::unwrap_used)]
|
|
59
|
+
mod payment_processing {
|
|
60
|
+
// Any .unwrap() in this module fails compilation
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Warn on a lint for a specific impl block
|
|
64
|
+
#[warn(clippy::cast_possible_truncation)]
|
|
65
|
+
impl Converter {
|
|
66
|
+
fn to_u32(&self, val: u64) -> u32 { val as u32 }
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Attribute precedence (highest to lowest): `#[forbid]` > `#[deny]` > `#[warn]` > `#[allow]`. `#[forbid]` cannot be overridden by inner attributes — use it for security-critical lints.
|
|
71
|
+
|
|
72
|
+
### `clippy.toml` / `.clippy.toml`
|
|
73
|
+
|
|
74
|
+
Project-level configuration for lint thresholds and behavior:
|
|
75
|
+
|
|
76
|
+
```toml
|
|
77
|
+
# clippy.toml
|
|
78
|
+
too-many-arguments-threshold = 10
|
|
79
|
+
type-complexity-threshold = 500
|
|
80
|
+
cognitive-complexity-threshold = 30
|
|
81
|
+
single-char-binding-names-threshold = 4
|
|
82
|
+
msrv = "1.70.0"
|
|
83
|
+
|
|
84
|
+
# Disallow certain types
|
|
85
|
+
disallowed-types = [
|
|
86
|
+
{ path = "std::collections::HashMap", reason = "Use indexmap::IndexMap for deterministic iteration" },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Disallow certain methods
|
|
90
|
+
disallowed-methods = [
|
|
91
|
+
{ path = "std::env::var", reason = "Use config::get() for environment access" },
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Disallow certain macros
|
|
95
|
+
disallowed-macros = [
|
|
96
|
+
{ path = "std::println", reason = "Use tracing::info! instead" },
|
|
97
|
+
]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`disallowed-types`, `disallowed-methods`, and `disallowed-macros` are particularly powerful — they let you ban specific APIs project-wide with custom error messages, no custom lint needed.
|
|
101
|
+
|
|
102
|
+
## Custom Lints
|
|
103
|
+
|
|
104
|
+
### dylint — custom Clippy-style lints
|
|
105
|
+
|
|
106
|
+
[dylint](https://github.com/trailofbits/dylint) loads lint libraries as dynamic libraries, giving you the same compiler internals that Clippy uses:
|
|
107
|
+
|
|
108
|
+
```rust
|
|
109
|
+
// my_lint/src/lib.rs
|
|
110
|
+
use clippy_utils::diagnostics::span_lint_and_help;
|
|
111
|
+
use rustc_lint::{LateContext, LateLintPass, LintArray, LintPass};
|
|
112
|
+
use rustc_session::{declare_lint, declare_lint_pass};
|
|
113
|
+
|
|
114
|
+
declare_lint! {
|
|
115
|
+
/// Disallow direct database calls outside the `db` module.
|
|
116
|
+
pub NO_DIRECT_DB_CALL,
|
|
117
|
+
Warn,
|
|
118
|
+
"direct database calls should go through the db module"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
declare_lint_pass!(NoDirectDbCall => [NO_DIRECT_DB_CALL]);
|
|
122
|
+
|
|
123
|
+
impl<'tcx> LateLintPass<'tcx> for NoDirectDbCall {
|
|
124
|
+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx rustc_hir::Expr<'_>) {
|
|
125
|
+
if is_direct_db_call(cx, expr) {
|
|
126
|
+
span_lint_and_help(
|
|
127
|
+
cx,
|
|
128
|
+
NO_DIRECT_DB_CALL,
|
|
129
|
+
expr.span,
|
|
130
|
+
"direct database calls are not allowed here",
|
|
131
|
+
None,
|
|
132
|
+
"use the repository pattern — import from `crate::db` instead",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**When to use dylint vs existing Clippy lints:**
|
|
140
|
+
|
|
141
|
+
| Situation | Use |
|
|
142
|
+
| ----------------------------------------- | ------------------------------------------- |
|
|
143
|
+
| Ban a specific function/type/macro | `clippy.toml` `disallowed-*` — zero code |
|
|
144
|
+
| Enforce naming or style convention | Existing Clippy pedantic/style lints |
|
|
145
|
+
| Detect a pattern needing type information | dylint — access to rustc type checker |
|
|
146
|
+
| Enforce architectural boundaries | dylint or `cargo-deny` for dependency rules |
|
|
147
|
+
| One-off "don't use X" in a single crate | `#[deny(clippy::...)]` attribute |
|
|
148
|
+
|
|
149
|
+
**dylint tradeoffs:**
|
|
150
|
+
|
|
151
|
+
- Tied to a specific Rust nightly version (compiler internals are unstable)
|
|
152
|
+
- Requires maintaining a separate crate with `rustc_private` dependencies
|
|
153
|
+
- `clippy_utils` provides helper functions but its API changes between releases
|
|
154
|
+
- Build times increase since the lint library compiles against `rustc` internals
|
|
155
|
+
|
|
156
|
+
For vigiles, reference dylint lints via: `enforce("clippy/no_direct_db_call", "Use the repository pattern.")` — the lint name maps to the `declare_lint!` identifier (snake_case).
|
|
157
|
+
|
|
158
|
+
## Edge Cases and Gotchas
|
|
159
|
+
|
|
160
|
+
### False positives from macros
|
|
161
|
+
|
|
162
|
+
Clippy lints operate on the expanded AST. Macros can trigger false positives because the expanded code looks different from what the author wrote:
|
|
163
|
+
|
|
164
|
+
```rust
|
|
165
|
+
// This macro expansion may trigger `clippy::redundant_clone`
|
|
166
|
+
// even though the clone is structurally necessary in the macro output
|
|
167
|
+
my_derive_macro! {
|
|
168
|
+
struct Foo { bar: String }
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Workarounds:
|
|
173
|
+
|
|
174
|
+
- `#[allow(clippy::...)]` on the macro invocation site
|
|
175
|
+
- Add `#[automatically_derived]` in proc-macro output — Clippy skips many lints for derived code
|
|
176
|
+
- File an issue upstream if the false positive is in a common macro pattern
|
|
177
|
+
|
|
178
|
+
### Nightly-only lints
|
|
179
|
+
|
|
180
|
+
Some Clippy lints require nightly Rust because they depend on unstable compiler features. The `nursery` group is the most common source. If you enable `nursery` lints in CI:
|
|
181
|
+
|
|
182
|
+
- Pin a specific nightly version: `rust-toolchain.toml` with `channel = "nightly-2025-01-15"`
|
|
183
|
+
- Expect breakage on nightly updates — nursery lints can be renamed, removed, or change behavior
|
|
184
|
+
- Never `#[deny]` nursery lints — use `#[warn]` so they don't block builds when behavior changes
|
|
185
|
+
|
|
186
|
+
### `allow` vs `warn` on groups
|
|
187
|
+
|
|
188
|
+
Enabling a group and then allowing individual lints works top-down:
|
|
189
|
+
|
|
190
|
+
```toml
|
|
191
|
+
# Cargo.toml
|
|
192
|
+
[lints.clippy]
|
|
193
|
+
pedantic = "warn" # Enable all pedantic lints
|
|
194
|
+
module_name_repetitions = "allow" # But suppress this one
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The reverse does NOT work — you cannot `allow` a group and then `warn` an individual lint from it. The group-level `allow` takes precedence. Always enable groups first, then suppress specific lints.
|
|
198
|
+
|
|
199
|
+
### Interaction with `#[must_use]`
|
|
200
|
+
|
|
201
|
+
Clippy's `must_use_candidate` lint (pedantic) suggests adding `#[must_use]` to functions that return values. This interacts with other lints:
|
|
202
|
+
|
|
203
|
+
- Once `#[must_use]` is added, callers who ignore the return value get `unused_must_use` (a rustc warning, not Clippy)
|
|
204
|
+
- `let _ = foo()` silences `unused_must_use` but triggers `let_underscore_must_use` (Clippy restriction)
|
|
205
|
+
- `drop(foo())` silences both but may trigger `drop_non_drop` if the type doesn't implement `Drop`
|
|
206
|
+
|
|
207
|
+
Recommendation: Enable `must_use_candidate` globally, but only `#[deny(unused_must_use)]` in modules where ignoring results is dangerous (I/O, error handling).
|
|
208
|
+
|
|
209
|
+
### Unsafe code linting
|
|
210
|
+
|
|
211
|
+
Clippy provides several lints for `unsafe` code, but they have limitations:
|
|
212
|
+
|
|
213
|
+
- `undocumented_unsafe_blocks` (restriction) — requires a `// SAFETY:` comment above every `unsafe` block. Enable this project-wide.
|
|
214
|
+
- `unsafe_derive_deserialize` (pedantic) — warns when `Deserialize` is derived on types with unsafe invariants
|
|
215
|
+
- `multiple_unsafe_ops_per_block` (restriction) — each `unsafe` block should contain exactly one unsafe operation for precise `// SAFETY:` documentation
|
|
216
|
+
|
|
217
|
+
These lints do NOT verify that safety invariants are actually upheld — they only enforce documentation conventions. Use `cargo miri test` and `#[cfg(miri)]` for runtime verification of unsafe code.
|
|
218
|
+
|
|
219
|
+
### MSRV-aware lints
|
|
220
|
+
|
|
221
|
+
Clippy respects the `msrv` field in `clippy.toml`. Some lints suggest replacements that require newer Rust versions. If you set `msrv = "1.65.0"`, Clippy won't suggest `let-else` (stabilized in 1.65) but will suppress suggestions for features from 1.66+. Always set this to match your `rust-version` in `Cargo.toml`.
|
|
222
|
+
|
|
223
|
+
## Mapping PR Feedback to Lint Strategy
|
|
224
|
+
|
|
225
|
+
| PR comment pattern | Best approach |
|
|
226
|
+
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
227
|
+
| "Don't use `.unwrap()`" | `enforce("clippy/unwrap_used", "Use expect() with context or propagate with ?.")` — pedantic, enable project-wide |
|
|
228
|
+
| "Don't call this function" | `clippy.toml` `disallowed-methods` — zero custom code, includes custom error message |
|
|
229
|
+
| "Don't use this type" | `clippy.toml` `disallowed-types` — same approach, works for `HashMap`, `Mutex`, etc. |
|
|
230
|
+
| "Don't use `println!`" | `enforce("clippy/print_stdout", "Use tracing macros.")` — restriction lint, opt-in |
|
|
231
|
+
| "Add error context" | Not Clippy — use `#[deny(clippy::unwrap_used)]` + guidance for `anyhow`/`thiserror` patterns |
|
|
232
|
+
| "This clone is unnecessary" | `enforce("clippy/redundant_clone", "Remove the unnecessary clone.")` — already in complexity group |
|
|
233
|
+
| "Use `Self` in impl blocks" | `enforce("clippy/use_self", "Use Self instead of repeating the type name.")` — nursery lint |
|
|
234
|
+
| "Document safety invariants" | `enforce("clippy/undocumented_unsafe_blocks", "Add a // SAFETY: comment.")` — restriction lint |
|
|
235
|
+
| "Too many function parameters" | `enforce("clippy/too_many_arguments", "Refactor into a config struct.")` — already in complexity group |
|
|
236
|
+
| "Wildcard deps in Cargo.toml" | `enforce("clippy/wildcard_dependencies", "Pin dependency versions.")` — cargo group |
|
|
237
|
+
| "Every public fn needs error docs" | `enforce("clippy/missing_errors_doc", "Document the errors this function can return.")` — pedantic |
|
|
238
|
+
| "Avoid `as` casts" | `enforce("clippy/cast_possible_truncation", "Use try_from() or explicit checked conversion.")` — pedantic |
|
|
239
|
+
| "Don't leave `todo!()` in code" | `enforce("clippy/todo", "Replace todo!() with an implementation or file an issue.")` — restriction lint |
|
|
240
|
+
| "Use the builder pattern" | Not Clippy — use vigiles `guidance()` for architectural patterns |
|
|
241
|
+
| "Enforce import boundaries between modules" | Not Clippy — use `cargo-deny` for crate-level boundaries or dylint for module-level enforcement |
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# ESLint — Reference
|
|
2
|
+
|
|
3
|
+
Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom rules).
|
|
4
|
+
|
|
5
|
+
## Check Existing Plugins First
|
|
6
|
+
|
|
7
|
+
Before writing a custom rule, search these plugins — the pattern may already be covered:
|
|
8
|
+
|
|
9
|
+
| Plugin | Scope | Key rules to know |
|
|
10
|
+
| --------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `@typescript-eslint` | Type-aware TS rules | `no-floating-promises`, `no-misused-promises`, `strict-boolean-expressions`, `naming-convention`, `no-restricted-imports` (type-aware variant) |
|
|
12
|
+
| `eslint-plugin-import-x` | Import hygiene (flat-config native) | `no-cycle`, `no-unresolved`, `no-extraneous-dependencies`, `order`, `no-internal-modules` |
|
|
13
|
+
| `eslint-plugin-boundaries` | Module boundary enforcement | `element-types`, `entry-point`, `external` — define allowed dependency directions between architectural layers |
|
|
14
|
+
| `eslint-plugin-sonarjs` | Code smells & complexity | `cognitive-complexity`, `no-duplicate-string`, `no-identical-functions`, `no-nested-conditional` |
|
|
15
|
+
| `eslint-plugin-unicorn` | Modern JS idioms | `prefer-node-protocol`, `no-array-for-each`, `prefer-top-level-await`, `filename-case` |
|
|
16
|
+
| `eslint-plugin-react` / `eslint-plugin-react-hooks` | React patterns | `rules-of-hooks`, `exhaustive-deps`, `no-unstable-nested-components`, `jsx-no-leaked-render` |
|
|
17
|
+
| `eslint-plugin-jsx-a11y` | Accessibility | `alt-text`, `anchor-is-valid`, `no-autofocus`, `click-events-have-key-events` |
|
|
18
|
+
| `eslint-plugin-n` | Node.js-specific | `no-sync`, `no-process-exit`, `prefer-global/buffer`, `no-unsupported-features` |
|
|
19
|
+
| `eslint-plugin-regexp` | Regex safety | `no-super-linear-backtracking`, `no-misleading-unicode-character`, `prefer-quantifier` |
|
|
20
|
+
| `eslint-plugin-security` | Security anti-patterns | `detect-object-injection`, `detect-non-literal-regexp`, `detect-child-process` |
|
|
21
|
+
| `@eslint/json` / `@eslint/markdown` | Non-JS file linting | Lint JSON and markdown files with ESLint flat config — useful for config validation |
|
|
22
|
+
|
|
23
|
+
**Tip:** `no-restricted-syntax` with an AST selector handles many one-off patterns without a custom rule. Try it first.
|
|
24
|
+
|
|
25
|
+
## Rule Anatomy
|
|
26
|
+
|
|
27
|
+
Every ESLint rule is an object with `meta` and `create`:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
31
|
+
module.exports = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: "suggestion", // "problem" | "suggestion" | "layout"
|
|
34
|
+
docs: {
|
|
35
|
+
description: "Disallow direct database calls outside the data layer",
|
|
36
|
+
recommended: false,
|
|
37
|
+
},
|
|
38
|
+
messages: {
|
|
39
|
+
noDirectDb:
|
|
40
|
+
"Use the repository pattern — import from 'src/data/' instead of calling {{ name }} directly.",
|
|
41
|
+
},
|
|
42
|
+
fixable: null, // "code" if you provide a fixer
|
|
43
|
+
schema: [], // JSON Schema for rule options
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
create(context) {
|
|
47
|
+
return {
|
|
48
|
+
// visitor methods keyed by AST node type or selector
|
|
49
|
+
CallExpression(node) {
|
|
50
|
+
if (isDirectDbCall(node)) {
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: "noDirectDb",
|
|
54
|
+
data: { name: node.callee.name },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### meta.type — choose correctly
|
|
64
|
+
|
|
65
|
+
| Type | When to use | Example |
|
|
66
|
+
| -------------- | ----------------------------------------- | ------------------------------------------------- |
|
|
67
|
+
| `"problem"` | Code that is/will be broken | Missing `await` on a promise |
|
|
68
|
+
| `"suggestion"` | Code that works but violates a convention | Using `console.log` instead of the project logger |
|
|
69
|
+
| `"layout"` | Whitespace/formatting only | Rarely used — Prettier handles this |
|
|
70
|
+
|
|
71
|
+
### meta.messages — be prescriptive
|
|
72
|
+
|
|
73
|
+
Bad: `"Don't do this."` — tells the developer nothing.
|
|
74
|
+
Good: `"Use the {{ replacement }} wrapper instead of {{ original }}."` — tells them exactly what to write.
|
|
75
|
+
|
|
76
|
+
Always include **what to do** in the message, not just what's wrong.
|
|
77
|
+
|
|
78
|
+
## AST Node Types — Cheat Sheet
|
|
79
|
+
|
|
80
|
+
Common patterns and the AST nodes that catch them:
|
|
81
|
+
|
|
82
|
+
| You want to detect | AST node / selector | Notes |
|
|
83
|
+
| -------------------------- | ------------------------------------------------------------------------------- | ----------------------------------- |
|
|
84
|
+
| Function call `foo()` | `CallExpression[callee.name="foo"]` | Selector form — no code needed |
|
|
85
|
+
| Method call `obj.method()` | `CallExpression[callee.type="MemberExpression"][callee.property.name="method"]` | |
|
|
86
|
+
| Import `from "module"` | `ImportDeclaration[source.value="module"]` | Static imports only |
|
|
87
|
+
| `require("module")` | `CallExpression[callee.name="require"][arguments.0.value="module"]` | CJS |
|
|
88
|
+
| Variable named X | `VariableDeclarator[id.name="X"]` | |
|
|
89
|
+
| Class with decorator | `ClassDeclaration > Decorator` | Experimental — needs parser support |
|
|
90
|
+
| JSX element `<Foo>` | `JSXOpeningElement[name.name="Foo"]` | Needs JSX parser |
|
|
91
|
+
| Template literal | `TemplateLiteral` | Includes tagged templates |
|
|
92
|
+
| `throw` statement | `ThrowStatement` | |
|
|
93
|
+
| `new Promise()` | `NewExpression[callee.name="Promise"]` | |
|
|
94
|
+
|
|
95
|
+
**Pro tip:** Use [AST Explorer](https://astexplorer.net) (parser: `@typescript-eslint/parser`, transform: `ESLint v4`) to prototype visitor logic interactively.
|
|
96
|
+
|
|
97
|
+
## ESTree Selectors
|
|
98
|
+
|
|
99
|
+
ESLint supports CSS-like AST selectors. These replace boilerplate visitor code:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
create(context) {
|
|
103
|
+
return {
|
|
104
|
+
// Match: import from any path containing "internal"
|
|
105
|
+
'ImportDeclaration[source.value=/internal/]'(node) {
|
|
106
|
+
context.report({ node, messageId: "noInternalImport" });
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Match: await inside a loop body
|
|
110
|
+
'ForStatement > BlockStatement AwaitExpression'(node) {
|
|
111
|
+
context.report({ node, messageId: "noAwaitInLoop" });
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Match: console.log, console.warn, console.error
|
|
115
|
+
'MemberExpression[object.name="console"]'(node) {
|
|
116
|
+
context.report({ node: node.parent, messageId: "useLogger" });
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Selector syntax:
|
|
123
|
+
|
|
124
|
+
- `A > B` — B is a direct child of A
|
|
125
|
+
- `A B` — B is a descendant of A
|
|
126
|
+
- `A[attr="val"]` — attribute match (string equality)
|
|
127
|
+
- `A[attr=/regex/]` — attribute match (regex)
|
|
128
|
+
- `A:exit` — fires when **leaving** the node (post-order)
|
|
129
|
+
- `:not(A)` — negation
|
|
130
|
+
- `A + B` — B immediately follows A (sibling)
|
|
131
|
+
|
|
132
|
+
## Accessing TypeScript Type Information
|
|
133
|
+
|
|
134
|
+
For type-aware rules, use `@typescript-eslint/utils`:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
138
|
+
|
|
139
|
+
const createRule = ESLintUtils.RuleCreator(
|
|
140
|
+
(name) => `https://example.com/rules/${name}`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
export default createRule({
|
|
144
|
+
name: "no-unhandled-promise",
|
|
145
|
+
meta: {
|
|
146
|
+
type: "problem",
|
|
147
|
+
docs: { description: "Require promises to be handled" },
|
|
148
|
+
messages: { unhandled: "This promise must be awaited or returned." },
|
|
149
|
+
schema: [],
|
|
150
|
+
},
|
|
151
|
+
defaultOptions: [],
|
|
152
|
+
create(context) {
|
|
153
|
+
const services = ESLintUtils.getParserServices(context);
|
|
154
|
+
const checker = services.program.getTypeChecker();
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
ExpressionStatement(node) {
|
|
158
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(node.expression);
|
|
159
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
160
|
+
if (isPromiseLike(checker, type)) {
|
|
161
|
+
context.report({ node, messageId: "unhandled" });
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Requirements for type-aware rules:**
|
|
170
|
+
|
|
171
|
+
1. The ESLint config must use `@typescript-eslint/parser` with `projectService: true` (or legacy `project` option)
|
|
172
|
+
2. The rule must live in a plugin (not standalone `eslint-rules/` dir) for parser services to work reliably
|
|
173
|
+
3. Type-aware rules are slower — only use when you genuinely need type information
|
|
174
|
+
|
|
175
|
+
## Auto-Fix and Suggestions
|
|
176
|
+
|
|
177
|
+
### `fix` — automatic, silent
|
|
178
|
+
|
|
179
|
+
Use for safe, semantics-preserving changes:
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
context.report({
|
|
183
|
+
node,
|
|
184
|
+
messageId: "preferNodeProtocol",
|
|
185
|
+
fix(fixer) {
|
|
186
|
+
return fixer.replaceText(node.source, `"node:${module}"`);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Safety rules for `fix`:
|
|
192
|
+
|
|
193
|
+
- **Never change runtime behavior.** If the fix might break code, use `suggest` instead.
|
|
194
|
+
- **Never remove code** unless it's provably dead.
|
|
195
|
+
- One `fix` per report. Multiple changes go in a single fix call using an array.
|
|
196
|
+
- Test your fix: `RuleTester` will verify the fix output matches `output` in your test case.
|
|
197
|
+
|
|
198
|
+
### `suggest` — manual, user-picks
|
|
199
|
+
|
|
200
|
+
Use for changes that might alter semantics:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
context.report({
|
|
204
|
+
node,
|
|
205
|
+
messageId: "noDirectDbCall",
|
|
206
|
+
suggest: [
|
|
207
|
+
{
|
|
208
|
+
messageId: "wrapWithRepo",
|
|
209
|
+
fix(fixer) {
|
|
210
|
+
return fixer.replaceText(
|
|
211
|
+
node,
|
|
212
|
+
`repository.${node.callee.property.name}(${argsText})`,
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Testing with RuleTester
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
import { RuleTester } from "eslint";
|
|
224
|
+
import rule from "./no-direct-db.js";
|
|
225
|
+
|
|
226
|
+
const tester = new RuleTester({
|
|
227
|
+
languageOptions: {
|
|
228
|
+
ecmaVersion: 2024,
|
|
229
|
+
sourceType: "module",
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
tester.run("no-direct-db", rule, {
|
|
234
|
+
valid: [
|
|
235
|
+
// Always include: correctly following the rule
|
|
236
|
+
`import { findUser } from "src/data/users";`,
|
|
237
|
+
// Edge case: dynamic import (intentionally allowed)
|
|
238
|
+
`const mod = await import("pg");`,
|
|
239
|
+
],
|
|
240
|
+
invalid: [
|
|
241
|
+
{
|
|
242
|
+
code: `import { Pool } from "pg";`,
|
|
243
|
+
errors: [{ messageId: "noDirectDb" }],
|
|
244
|
+
},
|
|
245
|
+
// If the rule has a fixer, test the output
|
|
246
|
+
{
|
|
247
|
+
code: `const fs = require("fs");`,
|
|
248
|
+
output: `const fs = require("node:fs");`,
|
|
249
|
+
errors: [{ messageId: "preferNodeProtocol" }],
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Testing best practices:**
|
|
256
|
+
|
|
257
|
+
1. **Test the valid cases first.** If valid cases fail, your rule is too aggressive.
|
|
258
|
+
2. **Include edge cases.** Dynamic imports, re-exports, type-only imports, destructured requires.
|
|
259
|
+
3. **Test error placement.** Use `errors: [{ messageId, line, column }]` to verify the squiggly appears on the right token.
|
|
260
|
+
4. **Test with options.** If your rule has configurable options, test each combination.
|
|
261
|
+
5. **For TypeScript rules**, use `@typescript-eslint/rule-tester` and provide `parser` + `parserOptions` with a real `tsconfig.json`.
|
|
262
|
+
|
|
263
|
+
## Flat Config Registration
|
|
264
|
+
|
|
265
|
+
ESLint 9+ uses flat config. Register custom rules via a local plugin object:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// eslint.config.js
|
|
269
|
+
import noDirectDb from "./eslint-rules/no-direct-db.js";
|
|
270
|
+
|
|
271
|
+
export default [
|
|
272
|
+
{
|
|
273
|
+
plugins: {
|
|
274
|
+
// Namespace all custom rules under "local"
|
|
275
|
+
local: {
|
|
276
|
+
rules: {
|
|
277
|
+
"no-direct-db": noDirectDb,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
rules: {
|
|
282
|
+
"local/no-direct-db": "error",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Important:** The config key in `rules` must be `<plugin-namespace>/<rule-name>`. The `enforce()` reference in vigiles matches this: `enforce("eslint/local/no-direct-db", "...")`.
|
|
289
|
+
|
|
290
|
+
If the project still uses legacy `.eslintrc`, the rule goes in `rulesDir` instead — but flat config is the path forward.
|
|
291
|
+
|
|
292
|
+
## Edge Cases and Gotchas
|
|
293
|
+
|
|
294
|
+
### TypeScript AST differences
|
|
295
|
+
|
|
296
|
+
TypeScript adds AST node types that ESTree doesn't have. Common ones to watch for:
|
|
297
|
+
|
|
298
|
+
- `TSAsExpression` — `x as string` wraps the expression; your visitor might miss the inner node
|
|
299
|
+
- `TSNonNullExpression` — `x!` adds a wrapper node
|
|
300
|
+
- `TSTypeAnnotation` — `: string` on parameters creates child nodes that break naive `node.params.length` checks
|
|
301
|
+
- `TSImportEqualsDeclaration` — `import x = require("y")` is NOT an `ImportDeclaration`
|
|
302
|
+
- `TSEnumDeclaration` — enums look like variable declarations but aren't
|
|
303
|
+
|
|
304
|
+
**Rule of thumb:** If your rule targets function parameters, imports, or expressions, test it with TypeScript annotations and assertions.
|
|
305
|
+
|
|
306
|
+
### `no-restricted-syntax` — the 80% solution
|
|
307
|
+
|
|
308
|
+
Before writing a custom rule, check if `no-restricted-syntax` covers it:
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
rules: {
|
|
312
|
+
"no-restricted-syntax": ["error",
|
|
313
|
+
{
|
|
314
|
+
selector: 'CallExpression[callee.object.name="console"]',
|
|
315
|
+
message: "Use the project logger from 'src/lib/logger'.",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
selector: 'ImportDeclaration[source.value="moment"]',
|
|
319
|
+
message: "Use dayjs — we're migrating off moment.",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
This works for any pattern you can express as an AST selector. You only need a custom rule when you need:
|
|
326
|
+
|
|
327
|
+
- Type information
|
|
328
|
+
- Multi-node analysis (e.g., "if X is imported, then Y must also be imported")
|
|
329
|
+
- Auto-fix or suggestions
|
|
330
|
+
- Configurable options via schema
|
|
331
|
+
|
|
332
|
+
### `no-restricted-imports` — the import-specific shortcut
|
|
333
|
+
|
|
334
|
+
For import bans specifically, `no-restricted-imports` is more ergonomic:
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
rules: {
|
|
338
|
+
"no-restricted-imports": ["error", {
|
|
339
|
+
paths: [
|
|
340
|
+
{ name: "lodash", message: "Import specific lodash functions: lodash/get" },
|
|
341
|
+
{ name: "moment", message: "Use dayjs instead." },
|
|
342
|
+
],
|
|
343
|
+
patterns: [
|
|
344
|
+
{ group: ["src/internal/*"], message: "Use the public API from src/index.ts" },
|
|
345
|
+
],
|
|
346
|
+
}],
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Auto-fix conflicts
|
|
351
|
+
|
|
352
|
+
If two rules try to fix the same range of code, ESLint drops both fixes. Avoid this by:
|
|
353
|
+
|
|
354
|
+
- Keeping fix ranges tight (fix only the exact tokens, not the whole statement)
|
|
355
|
+
- Using `suggest` instead of `fix` when the rule might overlap with Prettier or another fixer
|
|
356
|
+
- Testing with `--fix-dry-run` before enabling in CI
|
|
357
|
+
|
|
358
|
+
### Performance
|
|
359
|
+
|
|
360
|
+
- Avoid `Program:exit` handlers that walk the entire AST — use specific node visitors instead.
|
|
361
|
+
- Selector matching has overhead. For hot paths, a manual `CallExpression` check is faster than a complex selector.
|
|
362
|
+
- Type-aware rules add ~2-5x overhead because they invoke the TypeScript compiler. Group them in a separate config block with `files: ["src/**/*.ts"]` so they only run on TS files.
|
|
363
|
+
- If your custom rules directory has more than ~10 rules, bundle them into a proper plugin package for better caching.
|
|
364
|
+
|
|
365
|
+
### Monorepo considerations
|
|
366
|
+
|
|
367
|
+
- ESLint flat config is resolved from `cwd`, not from the file being linted. In a monorepo, set `cwd` to the package root, not the workspace root.
|
|
368
|
+
- `vigiles generate-types` discovers ESLint rules using `calculateConfigForFile("dummy.js")` from the project `basePath`. If your monorepo has different configs per package, run `generate-types` from each package root.
|
|
369
|
+
- Plugin rules must be installed in the `node_modules` visible from the config file's location. Hoisted deps in a monorepo can cause "plugin not found" errors — install them in the package's own `devDependencies`.
|
|
370
|
+
|
|
371
|
+
## Mapping PR Feedback to Rule Strategy
|
|
372
|
+
|
|
373
|
+
| PR comment pattern | Best approach |
|
|
374
|
+
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
375
|
+
| "Don't import from X" | `no-restricted-imports` — zero custom code |
|
|
376
|
+
| "Don't call X()" | `no-restricted-syntax` with a `CallExpression` selector |
|
|
377
|
+
| "Use X instead of Y" | `no-restricted-syntax` if detection is simple; custom rule with `fix` if you want auto-replacement |
|
|
378
|
+
| "Every file must have X" | Not ESLint — use vigiles `check(every(...).has(...))` |
|
|
379
|
+
| "This promise isn't awaited" | `@typescript-eslint/no-floating-promises` — already exists |
|
|
380
|
+
| "Use our custom hook" | Custom rule checking `CallExpression` against an allow-list |
|
|
381
|
+
| "Wrong naming convention" | `@typescript-eslint/naming-convention` — highly configurable, rarely needs custom code |
|
|
382
|
+
| "Don't use `any`" | `@typescript-eslint/no-explicit-any` — already exists |
|
|
383
|
+
| "Wrap API calls with error handler" | Custom rule: detect unwrapped `fetch`/`axios` calls, `suggest` the wrapper |
|
|
384
|
+
| "Don't mutate state directly" | `eslint-plugin-react` `no-direct-mutation-state` or custom rule checking assignment targets |
|