rails-architect 0.1.0 → 0.2.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 +309 -67
- data/config/rails_architect.yml +25 -0
- data/lib/rails_architect/dry_analyzer.rb +143 -0
- data/lib/rails_architect/kiss_analyzer.rb +130 -0
- data/lib/rails_architect/railtie.rb +11 -0
- data/lib/rails_architect/solid_analyzer.rb +196 -0
- data/lib/rails_architect/tasks/rails_architect.rake +35 -0
- data/lib/rails_architect/version.rb +5 -0
- data/lib/rails_architect.rb +30 -0
- data/test/rails_app/README.md +72 -0
- data/test/rails_app/app/controllers/posts_controller.rb +54 -0
- data/test/rails_app/app/controllers/users_controller.rb +104 -0
- data/test/rails_app/app/models/post.rb +34 -0
- data/test/rails_app/app/models/report_generator.rb +87 -0
- data/test/rails_app/app/models/user.rb +79 -0
- data/test/rails_app/config/rails_architect.yml +14 -0
- data/test/rails_app_integration_test.rb +50 -0
- data/test/rails_architect_test.rb +21 -0
- data/test/test_helper.rb +15 -0
- metadata +20 -7
- data/lib/rails/architect/version.rb +0 -7
- data/lib/rails/architect.rb +0 -10
- data/sig/rails/architect.rbs +0 -6
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsArchitect
|
|
4
|
+
class KissAnalyzer
|
|
5
|
+
attr_reader :issues
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@issues = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def analyze
|
|
12
|
+
check_method_complexity
|
|
13
|
+
check_class_size
|
|
14
|
+
check_nesting_depth
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def add_issue(file, type, message)
|
|
20
|
+
@issues << { file: file, type: type, message: message }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def check_method_complexity
|
|
26
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
27
|
+
content = File.read(file)
|
|
28
|
+
methods = extract_methods(content)
|
|
29
|
+
|
|
30
|
+
methods.each do |method_name, method_body|
|
|
31
|
+
complexity = calculate_cyclomatic_complexity(method_body)
|
|
32
|
+
lines = method_body.count("\n") + 1
|
|
33
|
+
|
|
34
|
+
if complexity > 10
|
|
35
|
+
class_name = extract_class_name(file)
|
|
36
|
+
add_issue(file, "kiss_complexity", "#{class_name}##{method_name} has cyclomatic complexity #{complexity}, keep it simple")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if lines > 25
|
|
40
|
+
class_name = extract_class_name(file)
|
|
41
|
+
add_issue(file, "kiss_length", "#{class_name}##{method_name} is #{lines} lines long, keep it simple")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def check_class_size
|
|
48
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
49
|
+
content = File.read(file)
|
|
50
|
+
lines = content.count("\n") + 1
|
|
51
|
+
|
|
52
|
+
if lines > 200
|
|
53
|
+
class_name = extract_class_name(file)
|
|
54
|
+
add_issue(file, "kiss_class_size", "#{class_name} is #{lines} lines long, consider splitting into smaller classes")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_nesting_depth
|
|
60
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
61
|
+
content = File.read(file)
|
|
62
|
+
max_depth = calculate_max_nesting_depth(content)
|
|
63
|
+
|
|
64
|
+
if max_depth > 4
|
|
65
|
+
class_name = extract_class_name(file)
|
|
66
|
+
add_issue(file, "kiss_nesting", "#{class_name} has nesting depth of #{max_depth}, keep it simple")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def extract_methods(content)
|
|
72
|
+
methods = {}
|
|
73
|
+
current_method = nil
|
|
74
|
+
indent_level = 0
|
|
75
|
+
method_start = false
|
|
76
|
+
|
|
77
|
+
content.each_line do |line|
|
|
78
|
+
if line.match?(/^\s*def \w+/)
|
|
79
|
+
current_method = line.strip.match(/def (\w+)/)[1]
|
|
80
|
+
methods[current_method] = ""
|
|
81
|
+
method_start = true
|
|
82
|
+
indent_level = line.match(/^\s*/).to_s.length
|
|
83
|
+
elsif method_start && line.match(/^\s*end\s*$/) && line.match(/^\s*/).to_s.length == indent_level
|
|
84
|
+
method_start = false
|
|
85
|
+
current_method = nil
|
|
86
|
+
elsif method_start
|
|
87
|
+
methods[current_method] += line
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
methods
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def calculate_cyclomatic_complexity(method_body)
|
|
95
|
+
complexity = 1 # base complexity
|
|
96
|
+
|
|
97
|
+
# Count control flow keywords
|
|
98
|
+
complexity += method_body.scan(/\b(if|unless|case|when|while|until|for|rescue|&&|\|\|)\b/).count
|
|
99
|
+
complexity += method_body.scan(/\?\s*:/).count # ternary operators
|
|
100
|
+
|
|
101
|
+
complexity
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def calculate_max_nesting_depth(content)
|
|
105
|
+
max_depth = 0
|
|
106
|
+
current_depth = 0
|
|
107
|
+
|
|
108
|
+
content.each_line do |line|
|
|
109
|
+
indent = line.match(/^\s*/).to_s.length / 2 # assuming 2 spaces per indent
|
|
110
|
+
|
|
111
|
+
# Increase depth for control structures
|
|
112
|
+
if line.match?(/\b(if|unless|case|while|until|for|begin|def|class|module)\b/)
|
|
113
|
+
current_depth = [current_depth, indent + 1].max
|
|
114
|
+
max_depth = [max_depth, current_depth].max
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
max_depth
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def extract_class_name(file)
|
|
122
|
+
relative_path = file.sub('app/', '').sub('.rb', '')
|
|
123
|
+
parts = relative_path.split('/')
|
|
124
|
+
class_parts = parts.map do |part|
|
|
125
|
+
part.split('_').map(&:capitalize).join
|
|
126
|
+
end
|
|
127
|
+
class_parts.join('::')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsArchitect
|
|
4
|
+
class SolidAnalyzer
|
|
5
|
+
attr_reader :issues
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@issues = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def analyze
|
|
12
|
+
check_single_responsibility
|
|
13
|
+
check_open_closed
|
|
14
|
+
check_liskov_substitution
|
|
15
|
+
check_interface_segregation
|
|
16
|
+
check_dependency_inversion
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def add_issue(file, type, message)
|
|
22
|
+
@issues << { file: file, type: type, message: message }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def check_single_responsibility
|
|
26
|
+
# Check for classes with too many public methods (fat interfaces)
|
|
27
|
+
Dir.glob("app/models/**/*.rb").each do |file|
|
|
28
|
+
content = File.read(file)
|
|
29
|
+
public_methods = count_public_methods(content)
|
|
30
|
+
if public_methods > 10
|
|
31
|
+
class_name = extract_class_name(file)
|
|
32
|
+
add_issue(file, "solid_srp", "#{class_name} has #{public_methods} public methods, violates Single Responsibility Principle (fat model)")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check for models with callbacks doing business logic
|
|
36
|
+
callback_methods = content.scan(/after_create|before_save|after_save|before_destroy|after_destroy/).count
|
|
37
|
+
if callback_methods > 2
|
|
38
|
+
class_name = extract_class_name(file)
|
|
39
|
+
add_issue(file, "solid_srp", "#{class_name} has #{callback_methods} callbacks, consider moving to service objects or jobs")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check for models with complex business logic methods
|
|
43
|
+
complex_methods = content.scan(/def \w+.*\n.*\n.*\n.*\n.*end/m).count
|
|
44
|
+
if complex_methods > 0
|
|
45
|
+
class_name = extract_class_name(file)
|
|
46
|
+
add_issue(file, "solid_srp", "#{class_name} has complex methods, extract to service objects")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Dir.glob("app/controllers/**/*.rb").each do |file|
|
|
51
|
+
content = File.read(file)
|
|
52
|
+
public_methods = count_public_methods(content)
|
|
53
|
+
if public_methods > 8
|
|
54
|
+
class_name = extract_class_name(file)
|
|
55
|
+
add_issue(file, "solid_srp", "#{class_name} has #{public_methods} public actions, violates Single Responsibility Principle (fat controller)")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check for controllers doing business logic
|
|
59
|
+
if content.include?("UserMailer") || content.include?("NotificationService") || content.include?("ActivityLogger")
|
|
60
|
+
class_name = extract_class_name(file)
|
|
61
|
+
add_issue(file, "solid_srp", "#{class_name} is doing business logic, extract to service objects")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_open_closed
|
|
67
|
+
# Check for long inheritance chains
|
|
68
|
+
Dir.glob("app/models/**/*.rb").each do |file|
|
|
69
|
+
content = File.read(file)
|
|
70
|
+
inheritance_depth = count_inheritance_depth(content)
|
|
71
|
+
if inheritance_depth > 3
|
|
72
|
+
class_name = extract_class_name(file)
|
|
73
|
+
add_issue(file, "solid_ocp", "#{class_name} has inheritance depth of #{inheritance_depth}, consider composition over inheritance")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check for classes that would need modification for new features
|
|
78
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
79
|
+
content = File.read(file)
|
|
80
|
+
if content.include?("case ") && content.scan(/when /).count > 3
|
|
81
|
+
class_name = extract_class_name(file)
|
|
82
|
+
add_issue(file, "solid_ocp", "#{class_name} uses case statements that may need modification, consider polymorphism")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_liskov_substitution
|
|
88
|
+
# Check for STI models that might violate LSP
|
|
89
|
+
Dir.glob("app/models/**/*.rb").each do |file|
|
|
90
|
+
content = File.read(file)
|
|
91
|
+
if content.include?("self.inheritance_column") || content.match?(/class \w+ < \w+/)
|
|
92
|
+
class_name = extract_class_name(file)
|
|
93
|
+
# Look for methods that might not be implemented in subclasses
|
|
94
|
+
methods_with_bang = content.scan(/def \w+!/).map { |m| m.sub("def ", "").sub("!", "") }
|
|
95
|
+
if methods_with_bang.any?
|
|
96
|
+
add_issue(file, "solid_lsp", "#{class_name} uses inheritance/STI, ensure subclasses implement #{methods_with_bang.join(', ')} methods")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check for polymorphic associations that might violate contracts
|
|
102
|
+
Dir.glob("app/models/**/*.rb").each do |file|
|
|
103
|
+
content = File.read(file)
|
|
104
|
+
if content.include?("belongs_to :") && content.include?("polymorphic: true")
|
|
105
|
+
class_name = extract_class_name(file)
|
|
106
|
+
add_issue(file, "solid_lsp", "#{class_name} uses polymorphic associations, ensure all associated classes honor the same interface")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def check_interface_segregation
|
|
112
|
+
# Check for controllers with many dependencies (includes/modules)
|
|
113
|
+
Dir.glob("app/controllers/**/*.rb").each do |file|
|
|
114
|
+
content = File.read(file)
|
|
115
|
+
includes_count = content.scan(/\binclude\b|\bextend\b|\bprepend\b/).count
|
|
116
|
+
if includes_count > 3
|
|
117
|
+
class_name = extract_class_name(file)
|
|
118
|
+
add_issue(file, "solid_isp", "#{class_name} includes #{includes_count} modules, consider interface segregation")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Check for heavy ActiveSupport::Concern usage
|
|
123
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
124
|
+
content = File.read(file)
|
|
125
|
+
concern_methods = content.scan(/def \w+/).count
|
|
126
|
+
if content.include?("ActiveSupport::Concern") && concern_methods > 5
|
|
127
|
+
class_name = extract_class_name(file)
|
|
128
|
+
add_issue(file, "solid_isp", "#{class_name} concern has #{concern_methods} methods, consider splitting into smaller concerns")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check for god services with many methods
|
|
133
|
+
Dir.glob("app/services/**/*.rb").each do |file|
|
|
134
|
+
content = File.read(file)
|
|
135
|
+
public_methods = count_public_methods(content)
|
|
136
|
+
if public_methods > 10
|
|
137
|
+
class_name = extract_class_name(file)
|
|
138
|
+
add_issue(file, "solid_isp", "#{class_name} service has #{public_methods} methods, split into smaller focused services")
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def check_dependency_inversion
|
|
144
|
+
# Check for direct instantiation of classes (new keyword)
|
|
145
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
146
|
+
content = File.read(file)
|
|
147
|
+
new_calls = content.scan(/\b\w+\.new\b/).count
|
|
148
|
+
if new_calls > 2
|
|
149
|
+
class_name = extract_class_name(file)
|
|
150
|
+
add_issue(file, "solid_dip", "#{class_name} has #{new_calls} direct instantiations, consider dependency injection")
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check for hard-coded external service dependencies
|
|
155
|
+
Dir.glob("app/**/*.rb").each do |file|
|
|
156
|
+
content = File.read(file)
|
|
157
|
+
if content.include?("TwilioClient.new") || content.include?("Stripe::") || content.include?("HTTParty.get")
|
|
158
|
+
class_name = extract_class_name(file)
|
|
159
|
+
add_issue(file, "solid_dip", "#{class_name} has hard-coded external dependencies, inject abstractions instead")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check for model callbacks triggering network calls
|
|
164
|
+
Dir.glob("app/models/**/*.rb").each do |file|
|
|
165
|
+
content = File.read(file)
|
|
166
|
+
callback_content = content.split(/\b(private|protected)\b/).first
|
|
167
|
+
if callback_content.include?("deliver") || callback_content.include?("HTTP") || callback_content.include?("Net::")
|
|
168
|
+
class_name = extract_class_name(file)
|
|
169
|
+
add_issue(file, "solid_dip", "#{class_name} has network calls in callbacks, move to background jobs")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def count_public_methods(content)
|
|
175
|
+
public_content = content.split(/\b(private|protected)\b/).first
|
|
176
|
+
public_content.scan(/def \w+/).count
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def count_inheritance_depth(content)
|
|
180
|
+
inheritance_matches = content.scan(/class \w+ < (\w+(?:::\w+)*)/)
|
|
181
|
+
return 0 if inheritance_matches.empty?
|
|
182
|
+
|
|
183
|
+
# Simple depth calculation - in real app would need to resolve inheritance tree
|
|
184
|
+
inheritance_matches.first.first.split('::').count
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def extract_class_name(file)
|
|
188
|
+
relative_path = file.sub('app/', '').sub('.rb', '')
|
|
189
|
+
parts = relative_path.split('/')
|
|
190
|
+
class_parts = parts.map do |part|
|
|
191
|
+
part.split('_').map(&:capitalize).join
|
|
192
|
+
end
|
|
193
|
+
class_parts.join('::')
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_architect'
|
|
4
|
+
|
|
5
|
+
namespace :rails_architect do
|
|
6
|
+
desc "Check SOLID principles"
|
|
7
|
+
task check_solid: :environment do
|
|
8
|
+
analyzer = RailsArchitect::SolidAnalyzer.new
|
|
9
|
+
analyzer.analyze
|
|
10
|
+
issues = analyzer.issues
|
|
11
|
+
puts "SOLID violations: #{issues.count}"
|
|
12
|
+
issues.each { |issue| puts "- #{issue[:message]}" }
|
|
13
|
+
exit 1 if issues.any?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Check KISS principle (Keep It Simple)"
|
|
17
|
+
task check_kiss: :environment do
|
|
18
|
+
analyzer = RailsArchitect::KissAnalyzer.new
|
|
19
|
+
analyzer.analyze
|
|
20
|
+
issues = analyzer.issues
|
|
21
|
+
puts "KISS violations: #{issues.count}"
|
|
22
|
+
issues.each { |issue| puts "- #{issue[:message]}" }
|
|
23
|
+
exit 1 if issues.any?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc "Check DRY principle (Don't Repeat Yourself)"
|
|
27
|
+
task check_dry: :environment do
|
|
28
|
+
analyzer = RailsArchitect::DryAnalyzer.new
|
|
29
|
+
analyzer.analyze
|
|
30
|
+
issues = analyzer.issues
|
|
31
|
+
puts "DRY violations: #{issues.count}"
|
|
32
|
+
issues.each { |issue| puts "- #{issue[:message]}" }
|
|
33
|
+
exit 1 if issues.any?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_architect/version"
|
|
4
|
+
require "rails_architect/solid_analyzer"
|
|
5
|
+
require "rails_architect/kiss_analyzer"
|
|
6
|
+
require "rails_architect/dry_analyzer"
|
|
7
|
+
require "rails_architect/railtie"
|
|
8
|
+
|
|
9
|
+
module RailsArchitect
|
|
10
|
+
def self.analyze
|
|
11
|
+
all_issues = []
|
|
12
|
+
|
|
13
|
+
# Run SOLID analysis
|
|
14
|
+
solid_analyzer = SolidAnalyzer.new
|
|
15
|
+
solid_analyzer.analyze
|
|
16
|
+
all_issues.concat(solid_analyzer.issues)
|
|
17
|
+
|
|
18
|
+
# Run KISS analysis
|
|
19
|
+
kiss_analyzer = KissAnalyzer.new
|
|
20
|
+
kiss_analyzer.analyze
|
|
21
|
+
all_issues.concat(kiss_analyzer.issues)
|
|
22
|
+
|
|
23
|
+
# Run DRY analysis
|
|
24
|
+
dry_analyzer = DryAnalyzer.new
|
|
25
|
+
dry_analyzer.analyze
|
|
26
|
+
all_issues.concat(dry_analyzer.issues)
|
|
27
|
+
|
|
28
|
+
all_issues
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Test Rails Application
|
|
2
|
+
|
|
3
|
+
This is a sample Rails application designed to demonstrate various code quality violations that Rails Architect can detect.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This app contains intentional violations of software design principles to test the analyzers.
|
|
8
|
+
|
|
9
|
+
## Violations by File
|
|
10
|
+
|
|
11
|
+
### Models
|
|
12
|
+
|
|
13
|
+
#### `app/models/user.rb`
|
|
14
|
+
- **SOLID Violation**: Fat Model with 15+ instance methods
|
|
15
|
+
- Violates Single Responsibility Principle
|
|
16
|
+
- Should be split into separate concerns/services
|
|
17
|
+
- Exceeds threshold of 10 methods per model
|
|
18
|
+
|
|
19
|
+
#### `app/models/report_generator.rb`
|
|
20
|
+
- **KISS Violation**: Overly complex `generate_complex_report` method
|
|
21
|
+
- Deeply nested conditionals (if/elsif chains)
|
|
22
|
+
- Multiple loop types (while, until, for)
|
|
23
|
+
- Complex case statements
|
|
24
|
+
- Exceeds complexity threshold of 10
|
|
25
|
+
|
|
26
|
+
- **DRY Violation**: Duplicated calculation logic
|
|
27
|
+
- `calculate_user_score_v1` and `calculate_user_score_v2` contain identical code
|
|
28
|
+
- Should be refactored to a single method
|
|
29
|
+
|
|
30
|
+
#### `app/models/post.rb`
|
|
31
|
+
- Example of a well-structured model (no violations)
|
|
32
|
+
|
|
33
|
+
### Controllers
|
|
34
|
+
|
|
35
|
+
#### `app/controllers/users_controller.rb`
|
|
36
|
+
- **SOLID Violation**: Controller doing too much
|
|
37
|
+
- Business logic in controller actions (calculating stats, reputation)
|
|
38
|
+
- Multiple responsibilities (data fetching, business logic, presentation)
|
|
39
|
+
- Should use service objects
|
|
40
|
+
|
|
41
|
+
- **KISS Violation**: Complex action logic
|
|
42
|
+
- Nested conditionals for permissions
|
|
43
|
+
- Multiple concerns mixed together in `create` action
|
|
44
|
+
|
|
45
|
+
#### `app/controllers/posts_controller.rb`
|
|
46
|
+
- **DRY Violation**: Repeated query and filtering patterns
|
|
47
|
+
- Same filtering logic in `index` and `recent` actions
|
|
48
|
+
- Repeated published/draft post selection
|
|
49
|
+
- Should be extracted to scopes or service methods
|
|
50
|
+
|
|
51
|
+
## Running Checks
|
|
52
|
+
|
|
53
|
+
From the gem root directory:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Check SOLID principles
|
|
57
|
+
bundle exec rake rails_architect:check_solid
|
|
58
|
+
|
|
59
|
+
# Check KISS principle
|
|
60
|
+
bundle exec rake rails_architect:check_kiss
|
|
61
|
+
|
|
62
|
+
# Check DRY principle
|
|
63
|
+
bundle exec rake rails_architect:check_dry
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Expected Results
|
|
67
|
+
|
|
68
|
+
When running the analyzers, you should see violations detected in:
|
|
69
|
+
- User model (too many methods)
|
|
70
|
+
- ReportGenerator (complex method, duplicated code)
|
|
71
|
+
- UsersController (fat controller)
|
|
72
|
+
- PostsController (code duplication)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
class PostsController < ApplicationController
|
|
2
|
+
# DRY Violation: Repeated query patterns
|
|
3
|
+
def index
|
|
4
|
+
@posts = Post.all.includes(:user)
|
|
5
|
+
|
|
6
|
+
# Potential N+1 query - accessing user.name for each post
|
|
7
|
+
@posts.each do |post|
|
|
8
|
+
puts "Author: #{post.user.name}"
|
|
9
|
+
puts "Comments: #{post.comments.count}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Repeated filtering logic (DRY violation)
|
|
13
|
+
@published_posts = @posts.select { |p| p.published? }
|
|
14
|
+
@draft_posts = @posts.reject { |p| p.published? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
@post = Post.find(params[:id])
|
|
19
|
+
@comments = @post.comments.includes(:user)
|
|
20
|
+
|
|
21
|
+
# Another potential N+1
|
|
22
|
+
@comments.each do |comment|
|
|
23
|
+
puts comment.user.email
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# DRY Violation: Similar logic to index
|
|
28
|
+
def recent
|
|
29
|
+
@posts = Post.all.includes(:user)
|
|
30
|
+
|
|
31
|
+
# Same filtering logic as in index
|
|
32
|
+
@published_posts = @posts.select { |p| p.published? }
|
|
33
|
+
@draft_posts = @posts.reject { |p| p.published? }
|
|
34
|
+
|
|
35
|
+
render :index
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create
|
|
39
|
+
@post = current_user.posts.build(post_params)
|
|
40
|
+
|
|
41
|
+
if @post.save
|
|
42
|
+
redirect_to @post
|
|
43
|
+
else
|
|
44
|
+
render :new
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def post_params
|
|
51
|
+
params.require(:post).permit(:title, :content, :published)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
class UsersController < ApplicationController
|
|
2
|
+
before_action :set_user, only: [:show, :edit, :update, :destroy]
|
|
3
|
+
before_action :authenticate_user!
|
|
4
|
+
before_action :authorize_admin, only: [:index, :destroy]
|
|
5
|
+
|
|
6
|
+
# SOLID Violation: Controller action doing too much (violates Single Responsibility)
|
|
7
|
+
def index
|
|
8
|
+
@users = User.all
|
|
9
|
+
@users = @users.where(organization_id: current_user.organization_id) unless current_user.admin?
|
|
10
|
+
@users = @users.page(params[:page]).per(20)
|
|
11
|
+
|
|
12
|
+
# Doing business logic in controller
|
|
13
|
+
@user_stats = @users.map do |user|
|
|
14
|
+
{
|
|
15
|
+
id: user.id,
|
|
16
|
+
posts_count: user.posts.count,
|
|
17
|
+
comments_count: user.comments.count,
|
|
18
|
+
reputation: user.posts.count * 5 + user.comments.count * 2
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# KISS Violation: Complex controller action
|
|
24
|
+
def show
|
|
25
|
+
@posts = @user.posts.includes(:comments).limit(10)
|
|
26
|
+
@recent_comments = @user.comments.order(created_at: :desc).limit(5)
|
|
27
|
+
|
|
28
|
+
# Complex conditional logic in controller
|
|
29
|
+
if @user.admin?
|
|
30
|
+
@permissions = ['all']
|
|
31
|
+
elsif @user.moderator?
|
|
32
|
+
@permissions = ['read', 'write', 'moderate']
|
|
33
|
+
else
|
|
34
|
+
@permissions = ['read']
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def new
|
|
39
|
+
@user = User.new
|
|
40
|
+
@organizations = Organization.all
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create
|
|
44
|
+
@user = User.new(user_params)
|
|
45
|
+
@user.organization = current_user.organization unless current_user.admin?
|
|
46
|
+
|
|
47
|
+
if @user.save
|
|
48
|
+
# Should be in a service object
|
|
49
|
+
UserMailer.welcome(@user).deliver_later
|
|
50
|
+
NotificationService.notify_admins(@user)
|
|
51
|
+
ActivityLogger.log_user_creation(@user)
|
|
52
|
+
|
|
53
|
+
redirect_to @user, notice: 'User was successfully created.'
|
|
54
|
+
else
|
|
55
|
+
@organizations = Organization.all
|
|
56
|
+
render :new
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def edit
|
|
61
|
+
@organizations = Organization.all
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def update
|
|
65
|
+
if @user.update(user_params)
|
|
66
|
+
redirect_to @user, notice: 'User was successfully updated.'
|
|
67
|
+
else
|
|
68
|
+
@organizations = Organization.all
|
|
69
|
+
render :edit
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def destroy
|
|
74
|
+
@user.destroy
|
|
75
|
+
redirect_to users_url, notice: 'User was successfully destroyed.'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def activate
|
|
79
|
+
@user = User.find(params[:id])
|
|
80
|
+
@user.activate
|
|
81
|
+
redirect_to @user, notice: 'User activated.'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def deactivate
|
|
85
|
+
@user = User.find(params[:id])
|
|
86
|
+
@user.deactivate
|
|
87
|
+
redirect_to @user, notice: 'User deactivated.'
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def set_user
|
|
93
|
+
@user = User.find(params[:id])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def user_params
|
|
97
|
+
params.require(:user).permit(:name, :email, :role, :organization_id)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def authorize_admin
|
|
101
|
+
redirect_to root_path unless current_user.admin?
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Post < ApplicationRecord
|
|
2
|
+
belongs_to :user
|
|
3
|
+
has_many :comments
|
|
4
|
+
|
|
5
|
+
validates :title, presence: true
|
|
6
|
+
validates :content, presence: true
|
|
7
|
+
|
|
8
|
+
scope :published, -> { where(published: true) }
|
|
9
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
10
|
+
|
|
11
|
+
def author_name
|
|
12
|
+
user.name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def comment_count
|
|
16
|
+
comments.count
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def published_comments
|
|
20
|
+
comments.where(published: true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def featured?
|
|
24
|
+
featured
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def publish
|
|
28
|
+
update(published: true, published_at: Time.current)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def unpublish
|
|
32
|
+
update(published: false, published_at: nil)
|
|
33
|
+
end
|
|
34
|
+
end
|