eager_eye 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +86 -0
- data/lib/eager_eye/analyzer.rb +8 -0
- data/lib/eager_eye/auto_fixer.rb +93 -0
- data/lib/eager_eye/cli.rb +71 -24
- data/lib/eager_eye/comment_parser.rb +144 -0
- data/lib/eager_eye/fixer_registry.rb +22 -0
- data/lib/eager_eye/fixers/base.rb +48 -0
- data/lib/eager_eye/fixers/count_to_size.rb +18 -0
- data/lib/eager_eye/fixers/pluck_to_select.rb +30 -0
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +6 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8d34dc13a536c66ca82e9495ba5e53b81c31f1e2764dea3ba8d64c434da69ec
|
|
4
|
+
data.tar.gz: d63753e0d80c366cc9b87fe6bcc5bc2f2ee1ba537e7363c2b6aadafffd5a4a05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf3ff2ba24314b8b6e22385c82685d8909729f12ffc9d48531bd7fe4c6a1683e2aadb5692934ec300430dfdbc0cde795489e9a68a98072658a9452cd8e6e338b
|
|
7
|
+
data.tar.gz: fd73786c8c983b2bcc3d3860bf1036dc1525523dcce8ef06767760dfce57b813ee415679fa42cc7631830e8c4780f0bbbdecc61ae4563b964a983ada5c2c0d42
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2025-12-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Auto-fix Suggestions (Experimental)** - Automatic fix capabilities for simple issues
|
|
15
|
+
- `--suggest-fixes` flag to show auto-fix suggestions in diff format
|
|
16
|
+
- `--fix` flag to apply auto-fixes interactively
|
|
17
|
+
- `--fix --force` to apply all fixes without confirmation
|
|
18
|
+
- Fixer for `.count` → `.size` transformation in iterations
|
|
19
|
+
- Fixer for inline `.pluck(:id)` → `.select(:id)` transformation
|
|
20
|
+
|
|
21
|
+
### Note
|
|
22
|
+
|
|
23
|
+
Auto-fix is experimental. Not all issues are auto-fixable. Always review changes and run your test suite after applying fixes.
|
|
24
|
+
|
|
25
|
+
## [0.6.0] - 2025-12-15
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Inline Suppression Comments** - RuboCop-style comment directives for suppressing false positives
|
|
30
|
+
- `# eager_eye:disable DetectorName` - Disable for single line (inline) or start block
|
|
31
|
+
- `# eager_eye:disable-next-line DetectorName` - Disable only the next line
|
|
32
|
+
- `# eager_eye:disable-file DetectorName` - Disable for entire file (must be in first 5 lines)
|
|
33
|
+
- `# eager_eye:enable DetectorName` - End a disable block
|
|
34
|
+
- Support for multiple detectors: `# eager_eye:disable LoopAssociation, CountInIteration`
|
|
35
|
+
- Support for reason comments: `# eager_eye:disable DetectorName -- reason here`
|
|
36
|
+
- `all` keyword to disable all detectors at once
|
|
37
|
+
- Both CamelCase (`LoopAssociation`) and snake_case (`loop_association`) detector names accepted
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Updated README with inline suppression documentation
|
|
42
|
+
- Added `CommentParser` module for parsing suppression directives
|
|
43
|
+
|
|
10
44
|
## [0.5.0] - 2025-12-15
|
|
11
45
|
|
|
12
46
|
### Added
|
data/README.md
CHANGED
|
@@ -272,6 +272,92 @@ Post.where(user_id: User.active.select(:id))
|
|
|
272
272
|
| `pluck` + `where` | 2 | ~80KB for IDs | ~45ms |
|
|
273
273
|
| `select` subquery | 1 | None | ~20ms |
|
|
274
274
|
|
|
275
|
+
## Inline Suppression
|
|
276
|
+
|
|
277
|
+
Suppress false positives using inline comments (RuboCop-style):
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Disable for single line
|
|
281
|
+
user.posts.count # eager_eye:disable CountInIteration
|
|
282
|
+
|
|
283
|
+
# Disable for next line
|
|
284
|
+
# eager_eye:disable-next-line LoopAssociation
|
|
285
|
+
@users.each { |u| u.profile }
|
|
286
|
+
|
|
287
|
+
# Disable block
|
|
288
|
+
# eager_eye:disable LoopAssociation, SerializerNesting
|
|
289
|
+
@users.each do |user|
|
|
290
|
+
user.posts.each { |p| p.author }
|
|
291
|
+
end
|
|
292
|
+
# eager_eye:enable LoopAssociation, SerializerNesting
|
|
293
|
+
|
|
294
|
+
# Disable entire file (must be in first 5 lines)
|
|
295
|
+
# eager_eye:disable-file CustomMethodQuery
|
|
296
|
+
|
|
297
|
+
# With reason
|
|
298
|
+
user.posts.count # eager_eye:disable CountInIteration -- using counter_cache
|
|
299
|
+
|
|
300
|
+
# Disable all detectors
|
|
301
|
+
# eager_eye:disable all
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Available Detector Names
|
|
305
|
+
|
|
306
|
+
Both CamelCase and snake_case formats are accepted:
|
|
307
|
+
|
|
308
|
+
| Detector | CamelCase | snake_case |
|
|
309
|
+
|----------|-----------|------------|
|
|
310
|
+
| Loop Association | `LoopAssociation` | `loop_association` |
|
|
311
|
+
| Serializer Nesting | `SerializerNesting` | `serializer_nesting` |
|
|
312
|
+
| Missing Counter Cache | `MissingCounterCache` | `missing_counter_cache` |
|
|
313
|
+
| Custom Method Query | `CustomMethodQuery` | `custom_method_query` |
|
|
314
|
+
| Count in Iteration | `CountInIteration` | `count_in_iteration` |
|
|
315
|
+
| Callback Query | `CallbackQuery` | `callback_query` |
|
|
316
|
+
| Pluck to Array | `PluckToArray` | `pluck_to_array` |
|
|
317
|
+
| All Detectors | `all` | `all` |
|
|
318
|
+
|
|
319
|
+
## Auto-fix (Experimental)
|
|
320
|
+
|
|
321
|
+
EagerEye can automatically fix some simple issues:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# Show fix suggestions
|
|
325
|
+
eager_eye --suggest-fixes
|
|
326
|
+
|
|
327
|
+
# Apply fixes interactively
|
|
328
|
+
eager_eye --fix
|
|
329
|
+
|
|
330
|
+
# Apply all fixes without confirmation
|
|
331
|
+
eager_eye --fix --force
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Currently Supported Auto-fixes
|
|
335
|
+
|
|
336
|
+
| Issue | Fix |
|
|
337
|
+
|-------|-----|
|
|
338
|
+
| `.count` in iteration | → `.size` |
|
|
339
|
+
| `.pluck(:id)` inline | → `.select(:id)` |
|
|
340
|
+
|
|
341
|
+
### Example
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
$ eager_eye --suggest-fixes
|
|
345
|
+
|
|
346
|
+
app/controllers/posts_controller.rb:
|
|
347
|
+
Line 15:
|
|
348
|
+
- user.posts.count
|
|
349
|
+
+ user.posts.size
|
|
350
|
+
|
|
351
|
+
$ eager_eye --fix
|
|
352
|
+
app/controllers/posts_controller.rb:15
|
|
353
|
+
- user.posts.count
|
|
354
|
+
+ user.posts.size
|
|
355
|
+
Apply this fix? [y/n/q] y
|
|
356
|
+
Applied
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
> **Warning:** Auto-fix is experimental. Always review changes and run your test suite after applying fixes.
|
|
360
|
+
|
|
275
361
|
## Configuration
|
|
276
362
|
|
|
277
363
|
### Config File (.eager_eye.yml)
|
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -58,8 +58,16 @@ module EagerEye
|
|
|
58
58
|
ast = parse_source(source)
|
|
59
59
|
return unless ast
|
|
60
60
|
|
|
61
|
+
comment_parser = CommentParser.new(source)
|
|
62
|
+
|
|
61
63
|
enabled_detectors.each do |detector|
|
|
62
64
|
file_issues = detector.detect(ast, file_path)
|
|
65
|
+
|
|
66
|
+
# Filter suppressed issues
|
|
67
|
+
file_issues.reject! do |issue|
|
|
68
|
+
comment_parser.disabled_at?(issue.line_number, issue.detector)
|
|
69
|
+
end
|
|
70
|
+
|
|
63
71
|
@issues.concat(file_issues)
|
|
64
72
|
end
|
|
65
73
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
class AutoFixer
|
|
5
|
+
def initialize(issues, interactive: true)
|
|
6
|
+
@issues = issues
|
|
7
|
+
@interactive = interactive
|
|
8
|
+
@files_cache = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
fixes = collect_fixes
|
|
13
|
+
return puts "No auto-fixable issues found." if fixes.empty?
|
|
14
|
+
|
|
15
|
+
if @interactive
|
|
16
|
+
apply_interactively(fixes)
|
|
17
|
+
else
|
|
18
|
+
apply_all(fixes)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def suggest
|
|
23
|
+
fixes = collect_fixes
|
|
24
|
+
return puts "No auto-fixable issues found." if fixes.empty?
|
|
25
|
+
|
|
26
|
+
fixes.group_by { |f| f[:file] }.each do |file, file_fixes|
|
|
27
|
+
puts "\n#{file}:"
|
|
28
|
+
file_fixes.each do |fix|
|
|
29
|
+
puts " Line #{fix[:line]}:"
|
|
30
|
+
puts " - #{fix[:original]}"
|
|
31
|
+
puts " + #{fix[:fixed]}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def collect_fixes
|
|
39
|
+
@issues.filter_map do |issue|
|
|
40
|
+
source = read_file(issue.file_path)
|
|
41
|
+
fixer = FixerRegistry.fixer_for(issue, source)
|
|
42
|
+
next unless fixer&.fixable?
|
|
43
|
+
|
|
44
|
+
fixer.diff
|
|
45
|
+
end.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read_file(path)
|
|
49
|
+
@files_cache[path] ||= File.read(path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def apply_interactively(fixes)
|
|
53
|
+
fixes.each do |fix|
|
|
54
|
+
puts "\n#{fix[:file]}:#{fix[:line]}"
|
|
55
|
+
puts " - #{fix[:original]}"
|
|
56
|
+
puts " + #{fix[:fixed]}"
|
|
57
|
+
print "Apply this fix? [y/n/q] "
|
|
58
|
+
|
|
59
|
+
response = $stdin.gets&.chomp&.downcase
|
|
60
|
+
case response
|
|
61
|
+
when "y"
|
|
62
|
+
apply_fix(fix)
|
|
63
|
+
puts " Applied"
|
|
64
|
+
when "q"
|
|
65
|
+
puts "Aborted."
|
|
66
|
+
break
|
|
67
|
+
else
|
|
68
|
+
puts " Skipped"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def apply_all(fixes)
|
|
74
|
+
# Group by file to minimize file operations
|
|
75
|
+
fixes.group_by { |f| f[:file] }.each do |file, file_fixes|
|
|
76
|
+
lines = File.readlines(file)
|
|
77
|
+
|
|
78
|
+
file_fixes.sort_by { |f| -f[:line] }.each do |fix|
|
|
79
|
+
lines[fix[:line] - 1] = "#{fix[:fixed]}\n"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
File.write(file, lines.join)
|
|
83
|
+
puts "Fixed #{file_fixes.size} issue(s) in #{file}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def apply_fix(fix)
|
|
88
|
+
lines = File.readlines(fix[:file])
|
|
89
|
+
lines[fix[:line] - 1] = "#{fix[:fixed]}\n"
|
|
90
|
+
File.write(fix[:file], lines.join)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/eager_eye/cli.rb
CHANGED
|
@@ -16,6 +16,19 @@ module EagerEye
|
|
|
16
16
|
return 0 if options[:help] || options[:version]
|
|
17
17
|
|
|
18
18
|
issues = analyze
|
|
19
|
+
|
|
20
|
+
if options[:suggest_fixes]
|
|
21
|
+
fixer = AutoFixer.new(issues)
|
|
22
|
+
fixer.suggest
|
|
23
|
+
return 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if options[:fix]
|
|
27
|
+
fixer = AutoFixer.new(issues, interactive: !options[:force])
|
|
28
|
+
fixer.run
|
|
29
|
+
return 0
|
|
30
|
+
end
|
|
31
|
+
|
|
19
32
|
output_report(issues)
|
|
20
33
|
exit_code(issues)
|
|
21
34
|
end
|
|
@@ -31,7 +44,10 @@ module EagerEye
|
|
|
31
44
|
fail_on_issues: true,
|
|
32
45
|
colorize: $stdout.tty?,
|
|
33
46
|
help: false,
|
|
34
|
-
version: false
|
|
47
|
+
version: false,
|
|
48
|
+
suggest_fixes: false,
|
|
49
|
+
fix: false,
|
|
50
|
+
force: false
|
|
35
51
|
}
|
|
36
52
|
end
|
|
37
53
|
|
|
@@ -50,35 +66,66 @@ module EagerEye
|
|
|
50
66
|
opts.separator ""
|
|
51
67
|
opts.separator "Options:"
|
|
52
68
|
|
|
53
|
-
opts
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
add_output_options(opts)
|
|
70
|
+
add_filter_options(opts)
|
|
71
|
+
add_behavior_options(opts)
|
|
72
|
+
add_info_options(opts)
|
|
73
|
+
add_fix_options(opts)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
def add_output_options(opts)
|
|
78
|
+
opts.on("-f", "--format FORMAT", %i[console json], "Output format (console, json)") do |format|
|
|
79
|
+
options[:format] = format
|
|
80
|
+
end
|
|
60
81
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
opts.on("--no-color", "Disable colored output") do
|
|
83
|
+
options[:colorize] = false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
def add_filter_options(opts)
|
|
88
|
+
opts.on("-e", "--exclude PATTERN", "Exclude files matching pattern") do |pattern|
|
|
89
|
+
options[:exclude] << pattern
|
|
90
|
+
end
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
92
|
+
opts.on("-o", "--only DETECTORS", "Run only specified detectors (comma-separated)") do |detectors|
|
|
93
|
+
options[:only] = detectors.split(",").map(&:strip).map(&:to_sym)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
def add_behavior_options(opts)
|
|
98
|
+
opts.on("--no-fail", "Exit with 0 even if issues found") do
|
|
99
|
+
options[:fail_on_issues] = false
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def add_info_options(opts)
|
|
104
|
+
opts.on("-v", "--version", "Show version") do
|
|
105
|
+
puts "EagerEye #{EagerEye::VERSION}"
|
|
106
|
+
options[:version] = true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
110
|
+
puts opts
|
|
111
|
+
options[:help] = true
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def add_fix_options(opts)
|
|
116
|
+
opts.separator ""
|
|
117
|
+
opts.separator "Auto-fix options (experimental):"
|
|
118
|
+
|
|
119
|
+
opts.on("--suggest-fixes", "Show auto-fix suggestions") do
|
|
120
|
+
options[:suggest_fixes] = true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
opts.on("--fix", "Apply auto-fixes interactively") do
|
|
124
|
+
options[:fix] = true
|
|
125
|
+
end
|
|
77
126
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
options[:help] = true
|
|
81
|
-
end
|
|
127
|
+
opts.on("--force", "Apply fixes without confirmation (use with --fix)") do
|
|
128
|
+
options[:force] = true
|
|
82
129
|
end
|
|
83
130
|
end
|
|
84
131
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
class CommentParser
|
|
5
|
+
DISABLE_PATTERN = /eager_eye:disable(?:-next-line|-file)?\s+(.+?)(?:\s+--|$)/i
|
|
6
|
+
ENABLE_PATTERN = /eager_eye:enable\s+(.+?)(?:\s+--|$)/i
|
|
7
|
+
INLINE_DISABLE_PATTERN = /eager_eye:disable\s+(.+?)(?:\s+--|$)/i
|
|
8
|
+
FILE_DISABLE_PATTERN = /eager_eye:disable-file\s+(.+?)(?:\s+--|$)/i
|
|
9
|
+
NEXT_LINE_PATTERN = /eager_eye:disable-next-line\s+(.+?)(?:\s+--|$)/i
|
|
10
|
+
BLOCK_DISABLE_PATTERN = /eager_eye:disable\s+(.+?)(?:\s+--|$)/i
|
|
11
|
+
|
|
12
|
+
def initialize(source_code)
|
|
13
|
+
@source_code = source_code
|
|
14
|
+
@lines = source_code.lines
|
|
15
|
+
@disabled_ranges = Hash.new { |h, k| h[k] = [] }
|
|
16
|
+
@file_disabled = Set.new
|
|
17
|
+
@current_disabled = Set.new
|
|
18
|
+
parse_comments
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def disabled_at?(line_number, detector_name)
|
|
22
|
+
return true if @file_disabled.include?(detector_name.to_s)
|
|
23
|
+
return true if @file_disabled.include?("all")
|
|
24
|
+
|
|
25
|
+
detector = detector_name.to_s
|
|
26
|
+
@disabled_ranges[detector].any? { |range| range.cover?(line_number) } ||
|
|
27
|
+
@disabled_ranges["all"].any? { |range| range.cover?(line_number) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_comments
|
|
33
|
+
@lines.each_with_index do |line, index|
|
|
34
|
+
line_num = index + 1
|
|
35
|
+
process_line(line, line_num)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
close_unclosed_blocks
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def process_line(line, line_num)
|
|
42
|
+
directive = detect_directive(line, line_num)
|
|
43
|
+
apply_directive(directive, line_num) if directive
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def detect_directive(line, line_num)
|
|
47
|
+
detect_file_directive(line, line_num) ||
|
|
48
|
+
detect_next_line_directive(line) ||
|
|
49
|
+
detect_block_directive(line) ||
|
|
50
|
+
detect_enable_directive(line) ||
|
|
51
|
+
detect_inline_directive(line)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def detect_file_directive(line, line_num)
|
|
55
|
+
return unless line_num <= 5 && line =~ FILE_DISABLE_PATTERN
|
|
56
|
+
|
|
57
|
+
{ type: :file, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def detect_next_line_directive(line)
|
|
61
|
+
return unless line =~ NEXT_LINE_PATTERN
|
|
62
|
+
|
|
63
|
+
{ type: :next_line, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def detect_block_directive(line)
|
|
67
|
+
return unless line =~ BLOCK_DISABLE_PATTERN && !inline_disable?(line)
|
|
68
|
+
|
|
69
|
+
{ type: :block_start, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def detect_enable_directive(line)
|
|
73
|
+
return unless line =~ ENABLE_PATTERN
|
|
74
|
+
|
|
75
|
+
{ type: :block_end, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def detect_inline_directive(line)
|
|
79
|
+
return unless line =~ INLINE_DISABLE_PATTERN
|
|
80
|
+
|
|
81
|
+
{ type: :inline, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def apply_directive(directive, line_num)
|
|
85
|
+
case directive[:type]
|
|
86
|
+
when :file then apply_file_disable(directive[:detectors])
|
|
87
|
+
when :next_line then apply_next_line_disable(directive[:detectors], line_num)
|
|
88
|
+
when :block_start then apply_block_start(directive[:detectors], line_num)
|
|
89
|
+
when :block_end then apply_block_end(directive[:detectors], line_num)
|
|
90
|
+
when :inline then apply_inline_disable(directive[:detectors], line_num)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_file_disable(detectors)
|
|
95
|
+
@file_disabled.merge(detectors)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def apply_next_line_disable(detectors, line_num)
|
|
99
|
+
next_line = line_num + 1
|
|
100
|
+
detectors.each { |d| @disabled_ranges[d] << (next_line..next_line) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def apply_block_start(detectors, line_num)
|
|
104
|
+
detectors.each { |d| @current_disabled << { detector: d, start: line_num } }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apply_block_end(detectors, line_num)
|
|
108
|
+
detectors.each do |d|
|
|
109
|
+
entry = @current_disabled.find { |e| e[:detector] == d }
|
|
110
|
+
next unless entry
|
|
111
|
+
|
|
112
|
+
@disabled_ranges[d] << (entry[:start]..line_num)
|
|
113
|
+
@current_disabled.delete(entry)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def apply_inline_disable(detectors, line_num)
|
|
118
|
+
detectors.each { |d| @disabled_ranges[d] << (line_num..line_num) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def close_unclosed_blocks
|
|
122
|
+
@current_disabled.each do |entry|
|
|
123
|
+
@disabled_ranges[entry[:detector]] << (entry[:start]..@lines.size)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def inline_disable?(line)
|
|
128
|
+
code_part = line.split("#").first
|
|
129
|
+
code_part && !code_part.strip.empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_detector_names(str)
|
|
133
|
+
str.split(/[,\s]+/).map(&:strip).reject(&:empty?).map do |name|
|
|
134
|
+
normalize_detector_name(name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def normalize_detector_name(name)
|
|
139
|
+
name.gsub(/([A-Z])/) { "_#{::Regexp.last_match(1).downcase}" }
|
|
140
|
+
.sub(/^_/, "")
|
|
141
|
+
.downcase
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
class FixerRegistry
|
|
5
|
+
FIXERS = {
|
|
6
|
+
count_in_iteration: Fixers::CountToSize,
|
|
7
|
+
pluck_to_array: Fixers::PluckToSelect
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
def self.fixer_for(issue, source_code)
|
|
11
|
+
fixer_class = FIXERS[issue.detector]
|
|
12
|
+
return nil unless fixer_class
|
|
13
|
+
|
|
14
|
+
fixer_class.new(issue, source_code)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.fixable?(issue, source_code)
|
|
18
|
+
fixer = fixer_for(issue, source_code)
|
|
19
|
+
fixer&.fixable? || false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Fixers
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :issue, :source_lines
|
|
7
|
+
|
|
8
|
+
def initialize(issue, source_code)
|
|
9
|
+
@issue = issue
|
|
10
|
+
@source_code = source_code
|
|
11
|
+
@source_lines = source_code.lines
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def fixable?
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fix
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def diff
|
|
23
|
+
return nil unless fixable?
|
|
24
|
+
|
|
25
|
+
original_line = @source_lines[issue.line_number - 1]
|
|
26
|
+
fixed_line = fixed_content
|
|
27
|
+
return nil if original_line == fixed_line
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
file: issue.file_path,
|
|
31
|
+
line: issue.line_number,
|
|
32
|
+
original: original_line.chomp,
|
|
33
|
+
fixed: fixed_line.chomp
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def fixed_content
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def line_content
|
|
44
|
+
@source_lines[issue.line_number - 1]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Fixers
|
|
5
|
+
class CountToSize < Base
|
|
6
|
+
def fixable?
|
|
7
|
+
issue.detector == :count_in_iteration &&
|
|
8
|
+
line_content&.include?(".count")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def fixed_content
|
|
14
|
+
line_content.gsub(/\.count\b/, ".size")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Fixers
|
|
5
|
+
class PluckToSelect < Base
|
|
6
|
+
# This fixer only works for single-line pluck + where patterns
|
|
7
|
+
# Two-line patterns are too complex to fix automatically
|
|
8
|
+
|
|
9
|
+
def fixable?
|
|
10
|
+
issue.detector == :pluck_to_array &&
|
|
11
|
+
single_line_pattern?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def fixed_content
|
|
17
|
+
# Model.where(col: OtherModel.pluck(:id)) -> Model.where(col: OtherModel.select(:id))
|
|
18
|
+
line_content.gsub(/\.pluck\((:\w+)\)/, '.select(\1)')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def single_line_pattern?
|
|
24
|
+
return false unless line_content
|
|
25
|
+
|
|
26
|
+
line_content.include?(".pluck(") && line_content.include?(".where(")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -11,7 +11,13 @@ require_relative "eager_eye/detectors/custom_method_query"
|
|
|
11
11
|
require_relative "eager_eye/detectors/count_in_iteration"
|
|
12
12
|
require_relative "eager_eye/detectors/callback_query"
|
|
13
13
|
require_relative "eager_eye/detectors/pluck_to_array"
|
|
14
|
+
require_relative "eager_eye/comment_parser"
|
|
14
15
|
require_relative "eager_eye/analyzer"
|
|
16
|
+
require_relative "eager_eye/fixers/base"
|
|
17
|
+
require_relative "eager_eye/fixers/count_to_size"
|
|
18
|
+
require_relative "eager_eye/fixers/pluck_to_select"
|
|
19
|
+
require_relative "eager_eye/fixer_registry"
|
|
20
|
+
require_relative "eager_eye/auto_fixer"
|
|
15
21
|
require_relative "eager_eye/reporters/base"
|
|
16
22
|
require_relative "eager_eye/reporters/console"
|
|
17
23
|
require_relative "eager_eye/reporters/json"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eager_eye
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
@@ -58,7 +58,9 @@ files:
|
|
|
58
58
|
- exe/eager_eye
|
|
59
59
|
- lib/eager_eye.rb
|
|
60
60
|
- lib/eager_eye/analyzer.rb
|
|
61
|
+
- lib/eager_eye/auto_fixer.rb
|
|
61
62
|
- lib/eager_eye/cli.rb
|
|
63
|
+
- lib/eager_eye/comment_parser.rb
|
|
62
64
|
- lib/eager_eye/configuration.rb
|
|
63
65
|
- lib/eager_eye/detectors/base.rb
|
|
64
66
|
- lib/eager_eye/detectors/callback_query.rb
|
|
@@ -68,6 +70,10 @@ files:
|
|
|
68
70
|
- lib/eager_eye/detectors/missing_counter_cache.rb
|
|
69
71
|
- lib/eager_eye/detectors/pluck_to_array.rb
|
|
70
72
|
- lib/eager_eye/detectors/serializer_nesting.rb
|
|
73
|
+
- lib/eager_eye/fixer_registry.rb
|
|
74
|
+
- lib/eager_eye/fixers/base.rb
|
|
75
|
+
- lib/eager_eye/fixers/count_to_size.rb
|
|
76
|
+
- lib/eager_eye/fixers/pluck_to_select.rb
|
|
71
77
|
- lib/eager_eye/generators/install_generator.rb
|
|
72
78
|
- lib/eager_eye/issue.rb
|
|
73
79
|
- lib/eager_eye/railtie.rb
|