rubocop-claude 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.txt +21 -0
- data/README.md +267 -0
- data/config/default.yml +202 -0
- data/exe/rubocop-claude +7 -0
- data/lib/rubocop/cop/claude/explicit_visibility.rb +139 -0
- data/lib/rubocop/cop/claude/mystery_regex.rb +46 -0
- data/lib/rubocop/cop/claude/no_backwards_compat_hacks.rb +140 -0
- data/lib/rubocop/cop/claude/no_commented_code.rb +182 -0
- data/lib/rubocop/cop/claude/no_fancy_unicode.rb +173 -0
- data/lib/rubocop/cop/claude/no_hardcoded_line_numbers.rb +142 -0
- data/lib/rubocop/cop/claude/no_overly_defensive_code.rb +160 -0
- data/lib/rubocop/cop/claude/tagged_comments.rb +78 -0
- data/lib/rubocop-claude.rb +19 -0
- data/lib/rubocop_claude/cli.rb +246 -0
- data/lib/rubocop_claude/generator.rb +90 -0
- data/lib/rubocop_claude/init_wizard/hooks_installer.rb +127 -0
- data/lib/rubocop_claude/init_wizard/linter_configurer.rb +88 -0
- data/lib/rubocop_claude/init_wizard/preferences_gatherer.rb +94 -0
- data/lib/rubocop_claude/plugin.rb +34 -0
- data/lib/rubocop_claude/version.rb +5 -0
- data/rubocop-claude.gemspec +41 -0
- data/templates/cops/class-structure.md +58 -0
- data/templates/cops/disable-cops-directive.md +33 -0
- data/templates/cops/explicit-visibility.md +52 -0
- data/templates/cops/metrics.md +73 -0
- data/templates/cops/mystery-regex.md +54 -0
- data/templates/cops/no-backwards-compat-hacks.md +101 -0
- data/templates/cops/no-commented-code.md +74 -0
- data/templates/cops/no-fancy-unicode.md +72 -0
- data/templates/cops/no-hardcoded-line-numbers.md +70 -0
- data/templates/cops/no-overly-defensive-code.md +117 -0
- data/templates/cops/tagged-comments.md +74 -0
- data/templates/hooks/settings.local.json +15 -0
- data/templates/linting.md +81 -0
- metadata +183 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d78f7699573dfffb71fa026fddbfaed2191d0185356e4dc4372d4743363b2b15
|
|
4
|
+
data.tar.gz: 3d497ed62af42361fbd1bfda74f08b4595b70167513c99160a63a78cb8e619e4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 22de6e579985bd72a3b85b1d0b3096b2bf95ab76a06200a149029a018dcac4da619dbdf86d9564e763eecaeb10c4864fcd97dffaf7cfeae3f781b892650951ff
|
|
7
|
+
data.tar.gz: 0d9f7b33281e32a341f0e65051b2457a69e06dd33bcc81df5708764f41a9f2adfab3ed59aed90921c12fbacbfcb27a1639cfe0b90277bb56751359c0b90633e0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2025-01-25
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Claude/NoHardcodedLineNumbers` - Flags hardcoded line numbers that become stale
|
|
10
|
+
|
|
11
|
+
## [0.0.1] - 2025-01-20
|
|
12
|
+
|
|
13
|
+
Initial pre-alpha release.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `Claude/NoFancyUnicode` - Flags emoji and fancy Unicode characters
|
|
18
|
+
- `Claude/TaggedComments` - Requires attribution on TODO/FIXME/NOTE comments
|
|
19
|
+
- `Claude/NoCommentedCode` - Detects commented-out code blocks
|
|
20
|
+
- `Claude/NoBackwardsCompatHacks` - Catches dead code preserved for compatibility
|
|
21
|
+
- `Claude/NoOverlyDefensiveCode` - Flags rescue nil, excessive &. chains, defensive nil checks
|
|
22
|
+
- `Claude/ExplicitVisibility` - Enforces consistent visibility style
|
|
23
|
+
- `Claude/MysteryRegex` - Flags long regexes that should be constants
|
|
24
|
+
- `rubocop-claude init` command for project setup
|
|
25
|
+
- StandardRB plugin support via lint_roller
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nicholas Marshall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# rubocop-claude
|
|
2
|
+
|
|
3
|
+
"CUT IT OUT, CLAUDE." doesn't work.
|
|
4
|
+
|
|
5
|
+
"Oh look, somehow you failed linting, how rough for you, better fix it! no naps!" does.
|
|
6
|
+
|
|
7
|
+
AI assistants are useful and can write functional code. They also:
|
|
8
|
+
|
|
9
|
+
- Love to nap on giant hoards of old code.
|
|
10
|
+
- Build defensive code fortresses EVERYWHERE.
|
|
11
|
+
- Handle explosive errors by adding "okay but what if we just ignored that! look, fixed it!"
|
|
12
|
+
- Add comments everywhere and then pretend you wrote them. 😒
|
|
13
|
+
- Reach for the world's least common Unicode symbols and then get confused when you say "stop it."
|
|
14
|
+
|
|
15
|
+
This gem tries to make them cut that out.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add to your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'rubocop-claude', require: false
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then run:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle install
|
|
29
|
+
rubocop-claude init
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `init` command will:
|
|
33
|
+
|
|
34
|
+
1. Create `.claude/linting.md` with instructions for AI assistants
|
|
35
|
+
2. Add `rubocop-claude` to your `.standard.yml` or `.rubocop.yml`
|
|
36
|
+
|
|
37
|
+
## Manual Setup
|
|
38
|
+
|
|
39
|
+
Add to `.standard.yml` or `.rubocop.yml`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
plugins:
|
|
43
|
+
- rubocop-claude
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Cops
|
|
47
|
+
|
|
48
|
+
| Cop | Description |
|
|
49
|
+
| ----- | ------------- |
|
|
50
|
+
| `Claude/NoFancyUnicode` | Flags emoji and fancy Unicode (curly quotes, em-dashes) |
|
|
51
|
+
| `Claude/TaggedComments` | Requires attribution on TODO/FIXME/NOTE comments |
|
|
52
|
+
| `Claude/NoCommentedCode` | Detects commented-out code blocks |
|
|
53
|
+
| `Claude/NoBackwardsCompatHacks` | Catches dead code preserved "for compatibility" |
|
|
54
|
+
| `Claude/NoOverlyDefensiveCode` | Flags `rescue nil`, excessive `&.` chains, defensive nil checks |
|
|
55
|
+
| `Claude/ExplicitVisibility` | Enforces consistent visibility style (grouped or modifier) |
|
|
56
|
+
| `Claude/MysteryRegex` | Flags long regexes that should be extracted to constants |
|
|
57
|
+
| `Claude/NoHardcodedLineNumbers` | Flags hardcoded line numbers that become stale |
|
|
58
|
+
|
|
59
|
+
## Configuration
|
|
60
|
+
|
|
61
|
+
All cops are enabled by default. Configure in `.rubocop.yml`:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
Claude/NoFancyUnicode:
|
|
65
|
+
AllowInStrings: true # Allow emoji in user-facing strings
|
|
66
|
+
AllowedUnicode:
|
|
67
|
+
- "\u2192" # Allow specific characters
|
|
68
|
+
|
|
69
|
+
Claude/TaggedComments:
|
|
70
|
+
Keywords:
|
|
71
|
+
- TODO
|
|
72
|
+
- FIXME
|
|
73
|
+
- NOTE
|
|
74
|
+
- HACK
|
|
75
|
+
|
|
76
|
+
Claude/NoCommentedCode:
|
|
77
|
+
MinLines: 2 # Only flag multi-line blocks (set to 1 for single lines)
|
|
78
|
+
AllowKeep: true # Allow KEEP [@handle]: comments to preserve code
|
|
79
|
+
|
|
80
|
+
Claude/NoOverlyDefensiveCode:
|
|
81
|
+
MaxSafeNavigationChain: 1 # Flag 2+ chained &. operators
|
|
82
|
+
AddSafeNavigator: false # Autocorrect to &. instead of direct call
|
|
83
|
+
|
|
84
|
+
Claude/ExplicitVisibility:
|
|
85
|
+
EnforcedStyle: grouped # or 'modifier' for `private def foo`
|
|
86
|
+
|
|
87
|
+
Claude/MysteryRegex:
|
|
88
|
+
MaxLength: 25
|
|
89
|
+
|
|
90
|
+
Claude/NoHardcodedLineNumbers:
|
|
91
|
+
CheckComments: true # Check comments (default: true)
|
|
92
|
+
CheckStrings: true # Check string literals (default: true)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Why These Cops?
|
|
96
|
+
|
|
97
|
+
### NoFancyUnicode
|
|
98
|
+
|
|
99
|
+
Me: "Okay, let's log that success."
|
|
100
|
+
Claude: A RAINBOW OF OBSCURE SYMBOLS EMERGES FROM THE MISTS.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# bad
|
|
104
|
+
puts "Deployment successful! 🚀"
|
|
105
|
+
logger.info "Task completed ✅"
|
|
106
|
+
|
|
107
|
+
# good
|
|
108
|
+
puts "Deployment successful!"
|
|
109
|
+
logger.info "Task completed"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### TaggedComments
|
|
113
|
+
|
|
114
|
+
"You added a comment, and it looks like I added a comment, and that comment is wrong."
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# bad
|
|
118
|
+
# TODO: fix this later
|
|
119
|
+
|
|
120
|
+
# good
|
|
121
|
+
# TODO: [@username] fix this later
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### NoCommentedCode
|
|
125
|
+
|
|
126
|
+
"No hoarding."
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# bad
|
|
130
|
+
# def old_implementation
|
|
131
|
+
# do_something_outdated
|
|
132
|
+
# end
|
|
133
|
+
|
|
134
|
+
# good - just delete it
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### NoBackwardsCompatHacks
|
|
138
|
+
|
|
139
|
+
"NO HOARDING."
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
# bad
|
|
143
|
+
_old_value = previous_calculation # keeping for reference
|
|
144
|
+
OldName = NewName # backwards compatibility
|
|
145
|
+
|
|
146
|
+
# good - delete it
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### NoOverlyDefensiveCode
|
|
150
|
+
|
|
151
|
+
"YOU ARE BUILDING A DOOMSDAY BUNKER FOR A METHOD THAT CAN'T FAIL, CLAUDE."
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# bad
|
|
155
|
+
result = dangerous_call rescue nil
|
|
156
|
+
user&.profile&.settings&.value
|
|
157
|
+
user && user.name
|
|
158
|
+
|
|
159
|
+
# good
|
|
160
|
+
result = dangerous_call
|
|
161
|
+
user.profile.settings.value
|
|
162
|
+
user.name
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### ExplicitVisibility
|
|
166
|
+
|
|
167
|
+
"The solution is **NOT** 'make all the private methods visible,' bud."
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# grouped style (default)
|
|
171
|
+
class Foo
|
|
172
|
+
def public_method; end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def private_method; end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# modifier style
|
|
180
|
+
class Foo
|
|
181
|
+
def public_method; end
|
|
182
|
+
private def private_method; end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### MysteryRegex
|
|
187
|
+
|
|
188
|
+
"I have no idea what your 300-character regex does, Claude, because you dumped that on the screen, said 'okay cool fixed' and then got distracted."
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# bad
|
|
192
|
+
if input.match?(/\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/)
|
|
193
|
+
|
|
194
|
+
# good
|
|
195
|
+
EMAIL_PATTERN = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
|
|
196
|
+
if input.match?(EMAIL_PATTERN)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### NoHardcodedLineNumbers
|
|
200
|
+
|
|
201
|
+
"That line reference is wrong, Claude. The code moved."
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# bad
|
|
205
|
+
# see line 42 for details
|
|
206
|
+
# Error at foo.rb:123
|
|
207
|
+
raise "check line 55"
|
|
208
|
+
|
|
209
|
+
# good
|
|
210
|
+
# see #validate_input for details
|
|
211
|
+
# Error in FooError class
|
|
212
|
+
raise "check validate_input method"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Suggested RuboCop Defaults
|
|
216
|
+
|
|
217
|
+
`rubocop-claude init` also enables some defaults in Rubocop that are aimed at keeping AI coders from getting weird with their Ruby. Full config with rationale in `config/default.yml`.
|
|
218
|
+
|
|
219
|
+
| Cop | Setting | Why |
|
|
220
|
+
| ----- | --------- | ----- |
|
|
221
|
+
| `Style/DisableCopsWithinSourceCodeDirective` | Enabled | AI "fixes" linting by disabling cops. No. |
|
|
222
|
+
| `Layout/ClassStructure` | Enabled | AI scatters methods randomly. Enforce order. |
|
|
223
|
+
| `Style/CommentAnnotation` | `RequireColon: true` | Works with TaggedComments. |
|
|
224
|
+
| `Lint/Debugger` | Enabled | AI leaves `binding.pry` in code. |
|
|
225
|
+
| `Layout/MultilineMethodCallIndentation` | `indented` | Leading dot, 2-space indent. |
|
|
226
|
+
| `Style/SafeNavigation` | `MaxChainLength: 1` | Complements NoOverlyDefensiveCode. |
|
|
227
|
+
| `Metrics/CyclomaticComplexity` | `Max: 7` | Flag spaghetti logic. |
|
|
228
|
+
| `Metrics/AbcSize` | `Max: 17` | Flag bloated methods. |
|
|
229
|
+
| `Metrics/MethodLength` | `Max: 10` | AI writes 80-line methods. 10 is plenty. |
|
|
230
|
+
| `Metrics/ClassLength` | `Max: 150` | Catches god classes. |
|
|
231
|
+
| `Metrics/ParameterLists` | `Max: 5` | Too many params = needs refactoring. |
|
|
232
|
+
| `Style/GuardClause` | Enabled | Early returns > nested conditionals. |
|
|
233
|
+
| `Style/RedundantReturn` | Enabled | Ruby returns last expression. |
|
|
234
|
+
| `Style/MutableConstant` | `strict` | Always `.freeze` constants. |
|
|
235
|
+
| `Lint/UnusedMethodArgument` | Enabled | Dead params = dead code. |
|
|
236
|
+
| `Style/NestedTernaryOperator` | Enabled | `a ? (b ? c : d) : e` is unreadable. |
|
|
237
|
+
| `Style/OptionalBooleanParameter` | Enabled | `foo(data, true)` - what's true mean? |
|
|
238
|
+
| `Naming/MethodParameterName` | `MinNameLength: 2` | No `x`, `y`, `z` params. Use real names. |
|
|
239
|
+
| `Style/ParallelAssignment` | Enabled | One assignment per line. |
|
|
240
|
+
|
|
241
|
+
**Not enabled:** `Lint/SuppressedException` and `Style/RescueModifier` - our `NoOverlyDefensiveCode` covers these with a unified "trust internal code" message.
|
|
242
|
+
|
|
243
|
+
## Claude Integration
|
|
244
|
+
|
|
245
|
+
`rubocop-claude init` will add files to .claude (or elsewhere if you provide it with a path).
|
|
246
|
+
|
|
247
|
+
These files are a bunch of reference docs for AI agents to gnaw on when they're asked to think about linting. It does not change any of your other config, and it does not try to integrate with the rest of your setup. It's just providing a structured starting point for getting AI agents to lint more effectively.
|
|
248
|
+
|
|
249
|
+
## WHAT THIS DOES NOT DO
|
|
250
|
+
|
|
251
|
+
This isn't a subsitute for reviewing your code or monitoring AI assistants. Static analysis is a wonderful tool for wrangling Ai coders, but it does not replace reviewing and monitoring changes.
|
|
252
|
+
|
|
253
|
+
Trying to force static analysis tools to fully handle every single edge case is silly, and trying to make these weird, enthusiastic pattern recognition engines get everything right on the first try isn't going to work. What we're trying to do is give tools like Claude a way to efficiently remember that they shouldn't be making weird decisions, and that they should make good decisions instead.
|
|
254
|
+
|
|
255
|
+
We can't always prevent AI tools from charging off in weird directions, but we CAN scatter rakes in their way and make them stop and think. This adds more rakes.
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
bundle install
|
|
261
|
+
bundle exec rspec
|
|
262
|
+
bundle exec rubocop
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## License
|
|
266
|
+
|
|
267
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# Default configuration for rubocop-claude cops.
|
|
4
|
+
# This file is loaded automatically by the plugin.
|
|
5
|
+
|
|
6
|
+
AllCops:
|
|
7
|
+
NewCops: disable
|
|
8
|
+
SuggestExtensions: false
|
|
9
|
+
|
|
10
|
+
# =============================================================================
|
|
11
|
+
# Existing RuboCop Cops - Enable/Configure
|
|
12
|
+
# =============================================================================
|
|
13
|
+
|
|
14
|
+
# Warn on inline rubocop:disable comments - fix the issue instead
|
|
15
|
+
Style/DisableCopsWithinSourceCodeDirective:
|
|
16
|
+
Enabled: true
|
|
17
|
+
AllowedCops: []
|
|
18
|
+
|
|
19
|
+
# Enforce consistent class structure ordering
|
|
20
|
+
Layout/ClassStructure:
|
|
21
|
+
Enabled: true
|
|
22
|
+
ExpectedOrder:
|
|
23
|
+
- module_inclusion
|
|
24
|
+
- constants
|
|
25
|
+
- public_class_methods
|
|
26
|
+
- initializer
|
|
27
|
+
- public_methods
|
|
28
|
+
- protected_methods
|
|
29
|
+
- private_methods
|
|
30
|
+
Categories:
|
|
31
|
+
module_inclusion:
|
|
32
|
+
- include
|
|
33
|
+
- prepend
|
|
34
|
+
- extend
|
|
35
|
+
|
|
36
|
+
# Enforce TODO/FIXME format with uppercase and colon
|
|
37
|
+
Style/CommentAnnotation:
|
|
38
|
+
Enabled: true
|
|
39
|
+
Keywords:
|
|
40
|
+
- TODO
|
|
41
|
+
- FIXME
|
|
42
|
+
- NOTE
|
|
43
|
+
- HACK
|
|
44
|
+
- OPTIMIZE
|
|
45
|
+
- REVIEW
|
|
46
|
+
RequireColon: true
|
|
47
|
+
|
|
48
|
+
# No debug statements left in code
|
|
49
|
+
Lint/Debugger:
|
|
50
|
+
Enabled: true
|
|
51
|
+
|
|
52
|
+
# Method chaining with indented style (leading dot, 2-space indent)
|
|
53
|
+
Layout/MultilineMethodCallIndentation:
|
|
54
|
+
Enabled: true
|
|
55
|
+
EnforcedStyle: indented
|
|
56
|
+
|
|
57
|
+
# Flag defensive .try calls and excessive && nil checks
|
|
58
|
+
Style/SafeNavigation:
|
|
59
|
+
Enabled: true
|
|
60
|
+
AllowedMethods: [] # Don't allow try/try! - use &. or trust the code
|
|
61
|
+
MaxChainLength: 1 # Consistent with NoOverlyDefensiveCode
|
|
62
|
+
|
|
63
|
+
# Flag overly complex methods
|
|
64
|
+
Metrics/CyclomaticComplexity:
|
|
65
|
+
Enabled: true
|
|
66
|
+
Max: 7
|
|
67
|
+
|
|
68
|
+
# Flag bloated methods
|
|
69
|
+
Metrics/AbcSize:
|
|
70
|
+
Enabled: true
|
|
71
|
+
Max: 17
|
|
72
|
+
|
|
73
|
+
# AI writes 80-line methods without blinking. 10 is plenty.
|
|
74
|
+
# CountAsOne prevents array literals from inflating the count.
|
|
75
|
+
Metrics/MethodLength:
|
|
76
|
+
Enabled: true
|
|
77
|
+
Max: 10
|
|
78
|
+
CountComments: false
|
|
79
|
+
CountAsOne: [array, hash, heredoc, method_call]
|
|
80
|
+
|
|
81
|
+
# AI creates god classes. 150 lines is generous but catches the worst offenders.
|
|
82
|
+
Metrics/ClassLength:
|
|
83
|
+
Enabled: true
|
|
84
|
+
Max: 150
|
|
85
|
+
CountComments: false
|
|
86
|
+
CountAsOne: [array, hash, heredoc, method_call]
|
|
87
|
+
|
|
88
|
+
# More than 5 params? Use an options hash or a parameter object.
|
|
89
|
+
Metrics/ParameterLists:
|
|
90
|
+
Enabled: true
|
|
91
|
+
Max: 5
|
|
92
|
+
CountKeywordArgs: true
|
|
93
|
+
MaxOptionalParameters: 3
|
|
94
|
+
|
|
95
|
+
# AI nests 5 levels deep instead of using early returns.
|
|
96
|
+
# Guard clauses are more readable: `return unless valid?` at the top.
|
|
97
|
+
Style/GuardClause:
|
|
98
|
+
Enabled: true
|
|
99
|
+
MinBodyLength: 1
|
|
100
|
+
|
|
101
|
+
# Ruby returns the last expression. Explicit `return` on the last line is noise.
|
|
102
|
+
# AllowMultipleReturnValues permits `return a, b` for tuple returns.
|
|
103
|
+
Style/RedundantReturn:
|
|
104
|
+
Enabled: true
|
|
105
|
+
AllowMultipleReturnValues: true
|
|
106
|
+
|
|
107
|
+
# ITEMS = ['a', 'b'] is mutable. Always .freeze constants.
|
|
108
|
+
# EnforcedStyle: strict requires freeze even for literals.
|
|
109
|
+
Style/MutableConstant:
|
|
110
|
+
Enabled: true
|
|
111
|
+
EnforcedStyle: strict
|
|
112
|
+
|
|
113
|
+
# Dead parameters after refactoring. Remove them or prefix with _ if interface requires.
|
|
114
|
+
Lint/UnusedMethodArgument:
|
|
115
|
+
Enabled: true
|
|
116
|
+
AllowUnusedKeywordArguments: false
|
|
117
|
+
IgnoreEmptyMethods: true
|
|
118
|
+
IgnoreNotImplementedMethods: true
|
|
119
|
+
|
|
120
|
+
# a ? (b ? c : d) : e is unreadable. Use if/else.
|
|
121
|
+
Style/NestedTernaryOperator:
|
|
122
|
+
Enabled: true
|
|
123
|
+
|
|
124
|
+
# def process(data, include_extras = false) -> callers see process(data, true).
|
|
125
|
+
# What does true mean? Use keyword args: process(data, include_extras: true).
|
|
126
|
+
Style/OptionalBooleanParameter:
|
|
127
|
+
Enabled: true
|
|
128
|
+
|
|
129
|
+
# def calculate(a, b, c) is cryptic. Use real names.
|
|
130
|
+
# Common 2-letter names (id, db, io) are allowed. x, y, z are forbidden.
|
|
131
|
+
Naming/MethodParameterName:
|
|
132
|
+
Enabled: true
|
|
133
|
+
MinNameLength: 2
|
|
134
|
+
AllowedNames: [id, ip, to, by, on, in, at, io, db]
|
|
135
|
+
ForbiddenNames: [x, y, z]
|
|
136
|
+
|
|
137
|
+
# a, b, c = 1, 2, 3 is harder to read than three separate lines.
|
|
138
|
+
# Exception: x, y = y, x for swaps is fine (cop handles this).
|
|
139
|
+
Style/ParallelAssignment:
|
|
140
|
+
Enabled: true
|
|
141
|
+
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# Custom Claude Cops
|
|
144
|
+
# =============================================================================
|
|
145
|
+
|
|
146
|
+
Claude/NoFancyUnicode:
|
|
147
|
+
Enabled: true
|
|
148
|
+
Description: "Avoid fancy Unicode. Use standard ASCII or add to AllowedUnicode."
|
|
149
|
+
AllowedUnicode: []
|
|
150
|
+
AllowInStrings: false
|
|
151
|
+
AllowInComments: false
|
|
152
|
+
Exclude:
|
|
153
|
+
- '**/no_fancy_unicode.rb'
|
|
154
|
+
- '**/no_fancy_unicode_spec.rb'
|
|
155
|
+
|
|
156
|
+
Claude/TaggedComments:
|
|
157
|
+
Enabled: true
|
|
158
|
+
Description: "Comments need attribution. Use format: # TAG: [@handle] description"
|
|
159
|
+
Keywords:
|
|
160
|
+
- TODO
|
|
161
|
+
- FIXME
|
|
162
|
+
- NOTE
|
|
163
|
+
- HACK
|
|
164
|
+
- OPTIMIZE
|
|
165
|
+
- REVIEW
|
|
166
|
+
|
|
167
|
+
Claude/NoCommentedCode:
|
|
168
|
+
Enabled: true
|
|
169
|
+
Description: "Delete commented-out code instead of leaving it. Version control preserves history."
|
|
170
|
+
MinLines: 1
|
|
171
|
+
AllowKeep: true
|
|
172
|
+
|
|
173
|
+
Claude/NoBackwardsCompatHacks:
|
|
174
|
+
Enabled: true
|
|
175
|
+
Description: "Delete dead code. Don't preserve it for backwards compatibility."
|
|
176
|
+
CheckUnderscoreAssignments: false
|
|
177
|
+
|
|
178
|
+
Claude/NoOverlyDefensiveCode:
|
|
179
|
+
Enabled: true
|
|
180
|
+
Description: "Trust internal code. Remove unnecessary defensive patterns."
|
|
181
|
+
MaxSafeNavigationChain: 1
|
|
182
|
+
AddSafeNavigator: false
|
|
183
|
+
|
|
184
|
+
Claude/ExplicitVisibility:
|
|
185
|
+
Enabled: true
|
|
186
|
+
Description: "Use explicit visibility. Group private methods under `private` keyword."
|
|
187
|
+
EnforcedStyle: grouped
|
|
188
|
+
|
|
189
|
+
Claude/MysteryRegex:
|
|
190
|
+
Enabled: true
|
|
191
|
+
Description: "Extract long regex to a named constant. Complex patterns deserve descriptive names."
|
|
192
|
+
MaxLength: 25
|
|
193
|
+
|
|
194
|
+
Claude/NoHardcodedLineNumbers:
|
|
195
|
+
Enabled: true
|
|
196
|
+
Description: "Avoid hardcoded line numbers. They become stale when code shifts."
|
|
197
|
+
CheckComments: true
|
|
198
|
+
CheckStrings: true
|
|
199
|
+
MinLineNumber: 1
|
|
200
|
+
Exclude:
|
|
201
|
+
- '**/no_hardcoded_line_numbers.rb'
|
|
202
|
+
- '**/no_hardcoded_line_numbers_spec.rb'
|
data/exe/rubocop-claude
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Claude
|
|
6
|
+
# Enforces visibility keyword placement style.
|
|
7
|
+
#
|
|
8
|
+
# The grouped style (`private` on its own line) is the dominant Ruby
|
|
9
|
+
# convention. The modifier style (`private def foo`) is less common.
|
|
10
|
+
#
|
|
11
|
+
# @example EnforcedStyle: grouped (default)
|
|
12
|
+
# # bad
|
|
13
|
+
# private def foo; end
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# private
|
|
17
|
+
# def foo; end
|
|
18
|
+
#
|
|
19
|
+
# @example EnforcedStyle: modifier
|
|
20
|
+
# # bad
|
|
21
|
+
# private
|
|
22
|
+
# def foo; end
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# private def foo; end
|
|
26
|
+
#
|
|
27
|
+
class ExplicitVisibility < Base
|
|
28
|
+
extend AutoCorrector
|
|
29
|
+
|
|
30
|
+
VISIBILITY_METHODS = %i[private protected public].freeze
|
|
31
|
+
|
|
32
|
+
MSG_USE_MODIFIER = 'Use explicit visibility. Place `%<visibility>s` before the method definition.'
|
|
33
|
+
MSG_USE_GROUPED = 'Use grouped visibility. Move method to `%<visibility>s` section.'
|
|
34
|
+
|
|
35
|
+
def on_send(node)
|
|
36
|
+
return unless standalone_visibility?(node)
|
|
37
|
+
return unless enforced_style == :modifier
|
|
38
|
+
|
|
39
|
+
methods = find_following_methods(node)
|
|
40
|
+
return if methods.empty?
|
|
41
|
+
|
|
42
|
+
add_offense(node, message: format(MSG_USE_MODIFIER, visibility: node.method_name)) do |corrector|
|
|
43
|
+
autocorrect_to_modifier(corrector, node, methods)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_def(node)
|
|
48
|
+
return unless enforced_style == :grouped
|
|
49
|
+
return unless inline_visibility?(node.parent)
|
|
50
|
+
|
|
51
|
+
visibility = node.parent.method_name
|
|
52
|
+
add_offense(node.parent, message: format(MSG_USE_GROUPED, visibility: visibility)) do |corrector|
|
|
53
|
+
autocorrect_to_grouped(corrector, node, node.parent, visibility)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def enforced_style
|
|
60
|
+
@enforced_style ||= cop_config.fetch('EnforcedStyle', 'grouped').to_sym
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def standalone_visibility?(node)
|
|
64
|
+
node.send_type? && VISIBILITY_METHODS.include?(node.method_name) &&
|
|
65
|
+
node.receiver.nil? && node.arguments.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def inline_visibility?(node)
|
|
69
|
+
node&.send_type? && %i[private protected].include?(node.method_name) &&
|
|
70
|
+
node.arguments.size == 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def find_following_methods(visibility_node)
|
|
74
|
+
siblings = visibility_node.parent.children
|
|
75
|
+
idx = siblings.index(visibility_node)
|
|
76
|
+
siblings[(idx + 1)..].take_while { |s| !standalone_visibility?(s) }.select(&:def_type?)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def autocorrect_to_modifier(corrector, visibility_node, methods)
|
|
80
|
+
corrector.remove(range_with_newlines(visibility_node))
|
|
81
|
+
methods.each { |m| corrector.insert_before(m, "#{visibility_node.method_name} ") }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def autocorrect_to_grouped(corrector, def_node, send_node, visibility)
|
|
85
|
+
class_node = def_node.each_ancestor(:class, :module).first
|
|
86
|
+
return unless class_node
|
|
87
|
+
|
|
88
|
+
insert_method_in_section(corrector, class_node, def_node, visibility)
|
|
89
|
+
corrector.remove(range_with_newlines(send_node))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def insert_method_in_section(corrector, class_node, def_node, visibility)
|
|
93
|
+
section = find_visibility_section(class_node, visibility)
|
|
94
|
+
ind = indent(def_node)
|
|
95
|
+
|
|
96
|
+
if section
|
|
97
|
+
pos = last_method_in_section(section)&.source_range || section.source_range
|
|
98
|
+
corrector.insert_after(pos, "\n\n#{ind}#{def_node.source}")
|
|
99
|
+
else
|
|
100
|
+
corrector.insert_before(class_node.loc.end.begin,
|
|
101
|
+
"\n\n#{ind}#{visibility}\n\n#{ind}#{def_node.source}\n")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def find_visibility_section(class_node, visibility)
|
|
106
|
+
class_node.body.each_child_node(:send).find { |n| n.method_name == visibility && n.arguments.empty? }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def last_method_in_section(visibility_node)
|
|
110
|
+
siblings = visibility_node.parent.children
|
|
111
|
+
idx = siblings.index(visibility_node)
|
|
112
|
+
siblings[(idx + 1)..].take_while { |s| !standalone_visibility?(s) }
|
|
113
|
+
.reverse.find(&:def_type?)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def indent(node)
|
|
117
|
+
node.source_range.source_line[/\A\s*/]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def range_with_newlines(node)
|
|
121
|
+
source = processed_source.buffer.source
|
|
122
|
+
begin_pos = adjust_begin_pos(node.source_range.begin_pos, source)
|
|
123
|
+
end_pos = adjust_end_pos(node.source_range.end_pos, source)
|
|
124
|
+
Parser::Source::Range.new(processed_source.buffer, begin_pos, end_pos)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def adjust_begin_pos(pos, source)
|
|
128
|
+
pos -= 1 while pos.positive? && source[pos - 1] =~ /[ \t]/
|
|
129
|
+
pos -= 1 if pos.positive? && source[pos - 1] == "\n"
|
|
130
|
+
pos
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def adjust_end_pos(pos, source)
|
|
134
|
+
(source[pos] == "\n") ? pos + 1 : pos
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|