blob: e678c3eda01ce0e269997f18622d25e67ebdd5ea [file]
// 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())
}
}