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}