railsforge 1.0.2 → 2.0.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +105 -444
  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 +14 -650
  12. data/lib/railsforge/cli_minimal.rb +8 -55
  13. data/lib/railsforge/doctor.rb +52 -225
  14. data/lib/railsforge/formatter.rb +102 -0
  15. data/lib/railsforge/issue.rb +23 -0
  16. data/lib/railsforge/loader.rb +4 -64
  17. data/lib/railsforge/version.rb +1 -1
  18. metadata +14 -82
  19. data/lib/railsforge/api_generator.rb +0 -397
  20. data/lib/railsforge/audit.rb +0 -289
  21. data/lib/railsforge/config.rb +0 -181
  22. data/lib/railsforge/database_analyzer.rb +0 -300
  23. data/lib/railsforge/feature_generator.rb +0 -560
  24. data/lib/railsforge/generator.rb +0 -313
  25. data/lib/railsforge/generators/api_generator.rb +0 -392
  26. data/lib/railsforge/generators/base_generator.rb +0 -75
  27. data/lib/railsforge/generators/demo_generator.rb +0 -307
  28. data/lib/railsforge/generators/devops_generator.rb +0 -287
  29. data/lib/railsforge/generators/form_generator.rb +0 -180
  30. data/lib/railsforge/generators/job_generator.rb +0 -176
  31. data/lib/railsforge/generators/monitoring_generator.rb +0 -134
  32. data/lib/railsforge/generators/policy_generator.rb +0 -220
  33. data/lib/railsforge/generators/presenter_generator.rb +0 -173
  34. data/lib/railsforge/generators/query_generator.rb +0 -174
  35. data/lib/railsforge/generators/serializer_generator.rb +0 -166
  36. data/lib/railsforge/generators/service_generator.rb +0 -122
  37. data/lib/railsforge/generators/stimulus_controller_generator.rb +0 -129
  38. data/lib/railsforge/generators/test_generator.rb +0 -289
  39. data/lib/railsforge/generators/view_component_generator.rb +0 -169
  40. data/lib/railsforge/graph.rb +0 -270
  41. data/lib/railsforge/mailer_generator.rb +0 -191
  42. data/lib/railsforge/plugins/plugin_loader.rb +0 -60
  43. data/lib/railsforge/plugins.rb +0 -30
  44. data/lib/railsforge/profiles.rb +0 -99
  45. data/lib/railsforge/refactor_analyzer.rb +0 -401
  46. data/lib/railsforge/refactor_controller.rb +0 -277
  47. data/lib/railsforge/refactors/refactor_controller.rb +0 -117
  48. data/lib/railsforge/template_loader.rb +0 -105
  49. data/lib/railsforge/templates/v1/form/spec_template.rb +0 -18
  50. data/lib/railsforge/templates/v1/form/template.rb +0 -28
  51. data/lib/railsforge/templates/v1/job/spec_template.rb +0 -17
  52. data/lib/railsforge/templates/v1/job/template.rb +0 -13
  53. data/lib/railsforge/templates/v1/policy/spec_template.rb +0 -41
  54. data/lib/railsforge/templates/v1/policy/template.rb +0 -57
  55. data/lib/railsforge/templates/v1/presenter/spec_template.rb +0 -12
  56. data/lib/railsforge/templates/v1/presenter/template.rb +0 -13
  57. data/lib/railsforge/templates/v1/query/spec_template.rb +0 -12
  58. data/lib/railsforge/templates/v1/query/template.rb +0 -16
  59. data/lib/railsforge/templates/v1/serializer/spec_template.rb +0 -13
  60. data/lib/railsforge/templates/v1/serializer/template.rb +0 -11
  61. data/lib/railsforge/templates/v1/service/spec_template.rb +0 -12
  62. data/lib/railsforge/templates/v1/service/template.rb +0 -25
  63. data/lib/railsforge/templates/v1/stimulus_controller/template.rb +0 -35
  64. data/lib/railsforge/templates/v1/view_component/template.rb +0 -24
  65. data/lib/railsforge/templates/v2/job/template.rb +0 -49
  66. data/lib/railsforge/templates/v2/query/template.rb +0 -66
  67. data/lib/railsforge/templates/v2/service/spec_template.rb +0 -33
  68. data/lib/railsforge/templates/v2/service/template.rb +0 -71
  69. data/lib/railsforge/templates/v3/job/template.rb +0 -72
  70. data/lib/railsforge/templates/v3/query/spec_template.rb +0 -54
  71. data/lib/railsforge/templates/v3/query/template.rb +0 -115
  72. data/lib/railsforge/templates/v3/service/spec_template.rb +0 -51
  73. data/lib/railsforge/templates/v3/service/template.rb +0 -93
  74. data/lib/railsforge/wizard.rb +0 -265
  75. data/lib/railsforge/wizard_tui.rb +0 -286
@@ -1,20 +1,22 @@
1
1
  # Refactor analyzer for RailsForge
2
2
  # Provides refactoring suggestions
3
3
 
4
- require_relative 'base_analyzer'
4
+ require_relative "base_analyzer"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # RefactorAnalyzer provides refactoring suggestions
9
8
  class RefactorAnalyzer < BaseAnalyzer
10
9
  class RefactorError < StandardError; end
11
10
 
12
- CONTROLLER_MAX_LINES = 150
11
+ CONTROLLER_MAX_LINES = 150
13
12
  CONTROLLER_MAX_METHODS = 10
14
- MODEL_MAX_LINES = 200
13
+ MODEL_MAX_LINES = 200
15
14
  MODEL_MAX_METHOD_LINES = 15
16
15
 
17
- # Analyze controllers
16
+ def self.analyze(base_path = nil)
17
+ analyze_controllers(base_path) + analyze_models(base_path)
18
+ end
19
+
18
20
  def self.analyze_controllers(base_path = nil)
19
21
  base_path ||= find_rails_app_path
20
22
  raise RefactorError, "Not in a Rails app" unless base_path
@@ -22,15 +24,13 @@ module RailsForge
22
24
  controllers_dir = File.join(base_path, "app", "controllers")
23
25
  return [] unless Dir.exist?(controllers_dir)
24
26
 
25
- results = []
27
+ issues = []
26
28
  Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
27
- result = analyze_file(file, :controller)
28
- results << result if result[:needs_refactoring]
29
+ issues.concat(analyze_file(file, :controller))
29
30
  end
30
- results
31
+ issues
31
32
  end
32
33
 
33
- # Analyze models
34
34
  def self.analyze_models(base_path = nil)
35
35
  base_path ||= find_rails_app_path
36
36
  raise RefactorError, "Not in a Rails app" unless base_path
@@ -38,81 +38,101 @@ module RailsForge
38
38
  models_dir = File.join(base_path, "app", "models")
39
39
  return [] unless Dir.exist?(models_dir)
40
40
 
41
- results = []
41
+ issues = []
42
42
  Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
43
- next if file.end_with?("_application.rb")
44
- result = analyze_file(file, :model)
45
- results << result if result[:needs_refactoring]
43
+ next if File.basename(file) == "application_record.rb"
44
+ issues.concat(analyze_file(file, :model))
46
45
  end
47
- results
46
+ issues
48
47
  end
49
48
 
50
49
  def self.analyze_file(file_path, type)
51
- content = File.read(file_path)
52
- lines = content.lines.count
53
- methods = extract_methods(content)
54
-
55
- issues = []
56
- suggestions = []
50
+ content = File.read(file_path)
51
+ lines = content.lines.count
52
+ methods = extract_methods(content)
53
+ basename = File.basename(file_path)
54
+ issues = []
57
55
 
58
56
  if type == :controller
59
- issues << "Exceeds #{CONTROLLER_MAX_LINES} lines" if lines > CONTROLLER_MAX_LINES
60
- issues << "Has #{methods.count} methods" if methods.count > CONTROLLER_MAX_METHODS
61
- suggestions << "Consider moving business logic to Service object" if lines > CONTROLLER_MAX_LINES
57
+ if lines > CONTROLLER_MAX_LINES
58
+ issues << RailsForge::Issue.new(
59
+ analyzer: :refactor,
60
+ type: :oversized_controller,
61
+ severity: "medium",
62
+ file: basename,
63
+ line: nil,
64
+ message: "#{basename} has #{lines} lines (max #{CONTROLLER_MAX_LINES})",
65
+ suggestion: "Extract business logic to Service objects"
66
+ )
67
+ end
68
+
69
+ if methods.count > CONTROLLER_MAX_METHODS
70
+ issues << RailsForge::Issue.new(
71
+ analyzer: :refactor,
72
+ type: :too_many_methods,
73
+ severity: "low",
74
+ file: basename,
75
+ line: nil,
76
+ message: "#{basename} has #{methods.count} methods (max #{CONTROLLER_MAX_METHODS})",
77
+ suggestion: "Consider splitting into focused controllers"
78
+ )
79
+ end
62
80
  else
63
- issues << "Exceeds #{MODEL_MAX_LINES} lines" if lines > MODEL_MAX_LINES
81
+ if lines > MODEL_MAX_LINES
82
+ issues << RailsForge::Issue.new(
83
+ analyzer: :refactor,
84
+ type: :oversized_model,
85
+ severity: "medium",
86
+ file: basename,
87
+ line: nil,
88
+ message: "#{basename} has #{lines} lines (max #{MODEL_MAX_LINES})",
89
+ suggestion: "Consider extracting concerns or service objects"
90
+ )
91
+ end
64
92
  end
65
93
 
66
94
  methods.each do |method|
67
- suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting" if method[:lines] > MODEL_MAX_METHOD_LINES
95
+ next unless method[:lines] > MODEL_MAX_METHOD_LINES
96
+
97
+ issues << RailsForge::Issue.new(
98
+ analyzer: :refactor,
99
+ type: :long_method,
100
+ severity: "info",
101
+ file: basename,
102
+ line: nil,
103
+ message: "Method `#{method[:name]}` has #{method[:lines]} lines",
104
+ suggestion: "Consider extracting to a private method or service"
105
+ )
68
106
  end
69
107
 
70
- {
71
- type: type,
72
- file: File.basename(file_path),
73
- path: file_path,
74
- lines: lines,
75
- methods: methods,
76
- issues: issues,
77
- suggestions: suggestions,
78
- needs_refactoring: issues.any? || suggestions.any?
79
- }
108
+ issues
80
109
  end
81
110
 
82
111
  def self.extract_methods(content)
83
- methods = []
84
- in_method = false
112
+ methods = []
113
+ in_method = false
114
+ method_name = nil
85
115
  method_lines = []
86
116
 
87
117
  content.lines.each do |line|
88
118
  if line =~ /\bdef\s+(\w+)/
89
- methods << { name: $1, lines: method_lines.count } if in_method
90
- in_method = true
119
+ methods << { name: method_name, lines: method_lines.count } if in_method && method_name
120
+ in_method = true
121
+ method_name = $1
91
122
  method_lines = [line]
92
123
  elsif in_method
93
124
  method_lines << line
94
- methods << { name: methods.last[:name], lines: method_lines.count } if line.strip == "end" && method_lines.count > 1
125
+ if line.strip == "end" && method_lines.count > 1
126
+ methods << { name: method_name, lines: method_lines.count }
127
+ in_method = false
128
+ method_name = nil
129
+ method_lines = []
130
+ end
95
131
  end
96
132
  end
97
133
 
98
134
  methods
99
135
  end
100
-
101
- def self.print_report(results)
102
- puts "\nRefactoring Suggestions"
103
- puts "-" * 40
104
-
105
- if results.empty?
106
- puts "✓ No refactoring needed"
107
- return
108
- end
109
-
110
- results.each do |result|
111
- puts "⚠ #{result[:file]}"
112
- result[:issues].each { |i| puts " - #{i}" }
113
- result[:suggestions].each { |s| puts " → #{s}" }
114
- end
115
- end
116
136
  end
117
137
  end
118
138
  end
@@ -1,25 +1,29 @@
1
1
  # Security analyzer for RailsForge
2
2
  # Checks for common security vulnerabilities
3
3
 
4
- require 'fileutils'
4
+ require "fileutils"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # SecurityAnalyzer scans code for security issues
9
8
  class SecurityAnalyzer < BaseAnalyzer
10
- # Security issue types
11
9
  ISSUE_TYPES = {
12
- sql_injection: "SQL Injection",
13
- mass_assignment: "Unsafe Mass Assignment",
14
- xss: "Cross-Site Scripting (XSS)",
15
- sensitive_data: "Sensitive Data Exposure",
16
- weak_crypto: "Weak Cryptography",
10
+ sql_injection: "SQL Injection",
11
+ mass_assignment: "Unsafe Mass Assignment",
12
+ xss: "Cross-Site Scripting (XSS)",
13
+ sensitive_data: "Sensitive Data Exposure",
14
+ weak_crypto: "Weak Cryptography",
17
15
  command_injection: "Command Injection"
18
16
  }.freeze
19
17
 
20
- # Analyze Rails app for security issues
21
- # @param base_path [String] Rails app root path
22
- # @return [Array<Hash>] Security issues found
18
+ SUGGESTIONS = {
19
+ sql_injection: "Use parameterized queries: Model.where(column: value)",
20
+ mass_assignment: "Use strong parameters with permit() instead of attr_accessible",
21
+ xss: "Avoid raw/html_safe; use Rails' built-in escaping",
22
+ sensitive_data: "Move secrets to environment variables or Rails credentials",
23
+ weak_crypto: "Use bcrypt for passwords; SHA-256 or better for hashing",
24
+ command_injection: "Avoid shell interpolation; use array form of system()"
25
+ }.freeze
26
+
23
27
  def self.analyze(base_path = nil)
24
28
  base_path ||= find_rails_app_path
25
29
  return [] unless base_path
@@ -27,141 +31,80 @@ 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 security issues
38
- # @return [Array<Hash>] Issues found
39
39
  def find_issues
40
40
  check_sql_injection
41
41
  check_mass_assignment
42
42
  check_xss
43
43
  check_sensitive_data
44
44
  check_weak_crypto
45
-
46
45
  @issues
47
46
  end
48
47
 
49
- # Print security report
50
- # @param issues [Array<Hash>] Issues to print
51
- def self.print_report(issues)
52
- puts "Security Analysis"
53
- puts "=" * 50
54
-
55
- if issues.empty?
56
- puts "✓ No security issues found"
57
- return
58
- end
59
-
60
- puts "Found #{issues.count} security issue(s):"
61
- puts ""
62
-
63
- issues.each do |issue|
64
- puts "⚠ #{issue[:type]}"
65
- puts " File: #{issue[:file]}"
66
- puts " Line: #{issue[:line]}"
67
- puts " Issue: #{issue[:message]}"
68
- puts ""
69
- end
70
- end
71
-
72
48
  private
73
49
 
74
- # Check for SQL injection vulnerabilities
75
50
  def check_sql_injection
76
51
  patterns = [
77
52
  /\.where\s*\(\s*params\./,
78
53
  /find_by\(.*\#\{.*\}/,
79
54
  /execute\s*\(.*\#\{.*\}/
80
55
  ]
81
-
82
56
  scan_files("app/models/**/*.rb", patterns, :sql_injection)
83
57
  scan_files("app/controllers/**/*.rb", patterns, :sql_injection)
84
58
  end
85
59
 
86
- # Check for unsafe mass assignment
87
60
  def check_mass_assignment
88
- patterns = [
89
- /attr_accessible/,
90
- /attr_protected/
91
- ]
92
-
61
+ patterns = [/attr_accessible/, /attr_protected/]
93
62
  scan_files("app/models/**/*.rb", patterns, :mass_assignment)
94
63
  end
95
64
 
96
- # Check for XSS vulnerabilities
97
65
  def check_xss
98
- patterns = [
99
- /raw\s*\(/,
100
- /html_safe\s*$/,
101
- /\<\%=\s*[^>]+without/
102
- ]
103
-
66
+ patterns = [/raw\s*\(/, /html_safe\s*$/, /\<\%=\s*[^>]+without/]
104
67
  scan_files("app/views/**/*.erb", patterns, :xss)
105
68
  scan_files("app/helpers/**/*.rb", patterns, :xss)
106
69
  end
107
70
 
108
- # Check for sensitive data exposure
109
71
  def check_sensitive_data
110
- patterns = [
111
- /password/,
112
- /secret/,
113
- /token/,
114
- /api_key/,
115
- /private_key/
116
- ]
117
-
72
+ patterns = [/password/, /secret/, /token/, /api_key/, /private_key/]
118
73
  scan_files("app/models/**/*.rb", patterns, :sensitive_data)
119
74
  scan_files("config/**/*.rb", patterns, :sensitive_data)
120
75
  end
121
76
 
122
- # Check for weak cryptography
123
77
  def check_weak_crypto
124
- patterns = [
125
- /MD5/,
126
- /SHA1/,
127
- /DES\.encrypt/,
128
- /\.encrypt\s+[^:]+$/
129
- ]
130
-
78
+ patterns = [/MD5/, /SHA1/, /DES\.encrypt/, /\.encrypt\s+[^:]+$/]
131
79
  scan_files("app/**/*.rb", patterns, :weak_crypto)
132
80
  end
133
81
 
134
- # Scan files for patterns
135
82
  def scan_files(glob, patterns, issue_type)
136
83
  Dir.glob(File.join(@base_path, glob)).each do |file|
137
- content = File.read(file)
138
- lines = content.lines
139
-
84
+ lines = File.read(file).lines
140
85
  lines.each_with_index do |line, index|
141
86
  patterns.each do |pattern|
142
- if line =~ pattern
143
- @issues << {
144
- type: ISSUE_TYPES[issue_type],
145
- file: file.gsub(@base_path, ""),
146
- line: index + 1,
147
- message: line.strip,
148
- severity: severity_for(issue_type)
149
- }
150
- end
87
+ next unless line =~ pattern
88
+
89
+ @issues << RailsForge::Issue.new(
90
+ analyzer: :security,
91
+ type: issue_type,
92
+ severity: severity_for(issue_type),
93
+ file: file.delete_prefix("#{@base_path}/"),
94
+ line: index + 1,
95
+ message: "#{ISSUE_TYPES[issue_type]}: #{line.strip}",
96
+ suggestion: SUGGESTIONS[issue_type]
97
+ )
151
98
  end
152
99
  end
153
100
  end
154
101
  end
155
102
 
156
- # Get severity for issue type
157
103
  def severity_for(issue_type)
158
104
  case issue_type
159
- when :sql_injection, :command_injection
160
- "high"
161
- when :mass_assignment, :xss
162
- "medium"
163
- else
164
- "low"
105
+ when :sql_injection, :command_injection then "high"
106
+ when :mass_assignment, :xss then "medium"
107
+ else "low"
165
108
  end
166
109
  end
167
110
  end
@@ -1,57 +1,43 @@
1
1
  # Spec analyzer for RailsForge
2
2
  # Checks for missing spec files
3
3
 
4
- require_relative 'base_analyzer'
4
+ require_relative "base_analyzer"
5
5
 
6
6
  module RailsForge
7
7
  module Analyzers
8
- # SpecAnalyzer checks spec coverage
9
8
  class SpecAnalyzer < BaseAnalyzer
10
9
  GENERATOR_TYPES = %w[services queries jobs forms presenters policies serializers].freeze
11
10
 
12
- # Analyze spec coverage
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
- results = []
18
- GENERATOR_TYPES.each do |type|
19
- app_folder = "app/#{type.chop}"
20
- spec_folder = "spec/#{type}"
15
+ issues = []
21
16
 
22
- app_path = File.join(base_path, app_folder)
23
- spec_path = File.join(base_path, spec_folder)
17
+ GENERATOR_TYPES.each do |type|
18
+ singular = type.chop
19
+ app_path = File.join(base_path, "app", singular)
20
+ spec_path = File.join(base_path, "spec", type)
24
21
 
25
22
  next unless Dir.exist?(app_path)
26
23
 
27
- app_files = Dir.glob(File.join(app_path, "**", "*.rb")).map { |f| File.basename(f, ".rb") }
24
+ app_files = Dir.glob(File.join(app_path, "**", "*.rb")).map { |f| File.basename(f, ".rb") }
28
25
  spec_files = Dir.exist?(spec_path) ? Dir.glob(File.join(spec_path, "**", "*_spec.rb")).map { |f| File.basename(f, "_spec.rb") } : []
29
26
 
30
27
  (app_files - spec_files).each do |missing|
31
- results << {
32
- type: type.chop,
33
- name: missing,
34
- has_spec: false
35
- }
28
+ issues << RailsForge::Issue.new(
29
+ analyzer: :spec,
30
+ type: :missing_spec,
31
+ severity: "low",
32
+ file: "app/#{singular}/#{missing}.rb",
33
+ line: nil,
34
+ message: "#{singular.capitalize} `#{missing}` has no spec",
35
+ suggestion: "Create spec/#{type}/#{missing}_spec.rb"
36
+ )
36
37
  end
37
38
  end
38
39
 
39
- results
40
- end
41
-
42
- def self.print_results(results)
43
- puts "\nSpec Coverage Analysis"
44
- puts "-" * 40
45
-
46
- if results.empty?
47
- puts "✓ All components have specs"
48
- return
49
- end
50
-
51
- puts "⚠ #{results.count} missing specs:"
52
- results.first(10).each do |result|
53
- puts " - #{result[:type]}: #{result[:name]}"
54
- end
40
+ issues
55
41
  end
56
42
  end
57
43
  end