001package gudusoft.gsqlparser.pp.score; 002 003/** 004 * One traceable scoring observation: which rule fired, where, how much it 005 * costs, whether the formatter can do anything about it, and a 006 * position-independent {@link #getFingerprint() fingerprint} that survives a 007 * reformat so two runs can be aligned finding-by-finding. 008 * 009 * <p>Immutable value object. Built by the rules through {@link Builder}; the 010 * aggregation step in {@code Explainer} may derive a copy with a different 011 * {@link #getImpact() impact} (cap / shadowing) via {@link #withImpact}. 012 */ 013public final class Finding { 014 015 private final String rule; 016 private final String dimension; 017 private final Severity severity; 018 private final int line; // 1-based, 0 when not line-anchored 019 private final int col; // 1-based, 0 when not column-anchored 020 private final int len; 021 private final boolean fixable; 022 private final String unfixableReason; 023 private final String fingerprint; 024 private final String evidence; 025 private final double impact; 026 private final String suggestion; 027 private final boolean shadowed; 028 private final String shadowedBy; 029 030 private Finding(Builder b) { 031 this.rule = b.rule; 032 this.dimension = b.dimension; 033 this.severity = b.severity; 034 this.line = b.line; 035 this.col = b.col; 036 this.len = b.len; 037 this.fixable = b.fixable; 038 this.unfixableReason = b.unfixableReason; 039 this.fingerprint = b.fingerprint; 040 this.evidence = b.evidence; 041 this.impact = b.impact; 042 this.suggestion = b.suggestion; 043 this.shadowed = b.shadowed; 044 this.shadowedBy = b.shadowedBy; 045 } 046 047 public String getRule() { return rule; } 048 public String getDimension() { return dimension; } 049 public Severity getSeverity() { return severity; } 050 public int getLine() { return line; } 051 public int getCol() { return col; } 052 public int getLen() { return len; } 053 public boolean isFixable() { return fixable; } 054 /** Non-null only when {@link #isFixable()} is false. */ 055 public String getUnfixableReason() { return unfixableReason; } 056 public String getFingerprint() { return fingerprint; } 057 public String getEvidence() { return evidence; } 058 /** Effective deduction on the owning dimension's 0-100 score (already capped / shadowed). */ 059 public double getImpact() { return impact; } 060 public String getSuggestion() { return suggestion; } 061 /** True when another finding on the same line and dimension outweighed this one. */ 062 public boolean isShadowed() { return shadowed; } 063 public String getShadowedBy() { return shadowedBy; } 064 065 /** Copy with a different effective impact (used for per-rule caps). */ 066 public Finding withImpact(double newImpact) { 067 Builder b = toBuilder(); 068 b.impact = newImpact; 069 return new Finding(b); 070 } 071 072 /** Copy marked as shadowed by {@code byRule} with zero effective impact. */ 073 public Finding shadowedBy(String byRule) { 074 Builder b = toBuilder(); 075 b.impact = 0.0; 076 b.shadowed = true; 077 b.shadowedBy = byRule; 078 return new Finding(b); 079 } 080 081 private Builder toBuilder() { 082 Builder b = new Builder(rule, dimension); 083 b.severity = severity; b.line = line; b.col = col; b.len = len; 084 b.fixable = fixable; b.unfixableReason = unfixableReason; 085 b.fingerprint = fingerprint; b.evidence = evidence; b.impact = impact; 086 b.suggestion = suggestion; b.shadowed = shadowed; b.shadowedBy = shadowedBy; 087 return b; 088 } 089 090 public static Builder builder(String rule, String dimension) { 091 return new Builder(rule, dimension); 092 } 093 094 @Override 095 public String toString() { 096 return rule + "@" + line + ":" + col + " impact=" + impact 097 + (fixable ? "" : " unfixable(" + unfixableReason + ")") 098 + " fp=" + fingerprint; 099 } 100 101 /** Mutable builder; every rule produces findings through it. */ 102 public static final class Builder { 103 private final String rule; 104 private final String dimension; 105 private Severity severity = Severity.MINOR; 106 private int line, col, len; 107 private boolean fixable = true; 108 private String unfixableReason; 109 private String fingerprint = ""; 110 private String evidence = ""; 111 private double impact; 112 private String suggestion; 113 private boolean shadowed; 114 private String shadowedBy; 115 116 Builder(String rule, String dimension) { 117 if (rule == null) throw new NullPointerException("rule"); 118 if (dimension == null) throw new NullPointerException("dimension"); 119 this.rule = rule; 120 this.dimension = dimension; 121 } 122 123 public Builder severity(Severity s) { this.severity = s; return this; } 124 public Builder at(int line1, int col1, int len) { this.line = line1; this.col = col1; this.len = len; return this; } 125 public Builder unfixable(String reason) { this.fixable = false; this.unfixableReason = reason; return this; } 126 public Builder fingerprint(String fp) { this.fingerprint = fp == null ? "" : fp; return this; } 127 public Builder evidence(String e) { this.evidence = e == null ? "" : e; return this; } 128 public Builder impact(double i) { this.impact = i; return this; } 129 public Builder suggestion(String s) { this.suggestion = s; return this; } 130 public Finding build() { return new Finding(this); } 131 } 132}