| // 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()) |
| } |
| } |