vigiles 2.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/settings.json +46 -0
- package/.claude/settings.local.json +8 -0
- package/.claude-plugin/hooks/post-edit.sh +34 -0
- package/.claude-plugin/hooks/pre-edit.sh +40 -0
- package/.claude-plugin/hooks/session-start.sh +38 -0
- package/.claude-plugin/marketplace.json +14 -0
- package/.claude-plugin/plugin.json +47 -0
- package/.github/workflows/ci.yml +81 -0
- package/.prettierignore +1 -0
- package/.vigiles/generated.d.ts +205 -0
- package/CLAUDE.md +95 -0
- package/CLAUDE.md.spec.ts +142 -0
- package/CONTRIBUTING.md +121 -0
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/action.yml +25 -0
- package/dist/action.d.ts +7 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +180 -0
- package/dist/action.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1267 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +650 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/compile.d.ts +101 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +503 -0
- package/dist/compile.js.map +1 -0
- package/dist/evolve.d.ts +132 -0
- package/dist/evolve.d.ts.map +1 -0
- package/dist/evolve.js +599 -0
- package/dist/evolve.js.map +1 -0
- package/dist/freshness.d.ts +67 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +244 -0
- package/dist/freshness.js.map +1 -0
- package/dist/freshness.test.d.ts +2 -0
- package/dist/freshness.test.d.ts.map +1 -0
- package/dist/freshness.test.js +356 -0
- package/dist/freshness.test.js.map +1 -0
- package/dist/generate-types.d.ts +34 -0
- package/dist/generate-types.d.ts.map +1 -0
- package/dist/generate-types.js +381 -0
- package/dist/generate-types.js.map +1 -0
- package/dist/inline.d.ts +58 -0
- package/dist/inline.d.ts.map +1 -0
- package/dist/inline.js +142 -0
- package/dist/inline.js.map +1 -0
- package/dist/inline.test.d.ts +5 -0
- package/dist/inline.test.d.ts.map +1 -0
- package/dist/inline.test.js +152 -0
- package/dist/inline.test.js.map +1 -0
- package/dist/linters.d.ts +38 -0
- package/dist/linters.d.ts.map +1 -0
- package/dist/linters.js +588 -0
- package/dist/linters.js.map +1 -0
- package/dist/proofs.d.ts +272 -0
- package/dist/proofs.d.ts.map +1 -0
- package/dist/proofs.js +622 -0
- package/dist/proofs.js.map +1 -0
- package/dist/proofs.test.d.ts +9 -0
- package/dist/proofs.test.d.ts.map +1 -0
- package/dist/proofs.test.js +952 -0
- package/dist/proofs.test.js.map +1 -0
- package/dist/spec.d.ts +258 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +113 -0
- package/dist/spec.js.map +1 -0
- package/dist/spec.test.d.ts +2 -0
- package/dist/spec.test.d.ts.map +1 -0
- package/dist/spec.test.js +1222 -0
- package/dist/spec.test.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +286 -0
- package/dist/validate.js.map +1 -0
- package/dist/validate.test.d.ts +2 -0
- package/dist/validate.test.d.ts.map +1 -0
- package/dist/validate.test.js +531 -0
- package/dist/validate.test.js.map +1 -0
- package/docs/agent-setup.md +85 -0
- package/docs/agent-workflows.md +103 -0
- package/docs/comparison.md +71 -0
- package/docs/freshness.md +124 -0
- package/docs/inline-mode.md +119 -0
- package/docs/linter-support.md +166 -0
- package/docs/spec-format.md +194 -0
- package/eslint.config.mjs +79 -0
- package/examples/CLAUDE.md +54 -0
- package/examples/CLAUDE.md.spec.ts +65 -0
- package/examples/SKILL.md +50 -0
- package/examples/SKILL.md.spec.ts +57 -0
- package/fixtures/example-project/CLAUDE.md +11 -0
- package/fixtures/example-project/package.json +9 -0
- package/fixtures/example-project/src/index.ts +3 -0
- package/fixtures/example-project/src/utils.test.ts +2 -0
- package/fixtures/example-project/src/utils.ts +3 -0
- package/logo.png +0 -0
- package/package.json +42 -0
- package/research/adoption-strategy.md +111 -0
- package/research/agent-integration.md +145 -0
- package/research/ai-code-quality.md +197 -0
- package/research/code-search-for-agents.md +313 -0
- package/research/competitive-landscape.md +163 -0
- package/research/doc-freshness.md +516 -0
- package/research/executable-specs.md +368 -0
- package/research/feature-ideas.md +464 -0
- package/research/formal-proofs-for-agents.md +338 -0
- package/research/fp-for-agent-harness.md +150 -0
- package/research/fp-for-deterministic-ai.md +131 -0
- package/research/self-evolving-specs.md +298 -0
- package/schemas/claude-md-strict.yml +18 -0
- package/schemas/claude-md.yml +6 -0
- package/schemas/skill-strict.yml +12 -0
- package/schemas/skill.yml +5 -0
- 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 +102 -0
- package/skills/generate-rule/SKILL.md +90 -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 +124 -0
- package/skills/pr-to-lint-rule/SKILL.md +97 -0
- package/skills/strengthen/SKILL.md +168 -0
- package/src/action.ts +214 -0
- package/src/cli.test.ts +914 -0
- package/src/cli.ts +1631 -0
- package/src/compile.ts +691 -0
- package/src/evolve.ts +781 -0
- package/src/freshness.test.ts +449 -0
- package/src/freshness.ts +299 -0
- package/src/generate-types.ts +448 -0
- package/src/inline.test.ts +206 -0
- package/src/inline.ts +164 -0
- package/src/linters.ts +739 -0
- package/src/proofs.test.ts +1314 -0
- package/src/proofs.ts +849 -0
- package/src/spec.test.ts +1471 -0
- package/src/spec.ts +427 -0
- package/src/types.ts +117 -0
- package/src/validate.test.ts +701 -0
- package/src/validate.ts +381 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# RuboCop — Reference
|
|
2
|
+
|
|
3
|
+
Shared linter reference for vigiles skills. Used by `strengthen` (find existing rules) and `pr-to-lint-rule` (write custom cops).
|
|
4
|
+
|
|
5
|
+
## Check Existing Gems First
|
|
6
|
+
|
|
7
|
+
Before writing a custom cop, search these gems — the pattern may already be covered:
|
|
8
|
+
|
|
9
|
+
| Gem | Scope | Key cops to know |
|
|
10
|
+
| --------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `rubocop` (core) | Style, Lint, Metrics, Naming, Security | `Style/FrozenStringLiteralComment`, `Lint/UnusedMethodArgument`, `Metrics/MethodLength`, `Naming/MethodName`, `Security/Eval` |
|
|
12
|
+
| `rubocop-rails` | Rails conventions | `Rails/HttpPositionalArguments`, `Rails/SkipsModelValidations`, `Rails/Output`, `Rails/HasAndBelongsToMany`, `Rails/DynamicFindBy` |
|
|
13
|
+
| `rubocop-rspec` | RSpec test patterns | `RSpec/ExampleLength`, `RSpec/MultipleExpectations`, `RSpec/NestedGroups`, `RSpec/LetSetup`, `RSpec/MessageSpies` |
|
|
14
|
+
| `rubocop-minitest` | Minitest patterns | `Minitest/AssertEmptyLiteral`, `Minitest/RefuteNil`, `Minitest/AssertInDelta` |
|
|
15
|
+
| `rubocop-performance` | Performance anti-patterns | `Performance/StringReplacement`, `Performance/Detect`, `Performance/Count`, `Performance/FlatMap`, `Performance/CaseWhenSplat` |
|
|
16
|
+
| `rubocop-rake` | Rakefile patterns | `Rake/Desc`, `Rake/DuplicateTask`, `Rake/MethodDefinitionInTask` |
|
|
17
|
+
| `rubocop-sorbet` | Sorbet type checking | `Sorbet/ForbidSuperclassConstLiteral`, `Sorbet/ValidSigil`, `Sorbet/SignatureBuildOrder` |
|
|
18
|
+
| `rubocop-graphql` | GraphQL conventions | `GraphQL/FieldDescription`, `GraphQL/ObjectDescription`, `GraphQL/ArgumentDescription` |
|
|
19
|
+
| `rubocop-factory_bot` | FactoryBot patterns | `FactoryBot/ConsistentParenthesesStyle`, `FactoryBot/CreateList`, `FactoryBot/SyntaxMethods` |
|
|
20
|
+
| `rubocop-capybara` | Capybara test patterns | `Capybara/CurrentPathExpectation`, `Capybara/VisibilityMatcher`, `Capybara/SpecificMatcher` |
|
|
21
|
+
|
|
22
|
+
**Tip:** Many patterns can be handled by configuring existing cops rather than writing new ones. Check if an existing cop has configurable `AllowedMethods`, `AllowedPatterns`, or `Include`/`Exclude` options first.
|
|
23
|
+
|
|
24
|
+
## Cop Anatomy
|
|
25
|
+
|
|
26
|
+
Every RuboCop cop inherits from `RuboCop::Cop::Base`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# frozen_string_literal: true
|
|
30
|
+
|
|
31
|
+
module RuboCop
|
|
32
|
+
module Cop
|
|
33
|
+
module Custom
|
|
34
|
+
# Disallow direct database queries outside the repository layer.
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# # bad
|
|
38
|
+
# User.where(active: true)
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
# UserRepository.active_users
|
|
42
|
+
class NoDirectDbQuery < Base
|
|
43
|
+
MSG = 'Use the repository pattern — call `%<model>sRepository` instead of `%<model>s.%<method>s`.'
|
|
44
|
+
|
|
45
|
+
RESTRICT_ON_SEND = %i[where find find_by first last all count pluck].freeze
|
|
46
|
+
|
|
47
|
+
# @!method ar_model_query?(node)
|
|
48
|
+
def_node_matcher :ar_model_query?, <<~PATTERN
|
|
49
|
+
(send (const nil? _) {:where :find :find_by :first :last :all :count :pluck} ...)
|
|
50
|
+
PATTERN
|
|
51
|
+
|
|
52
|
+
def on_send(node)
|
|
53
|
+
return unless ar_model_query?(node)
|
|
54
|
+
|
|
55
|
+
model = node.receiver.short_name
|
|
56
|
+
add_offense(node, message: format(MSG, model: model, method: node.method_name))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Key concepts
|
|
65
|
+
|
|
66
|
+
| Concept | Purpose | Notes |
|
|
67
|
+
| ------------------ | --------------------------------------------- | ------------------------------------------------------------------- |
|
|
68
|
+
| `MSG` | Error message string | Use `format` with `%<name>s` for interpolation |
|
|
69
|
+
| `RESTRICT_ON_SEND` | Whitelist of method names | Performance optimization — cop only runs on matching `send` nodes |
|
|
70
|
+
| `def_node_matcher` | AST pattern matcher | DSL for matching Ruby AST patterns — replaces manual node traversal |
|
|
71
|
+
| `def_node_search` | Like `def_node_matcher` but finds all matches | Returns an enumerator of matching nodes |
|
|
72
|
+
| `add_offense` | Report a violation | Accepts `node`, optional `message:` and `severity:` |
|
|
73
|
+
|
|
74
|
+
### Cop departments
|
|
75
|
+
|
|
76
|
+
| Department | When to use |
|
|
77
|
+
| ---------- | ----------------------------------------------------- |
|
|
78
|
+
| `Lint` | Code that is/will be broken |
|
|
79
|
+
| `Style` | Code that works but violates a convention |
|
|
80
|
+
| `Metrics` | Complexity thresholds (method length, ABC size, etc.) |
|
|
81
|
+
| `Naming` | Naming conventions |
|
|
82
|
+
| `Security` | Security anti-patterns |
|
|
83
|
+
| `Custom` | Your project-specific cops |
|
|
84
|
+
|
|
85
|
+
## Node Pattern DSL — Cheat Sheet
|
|
86
|
+
|
|
87
|
+
RuboCop's node pattern DSL is the primary way to match AST nodes:
|
|
88
|
+
|
|
89
|
+
| You want to detect | Pattern | Notes |
|
|
90
|
+
| --------------------- | ----------------------------------- | --------------------------------------------- |
|
|
91
|
+
| Method call `foo` | `(send nil? :foo ...)` | `nil?` = no receiver |
|
|
92
|
+
| Method call `obj.foo` | `(send _ :foo ...)` | `_` = any receiver |
|
|
93
|
+
| Method call `Foo.bar` | `(send (const nil? :Foo) :bar ...)` | Constant receiver |
|
|
94
|
+
| String literal `"x"` | `(str "x")` | |
|
|
95
|
+
| Symbol `:x` | `(sym :x)` | |
|
|
96
|
+
| Block `foo { }` | `(block (send nil? :foo) ...)` | |
|
|
97
|
+
| Class definition | `(class (const nil? :Name) ...)` | |
|
|
98
|
+
| `if` conditional | `(if _ _ _)` | Three children: condition, if-body, else-body |
|
|
99
|
+
| Instance variable | `(ivar :@name)` | |
|
|
100
|
+
| Constant assignment | `(casgn nil? :NAME _)` | |
|
|
101
|
+
| `require "x"` | `(send nil? :require (str "x"))` | |
|
|
102
|
+
|
|
103
|
+
**Wildcards and captures:**
|
|
104
|
+
|
|
105
|
+
- `_` — matches any single node
|
|
106
|
+
- `...` — matches any number of nodes (rest)
|
|
107
|
+
- `$_` — capture a node into a variable
|
|
108
|
+
- `nil?` — matches nil (no receiver for top-level calls)
|
|
109
|
+
- `{:foo :bar}` — matches either `:foo` or `:bar`
|
|
110
|
+
- `(send _ {:puts :print :p} ...)` — matches puts/print/p on any receiver
|
|
111
|
+
|
|
112
|
+
**Pro tip:** Run `ruby-parse -e 'your_code_here'` to see the AST for any Ruby expression. The `parser` gem must be installed.
|
|
113
|
+
|
|
114
|
+
## Auto-Correct
|
|
115
|
+
|
|
116
|
+
### `extend AutoCorrector` — opt-in auto-fix
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class NoDirectDbQuery < Base
|
|
120
|
+
extend AutoCorrector
|
|
121
|
+
|
|
122
|
+
def on_send(node)
|
|
123
|
+
return unless ar_model_query?(node)
|
|
124
|
+
|
|
125
|
+
add_offense(node) do |corrector|
|
|
126
|
+
model = node.receiver.short_name
|
|
127
|
+
corrector.replace(node, "#{model}Repository.#{node.method_name}")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Available corrector methods:
|
|
134
|
+
|
|
135
|
+
| Method | What it does |
|
|
136
|
+
| ------------------------------------- | -------------------------------- |
|
|
137
|
+
| `corrector.replace(node, text)` | Replace node with text |
|
|
138
|
+
| `corrector.insert_before(node, text)` | Insert text before node |
|
|
139
|
+
| `corrector.insert_after(node, text)` | Insert text after node |
|
|
140
|
+
| `corrector.remove(node)` | Remove node |
|
|
141
|
+
| `corrector.wrap(node, before, after)` | Wrap node with before/after text |
|
|
142
|
+
|
|
143
|
+
Safety rules for auto-correct:
|
|
144
|
+
|
|
145
|
+
- **Mark unsafe corrections** with `def autocorrect_enabled? = false` or use the cop's `Safe` / `SafeAutoCorrect` metadata in `.rubocop.yml`
|
|
146
|
+
- **Never change runtime behavior.** If the fix might break code, don't auto-correct.
|
|
147
|
+
- RuboCop runs `rubocop -a` (safe only) vs `rubocop -A` (all including unsafe). Default to safe.
|
|
148
|
+
|
|
149
|
+
## Testing Cops
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# frozen_string_literal: true
|
|
153
|
+
|
|
154
|
+
require "rubocop"
|
|
155
|
+
require "rubocop/rspec/expect_offense"
|
|
156
|
+
|
|
157
|
+
RSpec.describe RuboCop::Cop::Custom::NoDirectDbQuery, :config do
|
|
158
|
+
# Tests that valid code produces no offenses
|
|
159
|
+
it "accepts repository pattern calls" do
|
|
160
|
+
expect_no_offenses(<<~RUBY)
|
|
161
|
+
UserRepository.active_users
|
|
162
|
+
RUBY
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Tests that invalid code produces the right offense
|
|
166
|
+
it "registers offense for direct AR query" do
|
|
167
|
+
expect_offense(<<~RUBY)
|
|
168
|
+
User.where(active: true)
|
|
169
|
+
^^^^^^^^^^^^^^^^^^^^^^^^ Use the repository pattern — call `UserRepository` instead of `User.where`.
|
|
170
|
+
RUBY
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Tests auto-correction
|
|
174
|
+
it "auto-corrects to repository call" do
|
|
175
|
+
expect_offense(<<~RUBY)
|
|
176
|
+
User.find(1)
|
|
177
|
+
^^^^^^^^^^^^ Use the repository pattern — call `UserRepository` instead of `User.find`.
|
|
178
|
+
RUBY
|
|
179
|
+
|
|
180
|
+
expect_correction(<<~RUBY)
|
|
181
|
+
UserRepository.find(1)
|
|
182
|
+
RUBY
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Testing best practices:**
|
|
188
|
+
|
|
189
|
+
1. **Use `expect_offense` with caret markers.** The carets (`^`) must align with the exact offense location.
|
|
190
|
+
2. **Test edge cases.** Dynamic receivers, safe navigation (`&.`), chained methods, blocks.
|
|
191
|
+
3. **Test configuration.** If your cop respects `AllowedMethods`, test with and without the config.
|
|
192
|
+
4. **Use `:config` shared context** to get a default configuration object.
|
|
193
|
+
|
|
194
|
+
## Registration
|
|
195
|
+
|
|
196
|
+
### In `.rubocop.yml`
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
require:
|
|
200
|
+
- ./lib/rubocop/cop/custom/no_direct_db_query
|
|
201
|
+
|
|
202
|
+
Custom/NoDirectDbQuery:
|
|
203
|
+
Enabled: true
|
|
204
|
+
Include:
|
|
205
|
+
- "app/**/*.rb"
|
|
206
|
+
Exclude:
|
|
207
|
+
- "app/repositories/**/*.rb"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### vigiles enforce() reference
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
enforce(
|
|
214
|
+
"rubocop/Custom/NoDirectDbQuery",
|
|
215
|
+
"Use repository pattern for all DB queries.",
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
vigiles checks `rubocop --show-cops Custom/NoDirectDbQuery` and verifies `Enabled: true`.
|
|
220
|
+
|
|
221
|
+
## Edge Cases and Gotchas
|
|
222
|
+
|
|
223
|
+
### Safe vs unsafe cops
|
|
224
|
+
|
|
225
|
+
RuboCop distinguishes `Safe: true` (default) from `Safe: false`. Unsafe cops can produce false positives. When writing a custom cop that can't guarantee correctness (e.g., it doesn't understand metaprogramming), set:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
Custom/MyCop:
|
|
229
|
+
Safe: false
|
|
230
|
+
SafeAutoCorrect: false
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Metaprogramming blind spots
|
|
234
|
+
|
|
235
|
+
RuboCop's AST parser doesn't understand:
|
|
236
|
+
|
|
237
|
+
- `method_missing` / `respond_to_missing?`
|
|
238
|
+
- `define_method` with dynamic names
|
|
239
|
+
- `send` / `public_send` with variable method names
|
|
240
|
+
- ActiveRecord dynamic finders (`find_by_name` generated at runtime)
|
|
241
|
+
|
|
242
|
+
If your convention targets code that's often generated via metaprogramming, expect false negatives.
|
|
243
|
+
|
|
244
|
+
### Node matcher vs manual traversal
|
|
245
|
+
|
|
246
|
+
Use `def_node_matcher` for simple patterns. Switch to manual `node.children`, `node.each_descendant`, etc. when you need:
|
|
247
|
+
|
|
248
|
+
- Cross-method analysis (e.g., "if method A exists, method B must also exist")
|
|
249
|
+
- State tracking across the file (e.g., counting total occurrences)
|
|
250
|
+
- Complex conditional logic that the pattern DSL can't express
|
|
251
|
+
|
|
252
|
+
### Performance
|
|
253
|
+
|
|
254
|
+
- Always use `RESTRICT_ON_SEND` when targeting method calls — it skips the cop entirely for non-matching methods.
|
|
255
|
+
- Avoid `on_send` without `RESTRICT_ON_SEND` on large codebases — it fires for every method call.
|
|
256
|
+
- `def_node_search` iterates descendants and can be slow on deeply nested ASTs. Prefer `def_node_matcher` on specific node types.
|
|
257
|
+
|
|
258
|
+
### Monorepo considerations
|
|
259
|
+
|
|
260
|
+
- RuboCop resolves `.rubocop.yml` from the file being linted, walking up directories. Each sub-project can have its own config.
|
|
261
|
+
- vigiles checks `rubocop --show-cops <CopName>` from the project `basePath`. In a monorepo, run from each gem/app root.
|
|
262
|
+
- Custom cops must be `require`-able from the config file's location. Use relative paths or bundle them in a gem.
|
|
263
|
+
|
|
264
|
+
## Mapping PR Feedback to Cop Strategy
|
|
265
|
+
|
|
266
|
+
| PR comment pattern | Best approach |
|
|
267
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
268
|
+
| "Don't call X directly" | Check if an existing cop covers it; otherwise custom cop with `RESTRICT_ON_SEND` |
|
|
269
|
+
| "Use our wrapper for Y" | Custom cop — detect raw calls, suggest wrapper |
|
|
270
|
+
| "Method too long" | `Metrics/MethodLength` — configure `Max:` threshold |
|
|
271
|
+
| "Missing frozen_string_literal" | `Style/FrozenStringLiteralComment` — already exists |
|
|
272
|
+
| "Don't use `puts` in production" | Custom cop or `Rails/Output` if using Rails |
|
|
273
|
+
| "Always add description to GraphQL fields" | `rubocop-graphql` `GraphQL/FieldDescription` |
|
|
274
|
+
| "Test files too complex" | `RSpec/ExampleLength`, `RSpec/NestedGroups` — configure thresholds |
|
|
275
|
+
| "Naming convention violated" | `Naming/*` cops — highly configurable |
|
|
276
|
+
| "Security issue: eval" | `Security/Eval` — already exists |
|
|
277
|
+
| "Don't skip validations" | `Rails/SkipsModelValidations` — already exists |
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Ruff — 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 Rules First
|
|
6
|
+
|
|
7
|
+
Ruff reimplements 800+ rules from flake8, pylint, isort, pyupgrade, and others. Before writing a custom rule, check if Ruff already covers it:
|
|
8
|
+
|
|
9
|
+
| Prefix | Source | Key rules |
|
|
10
|
+
| --------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `E` / `W` | pycodestyle | `E501` (line-too-long), `W291` (trailing-whitespace), `E711` (none-comparison) |
|
|
12
|
+
| `F` | pyflakes | `F401` (unused-import), `F841` (unused-variable), `F811` (redefined-unused-name) |
|
|
13
|
+
| `I` | isort | `I001` (unsorted-imports), `I002` (missing-required-import) |
|
|
14
|
+
| `N` | pep8-naming | `N801` (invalid-class-name), `N802` (invalid-function-name), `N806` (non-lowercase-variable) |
|
|
15
|
+
| `UP` | pyupgrade | `UP006` (non-pep585-annotation), `UP007` (non-pep604-annotation), `UP035` (deprecated-import) |
|
|
16
|
+
| `S` | flake8-bandit | `S101` (assert), `S105` (hardcoded-password-string), `S301` (suspicious-pickle-usage) |
|
|
17
|
+
| `B` | flake8-bugbear | `B006` (mutable-argument-default), `B007` (unused-loop-control-variable), `B905` (zip-without-explicit-strict) |
|
|
18
|
+
| `A` | flake8-builtins | `A001` (builtin-variable-shadowing), `A002` (builtin-argument-shadowing) |
|
|
19
|
+
| `C4` | flake8-comprehensions | `C400` (unnecessary-generator-list), `C401` (unnecessary-generator-set), `C408` (unnecessary-collection-call) |
|
|
20
|
+
| `T20` | flake8-print | `T201` (print), `T203` (pprint) |
|
|
21
|
+
| `SIM` | flake8-simplify | `SIM102` (collapsible-if), `SIM108` (if-else-block-instead-of-if-exp), `SIM110` (reimplemented-builtin) |
|
|
22
|
+
| `PL` | pylint | `PLC0414` (useless-import-alias), `PLE1205` (logging-too-many-args), `PLR0913` (too-many-arguments), `PLW0602` (global-variable-not-assigned) |
|
|
23
|
+
| `PTH` | flake8-use-pathlib | `PTH100` (os-path-abspath), `PTH118` (os-path-join), `PTH123` (builtin-open) |
|
|
24
|
+
| `RUF` | ruff-specific | `RUF001` (ambiguous-unicode-character), `RUF005` (collection-literal-concatenation), `RUF013` (implicit-optional) |
|
|
25
|
+
| `D` | pydocstyle | `D100` (undocumented-public-module), `D103` (undocumented-public-function), `D400` (first-line-should-end-with-period) |
|
|
26
|
+
| `ANN` | flake8-annotations | `ANN001` (missing-type-function-argument), `ANN201` (missing-return-type-public-function) |
|
|
27
|
+
| `ARG` | flake8-unused-arguments | `ARG001` (unused-function-argument), `ARG002` (unused-method-argument) |
|
|
28
|
+
| `ERA` | eradicate | `ERA001` (commented-out-code) |
|
|
29
|
+
| `TCH` | flake8-type-checking | `TCH001` (typing-only-first-party-import), `TCH002` (typing-only-third-party-import) |
|
|
30
|
+
| `FBT` | flake8-boolean-trap | `FBT001` (boolean-typed-positional-argument), `FBT002` (boolean-default-value-positional-argument) |
|
|
31
|
+
| `ICN` | flake8-import-conventions | `ICN001` (unconventional-import-alias) — e.g., `import numpy as np` |
|
|
32
|
+
| `PIE` | flake8-pie | `PIE790` (unnecessary-placeholder), `PIE804` (no-unnecessary-dict-kwargs) |
|
|
33
|
+
| `RSE` | flake8-raise | `RSE102` (unnecessary-paren-on-raise-exception) |
|
|
34
|
+
| `RET` | flake8-return | `RET501` (unnecessary-return-none), `RET504` (unnecessary-assign) |
|
|
35
|
+
| `TID` | flake8-tidy-imports | `TID252` (relative-imports) — ban relative imports |
|
|
36
|
+
| `PERF` | perflint | `PERF101` (unnecessary-list-cast), `PERF401` (manual-list-comprehension) |
|
|
37
|
+
| `FURB` | refurb | `FURB105` (print-empty-string), `FURB118` (reimplemented-operator) |
|
|
38
|
+
|
|
39
|
+
**Tip:** Run `ruff rule <CODE>` to see the full description of any rule. Run `ruff linter` to see all available rule groups.
|
|
40
|
+
|
|
41
|
+
## Rule Selection
|
|
42
|
+
|
|
43
|
+
Configure in `pyproject.toml`:
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = [
|
|
48
|
+
"E", "W", # pycodestyle
|
|
49
|
+
"F", # pyflakes
|
|
50
|
+
"I", # isort
|
|
51
|
+
"B", # flake8-bugbear
|
|
52
|
+
"S", # flake8-bandit
|
|
53
|
+
"UP", # pyupgrade
|
|
54
|
+
"SIM", # flake8-simplify
|
|
55
|
+
"T20", # flake8-print
|
|
56
|
+
"RUF", # ruff-specific
|
|
57
|
+
]
|
|
58
|
+
ignore = [
|
|
59
|
+
"E501", # line-too-long (handled by formatter)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.per-file-ignores]
|
|
63
|
+
"tests/**" = ["S101"] # allow assert in tests
|
|
64
|
+
"__init__.py" = ["F401"] # allow unused imports in __init__
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or in `ruff.toml` (same syntax without `[tool.ruff]` prefix):
|
|
68
|
+
|
|
69
|
+
```toml
|
|
70
|
+
[lint]
|
|
71
|
+
select = ["E", "F", "I", "B"]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Selecting individual rules vs groups
|
|
75
|
+
|
|
76
|
+
- `"F"` enables all pyflakes rules
|
|
77
|
+
- `"F401"` enables only `F401`
|
|
78
|
+
- `"ALL"` enables everything (noisy — use `ignore` to subtract)
|
|
79
|
+
- Use `extend-select` to add rules without overriding the default set
|
|
80
|
+
|
|
81
|
+
## Custom Rules
|
|
82
|
+
|
|
83
|
+
**Ruff does not support user-defined rules.** If no existing rule covers your pattern:
|
|
84
|
+
|
|
85
|
+
1. **Configure existing rules** — many rules accept options (e.g., `flake8-import-conventions` lets you set allowed aliases)
|
|
86
|
+
2. **Use `ruff.lint.flake8-import-conventions.aliases`** — for import alias enforcement
|
|
87
|
+
3. **Use `ruff.lint.flake8-tidy-imports.banned-api`** — for banning specific imports/modules
|
|
88
|
+
4. **Use ast-grep** — for arbitrary AST pattern matching, reference via `enforce()` with an ast-grep rule
|
|
89
|
+
5. **Write a Pylint checker** — if you need the full power of a custom rule with AST analysis, use Pylint (Ruff can coexist with Pylint in CI)
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
# Ban specific APIs without a custom rule
|
|
93
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
94
|
+
"os.system".msg = "Use subprocess.run instead."
|
|
95
|
+
"typing.Dict".msg = "Use dict instead (PEP 585)."
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Auto-Fix
|
|
99
|
+
|
|
100
|
+
Ruff provides auto-fix for many rules:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
ruff check --fix # apply safe fixes only
|
|
104
|
+
ruff check --fix --unsafe-fixes # include unsafe fixes
|
|
105
|
+
ruff check --fix-only # only fix, don't report remaining violations
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Safe vs unsafe fixes:**
|
|
109
|
+
|
|
110
|
+
- **Safe** — guaranteed to not change semantics (e.g., removing unused imports)
|
|
111
|
+
- **Unsafe** — may change semantics (e.g., `UP007` rewriting `Optional[X]` to `X | None` can break runtime type checking)
|
|
112
|
+
|
|
113
|
+
Check fixability per rule with `ruff rule <CODE>` — it shows whether the rule has a fix and if it's safe.
|
|
114
|
+
|
|
115
|
+
### vigiles enforce() reference
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
enforce("ruff/F401", "Remove unused imports.");
|
|
119
|
+
enforce("ruff/T201", "Use logging module instead of print.");
|
|
120
|
+
enforce("ruff/S101", "Don't use assert in production code.");
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
vigiles checks `ruff rule <CODE>` and verifies the rule exists, then parses `ruff check --show-settings` to confirm it's enabled.
|
|
124
|
+
|
|
125
|
+
## Edge Cases and Gotchas
|
|
126
|
+
|
|
127
|
+
### Ruff vs Pylint (PL prefix)
|
|
128
|
+
|
|
129
|
+
Ruff reimplements many Pylint rules with the `PL` prefix. The mapping is not 1:1 — some Pylint rules have no Ruff equivalent, and Ruff's implementations may differ in edge cases.
|
|
130
|
+
|
|
131
|
+
| Ruff code | Pylint equivalent | Notes |
|
|
132
|
+
| --------- | -------------------------------------- | ------------- |
|
|
133
|
+
| `PLR0913` | `R0913` (too-many-arguments) | Same behavior |
|
|
134
|
+
| `PLC0414` | `C0414` (useless-import-alias) | Same behavior |
|
|
135
|
+
| `PLW0602` | `W0602` (global-variable-not-assigned) | Same behavior |
|
|
136
|
+
| `PLE1205` | `E1205` (logging-too-many-args) | Same behavior |
|
|
137
|
+
|
|
138
|
+
If the project uses both Ruff and Pylint, disable Pylint rules that Ruff already covers to avoid duplicate reports.
|
|
139
|
+
|
|
140
|
+
### Preview rules
|
|
141
|
+
|
|
142
|
+
Some rules are behind `--preview` flag. These are not stable and may change between versions. Don't use `enforce()` on preview rules unless you pin the Ruff version.
|
|
143
|
+
|
|
144
|
+
```toml
|
|
145
|
+
[tool.ruff]
|
|
146
|
+
preview = true # enables preview rules
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Formatter vs linter conflicts
|
|
150
|
+
|
|
151
|
+
Ruff has both a linter (`ruff check`) and a formatter (`ruff format`). Some linter rules conflict with the formatter:
|
|
152
|
+
|
|
153
|
+
- `E501` (line-too-long) — the formatter handles line length; disable in the linter
|
|
154
|
+
- `W291`/`W292`/`W293` — whitespace rules handled by formatter
|
|
155
|
+
- `COM812` (missing-trailing-comma) — conflicts with formatter in some cases
|
|
156
|
+
|
|
157
|
+
The Ruff docs recommend: `ignore = ["E501", "W291", "W292", "W293"]` when using `ruff format`.
|
|
158
|
+
|
|
159
|
+
### Monorepo config inheritance
|
|
160
|
+
|
|
161
|
+
Ruff resolves config by walking up from the file being linted. In a monorepo:
|
|
162
|
+
|
|
163
|
+
- Each package can have its own `pyproject.toml` with `[tool.ruff]`
|
|
164
|
+
- A root config applies to all packages unless overridden
|
|
165
|
+
- Use `extend` to inherit from a shared config: `extend = "../../pyproject.toml"`
|
|
166
|
+
|
|
167
|
+
vigiles discovers Ruff config at `basePath` only. In a monorepo, run vigiles from each package root.
|
|
168
|
+
|
|
169
|
+
## Mapping PR Feedback to Rule Strategy
|
|
170
|
+
|
|
171
|
+
| PR comment pattern | Best approach |
|
|
172
|
+
| ----------------------------- | ------------------------------------------------------------------ |
|
|
173
|
+
| "Remove unused imports" | `ruff/F401` — already exists |
|
|
174
|
+
| "Don't use print()" | `ruff/T201` — already exists |
|
|
175
|
+
| "Sort your imports" | `ruff/I001` — already exists |
|
|
176
|
+
| "Use pathlib not os.path" | `ruff/PTH*` — enable the PTH group |
|
|
177
|
+
| "Don't use assert in prod" | `ruff/S101` — already exists, ignore in tests via per-file-ignores |
|
|
178
|
+
| "Remove commented-out code" | `ruff/ERA001` — already exists |
|
|
179
|
+
| "Add type annotations" | `ruff/ANN*` — enable the ANN group |
|
|
180
|
+
| "Use comprehensions" | `ruff/C4*` — enable the C4 group |
|
|
181
|
+
| "Don't shadow builtins" | `ruff/A001` / `A002` — already exists |
|
|
182
|
+
| "Simplify this if" | `ruff/SIM102` / `SIM108` — already exists |
|
|
183
|
+
| "Don't use mutable defaults" | `ruff/B006` — already exists |
|
|
184
|
+
| "Ban specific import" | `flake8-tidy-imports.banned-api` config — no custom rule needed |
|
|
185
|
+
| "Naming convention violated" | `ruff/N*` — enable the N group |
|
|
186
|
+
| "Don't use os.system" | `flake8-tidy-imports.banned-api` config |
|
|
187
|
+
| "Complex pattern not in Ruff" | Write a Pylint checker or use ast-grep |
|