railsforge 1.0.2 → 2.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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +129 -435
  3. data/lib/railsforge/analyzers/controller_analyzer.rb +29 -55
  4. data/lib/railsforge/analyzers/database_analyzer.rb +16 -30
  5. data/lib/railsforge/analyzers/metrics_analyzer.rb +8 -22
  6. data/lib/railsforge/analyzers/model_analyzer.rb +29 -46
  7. data/lib/railsforge/analyzers/performance_analyzer.rb +34 -94
  8. data/lib/railsforge/analyzers/refactor_analyzer.rb +77 -57
  9. data/lib/railsforge/analyzers/security_analyzer.rb +34 -91
  10. data/lib/railsforge/analyzers/spec_analyzer.rb +17 -31
  11. data/lib/railsforge/cli.rb +28 -645
  12. data/lib/railsforge/cli_minimal.rb +10 -55
  13. data/lib/railsforge/diff.rb +57 -0
  14. data/lib/railsforge/doctor.rb +52 -225
  15. data/lib/railsforge/formatter.rb +128 -0
  16. data/lib/railsforge/issue.rb +23 -0
  17. data/lib/railsforge/loader.rb +5 -64
  18. data/lib/railsforge/version.rb +1 -1
  19. metadata +15 -82
  20. data/lib/railsforge/api_generator.rb +0 -397
  21. data/lib/railsforge/audit.rb +0 -289
  22. data/lib/railsforge/config.rb +0 -181
  23. data/lib/railsforge/database_analyzer.rb +0 -300
  24. data/lib/railsforge/feature_generator.rb +0 -560
  25. data/lib/railsforge/generator.rb +0 -313
  26. data/lib/railsforge/generators/api_generator.rb +0 -392
  27. data/lib/railsforge/generators/base_generator.rb +0 -75
  28. data/lib/railsforge/generators/demo_generator.rb +0 -307
  29. data/lib/railsforge/generators/devops_generator.rb +0 -287
  30. data/lib/railsforge/generators/form_generator.rb +0 -180
  31. data/lib/railsforge/generators/job_generator.rb +0 -176
  32. data/lib/railsforge/generators/monitoring_generator.rb +0 -134
  33. data/lib/railsforge/generators/policy_generator.rb +0 -220
  34. data/lib/railsforge/generators/presenter_generator.rb +0 -173
  35. data/lib/railsforge/generators/query_generator.rb +0 -174
  36. data/lib/railsforge/generators/serializer_generator.rb +0 -166
  37. data/lib/railsforge/generators/service_generator.rb +0 -122
  38. data/lib/railsforge/generators/stimulus_controller_generator.rb +0 -129
  39. data/lib/railsforge/generators/test_generator.rb +0 -289
  40. data/lib/railsforge/generators/view_component_generator.rb +0 -169
  41. data/lib/railsforge/graph.rb +0 -270
  42. data/lib/railsforge/mailer_generator.rb +0 -191
  43. data/lib/railsforge/plugins/plugin_loader.rb +0 -60
  44. data/lib/railsforge/plugins.rb +0 -30
  45. data/lib/railsforge/profiles.rb +0 -99
  46. data/lib/railsforge/refactor_analyzer.rb +0 -401
  47. data/lib/railsforge/refactor_controller.rb +0 -277
  48. data/lib/railsforge/refactors/refactor_controller.rb +0 -117
  49. data/lib/railsforge/template_loader.rb +0 -105
  50. data/lib/railsforge/templates/v1/form/spec_template.rb +0 -18
  51. data/lib/railsforge/templates/v1/form/template.rb +0 -28
  52. data/lib/railsforge/templates/v1/job/spec_template.rb +0 -17
  53. data/lib/railsforge/templates/v1/job/template.rb +0 -13
  54. data/lib/railsforge/templates/v1/policy/spec_template.rb +0 -41
  55. data/lib/railsforge/templates/v1/policy/template.rb +0 -57
  56. data/lib/railsforge/templates/v1/presenter/spec_template.rb +0 -12
  57. data/lib/railsforge/templates/v1/presenter/template.rb +0 -13
  58. data/lib/railsforge/templates/v1/query/spec_template.rb +0 -12
  59. data/lib/railsforge/templates/v1/query/template.rb +0 -16
  60. data/lib/railsforge/templates/v1/serializer/spec_template.rb +0 -13
  61. data/lib/railsforge/templates/v1/serializer/template.rb +0 -11
  62. data/lib/railsforge/templates/v1/service/spec_template.rb +0 -12
  63. data/lib/railsforge/templates/v1/service/template.rb +0 -25
  64. data/lib/railsforge/templates/v1/stimulus_controller/template.rb +0 -35
  65. data/lib/railsforge/templates/v1/view_component/template.rb +0 -24
  66. data/lib/railsforge/templates/v2/job/template.rb +0 -49
  67. data/lib/railsforge/templates/v2/query/template.rb +0 -66
  68. data/lib/railsforge/templates/v2/service/spec_template.rb +0 -33
  69. data/lib/railsforge/templates/v2/service/template.rb +0 -71
  70. data/lib/railsforge/templates/v3/job/template.rb +0 -72
  71. data/lib/railsforge/templates/v3/query/spec_template.rb +0 -54
  72. data/lib/railsforge/templates/v3/query/template.rb +0 -115
  73. data/lib/railsforge/templates/v3/service/spec_template.rb +0 -51
  74. data/lib/railsforge/templates/v3/service/template.rb +0 -93
  75. data/lib/railsforge/wizard.rb +0 -265
  76. data/lib/railsforge/wizard_tui.rb +0 -286
@@ -1,19 +1,14 @@
1
1
  # Controller analyzer for RailsForge
2
2
  # Scans controllers for code quality issues
3
3
 
4
- require_relative 'base_analyzer'
4
+ require_relative "base_analyzer"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # ControllerAnalyzer scans controllers for issues
9
8
  class ControllerAnalyzer < BaseAnalyzer
10
- # Configuration thresholds
11
- MAX_LINES = 150
9
+ MAX_LINES = 150
12
10
  MAX_METHODS = 10
13
11
 
14
- # Analyze controllers
15
- # @param base_path [String] Rails app root
16
- # @return [Array<Hash>] Analysis results
17
12
  def self.analyze(base_path = nil)
18
13
  base_path ||= find_rails_app_path
19
14
  return [] unless base_path
@@ -21,62 +16,41 @@ module RailsForge
21
16
  controllers_dir = File.join(base_path, "app", "controllers")
22
17
  return [] unless Dir.exist?(controllers_dir)
23
18
 
24
- results = []
19
+ issues = []
25
20
  Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
26
- next if file.end_with?("_application_controller.rb")
21
+ next if File.basename(file) == "application_controller.rb"
27
22
 
28
23
  content = File.read(file)
29
- lines = content.lines.count
24
+ lines = content.lines.count
30
25
  methods = content.scan(/def \w+/).count
31
-
32
- issues = []
33
- issues << "Exceeds #{MAX_LINES} lines" if lines > MAX_LINES
34
- issues << "Has #{methods} methods (max: #{MAX_METHODS})" if methods > MAX_METHODS
35
-
36
- if issues.any?
37
- results << {
38
- file: File.basename(file),
39
- path: file,
40
- lines: lines,
41
- methods: methods,
42
- issues: issues
43
- }
26
+ rel = file.delete_prefix("#{base_path}/")
27
+
28
+ if lines > MAX_LINES
29
+ issues << RailsForge::Issue.new(
30
+ analyzer: :controller,
31
+ type: :fat_controller,
32
+ severity: "medium",
33
+ file: rel,
34
+ line: nil,
35
+ message: "#{File.basename(file)} has #{lines} lines (max #{MAX_LINES})",
36
+ suggestion: "Extract business logic to Service objects"
37
+ )
44
38
  end
45
- end
46
39
 
47
- results
48
- end
49
-
50
- # Print results
51
- def self.print_results(results)
52
- puts "\nController Analysis"
53
- puts "-" * 40
54
-
55
- if results.empty?
56
- puts "✓ No issues found"
57
- return
58
- end
59
-
60
- results.each do |result|
61
- puts "⚠ #{result[:file]}"
62
- puts " Lines: #{result[:lines]}, Methods: #{result[:methods]}"
63
- result[:issues].each { |issue| puts " - #{issue}" }
64
- end
65
- end
66
-
67
- # Find Rails app path
68
- def self.find_rails_app_path
69
- path = Dir.pwd
70
- max_depth = 10
71
-
72
- max_depth.times do
73
- return path if File.exist?(File.join(path, "config", "application.rb"))
74
- parent = File.dirname(path)
75
- break if parent == path
76
- path = parent
40
+ if methods > MAX_METHODS
41
+ issues << RailsForge::Issue.new(
42
+ analyzer: :controller,
43
+ type: :too_many_methods,
44
+ severity: "low",
45
+ file: rel,
46
+ line: nil,
47
+ message: "#{File.basename(file)} has #{methods} methods (max #{MAX_METHODS})",
48
+ suggestion: "Consider splitting into focused controllers"
49
+ )
50
+ end
77
51
  end
78
52
 
79
- nil
53
+ issues
80
54
  end
81
55
  end
82
56
  end
@@ -1,54 +1,40 @@
1
1
  # Database analyzer for RailsForge
2
2
  # Scans database schema for issues
3
3
 
4
- require_relative 'base_analyzer'
4
+ require_relative "base_analyzer"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # DatabaseAnalyzer scans database/schema for issues
9
8
  class DatabaseAnalyzer < BaseAnalyzer
10
9
  class DatabaseError < StandardError; end
11
10
 
12
- # Analyze database
13
11
  def self.analyze(base_path = nil)
14
12
  base_path ||= find_rails_app_path
15
13
  raise DatabaseError, "Not in a Rails application" unless base_path
16
14
 
17
- results = []
18
15
  schema_file = File.join(base_path, "db", "schema.rb")
19
-
20
- return results unless File.exist?(schema_file)
16
+ return [] unless File.exist?(schema_file)
21
17
 
22
18
  content = File.read(schema_file)
23
- tables = content.scan(/create_table\s+"(\w+)"/).flatten
19
+ tables = content.scan(/create_table\s+"(\w+)"/).flatten
20
+ issues = []
24
21
 
25
22
  tables.each do |table|
26
23
  table_section = content[/create_table\s+"#{table}".*?(?=create_table|\z)/m]
27
- if table_section && table_section.include?("t.datetime")
28
- results << {
29
- type: :index,
30
- table: table,
31
- issue: "Consider adding index on datetime columns",
32
- suggestion: "add_index :#{table}, :created_at"
33
- }
34
- end
24
+ next unless table_section&.include?("t.datetime")
25
+
26
+ issues << RailsForge::Issue.new(
27
+ analyzer: :database,
28
+ type: :missing_index,
29
+ severity: "medium",
30
+ file: "db/schema.rb",
31
+ line: nil,
32
+ message: "Table `#{table}` has datetime columns without an index",
33
+ suggestion: "add_index :#{table}, :created_at"
34
+ )
35
35
  end
36
36
 
37
- results
38
- end
39
-
40
- def self.print_report(results)
41
- puts "\nDatabase Analysis"
42
- puts "-" * 40
43
-
44
- if results.empty?
45
- puts "✓ No issues found"
46
- return
47
- end
48
-
49
- results.each do |result|
50
- puts "⚠ #{result[:table]}: #{result[:issue]}"
51
- end
37
+ issues
52
38
  end
53
39
  end
54
40
  end
@@ -1,36 +1,31 @@
1
1
  # Metrics analyzer for RailsForge
2
- # Provides comprehensive metrics analysis
2
+ # Provides project-level metrics summary (not issues)
3
3
 
4
- require_relative 'base_analyzer'
4
+ require_relative "base_analyzer"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # MetricsAnalyzer provides code metrics
9
8
  class MetricsAnalyzer < BaseAnalyzer
10
9
  GENERATOR_TYPES = %w[services queries forms presenters policies serializers jobs].freeze
11
10
 
12
- # Analyze metrics
13
11
  def self.analyze(base_path = nil)
14
12
  base_path ||= find_rails_app_path
15
13
  return {} unless base_path
16
14
 
17
15
  {
18
16
  generator_counts: count_files(base_path),
19
- total_files: count_all_files(base_path),
20
- services_count: count_in_folder(base_path, "app/services"),
21
- queries_count: count_in_folder(base_path, "app/queries"),
22
- jobs_count: count_in_folder(base_path, "app/jobs")
17
+ total_files: count_all_files(base_path),
18
+ services_count: count_in_folder(base_path, "app/services"),
19
+ queries_count: count_in_folder(base_path, "app/queries"),
20
+ jobs_count: count_in_folder(base_path, "app/jobs")
23
21
  }
24
22
  end
25
23
 
26
24
  def self.count_files(base_path)
27
- counts = {}
28
- GENERATOR_TYPES.each do |type|
29
- folder = "app/#{type.chop}"
30
- folder = "app/jobs" if type == "jobs"
25
+ GENERATOR_TYPES.each_with_object({}) do |type, counts|
26
+ folder = type == "jobs" ? "app/jobs" : "app/#{type.chop}"
31
27
  counts[type] = count_in_folder(base_path, folder)
32
28
  end
33
- counts
34
29
  end
35
30
 
36
31
  def self.count_in_folder(base_path, folder)
@@ -41,15 +36,6 @@ module RailsForge
41
36
  def self.count_all_files(base_path)
42
37
  Dir.glob(File.join(base_path, "app", "**", "*.rb")).count
43
38
  end
44
-
45
- def self.print_results(results)
46
- puts "\nMetrics Analysis"
47
- puts "-" * 40
48
- puts "Total Files: #{results[:total_files]}"
49
- puts "Services: #{results[:services_count]}"
50
- puts "Queries: #{results[:queries_count]}"
51
- puts "Jobs: #{results[:jobs_count]}"
52
- end
53
39
  end
54
40
  end
55
41
  end
@@ -1,16 +1,14 @@
1
1
  # Model analyzer for RailsForge
2
2
  # Scans models for code quality issues
3
3
 
4
- require_relative 'base_analyzer'
4
+ require_relative "base_analyzer"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # ModelAnalyzer scans models for issues
9
8
  class ModelAnalyzer < BaseAnalyzer
9
+ MAX_LINES = 200
10
10
  MAX_METHODS = 15
11
- MAX_LINES = 200
12
11
 
13
- # Analyze models
14
12
  def self.analyze(base_path = nil)
15
13
  base_path ||= find_rails_app_path
16
14
  return [] unless base_path
@@ -18,56 +16,41 @@ module RailsForge
18
16
  models_dir = File.join(base_path, "app", "models")
19
17
  return [] unless Dir.exist?(models_dir)
20
18
 
21
- results = []
19
+ issues = []
22
20
  Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
23
- next if file.end_with?("_application.rb")
21
+ next if File.basename(file) == "application_record.rb"
24
22
 
25
23
  content = File.read(file)
26
- lines = content.lines.count
24
+ lines = content.lines.count
27
25
  methods = content.scan(/def \w+/).count
28
-
29
- issues = []
30
- issues << "Exceeds #{MAX_LINES} lines" if lines > MAX_LINES
31
- issues << "Has #{methods} methods" if methods > MAX_METHODS
32
-
33
- if issues.any?
34
- results << {
35
- file: File.basename(file),
36
- path: file,
37
- lines: lines,
38
- methods: methods,
39
- issues: issues
40
- }
26
+ rel = file.delete_prefix("#{base_path}/")
27
+
28
+ if lines > MAX_LINES
29
+ issues << RailsForge::Issue.new(
30
+ analyzer: :model,
31
+ type: :fat_model,
32
+ severity: "medium",
33
+ file: rel,
34
+ line: nil,
35
+ message: "#{File.basename(file)} has #{lines} lines (max #{MAX_LINES})",
36
+ suggestion: "Consider extracting concerns or service objects"
37
+ )
41
38
  end
42
- end
43
-
44
- results
45
- end
46
-
47
- def self.print_results(results)
48
- puts "\nModel Analysis"
49
- puts "-" * 40
50
39
 
51
- if results.empty?
52
- puts "✓ No issues found"
53
- return
40
+ if methods > MAX_METHODS
41
+ issues << RailsForge::Issue.new(
42
+ analyzer: :model,
43
+ type: :too_many_methods,
44
+ severity: "low",
45
+ file: rel,
46
+ line: nil,
47
+ message: "#{File.basename(file)} has #{methods} methods (max #{MAX_METHODS})",
48
+ suggestion: "Consider extracting business logic to service or concern"
49
+ )
50
+ end
54
51
  end
55
52
 
56
- results.each do |result|
57
- puts "⚠ #{result[:file]}"
58
- result[:issues].each { |issue| puts " - #{issue}" }
59
- end
60
- end
61
-
62
- def self.find_rails_app_path
63
- path = Dir.pwd
64
- 10.times do
65
- return path if File.exist?(File.join(path, "config", "application.rb"))
66
- parent = File.dirname(path)
67
- break if parent == path
68
- path = parent
69
- end
70
- nil
53
+ issues
71
54
  end
72
55
  end
73
56
  end
@@ -1,25 +1,29 @@
1
1
  # Performance analyzer for RailsForge
2
2
  # Checks for common performance issues
3
3
 
4
- require 'fileutils'
4
+ require "fileutils"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # PerformanceAnalyzer scans code for performance problems
9
8
  class PerformanceAnalyzer < BaseAnalyzer
10
- # Performance issue types
11
9
  ISSUE_TYPES = {
12
- n_plus_one: "N+1 Query",
13
- missing_index: "Missing Database Index",
14
- slow_method: "Slow Method",
10
+ n_plus_one: "N+1 Query",
11
+ missing_index: "Missing Database Index",
12
+ slow_method: "Slow Method",
15
13
  inefficient_query: "Inefficient Query",
16
- cache_miss: "Cache Miss",
17
- eager_loading: "Unnecessary Eager Loading"
14
+ cache_miss: "Cache Miss",
15
+ eager_loading: "Unnecessary Eager Loading"
16
+ }.freeze
17
+
18
+ SUGGESTIONS = {
19
+ n_plus_one: "Use eager loading: includes(:association) or preload(:association)",
20
+ missing_index: "Add a database index to the foreign key column",
21
+ slow_method: "Consider async processing or caching for slow operations",
22
+ inefficient_query: "Refactor query to avoid loading unnecessary records",
23
+ cache_miss: "Consider caching with Rails.cache or HTTP caching",
24
+ eager_loading: "Remove unnecessary includes() if association is not used"
18
25
  }.freeze
19
26
 
20
- # Analyze Rails app for performance issues
21
- # @param base_path [String] Rails app root path
22
- # @return [Array<Hash>] Performance issues found
23
27
  def self.analyze(base_path = nil)
24
28
  base_path ||= find_rails_app_path
25
29
  return [] unless base_path
@@ -27,133 +31,69 @@ module RailsForge
27
31
  new(base_path).find_issues
28
32
  end
29
33
 
30
- # Initialize analyzer
31
- # @param base_path [String] Rails app root path
32
34
  def initialize(base_path)
33
35
  @base_path = base_path
34
36
  @issues = []
35
37
  end
36
38
 
37
- # Find all performance issues
38
- # @return [Array<Hash>] Issues found
39
39
  def find_issues
40
40
  check_n_plus_one
41
41
  check_missing_indexes
42
42
  check_slow_methods
43
43
  check_inefficient_queries
44
-
45
44
  @issues
46
45
  end
47
46
 
48
- # Print performance report
49
- # @param issues [Array<Hash>] Issues to print
50
- def self.print_report(issues)
51
- puts "Performance Analysis"
52
- puts "=" * 50
53
-
54
- if issues.empty?
55
- puts "✓ No performance issues found"
56
- return
57
- end
58
-
59
- puts "Found #{issues.count} performance issue(s):"
60
- puts ""
61
-
62
- issues.each do |issue|
63
- puts "⚠ #{issue[:type]}"
64
- puts " File: #{issue[:file]}"
65
- puts " Line: #{issue[:line]}"
66
- puts " Issue: #{issue[:message]}"
67
- puts ""
68
- end
69
- end
70
-
71
47
  private
72
48
 
73
- # Check for N+1 query problems
74
49
  def check_n_plus_one
75
- # Look for loops with database queries inside
76
- patterns = [
77
- /\.each\s+do.*\.find/,
78
- /\.map.*\.where/,
79
- /@.*\.all\.each/
80
- ]
81
-
50
+ patterns = [/\.each\s+do.*\.find/, /\.map.*\.where/, /@.*\.all\.each/]
82
51
  scan_files("app/controllers/**/*.rb", patterns, :n_plus_one)
83
52
  scan_files("app/views/**/*.erb", patterns, :n_plus_one)
84
53
  end
85
54
 
86
- # Check for missing database indexes
87
55
  def check_missing_indexes
88
- # Look for foreign keys without indexes
89
- patterns = [
90
- /belongs_to\s+:\w+/,
91
- /add_reference/
92
- ]
93
-
56
+ patterns = [/belongs_to\s+:\w+/, /add_reference/]
94
57
  scan_files("db/migrate/**/*.rb", patterns, :missing_index)
95
58
  end
96
59
 
97
- # Check for slow methods
98
60
  def check_slow_methods
99
- # Look for potentially slow operations
100
- patterns = [
101
- /\.each\s+do\s*\n.*\.save/,
102
- /Process\.spawn/,
103
- /\.read/,
104
- /\.open\(.*\)/
105
- ]
106
-
61
+ patterns = [/\.each\s+do\s*\n.*\.save/, /Process\.spawn/, /\.read/, /\.open\(.*\)/]
107
62
  scan_files("app/**/*.rb", patterns, :slow_method)
108
63
  end
109
64
 
110
- # Check for inefficient queries
111
65
  def check_inefficient_queries
112
- # Look for inefficient query patterns
113
- patterns = [
114
- /\.order\(.*\)\.last/,
115
- /\.where\(.*\)\.first\! /,
116
- /\.all\.to_a/,
117
- /\.select\(.+\)\.map\//
118
- ]
119
-
66
+ patterns = [/\.order\(.*\)\.last/, /\.where\(.*\)\.first\! /, /\.all\.to_a/, /\.select\(.+\)\.map\//]
120
67
  scan_files("app/models/**/*.rb", patterns, :inefficient_query)
121
68
  scan_files("app/controllers/**/*.rb", patterns, :inefficient_query)
122
69
  end
123
70
 
124
- # Scan files for patterns
125
71
  def scan_files(glob, patterns, issue_type)
126
72
  Dir.glob(File.join(@base_path, glob)).each do |file|
127
- content = File.read(file)
128
- lines = content.lines
129
-
73
+ lines = File.read(file).lines
130
74
  lines.each_with_index do |line, index|
131
75
  patterns.each do |pattern|
132
- if line =~ pattern
133
- @issues << {
134
- type: ISSUE_TYPES[issue_type],
135
- file: file.gsub(@base_path, ""),
136
- line: index + 1,
137
- message: line.strip,
138
- severity: severity_for(issue_type)
139
- }
140
- end
76
+ next unless line =~ pattern
77
+
78
+ @issues << RailsForge::Issue.new(
79
+ analyzer: :performance,
80
+ type: issue_type,
81
+ severity: severity_for(issue_type),
82
+ file: file.delete_prefix("#{@base_path}/"),
83
+ line: index + 1,
84
+ message: "#{ISSUE_TYPES[issue_type]}: #{line.strip}",
85
+ suggestion: SUGGESTIONS[issue_type]
86
+ )
141
87
  end
142
88
  end
143
89
  end
144
90
  end
145
91
 
146
- # Get severity for issue type
147
92
  def severity_for(issue_type)
148
93
  case issue_type
149
- when :n_plus_one
150
- "high"
151
- when :missing_index
152
- "high"
153
- when :inefficient_query
154
- "medium"
155
- else
156
- "low"
94
+ when :n_plus_one, :missing_index then "high"
95
+ when :inefficient_query then "medium"
96
+ else "low"
157
97
  end
158
98
  end
159
99
  end