eager_eye 0.5.0 → 0.6.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 +19 -0
- data/README.md +44 -0
- data/lib/eager_eye/analyzer.rb +8 -0
- data/lib/eager_eye/comment_parser.rb +144 -0
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34e0c9e78e18d9d8c9a6cad8ee96a91c89ac469d9dc662e43a51f09d361a3200
|
|
4
|
+
data.tar.gz: 90554fbbe09380116398c8139c6d09ca5851d1e12d2eb52c32cf85f8c8fdc76a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86605c1632ed47aaae05e26d810c6611dac2f356503502b71a94a389b1d58148cbb5146e5e6cbc5e9f809f4b6bf2b78d983cbf8484c0bab6d1e9a046ab0a242b
|
|
7
|
+
data.tar.gz: bb3e07e2b8e799067aa475f6b8ffea6a303e797e3eac252f46f597eec751f42451f91fd97eacf024aae8a6129267ef2b425fdd26163a6b893714cb22c2d9ddfa
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2025-12-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Inline Suppression Comments** - RuboCop-style comment directives for suppressing false positives
|
|
15
|
+
- `# eager_eye:disable DetectorName` - Disable for single line (inline) or start block
|
|
16
|
+
- `# eager_eye:disable-next-line DetectorName` - Disable only the next line
|
|
17
|
+
- `# eager_eye:disable-file DetectorName` - Disable for entire file (must be in first 5 lines)
|
|
18
|
+
- `# eager_eye:enable DetectorName` - End a disable block
|
|
19
|
+
- Support for multiple detectors: `# eager_eye:disable LoopAssociation, CountInIteration`
|
|
20
|
+
- Support for reason comments: `# eager_eye:disable DetectorName -- reason here`
|
|
21
|
+
- `all` keyword to disable all detectors at once
|
|
22
|
+
- Both CamelCase (`LoopAssociation`) and snake_case (`loop_association`) detector names accepted
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Updated README with inline suppression documentation
|
|
27
|
+
- Added `CommentParser` module for parsing suppression directives
|
|
28
|
+
|
|
10
29
|
## [0.5.0] - 2025-12-15
|
|
11
30
|
|
|
12
31
|
### Added
|
data/README.md
CHANGED
|
@@ -272,6 +272,50 @@ 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
|
+
|
|
275
319
|
## Configuration
|
|
276
320
|
|
|
277
321
|
### 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,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
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -11,6 +11,7 @@ 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"
|
|
15
16
|
require_relative "eager_eye/reporters/base"
|
|
16
17
|
require_relative "eager_eye/reporters/console"
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/eager_eye.rb
|
|
60
60
|
- lib/eager_eye/analyzer.rb
|
|
61
61
|
- lib/eager_eye/cli.rb
|
|
62
|
+
- lib/eager_eye/comment_parser.rb
|
|
62
63
|
- lib/eager_eye/configuration.rb
|
|
63
64
|
- lib/eager_eye/detectors/base.rb
|
|
64
65
|
- lib/eager_eye/detectors/callback_query.rb
|