mirror of
https://github.com/Jaganpro/sf-skills.git
synced 2026-09-19 07:52:00 +08:00
8ace846293
2 3 Adds OOTB linting via sf code-analyzer CLI to augment custom 150-point 4 scoring. Hooks auto-trigger on Write/Edit of Apex and Flow files. 5 6 NEW INFRASTRUCTURE (shared/code_analyzer/): 7 - scanner.py: Core wrapper for `sf code-analyzer run` CLI 8 - dependency_checker.py: Multi-path Java detection (Homebrew support) 9 - score_merger.py: Combines custom scoring with CA violations 10 - parser.py: JSON result normalization 11 - formatter.py: Terminal output formatting 12 - config/code-analyzer.yml: Engine configuration 13 14 HOOK INTEGRATION: 15 - sf-apex/hooks: Added Edit matcher, post-tool-validate.py 16 - sf-flow/hooks: Added Edit matcher, post-tool-validate.py 17 - Timeout increased to 120s for CA scans 18 19 KEY FEATURES: 20 - 7 engines: PMD, CPD, SFGE, ESLint, RetireJS, Flow Scanner, Regex 21 - Graceful degradation when deps missing (Java, Node, Python) 22 - Auto-detects Java in Homebrew paths (/opt/homebrew/opt/openjdk@*) 23 - Propagates JAVA_HOME to sf CLI subprocess 24 - Filters engine errors from violation output 25 26 FIXES INCLUDED: 27 - Java detection: Handles Salesforce wrapper scripts 28 - SFGE errors: No longer shown as CRITICAL violations 29 - Class name parsing: Excludes "class" keyword in comments 30 - Flow Python: Explicit python_command in config 31 32 VALIDATION OUTPUT FORMAT: 33 🔍 Apex Validation: AccountService.cls 34 📊 Score: 138/150 ⭐⭐⭐⭐ Very Good 35 🔬 Code Analyzer: pmd, regex (3482ms) 36 ❗ Issues: [sf-skills] + [CA:pmd] combined 37 38 STATS: 14 files changed, ~3500 insertions
420 lines
14 KiB
Python
420 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Score Merger - Combines custom sf-skills scoring with Code Analyzer findings.
|
|
|
|
This module merges the existing sf-skills validation scores (150-point for Apex,
|
|
110-point for Flow, etc.) with Salesforce Code Analyzer V5 findings to produce
|
|
a unified report.
|
|
|
|
Strategy:
|
|
- Custom scoring remains the primary score
|
|
- Code Analyzer findings add additional deductions
|
|
- Critical CA findings reduce score by up to 20 points total
|
|
- High CA findings reduce by up to 10 points total
|
|
- Duplicate findings (same rule) are deduplicated
|
|
- Findings that overlap with custom scoring categories get mapped
|
|
|
|
Usage:
|
|
merger = ScoreMerger(
|
|
custom_scores={"bulkification": 25, "security": 20, ...},
|
|
custom_max_scores={"bulkification": 25, "security": 25, ...}
|
|
)
|
|
merged = merger.merge(ca_violations)
|
|
|
|
print(f"Final score: {merged.final_score}/{merged.final_max}")
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional, Any
|
|
from enum import Enum
|
|
|
|
|
|
class ScoreCategory(Enum):
|
|
"""Universal scoring categories across sf-skills."""
|
|
BULKIFICATION = "bulkification"
|
|
SECURITY = "security"
|
|
TESTING = "testing"
|
|
ARCHITECTURE = "architecture"
|
|
CLEAN_CODE = "clean_code"
|
|
ERROR_HANDLING = "error_handling"
|
|
PERFORMANCE = "performance"
|
|
DOCUMENTATION = "documentation"
|
|
# Flow-specific
|
|
DESIGN = "design"
|
|
LOGIC = "logic"
|
|
OBSERVABILITY = "observability"
|
|
GOVERNANCE = "governance"
|
|
|
|
|
|
@dataclass
|
|
class ScoreDeduction:
|
|
"""A single score deduction from CA findings."""
|
|
rule: str
|
|
engine: str
|
|
severity: int
|
|
severity_label: str
|
|
deduction: int
|
|
category: Optional[str]
|
|
message: str
|
|
line: int = 0
|
|
|
|
|
|
@dataclass
|
|
class MergedScore:
|
|
"""Combined score from custom validator + Code Analyzer."""
|
|
custom_score: int
|
|
custom_max: int
|
|
ca_violations_total: int
|
|
ca_critical: int
|
|
ca_high: int
|
|
ca_deductions: int
|
|
final_score: int
|
|
final_max: int
|
|
rating: str
|
|
rating_stars: int
|
|
deductions: List[ScoreDeduction]
|
|
engines_used: List[str] = field(default_factory=list)
|
|
engines_unavailable: List[str] = field(default_factory=list)
|
|
|
|
|
|
class ScoreMerger:
|
|
"""
|
|
Merges custom validation scores with Code Analyzer findings.
|
|
|
|
The merger applies deductions from CA violations on top of the custom score,
|
|
with caps to prevent excessive penalization.
|
|
"""
|
|
|
|
# Mapping from CA rules to sf-skills score categories
|
|
RULE_CATEGORY_MAP = {
|
|
# PMD Apex rules -> Categories
|
|
"AvoidSoqlInLoops": ScoreCategory.BULKIFICATION,
|
|
"AvoidDmlStatementsInLoops": ScoreCategory.BULKIFICATION,
|
|
"OperationWithLimitsInLoop": ScoreCategory.BULKIFICATION,
|
|
|
|
"ApexCRUDViolation": ScoreCategory.SECURITY,
|
|
"ApexSharingViolations": ScoreCategory.SECURITY,
|
|
"ApexSOQLInjection": ScoreCategory.SECURITY,
|
|
"ApexOpenRedirect": ScoreCategory.SECURITY,
|
|
"ApexCSRF": ScoreCategory.SECURITY,
|
|
"ApexBadCrypto": ScoreCategory.SECURITY,
|
|
"ApexInsecureEndpoint": ScoreCategory.SECURITY,
|
|
"ApexXSSFromURLParam": ScoreCategory.SECURITY,
|
|
"ApexXSSFromEscapeFalse": ScoreCategory.SECURITY,
|
|
|
|
"ApexUnitTestClassShouldHaveAsserts": ScoreCategory.TESTING,
|
|
"ApexUnitTestShouldNotUseSeeAllDataTrue": ScoreCategory.TESTING,
|
|
"ApexAssertionsShouldIncludeMessage": ScoreCategory.TESTING,
|
|
"ApexUnitTestMethodShouldHaveIsTestAnnotation": ScoreCategory.TESTING,
|
|
|
|
"CyclomaticComplexity": ScoreCategory.CLEAN_CODE,
|
|
"ExcessiveParameterList": ScoreCategory.CLEAN_CODE,
|
|
"ExcessiveClassLength": ScoreCategory.CLEAN_CODE,
|
|
"ExcessivePublicCount": ScoreCategory.CLEAN_CODE,
|
|
"TooManyFields": ScoreCategory.CLEAN_CODE,
|
|
"NcssMethodCount": ScoreCategory.CLEAN_CODE,
|
|
"NcssTypeCount": ScoreCategory.CLEAN_CODE,
|
|
"NcssConstructorCount": ScoreCategory.CLEAN_CODE,
|
|
"AvoidGlobalModifier": ScoreCategory.CLEAN_CODE,
|
|
"AvoidHardcodingId": ScoreCategory.CLEAN_CODE,
|
|
"AvoidLogicInTrigger": ScoreCategory.ARCHITECTURE,
|
|
"DebugsShouldUseLoggingLevel": ScoreCategory.CLEAN_CODE,
|
|
|
|
"EmptyCatchBlock": ScoreCategory.ERROR_HANDLING,
|
|
"EmptyTryOrFinallyBlock": ScoreCategory.ERROR_HANDLING,
|
|
"EmptyStatementBlock": ScoreCategory.ERROR_HANDLING,
|
|
|
|
"AvoidHardcodingId": ScoreCategory.PERFORMANCE,
|
|
"OperationWithLimitsInLoop": ScoreCategory.PERFORMANCE,
|
|
|
|
"ApexDoc": ScoreCategory.DOCUMENTATION,
|
|
|
|
# Regex rules
|
|
"HardcodedSalesforceUrl": ScoreCategory.CLEAN_CODE,
|
|
"MissingWithSharing": ScoreCategory.SECURITY,
|
|
"DeprecatedTestIsRunning": ScoreCategory.TESTING,
|
|
|
|
# Flow rules
|
|
"DbInLoop": ScoreCategory.BULKIFICATION,
|
|
"GetRecordsInLoop": ScoreCategory.BULKIFICATION,
|
|
"CyclicSubflow": ScoreCategory.ARCHITECTURE,
|
|
"MissingFaultHandler": ScoreCategory.ERROR_HANDLING,
|
|
"MissingFaultPath": ScoreCategory.ERROR_HANDLING,
|
|
"HardcodedId": ScoreCategory.CLEAN_CODE,
|
|
"MissingDescription": ScoreCategory.DOCUMENTATION,
|
|
"MissingNullHandler": ScoreCategory.ERROR_HANDLING,
|
|
"UnusedVariable": ScoreCategory.CLEAN_CODE,
|
|
}
|
|
|
|
# Deduction points per severity
|
|
SEVERITY_DEDUCTIONS = {
|
|
1: 5, # Critical: -5 per violation
|
|
2: 3, # High: -3 per violation
|
|
3: 1, # Moderate: -1 per violation
|
|
4: 0, # Low: informational only
|
|
5: 0, # Info: informational only
|
|
}
|
|
|
|
# Maximum total deductions by severity (caps)
|
|
MAX_DEDUCTIONS_BY_SEVERITY = {
|
|
1: 20, # Max -20 from critical violations
|
|
2: 10, # Max -10 from high violations
|
|
3: 5, # Max -5 from moderate violations
|
|
}
|
|
|
|
# Overall maximum deduction from CA
|
|
MAX_TOTAL_DEDUCTION = 30
|
|
|
|
# Rating thresholds (percentage of max score)
|
|
RATING_THRESHOLDS = [
|
|
(90, "Excellent", 5),
|
|
(75, "Very Good", 4),
|
|
(60, "Good", 3),
|
|
(45, "Needs Work", 2),
|
|
(0, "Critical Issues", 1),
|
|
]
|
|
|
|
def __init__(
|
|
self,
|
|
custom_scores: Dict[str, int],
|
|
custom_max_scores: Dict[str, int],
|
|
):
|
|
"""
|
|
Initialize merger with custom validation scores.
|
|
|
|
Args:
|
|
custom_scores: Dict of category -> current score
|
|
custom_max_scores: Dict of category -> max possible score
|
|
"""
|
|
self.custom_scores = custom_scores
|
|
self.custom_max_scores = custom_max_scores
|
|
self.deductions: List[ScoreDeduction] = []
|
|
|
|
def merge(
|
|
self,
|
|
ca_violations: List[Dict[str, Any]],
|
|
engines_used: Optional[List[str]] = None,
|
|
engines_unavailable: Optional[List[str]] = None,
|
|
) -> MergedScore:
|
|
"""
|
|
Merge Code Analyzer violations with custom scores.
|
|
|
|
Args:
|
|
ca_violations: List of normalized violations from CodeAnalyzerScanner
|
|
engines_used: List of engines that ran
|
|
engines_unavailable: List of engines that couldn't run
|
|
|
|
Returns:
|
|
MergedScore with combined results
|
|
"""
|
|
self.deductions = []
|
|
|
|
# Track deductions by severity
|
|
severity_totals = {1: 0, 2: 0, 3: 0}
|
|
processed_rules = set() # Dedupe same rule violations
|
|
|
|
critical_count = 0
|
|
high_count = 0
|
|
|
|
for violation in ca_violations:
|
|
rule = violation.get("rule", "")
|
|
severity = violation.get("severity", 5)
|
|
line = violation.get("line", 0)
|
|
|
|
# Create unique key for deduplication (rule + line)
|
|
dedup_key = f"{rule}:{line}"
|
|
if dedup_key in processed_rules:
|
|
continue
|
|
processed_rules.add(dedup_key)
|
|
|
|
# Count by severity
|
|
if severity == 1:
|
|
critical_count += 1
|
|
elif severity == 2:
|
|
high_count += 1
|
|
|
|
# Calculate deduction
|
|
base_deduction = self.SEVERITY_DEDUCTIONS.get(severity, 0)
|
|
if base_deduction <= 0:
|
|
continue # No deduction for low/info
|
|
|
|
# Check against severity cap
|
|
current_total = severity_totals.get(severity, 0)
|
|
max_for_severity = self.MAX_DEDUCTIONS_BY_SEVERITY.get(severity, 0)
|
|
actual_deduction = min(base_deduction, max_for_severity - current_total)
|
|
|
|
if actual_deduction <= 0:
|
|
continue # Cap reached for this severity
|
|
|
|
severity_totals[severity] += actual_deduction
|
|
|
|
# Map to category
|
|
category = self.RULE_CATEGORY_MAP.get(rule)
|
|
category_name = category.value if category else "general"
|
|
|
|
self.deductions.append(ScoreDeduction(
|
|
rule=rule,
|
|
engine=violation.get("engine", "unknown"),
|
|
severity=severity,
|
|
severity_label=violation.get("severity_label", "UNKNOWN"),
|
|
deduction=actual_deduction,
|
|
category=category_name,
|
|
message=violation.get("message", "")[:100],
|
|
line=line,
|
|
))
|
|
|
|
# Calculate totals
|
|
custom_total = sum(self.custom_scores.values())
|
|
custom_max = sum(self.custom_max_scores.values())
|
|
|
|
total_deductions = sum(severity_totals.values())
|
|
|
|
# Apply overall cap
|
|
total_deductions = min(total_deductions, self.MAX_TOTAL_DEDUCTION)
|
|
|
|
final_score = max(0, custom_total - total_deductions)
|
|
|
|
# Calculate rating
|
|
rating, rating_stars = self._calculate_rating(final_score, custom_max)
|
|
|
|
return MergedScore(
|
|
custom_score=custom_total,
|
|
custom_max=custom_max,
|
|
ca_violations_total=len(ca_violations),
|
|
ca_critical=critical_count,
|
|
ca_high=high_count,
|
|
ca_deductions=total_deductions,
|
|
final_score=final_score,
|
|
final_max=custom_max,
|
|
rating=rating,
|
|
rating_stars=rating_stars,
|
|
deductions=self.deductions,
|
|
engines_used=engines_used or [],
|
|
engines_unavailable=engines_unavailable or [],
|
|
)
|
|
|
|
def _calculate_rating(self, score: int, max_score: int) -> tuple:
|
|
"""Calculate star rating based on percentage."""
|
|
if max_score == 0:
|
|
return "N/A", 0
|
|
|
|
percentage = (score / max_score) * 100
|
|
|
|
for threshold, label, stars in self.RATING_THRESHOLDS:
|
|
if percentage >= threshold:
|
|
return label, stars
|
|
|
|
return "Critical Issues", 1
|
|
|
|
def get_category_impact(self) -> Dict[str, int]:
|
|
"""
|
|
Get total deductions by category.
|
|
|
|
Returns:
|
|
Dict mapping category name to total deduction points
|
|
"""
|
|
impact = {}
|
|
for d in self.deductions:
|
|
cat = d.category or "general"
|
|
impact[cat] = impact.get(cat, 0) + d.deduction
|
|
return impact
|
|
|
|
|
|
def merge_scores(
|
|
custom_scores: Dict[str, int],
|
|
custom_max_scores: Dict[str, int],
|
|
ca_violations: List[Dict[str, Any]],
|
|
engines_used: Optional[List[str]] = None,
|
|
engines_unavailable: Optional[List[str]] = None,
|
|
) -> MergedScore:
|
|
"""
|
|
Convenience function to merge scores.
|
|
|
|
Args:
|
|
custom_scores: Dict of category -> current score
|
|
custom_max_scores: Dict of category -> max possible score
|
|
ca_violations: List of normalized CA violations
|
|
engines_used: List of engines that ran
|
|
engines_unavailable: List of engines that couldn't run
|
|
|
|
Returns:
|
|
MergedScore with combined results
|
|
"""
|
|
merger = ScoreMerger(custom_scores, custom_max_scores)
|
|
return merger.merge(ca_violations, engines_used, engines_unavailable)
|
|
|
|
|
|
def format_rating_stars(stars: int) -> str:
|
|
"""Format rating as star icons."""
|
|
return "" * stars + "" * (5 - stars)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Demo with sample data
|
|
custom_scores = {
|
|
"bulkification": 25,
|
|
"security": 20,
|
|
"testing": 25,
|
|
"architecture": 18,
|
|
"clean_code": 20,
|
|
"error_handling": 15,
|
|
"performance": 10,
|
|
"documentation": 7,
|
|
}
|
|
|
|
custom_max = {
|
|
"bulkification": 25,
|
|
"security": 25,
|
|
"testing": 25,
|
|
"architecture": 20,
|
|
"clean_code": 20,
|
|
"error_handling": 15,
|
|
"performance": 10,
|
|
"documentation": 10,
|
|
}
|
|
|
|
ca_violations = [
|
|
{
|
|
"rule": "AvoidSoqlInLoops",
|
|
"engine": "pmd",
|
|
"severity": 1,
|
|
"severity_label": "CRITICAL",
|
|
"message": "SOQL query found inside loop",
|
|
"line": 25,
|
|
},
|
|
{
|
|
"rule": "EmptyCatchBlock",
|
|
"engine": "pmd",
|
|
"severity": 2,
|
|
"severity_label": "HIGH",
|
|
"message": "Empty catch block swallows exception",
|
|
"line": 40,
|
|
},
|
|
{
|
|
"rule": "CyclomaticComplexity",
|
|
"engine": "pmd",
|
|
"severity": 3,
|
|
"severity_label": "MODERATE",
|
|
"message": "Method complexity is 15 (threshold 10)",
|
|
"line": 50,
|
|
},
|
|
]
|
|
|
|
merged = merge_scores(
|
|
custom_scores,
|
|
custom_max,
|
|
ca_violations,
|
|
engines_used=["pmd", "regex"],
|
|
engines_unavailable=["sfge"],
|
|
)
|
|
|
|
print(f"Custom Score: {merged.custom_score}/{merged.custom_max}")
|
|
print(f"CA Violations: {merged.ca_violations_total} ({merged.ca_critical} critical, {merged.ca_high} high)")
|
|
print(f"CA Deductions: -{merged.ca_deductions}")
|
|
print(f"Final Score: {merged.final_score}/{merged.final_max}")
|
|
print(f"Rating: {format_rating_stars(merged.rating_stars)} {merged.rating}")
|
|
print()
|
|
print("Deductions:")
|
|
for d in merged.deductions:
|
|
print(f" -{d.deduction} [{d.category}] {d.rule}: {d.message}")
|