railsforge 1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +528 -0
- data/bin/railsforge +8 -0
- data/lib/railsforge/analyzers/base_analyzer.rb +41 -0
- data/lib/railsforge/analyzers/controller_analyzer.rb +83 -0
- data/lib/railsforge/analyzers/database_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/metrics_analyzer.rb +55 -0
- data/lib/railsforge/analyzers/model_analyzer.rb +74 -0
- data/lib/railsforge/analyzers/performance_analyzer.rb +161 -0
- data/lib/railsforge/analyzers/refactor_analyzer.rb +118 -0
- data/lib/railsforge/analyzers/security_analyzer.rb +169 -0
- data/lib/railsforge/analyzers/spec_analyzer.rb +58 -0
- data/lib/railsforge/api_generator.rb +397 -0
- data/lib/railsforge/audit.rb +289 -0
- data/lib/railsforge/cli.rb +671 -0
- data/lib/railsforge/config.rb +181 -0
- data/lib/railsforge/database_analyzer.rb +300 -0
- data/lib/railsforge/doctor.rb +250 -0
- data/lib/railsforge/feature_generator.rb +560 -0
- data/lib/railsforge/generator.rb +313 -0
- data/lib/railsforge/generators/base_generator.rb +70 -0
- data/lib/railsforge/generators/demo_generator.rb +307 -0
- data/lib/railsforge/generators/devops_generator.rb +287 -0
- data/lib/railsforge/generators/monitoring_generator.rb +134 -0
- data/lib/railsforge/generators/service_generator.rb +122 -0
- data/lib/railsforge/generators/stimulus_controller_generator.rb +129 -0
- data/lib/railsforge/generators/test_generator.rb +289 -0
- data/lib/railsforge/generators/view_component_generator.rb +169 -0
- data/lib/railsforge/graph.rb +270 -0
- data/lib/railsforge/loader.rb +56 -0
- data/lib/railsforge/mailer_generator.rb +191 -0
- data/lib/railsforge/plugins/plugin_loader.rb +60 -0
- data/lib/railsforge/plugins.rb +30 -0
- data/lib/railsforge/profiles/admin_app.yml +49 -0
- data/lib/railsforge/profiles/api_only.yml +47 -0
- data/lib/railsforge/profiles/blog.yml +47 -0
- data/lib/railsforge/profiles/standard.yml +44 -0
- data/lib/railsforge/profiles.rb +99 -0
- data/lib/railsforge/refactor_analyzer.rb +401 -0
- data/lib/railsforge/refactor_controller.rb +277 -0
- data/lib/railsforge/refactors/refactor_controller.rb +117 -0
- data/lib/railsforge/template_loader.rb +105 -0
- data/lib/railsforge/templates/v1/form/spec_template.rb +18 -0
- data/lib/railsforge/templates/v1/form/template.rb +28 -0
- data/lib/railsforge/templates/v1/job/spec_template.rb +17 -0
- data/lib/railsforge/templates/v1/job/template.rb +13 -0
- data/lib/railsforge/templates/v1/policy/spec_template.rb +41 -0
- data/lib/railsforge/templates/v1/policy/template.rb +57 -0
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/presenter/template.rb +13 -0
- data/lib/railsforge/templates/v1/query/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/query/template.rb +16 -0
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +13 -0
- data/lib/railsforge/templates/v1/serializer/template.rb +11 -0
- data/lib/railsforge/templates/v1/service/spec_template.rb +12 -0
- data/lib/railsforge/templates/v1/service/template.rb +25 -0
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +35 -0
- data/lib/railsforge/templates/v1/view_component/template.rb +24 -0
- data/lib/railsforge/templates/v2/job/template.rb +49 -0
- data/lib/railsforge/templates/v2/query/template.rb +66 -0
- data/lib/railsforge/templates/v2/service/spec_template.rb +33 -0
- data/lib/railsforge/templates/v2/service/template.rb +71 -0
- data/lib/railsforge/templates/v3/job/template.rb +72 -0
- data/lib/railsforge/templates/v3/query/spec_template.rb +54 -0
- data/lib/railsforge/templates/v3/query/template.rb +115 -0
- data/lib/railsforge/templates/v3/service/spec_template.rb +51 -0
- data/lib/railsforge/templates/v3/service/template.rb +84 -0
- data/lib/railsforge/version.rb +5 -0
- data/lib/railsforge/wizard.rb +265 -0
- data/lib/railsforge/wizard_tui.rb +286 -0
- data/lib/railsforge.rb +13 -0
- metadata +216 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Database analyzer for RailsForge
|
|
2
|
+
# Scans database schema for issues
|
|
3
|
+
|
|
4
|
+
require_relative 'base_analyzer'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# DatabaseAnalyzer scans database/schema for issues
|
|
9
|
+
class DatabaseAnalyzer < BaseAnalyzer
|
|
10
|
+
class DatabaseError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Analyze database
|
|
13
|
+
def self.analyze(base_path = nil)
|
|
14
|
+
base_path ||= find_rails_app_path
|
|
15
|
+
raise DatabaseError, "Not in a Rails application" unless base_path
|
|
16
|
+
|
|
17
|
+
results = []
|
|
18
|
+
schema_file = File.join(base_path, "db", "schema.rb")
|
|
19
|
+
|
|
20
|
+
return results unless File.exist?(schema_file)
|
|
21
|
+
|
|
22
|
+
content = File.read(schema_file)
|
|
23
|
+
tables = content.scan(/create_table\s+"(\w+)"/).flatten
|
|
24
|
+
|
|
25
|
+
tables.each do |table|
|
|
26
|
+
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
|
|
35
|
+
end
|
|
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
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Metrics analyzer for RailsForge
|
|
2
|
+
# Provides comprehensive metrics analysis
|
|
3
|
+
|
|
4
|
+
require_relative 'base_analyzer'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# MetricsAnalyzer provides code metrics
|
|
9
|
+
class MetricsAnalyzer < BaseAnalyzer
|
|
10
|
+
GENERATOR_TYPES = %w[services queries forms presenters policies serializers jobs].freeze
|
|
11
|
+
|
|
12
|
+
# Analyze metrics
|
|
13
|
+
def self.analyze(base_path = nil)
|
|
14
|
+
base_path ||= find_rails_app_path
|
|
15
|
+
return {} unless base_path
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
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")
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
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"
|
|
31
|
+
counts[type] = count_in_folder(base_path, folder)
|
|
32
|
+
end
|
|
33
|
+
counts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.count_in_folder(base_path, folder)
|
|
37
|
+
path = File.join(base_path, folder)
|
|
38
|
+
Dir.exist?(path) ? Dir.glob(File.join(path, "**", "*.rb")).count : 0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.count_all_files(base_path)
|
|
42
|
+
Dir.glob(File.join(base_path, "app", "**", "*.rb")).count
|
|
43
|
+
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
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Model analyzer for RailsForge
|
|
2
|
+
# Scans models for code quality issues
|
|
3
|
+
|
|
4
|
+
require_relative 'base_analyzer'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# ModelAnalyzer scans models for issues
|
|
9
|
+
class ModelAnalyzer < BaseAnalyzer
|
|
10
|
+
MAX_METHODS = 15
|
|
11
|
+
MAX_LINES = 200
|
|
12
|
+
|
|
13
|
+
# Analyze models
|
|
14
|
+
def self.analyze(base_path = nil)
|
|
15
|
+
base_path ||= find_rails_app_path
|
|
16
|
+
return [] unless base_path
|
|
17
|
+
|
|
18
|
+
models_dir = File.join(base_path, "app", "models")
|
|
19
|
+
return [] unless Dir.exist?(models_dir)
|
|
20
|
+
|
|
21
|
+
results = []
|
|
22
|
+
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
23
|
+
next if file.end_with?("_application.rb")
|
|
24
|
+
|
|
25
|
+
content = File.read(file)
|
|
26
|
+
lines = content.lines.count
|
|
27
|
+
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
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
results
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.print_results(results)
|
|
48
|
+
puts "\nModel Analysis"
|
|
49
|
+
puts "-" * 40
|
|
50
|
+
|
|
51
|
+
if results.empty?
|
|
52
|
+
puts "✓ No issues found"
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
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
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Performance analyzer for RailsForge
|
|
2
|
+
# Checks for common performance issues
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# PerformanceAnalyzer scans code for performance problems
|
|
9
|
+
class PerformanceAnalyzer < BaseAnalyzer
|
|
10
|
+
# Performance issue types
|
|
11
|
+
ISSUE_TYPES = {
|
|
12
|
+
n_plus_one: "N+1 Query",
|
|
13
|
+
missing_index: "Missing Database Index",
|
|
14
|
+
slow_method: "Slow Method",
|
|
15
|
+
inefficient_query: "Inefficient Query",
|
|
16
|
+
cache_miss: "Cache Miss",
|
|
17
|
+
eager_loading: "Unnecessary Eager Loading"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Analyze Rails app for performance issues
|
|
21
|
+
# @param base_path [String] Rails app root path
|
|
22
|
+
# @return [Array<Hash>] Performance issues found
|
|
23
|
+
def self.analyze(base_path = nil)
|
|
24
|
+
base_path ||= find_rails_app_path
|
|
25
|
+
return [] unless base_path
|
|
26
|
+
|
|
27
|
+
new(base_path).find_issues
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Initialize analyzer
|
|
31
|
+
# @param base_path [String] Rails app root path
|
|
32
|
+
def initialize(base_path)
|
|
33
|
+
@base_path = base_path
|
|
34
|
+
@issues = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Find all performance issues
|
|
38
|
+
# @return [Array<Hash>] Issues found
|
|
39
|
+
def find_issues
|
|
40
|
+
check_n_plus_one
|
|
41
|
+
check_missing_indexes
|
|
42
|
+
check_slow_methods
|
|
43
|
+
check_inefficient_queries
|
|
44
|
+
|
|
45
|
+
@issues
|
|
46
|
+
end
|
|
47
|
+
|
|
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
|
+
private
|
|
72
|
+
|
|
73
|
+
# Check for N+1 query problems
|
|
74
|
+
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
|
+
|
|
82
|
+
scan_files("app/controllers/**/*.rb", patterns, :n_plus_one)
|
|
83
|
+
scan_files("app/views/**/*.erb", patterns, :n_plus_one)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check for missing database indexes
|
|
87
|
+
def check_missing_indexes
|
|
88
|
+
# Look for foreign keys without indexes
|
|
89
|
+
patterns = [
|
|
90
|
+
/belongs_to\s+:\w+/,
|
|
91
|
+
/add_reference/
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
scan_files("db/migrate/**/*.rb", patterns, :missing_index)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check for slow methods
|
|
98
|
+
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
|
+
|
|
107
|
+
scan_files("app/**/*.rb", patterns, :slow_method)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check for inefficient queries
|
|
111
|
+
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
|
+
|
|
120
|
+
scan_files("app/models/**/*.rb", patterns, :inefficient_query)
|
|
121
|
+
scan_files("app/controllers/**/*.rb", patterns, :inefficient_query)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Scan files for patterns
|
|
125
|
+
def scan_files(glob, patterns, issue_type)
|
|
126
|
+
Dir.glob(File.join(@base_path, glob)).each do |file|
|
|
127
|
+
content = File.read(file)
|
|
128
|
+
lines = content.lines
|
|
129
|
+
|
|
130
|
+
lines.each_with_index do |line, index|
|
|
131
|
+
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
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Get severity for issue type
|
|
147
|
+
def severity_for(issue_type)
|
|
148
|
+
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"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Refactor analyzer for RailsForge
|
|
2
|
+
# Provides refactoring suggestions
|
|
3
|
+
|
|
4
|
+
require_relative 'base_analyzer'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# RefactorAnalyzer provides refactoring suggestions
|
|
9
|
+
class RefactorAnalyzer < BaseAnalyzer
|
|
10
|
+
class RefactorError < StandardError; end
|
|
11
|
+
|
|
12
|
+
CONTROLLER_MAX_LINES = 150
|
|
13
|
+
CONTROLLER_MAX_METHODS = 10
|
|
14
|
+
MODEL_MAX_LINES = 200
|
|
15
|
+
MODEL_MAX_METHOD_LINES = 15
|
|
16
|
+
|
|
17
|
+
# Analyze controllers
|
|
18
|
+
def self.analyze_controllers(base_path = nil)
|
|
19
|
+
base_path ||= find_rails_app_path
|
|
20
|
+
raise RefactorError, "Not in a Rails app" unless base_path
|
|
21
|
+
|
|
22
|
+
controllers_dir = File.join(base_path, "app", "controllers")
|
|
23
|
+
return [] unless Dir.exist?(controllers_dir)
|
|
24
|
+
|
|
25
|
+
results = []
|
|
26
|
+
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
|
+
end
|
|
30
|
+
results
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Analyze models
|
|
34
|
+
def self.analyze_models(base_path = nil)
|
|
35
|
+
base_path ||= find_rails_app_path
|
|
36
|
+
raise RefactorError, "Not in a Rails app" unless base_path
|
|
37
|
+
|
|
38
|
+
models_dir = File.join(base_path, "app", "models")
|
|
39
|
+
return [] unless Dir.exist?(models_dir)
|
|
40
|
+
|
|
41
|
+
results = []
|
|
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]
|
|
46
|
+
end
|
|
47
|
+
results
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
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 = []
|
|
57
|
+
|
|
58
|
+
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
|
|
62
|
+
else
|
|
63
|
+
issues << "Exceeds #{MODEL_MAX_LINES} lines" if lines > MODEL_MAX_LINES
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
methods.each do |method|
|
|
67
|
+
suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting" if method[:lines] > MODEL_MAX_METHOD_LINES
|
|
68
|
+
end
|
|
69
|
+
|
|
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
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.extract_methods(content)
|
|
83
|
+
methods = []
|
|
84
|
+
in_method = false
|
|
85
|
+
method_lines = []
|
|
86
|
+
|
|
87
|
+
content.lines.each do |line|
|
|
88
|
+
if line =~ /\bdef\s+(\w+)/
|
|
89
|
+
methods << { name: $1, lines: method_lines.count } if in_method
|
|
90
|
+
in_method = true
|
|
91
|
+
method_lines = [line]
|
|
92
|
+
elsif in_method
|
|
93
|
+
method_lines << line
|
|
94
|
+
methods << { name: methods.last[:name], lines: method_lines.count } if line.strip == "end" && method_lines.count > 1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
methods
|
|
99
|
+
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
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Security analyzer for RailsForge
|
|
2
|
+
# Checks for common security vulnerabilities
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# SecurityAnalyzer scans code for security issues
|
|
9
|
+
class SecurityAnalyzer < BaseAnalyzer
|
|
10
|
+
# Security issue types
|
|
11
|
+
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",
|
|
17
|
+
command_injection: "Command Injection"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Analyze Rails app for security issues
|
|
21
|
+
# @param base_path [String] Rails app root path
|
|
22
|
+
# @return [Array<Hash>] Security issues found
|
|
23
|
+
def self.analyze(base_path = nil)
|
|
24
|
+
base_path ||= find_rails_app_path
|
|
25
|
+
return [] unless base_path
|
|
26
|
+
|
|
27
|
+
new(base_path).find_issues
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Initialize analyzer
|
|
31
|
+
# @param base_path [String] Rails app root path
|
|
32
|
+
def initialize(base_path)
|
|
33
|
+
@base_path = base_path
|
|
34
|
+
@issues = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Find all security issues
|
|
38
|
+
# @return [Array<Hash>] Issues found
|
|
39
|
+
def find_issues
|
|
40
|
+
check_sql_injection
|
|
41
|
+
check_mass_assignment
|
|
42
|
+
check_xss
|
|
43
|
+
check_sensitive_data
|
|
44
|
+
check_weak_crypto
|
|
45
|
+
|
|
46
|
+
@issues
|
|
47
|
+
end
|
|
48
|
+
|
|
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
|
+
private
|
|
73
|
+
|
|
74
|
+
# Check for SQL injection vulnerabilities
|
|
75
|
+
def check_sql_injection
|
|
76
|
+
patterns = [
|
|
77
|
+
/\.where\s*\(\s*params\./,
|
|
78
|
+
/find_by\(.*\#\{.*\}/,
|
|
79
|
+
/execute\s*\(.*\#\{.*\}/
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
scan_files("app/models/**/*.rb", patterns, :sql_injection)
|
|
83
|
+
scan_files("app/controllers/**/*.rb", patterns, :sql_injection)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check for unsafe mass assignment
|
|
87
|
+
def check_mass_assignment
|
|
88
|
+
patterns = [
|
|
89
|
+
/attr_accessible/,
|
|
90
|
+
/attr_protected/
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
scan_files("app/models/**/*.rb", patterns, :mass_assignment)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check for XSS vulnerabilities
|
|
97
|
+
def check_xss
|
|
98
|
+
patterns = [
|
|
99
|
+
/raw\s*\(/,
|
|
100
|
+
/html_safe\s*$/,
|
|
101
|
+
/\<\%=\s*[^>]+without/
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
scan_files("app/views/**/*.erb", patterns, :xss)
|
|
105
|
+
scan_files("app/helpers/**/*.rb", patterns, :xss)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check for sensitive data exposure
|
|
109
|
+
def check_sensitive_data
|
|
110
|
+
patterns = [
|
|
111
|
+
/password/,
|
|
112
|
+
/secret/,
|
|
113
|
+
/token/,
|
|
114
|
+
/api_key/,
|
|
115
|
+
/private_key/
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
scan_files("app/models/**/*.rb", patterns, :sensitive_data)
|
|
119
|
+
scan_files("config/**/*.rb", patterns, :sensitive_data)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check for weak cryptography
|
|
123
|
+
def check_weak_crypto
|
|
124
|
+
patterns = [
|
|
125
|
+
/MD5/,
|
|
126
|
+
/SHA1/,
|
|
127
|
+
/DES\.encrypt/,
|
|
128
|
+
/\.encrypt\s+[^:]+$/
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
scan_files("app/**/*.rb", patterns, :weak_crypto)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Scan files for patterns
|
|
135
|
+
def scan_files(glob, patterns, issue_type)
|
|
136
|
+
Dir.glob(File.join(@base_path, glob)).each do |file|
|
|
137
|
+
content = File.read(file)
|
|
138
|
+
lines = content.lines
|
|
139
|
+
|
|
140
|
+
lines.each_with_index do |line, index|
|
|
141
|
+
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
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get severity for issue type
|
|
157
|
+
def severity_for(issue_type)
|
|
158
|
+
case issue_type
|
|
159
|
+
when :sql_injection, :command_injection
|
|
160
|
+
"high"
|
|
161
|
+
when :mass_assignment, :xss
|
|
162
|
+
"medium"
|
|
163
|
+
else
|
|
164
|
+
"low"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Spec analyzer for RailsForge
|
|
2
|
+
# Checks for missing spec files
|
|
3
|
+
|
|
4
|
+
require_relative 'base_analyzer'
|
|
5
|
+
|
|
6
|
+
module RailsForge
|
|
7
|
+
module Analyzers
|
|
8
|
+
# SpecAnalyzer checks spec coverage
|
|
9
|
+
class SpecAnalyzer < BaseAnalyzer
|
|
10
|
+
GENERATOR_TYPES = %w[services queries jobs forms presenters policies serializers].freeze
|
|
11
|
+
|
|
12
|
+
# Analyze spec coverage
|
|
13
|
+
def self.analyze(base_path = nil)
|
|
14
|
+
base_path ||= find_rails_app_path
|
|
15
|
+
return [] unless base_path
|
|
16
|
+
|
|
17
|
+
results = []
|
|
18
|
+
GENERATOR_TYPES.each do |type|
|
|
19
|
+
app_folder = "app/#{type.chop}"
|
|
20
|
+
spec_folder = "spec/#{type}"
|
|
21
|
+
|
|
22
|
+
app_path = File.join(base_path, app_folder)
|
|
23
|
+
spec_path = File.join(base_path, spec_folder)
|
|
24
|
+
|
|
25
|
+
next unless Dir.exist?(app_path)
|
|
26
|
+
|
|
27
|
+
app_files = Dir.glob(File.join(app_path, "**", "*.rb")).map { |f| File.basename(f, ".rb") }
|
|
28
|
+
spec_files = Dir.exist?(spec_path) ? Dir.glob(File.join(spec_path, "**", "*_spec.rb")).map { |f| File.basename(f, "_spec.rb") } : []
|
|
29
|
+
|
|
30
|
+
(app_files - spec_files).each do |missing|
|
|
31
|
+
results << {
|
|
32
|
+
type: type.chop,
|
|
33
|
+
name: missing,
|
|
34
|
+
has_spec: false
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
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
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|