solid_score 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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +427 -0
  4. data/config/default.yml +36 -0
  5. data/exe/solid-score +6 -0
  6. data/lib/rubocop/cop/solid_score/dependency_inversion.rb +22 -0
  7. data/lib/rubocop/cop/solid_score/helpers.rb +74 -0
  8. data/lib/rubocop/cop/solid_score/interface_segregation.rb +22 -0
  9. data/lib/rubocop/cop/solid_score/liskov_substitution.rb +22 -0
  10. data/lib/rubocop/cop/solid_score/open_closed.rb +22 -0
  11. data/lib/rubocop/cop/solid_score/principle_base.rb +47 -0
  12. data/lib/rubocop/cop/solid_score/single_responsibility.rb +22 -0
  13. data/lib/rubocop/cop/solid_score/total_score.rb +54 -0
  14. data/lib/rubocop/solid_score/inject.rb +17 -0
  15. data/lib/rubocop-solid_score.rb +16 -0
  16. data/lib/solid_score/analyzers/base_analyzer.rb +17 -0
  17. data/lib/solid_score/analyzers/dip_analyzer.rb +173 -0
  18. data/lib/solid_score/analyzers/isp_analyzer.rb +131 -0
  19. data/lib/solid_score/analyzers/lsp_analyzer.rb +54 -0
  20. data/lib/solid_score/analyzers/mitigations.rb +19 -0
  21. data/lib/solid_score/analyzers/ocp_analyzer.rb +104 -0
  22. data/lib/solid_score/analyzers/srp_analyzer.rb +177 -0
  23. data/lib/solid_score/cli.rb +83 -0
  24. data/lib/solid_score/configuration.rb +159 -0
  25. data/lib/solid_score/diff_analyzer.rb +58 -0
  26. data/lib/solid_score/diff_classifier.rb +49 -0
  27. data/lib/solid_score/formatters/base_formatter.rb +11 -0
  28. data/lib/solid_score/formatters/html_formatter.rb +223 -0
  29. data/lib/solid_score/formatters/json_formatter.rb +45 -0
  30. data/lib/solid_score/formatters/text_formatter.rb +54 -0
  31. data/lib/solid_score/models/class_info.rb +150 -0
  32. data/lib/solid_score/models/method_info.rb +82 -0
  33. data/lib/solid_score/models/score_result.rb +95 -0
  34. data/lib/solid_score/parser/ruby_parser.rb +451 -0
  35. data/lib/solid_score/presets.rb +64 -0
  36. data/lib/solid_score/runner.rb +82 -0
  37. data/lib/solid_score/scorer.rb +35 -0
  38. data/lib/solid_score/version.rb +5 -0
  39. data/lib/solid_score.rb +28 -0
  40. metadata +114 -0
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidScore
4
+ module Formatters
5
+ class HtmlFormatter < BaseFormatter
6
+ def format(results)
7
+ return empty_html if results.empty?
8
+
9
+ build_html(results)
10
+ end
11
+
12
+ private
13
+
14
+ def empty_html
15
+ build_page("No classes found to analyze.", "")
16
+ end
17
+
18
+ def build_html(results)
19
+ summary = build_summary(results)
20
+ chart = build_chart(results)
21
+ table = build_table(results)
22
+
23
+ build_page(summary + chart, table)
24
+ end
25
+
26
+ def build_page(header_content, body_content)
27
+ <<~HTML
28
+ <!DOCTYPE html>
29
+ <html lang="en">
30
+ <head>
31
+ <meta charset="utf-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1">
33
+ <title>solid-score Report</title>
34
+ <style>#{css}</style>
35
+ </head>
36
+ <body>
37
+ <div class="container">
38
+ <h1>solid-score Report <span class="version">v#{VERSION}</span></h1>
39
+ #{header_content}
40
+ #{body_content}
41
+ <footer>Generated by solid-score v#{VERSION}</footer>
42
+ </div>
43
+ </body>
44
+ </html>
45
+ HTML
46
+ end
47
+
48
+ def build_summary(results)
49
+ avg_total = (results.sum(&:total) / results.size).round(1)
50
+ avgs = %i[srp ocp lsp isp dip].map do |p|
51
+ [p, (results.sum(&p) / results.size).round(1)]
52
+ end.to_h
53
+
54
+ <<~HTML
55
+ <div class="summary">
56
+ <div class="summary-card summary-total">
57
+ <div class="summary-label">Average Score</div>
58
+ <div class="summary-value #{score_color_class(avg_total)}">#{avg_total}</div>
59
+ </div>
60
+ <div class="summary-card">
61
+ <div class="summary-label">Classes Analyzed</div>
62
+ <div class="summary-value">#{results.size}</div>
63
+ </div>
64
+ #{avgs.map { |p, v| principle_card(p, v) }.join}
65
+ </div>
66
+ HTML
67
+ end
68
+
69
+ def principle_card(principle, value)
70
+ <<~HTML
71
+ <div class="summary-card">
72
+ <div class="summary-label">#{principle.to_s.upcase}</div>
73
+ <div class="summary-value #{score_color_class(value)}">#{value}</div>
74
+ </div>
75
+ HTML
76
+ end
77
+
78
+ def build_chart(results)
79
+ avgs = %i[srp ocp lsp isp dip].map do |p|
80
+ [p.to_s.upcase, (results.sum(&p) / results.size).round(1)]
81
+ end
82
+
83
+ bar_height = 32
84
+ gap = 8
85
+ chart_height = avgs.size * (bar_height + gap) + 20
86
+ label_width = 40
87
+ chart_width = 500
88
+
89
+ bars = avgs.each_with_index.map do |(label, value), i|
90
+ y = i * (bar_height + gap) + 10
91
+ bar_w = [(value / 100.0 * (chart_width - label_width - 60)).round(1), 0].max
92
+ color = score_hex_color(value)
93
+
94
+ <<~SVG
95
+ <text x="#{label_width - 5}" y="#{y + bar_height / 2 + 5}" text-anchor="end" class="chart-label">#{label}</text>
96
+ <rect x="#{label_width}" y="#{y}" width="#{bar_w}" height="#{bar_height}" rx="4" fill="#{color}" />
97
+ <text x="#{label_width + bar_w + 5}" y="#{y + bar_height / 2 + 5}" class="chart-value">#{value}</text>
98
+ SVG
99
+ end.join
100
+
101
+ <<~HTML
102
+ <div class="chart-container">
103
+ <h2>Score Overview</h2>
104
+ <svg width="#{chart_width}" height="#{chart_height}" class="chart">
105
+ #{bars}
106
+ </svg>
107
+ </div>
108
+ HTML
109
+ end
110
+
111
+ def build_table(results)
112
+ rows = results.sort_by(&:total).map { |r| table_row(r) }.join
113
+
114
+ <<~HTML
115
+ <h2>Details (#{results.size} classes)</h2>
116
+ <div class="table-wrapper">
117
+ <table>
118
+ <thead>
119
+ <tr>
120
+ <th>Class</th>
121
+ <th>File</th>
122
+ <th>SRP</th>
123
+ <th>OCP</th>
124
+ <th>LSP</th>
125
+ <th>ISP</th>
126
+ <th>DIP</th>
127
+ <th>Total</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody>
131
+ #{rows}
132
+ </tbody>
133
+ </table>
134
+ </div>
135
+ HTML
136
+ end
137
+
138
+ def table_row(result)
139
+ <<~HTML
140
+ <tr>
141
+ <td class="class-name">#{escape_html(result.class_name)}</td>
142
+ <td class="file-path">#{escape_html(result.file_path)}</td>
143
+ #{score_cell(result.srp)}
144
+ #{score_cell(result.ocp)}
145
+ #{score_cell(result.lsp)}
146
+ #{score_cell(result.isp)}
147
+ #{score_cell(result.dip)}
148
+ #{score_cell(result.total.round(1))}
149
+ </tr>
150
+ HTML
151
+ end
152
+
153
+ def score_cell(value)
154
+ %(<td class="score #{score_color_class(value)}">#{value}</td>)
155
+ end
156
+
157
+ def score_color_class(score)
158
+ if score >= 80
159
+ "score-green"
160
+ elsif score >= 50
161
+ "score-yellow"
162
+ else
163
+ "score-red"
164
+ end
165
+ end
166
+
167
+ def score_hex_color(score)
168
+ if score >= 80
169
+ "#22c55e"
170
+ elsif score >= 50
171
+ "#eab308"
172
+ else
173
+ "#ef4444"
174
+ end
175
+ end
176
+
177
+ def escape_html(text)
178
+ text.to_s
179
+ .gsub("&", "&amp;")
180
+ .gsub("<", "&lt;")
181
+ .gsub(">", "&gt;")
182
+ .gsub('"', "&quot;")
183
+ end
184
+
185
+ def css
186
+ <<~CSS
187
+ * { margin: 0; padding: 0; box-sizing: border-box; }
188
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f8fafc; color: #1e293b; line-height: 1.6; }
189
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
190
+ h1 { font-size: 1.5rem; margin-bottom: 1.5rem; }
191
+ h1 .version { font-weight: normal; color: #64748b; font-size: 0.875rem; }
192
+ h2 { font-size: 1.125rem; margin: 1.5rem 0 0.75rem; }
193
+
194
+ .summary { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1.5rem; }
195
+ .summary-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 0.75rem 1rem; min-width: 100px; text-align: center; }
196
+ .summary-total { border-width: 2px; }
197
+ .summary-label { font-size: 0.75rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
198
+ .summary-value { font-size: 1.5rem; font-weight: 700; }
199
+
200
+ .chart-container { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; }
201
+ .chart-label { font-size: 13px; fill: #475569; font-family: inherit; }
202
+ .chart-value { font-size: 13px; fill: #475569; font-family: inherit; }
203
+
204
+ .table-wrapper { overflow-x: auto; }
205
+ table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 0.875rem; }
206
+ thead { background: #f1f5f9; }
207
+ th { padding: 0.625rem 0.75rem; text-align: left; font-weight: 600; border-bottom: 2px solid #e2e8f0; }
208
+ td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #f1f5f9; }
209
+ tr:hover { background: #f8fafc; }
210
+ .class-name { font-weight: 500; white-space: nowrap; }
211
+ .file-path { color: #64748b; font-size: 0.8rem; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
212
+ .score { text-align: right; font-variant-numeric: tabular-nums; font-weight: 500; }
213
+
214
+ .score-green { color: #16a34a; }
215
+ .score-yellow { color: #ca8a04; }
216
+ .score-red { color: #dc2626; }
217
+
218
+ footer { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #e2e8f0; color: #94a3b8; font-size: 0.75rem; text-align: center; }
219
+ CSS
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SolidScore
6
+ module Formatters
7
+ class JsonFormatter < BaseFormatter
8
+ def format(results)
9
+ data = {
10
+ version: VERSION,
11
+ classes: results.map { |r| format_result(r) },
12
+ summary: build_summary(results)
13
+ }
14
+
15
+ JSON.pretty_generate(data)
16
+ end
17
+
18
+ private
19
+
20
+ def format_result(result)
21
+ {
22
+ class_name: result.class_name,
23
+ file_path: result.file_path,
24
+ srp: result.srp,
25
+ ocp: result.ocp,
26
+ lsp: result.lsp,
27
+ isp: result.isp,
28
+ dip: result.dip,
29
+ total: result.total.round(1),
30
+ confidence: result.confidence.transform_values(&:to_s),
31
+ subscores: result.subscores
32
+ }
33
+ end
34
+
35
+ def build_summary(results)
36
+ return { total_classes: 0, average_score: 0.0 } if results.empty?
37
+
38
+ {
39
+ total_classes: results.size,
40
+ average_score: (results.sum(&:total) / results.size).round(1)
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidScore
4
+ module Formatters
5
+ class TextFormatter < BaseFormatter
6
+ def format(results)
7
+ return "No classes found to analyze.\n" if results.empty?
8
+
9
+ lines = []
10
+ lines << "solid-score v#{VERSION}\n"
11
+ lines << "Analyzed #{results.size} class(es)\n\n"
12
+ lines << header_line
13
+ lines << separator_line
14
+
15
+ results.each { |r| lines << result_line(r) }
16
+
17
+ lines << separator_line
18
+ lines << average_line(results)
19
+ lines << ""
20
+
21
+ lines.join("\n")
22
+ end
23
+
24
+ private
25
+
26
+ def header_line
27
+ "Class SRP OCP LSP ISP DIP Total"
28
+ end
29
+
30
+ def separator_line
31
+ "-" * 75
32
+ end
33
+
34
+ def result_line(result)
35
+ Kernel.format("%-40s %5.1f %5.1f %5.1f %5.1f %5.1f %7.1f",
36
+ truncate(result.class_name, 40),
37
+ result.srp, result.ocp, result.lsp, result.isp, result.dip, result.total)
38
+ end
39
+
40
+ def average_line(results)
41
+ avg = ->(method) { results.sum(&method) / results.size.to_f }
42
+
43
+ Kernel.format("%-40s %5.1f %5.1f %5.1f %5.1f %5.1f %7.1f",
44
+ "Average",
45
+ avg.call(:srp), avg.call(:ocp), avg.call(:lsp),
46
+ avg.call(:isp), avg.call(:dip), avg.call(:total))
47
+ end
48
+
49
+ def truncate(str, max)
50
+ str.length > max ? "#{str[0..(max - 4)]}..." : str
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidScore
4
+ module Models
5
+ class ClassInfo
6
+ attr_reader :name, :file_path, :line_start, :line_end,
7
+ :methods, :superclass, :includes, :extends,
8
+ :instance_variables, :attr_readers, :attr_writers,
9
+ :kind, :dsl_calls
10
+
11
+ def initialize(name:, file_path: "", line_start: 0, line_end: 0,
12
+ methods: [], superclass: nil, includes: [], extends: [],
13
+ instance_variables: [], attr_readers: [], attr_writers: [],
14
+ kind: :class, dsl_calls: [])
15
+ @name = name
16
+ @file_path = file_path
17
+ @line_start = line_start
18
+ @line_end = line_end
19
+ @methods = methods
20
+ @superclass = superclass
21
+ @includes = includes
22
+ @extends = extends
23
+ @instance_variables = instance_variables
24
+ @attr_readers = attr_readers
25
+ @attr_writers = attr_writers
26
+ @kind = kind
27
+ @dsl_calls = dsl_calls
28
+ end
29
+
30
+ def module?
31
+ kind == :module
32
+ end
33
+
34
+ def public_methods_list
35
+ methods.select { |m| m.public? && m.name != :initialize }
36
+ end
37
+
38
+ def has_superclass?
39
+ !superclass.nil? && !superclass.empty?
40
+ end
41
+
42
+ def data_class?
43
+ all_attrs = attr_readers + attr_writers
44
+ return false if all_attrs.empty?
45
+
46
+ non_init_methods = methods.reject { |m| m.name == :initialize }
47
+ non_init_methods.all? { |m| all_attrs.include?(m.name) }
48
+ end
49
+
50
+ # Phase 2c: レイヤー判別
51
+ # file_pathからRailsのレイヤーを自動判別する
52
+ def layer
53
+ @layer ||= determine_layer
54
+ end
55
+
56
+ # Phase 2c: フレームワーク基盤クラスかどうか
57
+ # ApplicationController, ApplicationRecord 等
58
+ def framework_base_class?
59
+ return false unless has_superclass?
60
+
61
+ FRAMEWORK_DIRECT_BASES.include?(superclass)
62
+ end
63
+
64
+ # Issue #9: Inspection / diagnostic class detection.
65
+ # Console-only or read-only diagnostic helpers grow naturally and should
66
+ # not be penalised for their public surface.
67
+ #
68
+ # Suffixes are anchored with `\z` to avoid matching mid-word names
69
+ # (e.g. `DiagnosticReport` should not be treated as a diagnostic
70
+ # helper). `Tools` and `Debug` are intentionally narrow because they
71
+ # appear in many domain models.
72
+ INSPECTION_NAME_PATTERNS = [
73
+ /Inspect\z/, /Console\z/, /Debug\z/,
74
+ /Diagnostic\z/, /Diagnostics\z/, /Tools\z/
75
+ ].freeze
76
+ INSPECTION_PATH_PATTERN = %r{/(inspectors?|diagnostics?|debug)/}
77
+
78
+ def inspection_class?
79
+ return true if INSPECTION_NAME_PATTERNS.any? { |p| name.match?(p) }
80
+ return true if file_path.match?(INSPECTION_PATH_PATTERN)
81
+
82
+ false
83
+ end
84
+
85
+ # Phase 2c: HTTPクライアントパターンかどうか
86
+ # 全publicメソッドが共通のクライアント系インスタンス変数を参照する構造
87
+ def http_client_pattern?
88
+ client_ivars = instance_variables.select do |iv|
89
+ iv.to_s.match?(/client|http|connection|api/)
90
+ end
91
+ return false if client_ivars.empty?
92
+
93
+ pub = public_methods_list
94
+ return false if pub.size < 2
95
+
96
+ pub.all? do |method|
97
+ client_ivars.any? { |iv| method.instance_variables.include?(iv) }
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ FRAMEWORK_DIRECT_BASES = %w[
104
+ ActiveRecord::Base ActionController::Base ActionController::API
105
+ ActiveJob::Base ActionMailer::Base
106
+ ActionCable::Channel::Base ActionCable::Connection::Base
107
+ ].freeze
108
+
109
+ LAYER_PATH_PATTERNS = {
110
+ controller: "/controllers/",
111
+ model: "/models/",
112
+ service: "/services/",
113
+ job: "/jobs/",
114
+ mailer: "/mailers/",
115
+ form: "/forms/",
116
+ presenter: "/presenters/",
117
+ serializer: "/serializers/",
118
+ validator: "/validators/",
119
+ lib: "/lib/"
120
+ }.freeze
121
+
122
+ SUPERCLASS_LAYERS = {
123
+ "ApplicationRecord" => :model,
124
+ "ActiveRecord::Base" => :model,
125
+ "ApplicationController" => :controller,
126
+ "ActionController::Base" => :controller,
127
+ "ActionController::API" => :controller,
128
+ "ApplicationJob" => :job,
129
+ "ActiveJob::Base" => :job,
130
+ "ApplicationMailer" => :mailer,
131
+ "ActionMailer::Base" => :mailer
132
+ }.freeze
133
+
134
+ def determine_layer
135
+ # file_pathから判別(最も確実)
136
+ LAYER_PATH_PATTERNS.each do |layer, pattern|
137
+ return layer if file_path.include?(pattern)
138
+ end
139
+
140
+ # superclassから判別(file_pathで判別できない場合のフォールバック)
141
+ if has_superclass?
142
+ layer = SUPERCLASS_LAYERS[superclass]
143
+ return layer if layer
144
+ end
145
+
146
+ :unknown
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidScore
4
+ module Models
5
+ # Represents information about a single method call.
6
+ # Phase 1 改善: レシーバ情報を収集するために追加
7
+ class MethodCallInfo
8
+ attr_reader :method_name, :receiver, :receiver_type
9
+
10
+ # @param method_name [Symbol] メソッド名
11
+ # @param receiver [String, nil] レシーバの名前(定数名、変数名など)
12
+ # @param receiver_type [Symbol] レシーバの種類 (:const, :ivar, :lvar, :self, :unknown)
13
+ def initialize(method_name:, receiver: nil, receiver_type: :unknown)
14
+ @method_name = method_name
15
+ @receiver = receiver
16
+ @receiver_type = receiver_type
17
+ end
18
+
19
+ # Check if this is a .new call on a constant (class instantiation)
20
+ def new_call_on_const?
21
+ method_name == :new && receiver_type == :const
22
+ end
23
+ end
24
+
25
+ class MethodInfo
26
+ attr_reader :name, :visibility, :line_start, :line_end,
27
+ :instance_variables, :called_methods, :parameters,
28
+ :cyclomatic_complexity, :raises, :calls_super,
29
+ :method_calls, :case_when_count, :kind,
30
+ :effective_statement_count, :memoized_factory_receiver
31
+
32
+ def initialize(name:, visibility: :public, line_start: 0, line_end: 0,
33
+ instance_variables: [], called_methods: [], parameters: [],
34
+ cyclomatic_complexity: 1, raises: [], calls_super: false,
35
+ method_calls: [], case_when_count: 0, kind: :instance,
36
+ effective_statement_count: 0,
37
+ memoized_factory_receiver: nil, memoized_factory: nil)
38
+ @name = name
39
+ @visibility = visibility
40
+ @line_start = line_start
41
+ @line_end = line_end
42
+ @instance_variables = instance_variables
43
+ @called_methods = called_methods
44
+ @parameters = parameters
45
+ @cyclomatic_complexity = cyclomatic_complexity
46
+ @raises = raises
47
+ @calls_super = calls_super
48
+ @method_calls = method_calls
49
+ @case_when_count = case_when_count
50
+ @kind = kind
51
+ @effective_statement_count = effective_statement_count
52
+ # Issue #12: store the receiver constant of a memoised factory call
53
+ # (e.g. "ProvisioningService" for `@svc ||= ProvisioningService.new`).
54
+ # Older callers used `memoized_factory: true/false` as a boolean flag;
55
+ # accept that for backwards-compat in tests by mapping `true` to a
56
+ # placeholder string. New code should provide the receiver name.
57
+ @memoized_factory_receiver = memoized_factory_receiver
58
+ @memoized_factory_receiver ||= "<unknown>" if memoized_factory == true
59
+ end
60
+
61
+ def public?
62
+ visibility == :public
63
+ end
64
+
65
+ def memoized_factory?
66
+ !@memoized_factory_receiver.nil?
67
+ end
68
+
69
+ def class_method?
70
+ kind == :class
71
+ end
72
+
73
+ def instance_method?
74
+ kind == :instance
75
+ end
76
+
77
+ def empty?
78
+ line_start == line_end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidScore
4
+ module Models
5
+ class ScoreResult
6
+ DEFAULT_WEIGHTS = {
7
+ srp: 0.30,
8
+ ocp: 0.15,
9
+ lsp: 0.10,
10
+ isp: 0.20,
11
+ dip: 0.25
12
+ }.freeze
13
+
14
+ # 基本信頼度(解析手法の本質的な精度)
15
+ BASE_CONFIDENCE = {
16
+ srp: :high,
17
+ ocp: :medium,
18
+ lsp: :medium,
19
+ isp: :medium_high,
20
+ dip: :high
21
+ }.freeze
22
+
23
+ CONFIDENCE_ORDER = %i[low low_medium medium medium_high high].freeze
24
+
25
+ attr_reader :class_name, :file_path, :srp, :ocp, :lsp, :isp, :dip,
26
+ :weights, :class_info, :subscores
27
+
28
+ def initialize(class_name:, file_path: "", srp: 0.0, ocp: 0.0, lsp: 0.0, isp: 0.0, dip: 0.0,
29
+ weights: DEFAULT_WEIGHTS, class_info: nil, subscores: {})
30
+ @class_name = class_name
31
+ @file_path = file_path
32
+ @srp = srp
33
+ @ocp = ocp
34
+ @lsp = lsp
35
+ @isp = isp
36
+ @dip = dip
37
+ @weights = weights
38
+ @class_info = class_info
39
+ @subscores = subscores
40
+ end
41
+
42
+ def total
43
+ (srp * weights[:srp]) +
44
+ (ocp * weights[:ocp]) +
45
+ (lsp * weights[:lsp]) +
46
+ (isp * weights[:isp]) +
47
+ (dip * weights[:dip])
48
+ end
49
+
50
+ # Phase 2b: class_infoに基づく動的な信頼度計算
51
+ def confidence
52
+ return BASE_CONFIDENCE unless class_info
53
+
54
+ {
55
+ srp: adjusted_confidence(:srp),
56
+ ocp: adjusted_confidence(:ocp),
57
+ lsp: adjusted_confidence(:lsp),
58
+ isp: adjusted_confidence(:isp),
59
+ dip: adjusted_confidence(:dip)
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def adjusted_confidence(principle)
66
+ base = BASE_CONFIDENCE[principle]
67
+
68
+ # メソッド数が少ない場合、全原則の信頼度が低下
69
+ base = lower_confidence(base) if few_methods?
70
+
71
+ case principle
72
+ when :lsp
73
+ # モジュールの場合はLSPは常にhigh(継承なしで100点固定)
74
+ return :high if class_info.module?
75
+ # 継承なしもhigh
76
+ return :high unless class_info.has_superclass?
77
+ when :srp
78
+ # メソッド数が非常に少ない場合、LCOM4の意味が薄い
79
+ base = lower_confidence(base) if class_info.methods.size <= 2
80
+ end
81
+
82
+ base
83
+ end
84
+
85
+ def few_methods?
86
+ class_info.methods.size <= 1
87
+ end
88
+
89
+ def lower_confidence(level)
90
+ idx = CONFIDENCE_ORDER.index(level) || 0
91
+ CONFIDENCE_ORDER[[idx - 1, 0].max]
92
+ end
93
+ end
94
+ end
95
+ end