eager_eye 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34e0c9e78e18d9d8c9a6cad8ee96a91c89ac469d9dc662e43a51f09d361a3200
4
- data.tar.gz: 90554fbbe09380116398c8139c6d09ca5851d1e12d2eb52c32cf85f8c8fdc76a
3
+ metadata.gz: a8d34dc13a536c66ca82e9495ba5e53b81c31f1e2764dea3ba8d64c434da69ec
4
+ data.tar.gz: d63753e0d80c366cc9b87fe6bcc5bc2f2ee1ba537e7363c2b6aadafffd5a4a05
5
5
  SHA512:
6
- metadata.gz: 86605c1632ed47aaae05e26d810c6611dac2f356503502b71a94a389b1d58148cbb5146e5e6cbc5e9f809f4b6bf2b78d983cbf8484c0bab6d1e9a046ab0a242b
7
- data.tar.gz: bb3e07e2b8e799067aa475f6b8ffea6a303e797e3eac252f46f597eec751f42451f91fd97eacf024aae8a6129267ef2b425fdd26163a6b893714cb22c2d9ddfa
6
+ metadata.gz: bf3ff2ba24314b8b6e22385c82685d8909729f12ffc9d48531bd7fe4c6a1683e2aadb5692934ec300430dfdbc0cde795489e9a68a98072658a9452cd8e6e338b
7
+ data.tar.gz: fd73786c8c983b2bcc3d3860bf1036dc1525523dcce8ef06767760dfce57b813ee415679fa42cc7631830e8c4780f0bbbdecc61ae4563b964a983ada5c2c0d42
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ 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
+
10
25
  ## [0.6.0] - 2025-12-15
11
26
 
12
27
  ### Added
data/README.md CHANGED
@@ -316,6 +316,48 @@ 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
+
319
361
  ## Configuration
320
362
 
321
363
  ### 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.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.7.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,6 +70,10 @@ 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