eager_eye 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34e0c9e78e18d9d8c9a6cad8ee96a91c89ac469d9dc662e43a51f09d361a3200
4
- data.tar.gz: 90554fbbe09380116398c8139c6d09ca5851d1e12d2eb52c32cf85f8c8fdc76a
3
+ metadata.gz: 39f1ea38bf95b25a50a9dc854b1727811ffeadaeb5724e99f3e6160b2867dd79
4
+ data.tar.gz: db049f2205a44e3317d891d2c6c2dbda8f716562916bde05e151b97e532181af
5
5
  SHA512:
6
- metadata.gz: 86605c1632ed47aaae05e26d810c6611dac2f356503502b71a94a389b1d58148cbb5146e5e6cbc5e9f809f4b6bf2b78d983cbf8484c0bab6d1e9a046ab0a242b
7
- data.tar.gz: bb3e07e2b8e799067aa475f6b8ffea6a303e797e3eac252f46f597eec751f42451f91fd97eacf024aae8a6129267ef2b425fdd26163a6b893714cb22c2d9ddfa
6
+ metadata.gz: bb4e89fa8d8206a556c9ca7d12d33ee0412091aba756abc0d0581b8ca20907898eb0ed74940f75c2afae727ed2825dd65472222edd566d9cfa8a8978969f0d3e
7
+ data.tar.gz: b34651d34df75eafa06175af3f715948995b9a2896910a4e4ec69f9c7464a1f4d7fdbf1efb844e34b50fa2fb9d5a4f55da8089fb0b1b50aef767bff70695e327
data/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2025-12-16
11
+
12
+ ### Added
13
+
14
+ - **RSpec Integration** - RSpec matchers for testing your codebase
15
+ - `pass_eager_eye` matcher for testing files and directories
16
+ - `only` option to run specific detectors
17
+ - `exclude` option to exclude files by glob pattern
18
+ - `max_issues` option for gradual migration (allows up to N issues)
19
+ - `require "eager_eye/rspec"` for easy integration
20
+ - Helpful failure messages with issue details
21
+
22
+ ## [0.7.0] - 2025-12-15
23
+
24
+ ### Added
25
+
26
+ - **Auto-fix Suggestions (Experimental)** - Automatic fix capabilities for simple issues
27
+ - `--suggest-fixes` flag to show auto-fix suggestions in diff format
28
+ - `--fix` flag to apply auto-fixes interactively
29
+ - `--fix --force` to apply all fixes without confirmation
30
+ - Fixer for `.count` → `.size` transformation in iterations
31
+ - Fixer for inline `.pluck(:id)` → `.select(:id)` transformation
32
+
33
+ ### Note
34
+
35
+ Auto-fix is experimental. Not all issues are auto-fixable. Always review changes and run your test suite after applying fixes.
36
+
10
37
  ## [0.6.0] - 2025-12-15
11
38
 
12
39
  ### Added
data/README.md CHANGED
@@ -316,6 +316,88 @@ Both CamelCase and snake_case formats are accepted:
316
316
  | Pluck to Array | `PluckToArray` | `pluck_to_array` |
317
317
  | All Detectors | `all` | `all` |
318
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
+
361
+ ## RSpec Integration
362
+
363
+ EagerEye provides RSpec matchers for testing your codebase:
364
+
365
+ ```ruby
366
+ # spec/rails_helper.rb
367
+ require "eager_eye/rspec"
368
+
369
+ # spec/eager_eye_spec.rb
370
+ RSpec.describe "EagerEye Analysis" do
371
+ it "controllers have no N+1 issues" do
372
+ expect("app/controllers").to pass_eager_eye
373
+ end
374
+
375
+ it "serializers are clean" do
376
+ expect("app/serializers").to pass_eager_eye(only: [:serializer_nesting])
377
+ end
378
+
379
+ # Allow some issues during migration
380
+ it "legacy code is acceptable" do
381
+ expect("app/services/legacy").to pass_eager_eye(max_issues: 10)
382
+ end
383
+
384
+ it "models have no callback issues except legacy" do
385
+ expect("app/models").to pass_eager_eye(
386
+ only: [:callback_query],
387
+ exclude: ["app/models/legacy/**"]
388
+ )
389
+ end
390
+ end
391
+ ```
392
+
393
+ ### Matcher Options
394
+
395
+ | Option | Type | Description |
396
+ |--------|------|-------------|
397
+ | `only` | `Array<Symbol>` | Run only specified detectors |
398
+ | `exclude` | `Array<String>` | Glob patterns to exclude |
399
+ | `max_issues` | `Integer` | Maximum allowed issues (default: 0) |
400
+
319
401
  ## Configuration
320
402
 
321
403
  ### Config File (.eager_eye.yml)
@@ -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.on("-f", "--format FORMAT", %i[console json], "Output format (console, json)") do |format|
54
- options[:format] = format
55
- end
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
- opts.on("-e", "--exclude PATTERN", "Exclude files matching pattern (can be used multiple times)") do |pattern|
58
- options[:exclude] << pattern
59
- end
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
- opts.on("-o", "--only DETECTORS", "Run only specified detectors (comma-separated)") do |detectors|
62
- options[:only] = detectors.split(",").map(&:strip).map(&:to_sym)
63
- end
82
+ opts.on("--no-color", "Disable colored output") do
83
+ options[:colorize] = false
84
+ end
85
+ end
64
86
 
65
- opts.on("--no-fail", "Exit with 0 even if issues found") do
66
- options[:fail_on_issues] = false
67
- end
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
- opts.on("--no-color", "Disable colored output") do
70
- options[:colorize] = false
71
- end
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
- opts.on("-v", "--version", "Show version") do
74
- puts "EagerEye #{EagerEye::VERSION}"
75
- options[:version] = true
76
- end
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
- opts.on("-h", "--help", "Show this help message") do
79
- puts opts
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,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
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ module RSpec
5
+ module Matchers
6
+ def pass_eager_eye(options = {})
7
+ PassEagerEyeMatcher.new(options)
8
+ end
9
+
10
+ class PassEagerEyeMatcher
11
+ def initialize(options = {})
12
+ @only = options[:only]
13
+ @exclude = options[:exclude] || []
14
+ @max_issues = options[:max_issues] || 0
15
+ @issues = []
16
+ @path = nil
17
+ end
18
+
19
+ def matches?(path)
20
+ @path = path
21
+ configure_eager_eye
22
+ analyzer = build_analyzer
23
+ @issues = analyzer.run
24
+ @issues.count <= @max_issues
25
+ end
26
+
27
+ def failure_message
28
+ message = "expected #{@path} to pass EagerEye analysis"
29
+ message += " (max #{@max_issues} issues)" if @max_issues.positive?
30
+ message += ", but found #{@issues.count} issue(s):\n\n"
31
+
32
+ @issues.group_by(&:file_path).each do |file, file_issues|
33
+ message += "#{file}:\n"
34
+ file_issues.each do |issue|
35
+ message += " Line #{issue.line_number}: [#{issue.detector}] #{issue.message}\n"
36
+ end
37
+ message += "\n"
38
+ end
39
+
40
+ message
41
+ end
42
+
43
+ def failure_message_when_negated
44
+ "expected #{@path} to have EagerEye issues, but it passed"
45
+ end
46
+
47
+ def description
48
+ desc = "pass EagerEye analysis"
49
+ desc += " for #{@only.join(", ")}" if @only
50
+ desc += " (max #{@max_issues} issues)" if @max_issues.positive?
51
+ desc
52
+ end
53
+
54
+ private
55
+
56
+ def configure_eager_eye
57
+ EagerEye.reset_configuration!
58
+ EagerEye.configure do |config|
59
+ config.enabled_detectors = @only if @only
60
+ config.excluded_paths = @exclude unless @exclude.empty?
61
+ config.fail_on_issues = false
62
+ end
63
+ end
64
+
65
+ def build_analyzer
66
+ EagerEye::Analyzer.new(paths: [@path])
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eager_eye"
4
+ require "eager_eye/rspec/matchers"
5
+
6
+ RSpec.configure do |config|
7
+ config.include EagerEye::RSpec::Matchers
8
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/eager_eye.rb CHANGED
@@ -13,6 +13,11 @@ require_relative "eager_eye/detectors/callback_query"
13
13
  require_relative "eager_eye/detectors/pluck_to_array"
14
14
  require_relative "eager_eye/comment_parser"
15
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"
16
21
  require_relative "eager_eye/reporters/base"
17
22
  require_relative "eager_eye/reporters/console"
18
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.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
@@ -58,6 +58,7 @@ 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
62
63
  - lib/eager_eye/comment_parser.rb
63
64
  - lib/eager_eye/configuration.rb
@@ -69,12 +70,18 @@ files:
69
70
  - lib/eager_eye/detectors/missing_counter_cache.rb
70
71
  - lib/eager_eye/detectors/pluck_to_array.rb
71
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
72
77
  - lib/eager_eye/generators/install_generator.rb
73
78
  - lib/eager_eye/issue.rb
74
79
  - lib/eager_eye/railtie.rb
75
80
  - lib/eager_eye/reporters/base.rb
76
81
  - lib/eager_eye/reporters/console.rb
77
82
  - lib/eager_eye/reporters/json.rb
83
+ - lib/eager_eye/rspec.rb
84
+ - lib/eager_eye/rspec/matchers.rb
78
85
  - lib/eager_eye/version.rb
79
86
  - sig/eager_eye.rbs
80
87
  homepage: https://github.com/hamzagedikkaya/eager_eye