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.
- checksums.yaml +4 -4
- data/README.md +105 -444
- data/lib/railsforge/analyzers/controller_analyzer.rb +29 -55
- data/lib/railsforge/analyzers/database_analyzer.rb +16 -30
- data/lib/railsforge/analyzers/metrics_analyzer.rb +8 -22
- data/lib/railsforge/analyzers/model_analyzer.rb +29 -46
- data/lib/railsforge/analyzers/performance_analyzer.rb +34 -94
- data/lib/railsforge/analyzers/refactor_analyzer.rb +77 -57
- data/lib/railsforge/analyzers/security_analyzer.rb +34 -91
- data/lib/railsforge/analyzers/spec_analyzer.rb +17 -31
- data/lib/railsforge/cli.rb +14 -650
- data/lib/railsforge/cli_minimal.rb +8 -55
- data/lib/railsforge/doctor.rb +52 -225
- data/lib/railsforge/formatter.rb +102 -0
- data/lib/railsforge/issue.rb +23 -0
- data/lib/railsforge/loader.rb +4 -64
- data/lib/railsforge/version.rb +1 -1
- metadata +14 -82
- data/lib/railsforge/api_generator.rb +0 -397
- data/lib/railsforge/audit.rb +0 -289
- data/lib/railsforge/config.rb +0 -181
- data/lib/railsforge/database_analyzer.rb +0 -300
- data/lib/railsforge/feature_generator.rb +0 -560
- data/lib/railsforge/generator.rb +0 -313
- data/lib/railsforge/generators/api_generator.rb +0 -392
- data/lib/railsforge/generators/base_generator.rb +0 -75
- data/lib/railsforge/generators/demo_generator.rb +0 -307
- data/lib/railsforge/generators/devops_generator.rb +0 -287
- data/lib/railsforge/generators/form_generator.rb +0 -180
- data/lib/railsforge/generators/job_generator.rb +0 -176
- data/lib/railsforge/generators/monitoring_generator.rb +0 -134
- data/lib/railsforge/generators/policy_generator.rb +0 -220
- data/lib/railsforge/generators/presenter_generator.rb +0 -173
- data/lib/railsforge/generators/query_generator.rb +0 -174
- data/lib/railsforge/generators/serializer_generator.rb +0 -166
- data/lib/railsforge/generators/service_generator.rb +0 -122
- data/lib/railsforge/generators/stimulus_controller_generator.rb +0 -129
- data/lib/railsforge/generators/test_generator.rb +0 -289
- data/lib/railsforge/generators/view_component_generator.rb +0 -169
- data/lib/railsforge/graph.rb +0 -270
- data/lib/railsforge/mailer_generator.rb +0 -191
- data/lib/railsforge/plugins/plugin_loader.rb +0 -60
- data/lib/railsforge/plugins.rb +0 -30
- data/lib/railsforge/profiles.rb +0 -99
- data/lib/railsforge/refactor_analyzer.rb +0 -401
- data/lib/railsforge/refactor_controller.rb +0 -277
- data/lib/railsforge/refactors/refactor_controller.rb +0 -117
- data/lib/railsforge/template_loader.rb +0 -105
- data/lib/railsforge/templates/v1/form/spec_template.rb +0 -18
- data/lib/railsforge/templates/v1/form/template.rb +0 -28
- data/lib/railsforge/templates/v1/job/spec_template.rb +0 -17
- data/lib/railsforge/templates/v1/job/template.rb +0 -13
- data/lib/railsforge/templates/v1/policy/spec_template.rb +0 -41
- data/lib/railsforge/templates/v1/policy/template.rb +0 -57
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +0 -12
- data/lib/railsforge/templates/v1/presenter/template.rb +0 -13
- data/lib/railsforge/templates/v1/query/spec_template.rb +0 -12
- data/lib/railsforge/templates/v1/query/template.rb +0 -16
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +0 -13
- data/lib/railsforge/templates/v1/serializer/template.rb +0 -11
- data/lib/railsforge/templates/v1/service/spec_template.rb +0 -12
- data/lib/railsforge/templates/v1/service/template.rb +0 -25
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +0 -35
- data/lib/railsforge/templates/v1/view_component/template.rb +0 -24
- data/lib/railsforge/templates/v2/job/template.rb +0 -49
- data/lib/railsforge/templates/v2/query/template.rb +0 -66
- data/lib/railsforge/templates/v2/service/spec_template.rb +0 -33
- data/lib/railsforge/templates/v2/service/template.rb +0 -71
- data/lib/railsforge/templates/v3/job/template.rb +0 -72
- data/lib/railsforge/templates/v3/query/spec_template.rb +0 -54
- data/lib/railsforge/templates/v3/query/template.rb +0 -115
- data/lib/railsforge/templates/v3/service/spec_template.rb +0 -51
- data/lib/railsforge/templates/v3/service/template.rb +0 -93
- data/lib/railsforge/wizard.rb +0 -265
- 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
|
|
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
|
|
11
|
+
CONTROLLER_MAX_LINES = 150
|
|
13
12
|
CONTROLLER_MAX_METHODS = 10
|
|
14
|
-
MODEL_MAX_LINES
|
|
13
|
+
MODEL_MAX_LINES = 200
|
|
15
14
|
MODEL_MAX_METHOD_LINES = 15
|
|
16
15
|
|
|
17
|
-
|
|
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
|
-
|
|
27
|
+
issues = []
|
|
26
28
|
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
27
|
-
|
|
28
|
-
results << result if result[:needs_refactoring]
|
|
29
|
+
issues.concat(analyze_file(file, :controller))
|
|
29
30
|
end
|
|
30
|
-
|
|
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
|
-
|
|
41
|
+
issues = []
|
|
42
42
|
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
43
|
-
next if
|
|
44
|
-
|
|
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
|
-
|
|
46
|
+
issues
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
def self.analyze_file(file_path, type)
|
|
51
|
-
content
|
|
52
|
-
lines
|
|
53
|
-
methods
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
90
|
-
in_method
|
|
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
|
-
|
|
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
|
|
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:
|
|
13
|
-
mass_assignment:
|
|
14
|
-
xss:
|
|
15
|
-
sensitive_data:
|
|
16
|
-
weak_crypto:
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
GENERATOR_TYPES.each do |type|
|
|
19
|
-
app_folder = "app/#{type.chop}"
|
|
20
|
-
spec_folder = "spec/#{type}"
|
|
15
|
+
issues = []
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|