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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RubocopClaude
|
|
6
|
+
class InitWizard
|
|
7
|
+
# Handles installing Claude Code hooks for auto-linting
|
|
8
|
+
class HooksInstaller
|
|
9
|
+
def initialize(wizard)
|
|
10
|
+
@wizard = wizard
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
create_hooks_directory
|
|
15
|
+
create_hook_script
|
|
16
|
+
create_hooks_settings
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def create_hooks_directory
|
|
22
|
+
return if Dir.exist?('.claude/hooks')
|
|
23
|
+
|
|
24
|
+
FileUtils.mkdir_p('.claude/hooks')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_hook_script
|
|
28
|
+
dest = '.claude/hooks/ruby-lint.sh'
|
|
29
|
+
File.write(dest, generate_hook_script)
|
|
30
|
+
FileUtils.chmod(0o755, dest)
|
|
31
|
+
@wizard.add_change('Created .claude/hooks/ruby-lint.sh')
|
|
32
|
+
puts " Created #{dest}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def generate_hook_script
|
|
36
|
+
linter_command = if @wizard.hook_linter == 'standardrb'
|
|
37
|
+
'standardrb --fix "$file" 2>&1'
|
|
38
|
+
else
|
|
39
|
+
# Prefer binstub, fall back to bundle exec
|
|
40
|
+
# Use $CLAUDE_PROJECT_DIR to ensure correct path regardless of cwd
|
|
41
|
+
<<~CMD.chomp
|
|
42
|
+
if [[ -x "$CLAUDE_PROJECT_DIR/bin/rubocop" ]]; then
|
|
43
|
+
"$CLAUDE_PROJECT_DIR/bin/rubocop" -a "$file" --format simple 2>&1
|
|
44
|
+
else
|
|
45
|
+
bundle exec rubocop -a "$file" --format simple 2>&1
|
|
46
|
+
fi
|
|
47
|
+
CMD
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
<<~SCRIPT
|
|
51
|
+
#!/usr/bin/env bash
|
|
52
|
+
# Claude Code hook: Auto-lint Ruby files after edits
|
|
53
|
+
# Generated by rubocop-claude init --hooks
|
|
54
|
+
|
|
55
|
+
set -e
|
|
56
|
+
|
|
57
|
+
file="$CLAUDE_EDITED_FILE"
|
|
58
|
+
[[ -z "$file" ]] && exit 0
|
|
59
|
+
[[ "$file" != *.rb ]] && exit 0
|
|
60
|
+
[[ ! -f "$file" ]] && exit 0
|
|
61
|
+
|
|
62
|
+
# Autocorrect and report remaining issues
|
|
63
|
+
#{linter_command}
|
|
64
|
+
SCRIPT
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_hooks_settings
|
|
68
|
+
dest = '.claude/settings.local.json'
|
|
69
|
+
source = File.join(CLI::TEMPLATES_DIR, 'hooks', 'settings.local.json')
|
|
70
|
+
|
|
71
|
+
if File.exist?(dest)
|
|
72
|
+
merge_hooks_settings(dest, source)
|
|
73
|
+
else
|
|
74
|
+
FileUtils.cp(source, dest)
|
|
75
|
+
@wizard.add_change('Created .claude/settings.local.json with hooks')
|
|
76
|
+
puts " Created #{dest}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def merge_hooks_settings(dest, source)
|
|
81
|
+
existing = load_json(dest)
|
|
82
|
+
new_hooks = load_json(source)
|
|
83
|
+
|
|
84
|
+
ensure_hooks_structure(existing)
|
|
85
|
+
warn_if_existing_lint_hook(existing['hooks']['PostToolUse'])
|
|
86
|
+
existing['hooks']['PostToolUse'].concat(new_hooks['hooks']['PostToolUse'])
|
|
87
|
+
|
|
88
|
+
File.write(dest, JSON.pretty_generate(existing))
|
|
89
|
+
@wizard.add_change('Added Ruby lint hook to .claude/settings.local.json')
|
|
90
|
+
puts " Updated #{dest}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def load_json(path)
|
|
94
|
+
JSON.parse(File.read(path))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def ensure_hooks_structure(config)
|
|
98
|
+
config['hooks'] ||= {}
|
|
99
|
+
config['hooks']['PostToolUse'] ||= []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def warn_if_existing_lint_hook(post_tool_use_hooks)
|
|
103
|
+
existing = find_existing_lint_command(post_tool_use_hooks)
|
|
104
|
+
return unless existing
|
|
105
|
+
|
|
106
|
+
truncated = (existing.length > 60) ? "#{existing[0, 57]}..." : existing
|
|
107
|
+
puts " [!] Found existing lint hook: #{truncated}"
|
|
108
|
+
puts ' You may want to remove duplicate hooks from settings.local.json'
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def find_existing_lint_command(post_tool_use_hooks)
|
|
112
|
+
lint_patterns = [/rubocop/i, /standardrb/i, /lint/i, /\.rb/]
|
|
113
|
+
|
|
114
|
+
post_tool_use_hooks.each do |hook_config|
|
|
115
|
+
next unless hook_config['matcher'].to_s.match?(/Edit|Write/i)
|
|
116
|
+
|
|
117
|
+
(hook_config['hooks'] || []).each do |hook|
|
|
118
|
+
command = hook['command'].to_s
|
|
119
|
+
return command if lint_patterns.any? { |pat| command.match?(pat) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubocopClaude
|
|
4
|
+
class InitWizard
|
|
5
|
+
# Handles linter config detection and setup (StandardRB or RuboCop)
|
|
6
|
+
class LinterConfigurer
|
|
7
|
+
def initialize(wizard)
|
|
8
|
+
@wizard = wizard
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
config_type = detect_config_type
|
|
13
|
+
case config_type
|
|
14
|
+
when :standard then setup_standard_config
|
|
15
|
+
when :rubocop then setup_rubocop_config
|
|
16
|
+
else create_new_config
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def detect_config_type
|
|
23
|
+
if File.exist?('.standard.yml')
|
|
24
|
+
:standard
|
|
25
|
+
elsif File.exist?('.rubocop.yml')
|
|
26
|
+
:rubocop
|
|
27
|
+
else
|
|
28
|
+
:none
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def setup_standard_config
|
|
33
|
+
@wizard.using_standard = true
|
|
34
|
+
puts 'Detected: StandardRB (.standard.yml)'
|
|
35
|
+
add_to_linter_config('.standard.yml', 'plugins', 'plugin')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def setup_rubocop_config
|
|
39
|
+
puts 'Detected: RuboCop (.rubocop.yml)'
|
|
40
|
+
add_to_linter_config('.rubocop.yml', 'require', 'require')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_to_linter_config(file, key, prompt_label)
|
|
44
|
+
config = @wizard.load_yaml(file)
|
|
45
|
+
existing = config[key] || []
|
|
46
|
+
|
|
47
|
+
if existing.include?('rubocop-claude')
|
|
48
|
+
puts ' rubocop-claude already configured'
|
|
49
|
+
elsif @wizard.prompt_yes?(" Add rubocop-claude #{prompt_label}?", default: true)
|
|
50
|
+
config[key] = existing + ['rubocop-claude']
|
|
51
|
+
@wizard.save_yaml(file, config)
|
|
52
|
+
@wizard.add_change("Added rubocop-claude to #{file} #{key}")
|
|
53
|
+
end
|
|
54
|
+
puts
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def create_new_config
|
|
58
|
+
puts 'No linter config found.'
|
|
59
|
+
choice = @wizard.prompt_choice(
|
|
60
|
+
'Which linter do you use?',
|
|
61
|
+
{'s' => 'StandardRB', 'r' => 'RuboCop', 'n' => 'Skip'},
|
|
62
|
+
default: 's'
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
case choice
|
|
66
|
+
when 's' then create_standard_config
|
|
67
|
+
when 'r' then create_rubocop_config
|
|
68
|
+
end
|
|
69
|
+
puts
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def create_standard_config
|
|
73
|
+
@wizard.using_standard = true
|
|
74
|
+
config = {'plugins' => ['rubocop-claude']}
|
|
75
|
+
@wizard.save_yaml('.standard.yml', config)
|
|
76
|
+
@wizard.add_change('Created .standard.yml with rubocop-claude')
|
|
77
|
+
puts ' Created .standard.yml'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_rubocop_config
|
|
81
|
+
config = {'require' => ['rubocop-claude']}
|
|
82
|
+
@wizard.save_yaml('.rubocop.yml', config)
|
|
83
|
+
@wizard.add_change('Created .rubocop.yml with rubocop-claude')
|
|
84
|
+
puts ' Created .rubocop.yml'
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubocopClaude
|
|
4
|
+
class InitWizard
|
|
5
|
+
# Handles gathering user preferences for cop configuration
|
|
6
|
+
class PreferencesGatherer
|
|
7
|
+
def initialize(wizard)
|
|
8
|
+
@wizard = wizard
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
puts 'Configuration options:'
|
|
13
|
+
puts
|
|
14
|
+
|
|
15
|
+
gather_ai_defaults
|
|
16
|
+
gather_visibility_style
|
|
17
|
+
gather_emoji_preference
|
|
18
|
+
gather_commented_code_preference
|
|
19
|
+
gather_hooks_preference
|
|
20
|
+
puts
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def gather_ai_defaults
|
|
26
|
+
if @wizard.using_standard
|
|
27
|
+
puts ' Metrics cops (MethodLength, ClassLength, etc.) will be enabled.'
|
|
28
|
+
puts ' (StandardRB handles style; we add metrics enforcement)'
|
|
29
|
+
@wizard.add_change('Metrics cops enabled (StandardRB mode)')
|
|
30
|
+
else
|
|
31
|
+
puts ' AI Defaults: Stricter metrics, guard clauses, frozen constants, etc.'
|
|
32
|
+
puts ' (See README "Suggested RuboCop Defaults" for full list)'
|
|
33
|
+
ai_defaults = @wizard.prompt_yes?(' Enable AI defaults?', default: true)
|
|
34
|
+
@wizard.add_change(ai_defaults ? 'AI defaults enabled' : 'AI defaults disabled (Claude cops only)')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def gather_visibility_style
|
|
39
|
+
choice = @wizard.prompt_choice(
|
|
40
|
+
' Visibility style?',
|
|
41
|
+
{'g' => 'grouped (private section)', 'm' => 'modifier (private def)'},
|
|
42
|
+
default: 'g'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
style = (choice == 'm') ? 'modifier' : 'grouped'
|
|
46
|
+
@wizard.add_config_override('Claude/ExplicitVisibility', {'EnforcedStyle' => style})
|
|
47
|
+
@wizard.add_change("Visibility style: #{style}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def gather_emoji_preference
|
|
51
|
+
allow_emoji = @wizard.prompt_yes?(' Allow emoji in strings?', default: false)
|
|
52
|
+
|
|
53
|
+
if allow_emoji
|
|
54
|
+
@wizard.add_config_override('Claude/NoFancyUnicode', {'AllowInStrings' => true})
|
|
55
|
+
@wizard.add_change('Emoji allowed in strings')
|
|
56
|
+
else
|
|
57
|
+
@wizard.add_change('Emoji not allowed anywhere')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def gather_commented_code_preference
|
|
62
|
+
choice = @wizard.prompt_choice(
|
|
63
|
+
' Flag commented code:',
|
|
64
|
+
{'1' => 'Single lines too', '2' => 'Only multi-line blocks'},
|
|
65
|
+
default: '2'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
min_lines = choice.to_i
|
|
69
|
+
@wizard.add_config_override('Claude/NoCommentedCode', {'MinLines' => min_lines})
|
|
70
|
+
@wizard.add_change("Commented code detection: #{(min_lines == 1) ? "single lines" : "multi-line only"}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def gather_hooks_preference
|
|
74
|
+
puts
|
|
75
|
+
puts ' Claude Code hooks auto-lint Ruby files after each edit.'
|
|
76
|
+
@wizard.install_hooks = @wizard.prompt_yes?(' Install Claude Code hooks?', default: false)
|
|
77
|
+
return unless @wizard.install_hooks
|
|
78
|
+
|
|
79
|
+
gather_hook_linter_preference
|
|
80
|
+
@wizard.add_change("Claude Code hooks enabled (#{@wizard.hook_linter})")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def gather_hook_linter_preference
|
|
84
|
+
choice = @wizard.prompt_choice(
|
|
85
|
+
' Linter for hooks?',
|
|
86
|
+
{'r' => 'RuboCop', 's' => 'StandardRB'},
|
|
87
|
+
default: 'r'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@wizard.hook_linter = (choice == 's') ? 'standardrb' : 'rubocop'
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lint_roller'
|
|
4
|
+
|
|
5
|
+
module RubocopClaude
|
|
6
|
+
class Plugin < LintRoller::Plugin
|
|
7
|
+
def about
|
|
8
|
+
LintRoller::About.new(
|
|
9
|
+
name: 'rubocop-claude',
|
|
10
|
+
version: VERSION,
|
|
11
|
+
homepage: 'https://github.com/nabm/rubocop-claude',
|
|
12
|
+
description: 'AI-focused Ruby linting - catches common AI coding anti-patterns'
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def supported?(context)
|
|
17
|
+
context.engine == :rubocop
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rules(_context)
|
|
21
|
+
LintRoller::Rules.new(
|
|
22
|
+
type: :path,
|
|
23
|
+
config_format: :rubocop,
|
|
24
|
+
value: config_path
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def config_path
|
|
31
|
+
File.expand_path('../../config/default.yml', __dir__)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/rubocop_claude/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'rubocop-claude'
|
|
7
|
+
spec.version = RubocopClaude::VERSION
|
|
8
|
+
spec.authors = ['Nicholas Marshall']
|
|
9
|
+
spec.email = ['nialbima@users.noreply.github.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'AI-focused Ruby linting via StandardRB plugin'
|
|
12
|
+
spec.description = 'A Rubocop/StandardRB plugin providing automated Rubocop foot-rake feedback to AI coding agents.'
|
|
13
|
+
spec.homepage = 'https://github.com/nabm/rubocop-claude'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.1.0'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
20
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
21
|
+
spec.metadata['default_lint_roller_plugin'] = 'RubocopClaude::Plugin'
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.glob('{config,lib,templates,exe}/**/*') + %w[
|
|
24
|
+
LICENSE.txt
|
|
25
|
+
README.md
|
|
26
|
+
CHANGELOG.md
|
|
27
|
+
rubocop-claude.gemspec
|
|
28
|
+
]
|
|
29
|
+
spec.bindir = 'exe'
|
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
31
|
+
spec.require_paths = ['lib']
|
|
32
|
+
|
|
33
|
+
spec.add_dependency 'lint_roller', '~> 1.1'
|
|
34
|
+
spec.add_dependency 'rubocop', '>= 1.50'
|
|
35
|
+
|
|
36
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
37
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
38
|
+
spec.add_development_dependency 'rubocop-packaging', '~> 0.6.0'
|
|
39
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 3.9'
|
|
40
|
+
spec.add_development_dependency 'standard', '~> 1.40'
|
|
41
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Layout/ClassStructure
|
|
2
|
+
|
|
3
|
+
**What it catches:** Class/module contents in wrong order.
|
|
4
|
+
|
|
5
|
+
**Why it matters:** Consistent ordering makes code easier to navigate.
|
|
6
|
+
|
|
7
|
+
## Expected Order
|
|
8
|
+
|
|
9
|
+
1. `include`, `extend`, `prepend`
|
|
10
|
+
2. Constants
|
|
11
|
+
3. Class methods (`def self.foo`)
|
|
12
|
+
4. `initialize`
|
|
13
|
+
5. Public instance methods
|
|
14
|
+
6. Protected methods
|
|
15
|
+
7. Private methods
|
|
16
|
+
|
|
17
|
+
## How to Fix
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
# BAD
|
|
21
|
+
class User
|
|
22
|
+
def greet
|
|
23
|
+
"Hello"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
include Comparable
|
|
27
|
+
|
|
28
|
+
ROLE = "admin"
|
|
29
|
+
|
|
30
|
+
def initialize(name)
|
|
31
|
+
@name = name
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# GOOD
|
|
36
|
+
class User
|
|
37
|
+
include Comparable
|
|
38
|
+
|
|
39
|
+
ROLE = "admin"
|
|
40
|
+
|
|
41
|
+
def initialize(name)
|
|
42
|
+
@name = name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def greet
|
|
46
|
+
"Hello"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Autocorrection
|
|
52
|
+
|
|
53
|
+
**This cop autocorrects** - it will reorder methods automatically.
|
|
54
|
+
|
|
55
|
+
## Decision Criteria
|
|
56
|
+
|
|
57
|
+
- Let the autocorrect handle it
|
|
58
|
+
- If it produces odd results, review manually and adjust
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Style/DisableCopsWithinSourceCodeDirective
|
|
2
|
+
|
|
3
|
+
**What it catches:** `# rubocop:disable` comments inline.
|
|
4
|
+
|
|
5
|
+
**Why it matters:** Disabling cops hides problems rather than fixing them.
|
|
6
|
+
|
|
7
|
+
## How to Fix
|
|
8
|
+
|
|
9
|
+
**Don't disable the cop.** Fix the underlying issue instead.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# BAD
|
|
13
|
+
# rubocop:disable Metrics/MethodLength
|
|
14
|
+
def very_long_method
|
|
15
|
+
# ... lots of code ...
|
|
16
|
+
end
|
|
17
|
+
# rubocop:enable Metrics/MethodLength
|
|
18
|
+
|
|
19
|
+
# GOOD - refactor the method
|
|
20
|
+
def method_part_one
|
|
21
|
+
# ...
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def method_part_two
|
|
25
|
+
# ...
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Decision Criteria
|
|
30
|
+
|
|
31
|
+
- If you genuinely can't fix the issue, **ask the human**
|
|
32
|
+
- Never add `# rubocop:disable` without explicit human approval
|
|
33
|
+
- If the cop seems wrong for this codebase, discuss with the human about configuring it globally
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Claude/ExplicitVisibility
|
|
2
|
+
|
|
3
|
+
**What it catches:** (in grouped mode) Inline `private def foo` instead of grouped `private` sections.
|
|
4
|
+
|
|
5
|
+
**Why it matters:** Grouped visibility is the dominant Ruby convention. All private methods appear under the `private` keyword, making it easy to scan a class and see its public interface at the top.
|
|
6
|
+
|
|
7
|
+
## How to Fix
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# BAD - modifier style (inline)
|
|
11
|
+
class User
|
|
12
|
+
def public_method
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private def secret_method
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private def another_secret
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# GOOD - grouped style
|
|
23
|
+
class User
|
|
24
|
+
def public_method
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def secret_method
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def another_secret
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
Claude/ExplicitVisibility:
|
|
41
|
+
Enabled: true
|
|
42
|
+
EnforcedStyle: grouped # default - or 'modifier' for inline style
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## For AI Assistants
|
|
46
|
+
|
|
47
|
+
**When adding private methods:**
|
|
48
|
+
1. Place them after the `private` keyword at the bottom of the class
|
|
49
|
+
2. If no `private` section exists, add one before your new method
|
|
50
|
+
3. Don't use `private def` inline style unless the project consistently uses it
|
|
51
|
+
|
|
52
|
+
**When you see `private def`:** The project may prefer modifier style. Check other files before "fixing" it.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Metrics/* Cops
|
|
2
|
+
|
|
3
|
+
**What they catch:** Methods that are too complex or too long.
|
|
4
|
+
|
|
5
|
+
- `Metrics/CyclomaticComplexity` - Too many branching paths
|
|
6
|
+
- `Metrics/AbcSize` - Too many assignments, branches, conditions
|
|
7
|
+
- `Metrics/MethodLength` - Too many lines
|
|
8
|
+
|
|
9
|
+
**Why it matters:** Complex methods are hard to understand, test, and maintain.
|
|
10
|
+
|
|
11
|
+
## How to Fix
|
|
12
|
+
|
|
13
|
+
### Extract Helper Methods
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# BAD - one big method
|
|
17
|
+
def process_order(order)
|
|
18
|
+
# validate order
|
|
19
|
+
# calculate totals
|
|
20
|
+
# apply discounts
|
|
21
|
+
# process payment
|
|
22
|
+
# send notifications
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# GOOD - broken into focused methods
|
|
26
|
+
def process_order(order)
|
|
27
|
+
validate_order(order)
|
|
28
|
+
totals = calculate_totals(order)
|
|
29
|
+
totals = apply_discounts(totals, order.customer)
|
|
30
|
+
process_payment(order, totals)
|
|
31
|
+
send_notifications(order)
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Reduce Nesting
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# BAD - deep nesting
|
|
39
|
+
def process(data)
|
|
40
|
+
if data.valid?
|
|
41
|
+
if data.complete?
|
|
42
|
+
if data.authorized?
|
|
43
|
+
# actual work
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# GOOD - early returns
|
|
50
|
+
def process(data)
|
|
51
|
+
return unless data.valid?
|
|
52
|
+
return unless data.complete?
|
|
53
|
+
return unless data.authorized?
|
|
54
|
+
|
|
55
|
+
# actual work
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Simplify Conditionals
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# BAD - complex conditional
|
|
63
|
+
if user.admin? || (user.moderator? && post.reported?) || user.id == post.author_id
|
|
64
|
+
|
|
65
|
+
# GOOD - named method
|
|
66
|
+
if can_moderate_post?(user, post)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Decision Criteria
|
|
70
|
+
|
|
71
|
+
- If a method triggers these, it probably needs refactoring
|
|
72
|
+
- Ask if you're not sure how to split it up
|
|
73
|
+
- Don't just add `# rubocop:disable` - that's never the answer
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Claude/MysteryRegex
|
|
2
|
+
|
|
3
|
+
**What it catches:** Inline regexes longer than 25 characters (configurable).
|
|
4
|
+
|
|
5
|
+
**Why it matters:** Complex regexes are cryptic. A named constant with a descriptive name makes the intent clear. Comments can explain what the pattern matches.
|
|
6
|
+
|
|
7
|
+
## How to Fix
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# BAD - what does this match?
|
|
11
|
+
text.match?(/\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/)
|
|
12
|
+
|
|
13
|
+
# BAD - still cryptic even in a method
|
|
14
|
+
def valid_email?(text)
|
|
15
|
+
text.match?(/\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GOOD - name explains intent
|
|
19
|
+
EMAIL_PATTERN = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
|
|
20
|
+
|
|
21
|
+
def valid_email?(text)
|
|
22
|
+
text.match?(EMAIL_PATTERN)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# BETTER - comment explains the pattern
|
|
26
|
+
# Matches email addresses: local-part@domain.tld
|
|
27
|
+
# Simplified from RFC 5322 - allows common characters only
|
|
28
|
+
EMAIL_PATTERN = /\A[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Naming Conventions
|
|
32
|
+
|
|
33
|
+
| Pattern Type | Naming Examples |
|
|
34
|
+
|--------------|-----------------|
|
|
35
|
+
| Email | `EMAIL_PATTERN`, `EMAIL_REGEX` |
|
|
36
|
+
| URL | `URL_PATTERN`, `HTTP_URL_REGEX` |
|
|
37
|
+
| Phone | `PHONE_NUMBER_PATTERN` |
|
|
38
|
+
| Date | `ISO_DATE_PATTERN`, `DATE_REGEX` |
|
|
39
|
+
| Validation | `VALID_USERNAME_PATTERN` |
|
|
40
|
+
| Parsing | `LOG_LINE_PATTERN`, `CSV_ROW_REGEX` |
|
|
41
|
+
|
|
42
|
+
## When Short Regexes Are Fine
|
|
43
|
+
|
|
44
|
+
These are OK inline:
|
|
45
|
+
- `/\A\d+\z/` - just digits
|
|
46
|
+
- `/\s+/` - whitespace
|
|
47
|
+
- `/[,;]/` - simple character class
|
|
48
|
+
- `/\.rb\z/` - file extension
|
|
49
|
+
|
|
50
|
+
## Decision Criteria
|
|
51
|
+
|
|
52
|
+
- Extract to constant if you'd need to think about what it matches
|
|
53
|
+
- Always add a comment for non-obvious patterns
|
|
54
|
+
- Use `_PATTERN` or `_REGEX` suffix consistently in the project
|