// Copyright 2026 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package specdoc import ( "cmp" "fmt" "go/token" "io" "slices" "strings" ) // Finding is an individual finding in a [Report]. type Finding interface { String() string Decl() Decl } // Report collects all findings from specfill across checked declarations. // It implements the error interface. type Report struct { TypeMismatches []TypeMismatch NameMismatches []NameMismatch DocOrderViolations []DocOrderViolation UnknownDecls []UnknownDecl UnexpectedDocs []UnexpectedDoc } // Error formats the report as a string, implementing the error interface. func (r *Report) Error() string { var b strings.Builder r.Print(&b) return b.String() } // Decl identifies an AST declaration and its location in the source code. type Decl struct { Pos token.Position Recv string Name string } // Compare orders declarations by source position (filename, line, column, // offset), breaking ties by receiver and name. func (d Decl) Compare(other Decl) int { if c := cmp.Compare(d.Pos.Filename, other.Pos.Filename); c != 0 { return c } if c := cmp.Compare(d.Pos.Offset, other.Pos.Offset); c != 0 { return c } if c := cmp.Compare(d.Recv, other.Recv); c != 0 { return c } return cmp.Compare(d.Name, other.Name) } // String formats the declaration as "(Recv) Name" or "Name". func (d Decl) String() string { if d.Recv != "" { return fmt.Sprintf("(%s) %s", d.Recv, d.Name) } return d.Name } // TypeMismatch records a disagreement between spec parameter/result types // and an AST declaration's parameter/result types. type TypeMismatch struct { D Decl DeclSig string SpecSig string Details []string } func (m TypeMismatch) Decl() Decl { return m.D } func (m TypeMismatch) String() string { var b strings.Builder fmt.Fprintf(&b, "%s: %s: type mismatch\n", m.D.Pos, m.D) fmt.Fprintf(&b, " decl: %s\n", m.DeclSig) fmt.Fprintf(&b, " spec: %s\n", m.SpecSig) for _, d := range m.Details { fmt.Fprintf(&b, " - %s\n", d) } return b.String() } // NameMismatch records a disagreement between spec parameter/result names // and an AST declaration's parameter/result names. type NameMismatch struct { D Decl DeclSig string SpecSig string Details []string } func (m NameMismatch) Decl() Decl { return m.D } func (m NameMismatch) String() string { var b strings.Builder fmt.Fprintf(&b, "%s: %s: name mismatch\n", m.D.Pos, m.D) fmt.Fprintf(&b, " decl: %s\n", m.DeclSig) fmt.Fprintf(&b, " spec: %s\n", m.SpecSig) for _, d := range m.Details { fmt.Fprintf(&b, " - %s\n", d) } return b.String() } // DocOrderViolation records a comment ordering violation in an exported // declaration's doc comment, which would result in non-idempotent rewriting. type DocOrderViolation struct { D Decl Err error } func (v DocOrderViolation) Decl() Decl { return v.D } func (v DocOrderViolation) String() string { return fmt.Sprintf("%s: %s: %v\n", v.D.Pos, v.D, v.Err) } // UnknownDecl records an exported declaration that has no matching entry in spec. type UnknownDecl struct { D Decl } func (u UnknownDecl) Decl() Decl { return u.D } func (u UnknownDecl) String() string { return fmt.Sprintf("%s: %s: declaration not in spec\n", u.D.Pos, u.D) } // UnexpectedDoc records an exported declaration containing an existing // spec-owned doc comment when AllowDocRewrite is false. type UnexpectedDoc struct { D Decl } func (u UnexpectedDoc) Decl() Decl { return u.D } func (u UnexpectedDoc) String() string { return fmt.Sprintf("%s: %s: declaration contains unexpected doc comment\n", u.D.Pos, u.D) } // Merge combines other into r. func (r *Report) Merge(other *Report) { r.TypeMismatches = append(r.TypeMismatches, other.TypeMismatches...) r.NameMismatches = append(r.NameMismatches, other.NameMismatches...) r.DocOrderViolations = append(r.DocOrderViolations, other.DocOrderViolations...) r.UnknownDecls = append(r.UnknownDecls, other.UnknownDecls...) r.UnexpectedDocs = append(r.UnexpectedDocs, other.UnexpectedDocs...) } // Empty reports whether the report contains no items. func (r Report) Empty() bool { return len(r.TypeMismatches) == 0 && len(r.NameMismatches) == 0 && len(r.DocOrderViolations) == 0 && len(r.UnknownDecls) == 0 && len(r.UnexpectedDocs) == 0 } func convertAppend[From Finding](ys []Finding, xs []From) []Finding { for _, x := range xs { ys = append(ys, x) } return ys } // Findings returns all findings in r as a slice of [Finding]. func (r Report) Findings() []Finding { var items []Finding items = convertAppend(items, r.TypeMismatches) items = convertAppend(items, r.NameMismatches) items = convertAppend(items, r.DocOrderViolations) items = convertAppend(items, r.UnknownDecls) items = convertAppend(items, r.UnexpectedDocs) return items } // Print writes the report to w sorted by position. If there are no findings, // nothing is written. func (r Report) Print(w io.Writer) { items := r.Findings() slices.SortStableFunc(items, func(a, b Finding) int { if c := a.Decl().Compare(b.Decl()); c != 0 { return c } return cmp.Compare(a.String(), b.String()) }) for _, item := range items { fmt.Fprint(w, item.String()) } }