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.
- checksums.yaml +4 -4
- data/README.md +129 -435
- 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 +28 -645
- data/lib/railsforge/cli_minimal.rb +10 -55
- data/lib/railsforge/diff.rb +57 -0
- data/lib/railsforge/doctor.rb +52 -225
- data/lib/railsforge/formatter.rb +128 -0
- data/lib/railsforge/issue.rb +23 -0
- data/lib/railsforge/loader.rb +5 -64
- data/lib/railsforge/version.rb +1 -1
- metadata +15 -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,19 +1,14 @@
|
|
|
1
1
|
# Controller analyzer for RailsForge
|
|
2
2
|
# Scans controllers for code quality issues
|
|
3
3
|
|
|
4
|
-
require_relative
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
issues = []
|
|
25
20
|
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
26
|
-
next if
|
|
21
|
+
next if File.basename(file) == "application_controller.rb"
|
|
27
22
|
|
|
28
23
|
content = File.read(file)
|
|
29
|
-
lines
|
|
24
|
+
lines = content.lines.count
|
|
30
25
|
methods = content.scan(/def \w+/).count
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
file:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
2
|
+
# Provides project-level metrics summary (not issues)
|
|
3
3
|
|
|
4
|
-
require_relative
|
|
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:
|
|
20
|
-
services_count:
|
|
21
|
-
queries_count:
|
|
22
|
-
jobs_count:
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
19
|
+
issues = []
|
|
22
20
|
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
23
|
-
next if
|
|
21
|
+
next if File.basename(file) == "application_record.rb"
|
|
24
22
|
|
|
25
23
|
content = File.read(file)
|
|
26
|
-
lines
|
|
24
|
+
lines = content.lines.count
|
|
27
25
|
methods = content.scan(/def \w+/).count
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
file:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
13
|
-
missing_index:
|
|
14
|
-
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:
|
|
17
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
"
|
|
151
|
-
|
|
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
|