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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +267 -0
  5. data/config/default.yml +202 -0
  6. data/exe/rubocop-claude +7 -0
  7. data/lib/rubocop/cop/claude/explicit_visibility.rb +139 -0
  8. data/lib/rubocop/cop/claude/mystery_regex.rb +46 -0
  9. data/lib/rubocop/cop/claude/no_backwards_compat_hacks.rb +140 -0
  10. data/lib/rubocop/cop/claude/no_commented_code.rb +182 -0
  11. data/lib/rubocop/cop/claude/no_fancy_unicode.rb +173 -0
  12. data/lib/rubocop/cop/claude/no_hardcoded_line_numbers.rb +142 -0
  13. data/lib/rubocop/cop/claude/no_overly_defensive_code.rb +160 -0
  14. data/lib/rubocop/cop/claude/tagged_comments.rb +78 -0
  15. data/lib/rubocop-claude.rb +19 -0
  16. data/lib/rubocop_claude/cli.rb +246 -0
  17. data/lib/rubocop_claude/generator.rb +90 -0
  18. data/lib/rubocop_claude/init_wizard/hooks_installer.rb +127 -0
  19. data/lib/rubocop_claude/init_wizard/linter_configurer.rb +88 -0
  20. data/lib/rubocop_claude/init_wizard/preferences_gatherer.rb +94 -0
  21. data/lib/rubocop_claude/plugin.rb +34 -0
  22. data/lib/rubocop_claude/version.rb +5 -0
  23. data/rubocop-claude.gemspec +41 -0
  24. data/templates/cops/class-structure.md +58 -0
  25. data/templates/cops/disable-cops-directive.md +33 -0
  26. data/templates/cops/explicit-visibility.md +52 -0
  27. data/templates/cops/metrics.md +73 -0
  28. data/templates/cops/mystery-regex.md +54 -0
  29. data/templates/cops/no-backwards-compat-hacks.md +101 -0
  30. data/templates/cops/no-commented-code.md +74 -0
  31. data/templates/cops/no-fancy-unicode.md +72 -0
  32. data/templates/cops/no-hardcoded-line-numbers.md +70 -0
  33. data/templates/cops/no-overly-defensive-code.md +117 -0
  34. data/templates/cops/tagged-comments.md +74 -0
  35. data/templates/hooks/settings.local.json +15 -0
  36. data/templates/linting.md +81 -0
  37. 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).
@@ -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'
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
5
+ require 'rubocop_claude/cli'
6
+
7
+ RubocopClaude::CLI.run(ARGV)
@@ -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