class RailsCodeAuditor::Scorer
def self.calculate_score(issue_count, active_tool_count)
def self.calculate_score(issue_count, active_tool_count) return 100 if issue_count == 0 return 0 if active_tool_count == 0 score = 100 - (issue_count.to_f / (active_tool_count * 10)) * 10 [[score.round, 0].max, 100].min end
def self.code_quality_score(results)
def self.code_quality_score(results) issue_counts = [ extract_issue_count(results.dig(:rubocop, :status)), extract_issue_count(results.dig(:rails_best_practices, :status)), extract_issue_count(results.dig(:reek, :status)), extract_issue_count(results.dig(:flay, :status)), extract_issue_count(results.dig(:flog, :status)), extract_issue_count(results.dig(:fasterer, :status)) ].compact total_issues = issue_counts.sum active_tool_count = issue_counts.size issue_score = calculate_score(total_issues, active_tool_count) # Handle RubyCritic separately rubycritic_score = extract_rubycritic_score(results.dig(:rubycritic, :status)) if rubycritic_score ((issue_score + rubycritic_score) / 2.0).round else issue_score end end
def self.dependency_score(results)
def self.dependency_score(results) count = extract_issue_count(results.dig(:license_finder, :status)) return 100 if count.nil? # Tool skipped calculate_score(count, 1) end
def self.extract_issue_count(status)
def self.extract_issue_count(status) return nil unless status.is_a?(String) return nil if status.downcase.include?("skipped") || status.downcase.include?("not run") if match = status.match(/(\d+)/) match[1].to_i else 0 end end
def self.extract_rubycritic_score(status)
def self.extract_rubycritic_score(status) return nil unless status.is_a?(String) return nil if status.downcase.include?("skipped") || status.downcase.include?("not run") return unless match = status.match(/Score:\s*([0-9.]+)/) match[1].to_f.round end
def self.overall_score(scores_hash)
def self.overall_score(scores_hash) category_scores = scores_hash.values.map { |v| v[:score] }.compact return 0 if category_scores.empty? (category_scores.sum / category_scores.size.to_f).round end
def self.remark_for(score)
def self.remark_for(score) case score when 90..100 then "Excellent" when 75..89 then "Good" when 60..74 then "Fair" else "Needs Improvement" end end
def self.score(results)
def self.score(results) scores = { security: { score: security_score(results), remark: remark_for(security_score(results)) }, code_quality: { score: code_quality_score(results), remark: remark_for(code_quality_score(results)) }, dependencies: { score: dependency_score(results), remark: remark_for(dependency_score(results)) }, test_coverage: { score: test_coverage_score(results), remark: remark_for(test_coverage_score(results)) } } overall = overall_score(scores) scores[:overall] = { score: overall, remark: remark_for(overall) } scores end
def self.security_score(results)
def self.security_score(results) tool_scores = [ extract_issue_count(results.dig(:brakeman, :status)), extract_issue_count(results.dig(:bundler_audit, :status)) ].compact total = tool_scores.sum active_tools = tool_scores.size calculate_score(total, active_tools) end
def self.test_coverage_score(results)
def self.test_coverage_score(results) status = results.dig(:simplecov, :status) return 100 if !status.is_a?(String) || status.downcase.include?("skipped") || status.downcase.include?("not run") if status.match(/Coverage:\s*([\d.]+)/) ::Regexp.last_match(1).to_f.round else 0 end end