feat(11.1-02): check module identifiers in docs and READMEs
- go/parser index of every modules/ package and sub-package, with methods, fields, interface methods and promoted members - code spans in docs pages, module READMEs and the root README fail Check and docs:build when the named identifier does not exist - scripts/check-phase11.1.sh with preconditions, deps, docs, forbidden, go and a self-test that plants one violation per rule
This commit is contained in:
134
internal/docsite/checks_test.go
Normal file
134
internal/docsite/checks_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package docsite
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fixtureModule is a small package exercising every declaration kind the
|
||||
// identifier index records.
|
||||
const fixtureModule = `package fixture
|
||||
|
||||
import "context"
|
||||
|
||||
// Bus is a generic-method host with a field and an embedded type.
|
||||
type Bus struct {
|
||||
Name string
|
||||
Base
|
||||
}
|
||||
|
||||
// Base is embedded in Bus.
|
||||
type Base struct{ ID int }
|
||||
|
||||
// Ping is promoted to Bus.
|
||||
func (Base) Ping() {}
|
||||
|
||||
// Handler is an interface.
|
||||
type Handler interface {
|
||||
Handle(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Mode is a const.
|
||||
const Mode = 1
|
||||
|
||||
// Default is a var.
|
||||
var Default = &Bus{}
|
||||
|
||||
// New builds a Bus.
|
||||
func New() *Bus { return &Bus{} }
|
||||
|
||||
// Fire is a generic method.
|
||||
func (b *Bus) Fire[T any](v T) {}
|
||||
|
||||
// Close is a value-receiver method.
|
||||
func (b Bus) Close() error { return nil }
|
||||
`
|
||||
|
||||
func identFixture(t *testing.T, indexBody, readme string) string {
|
||||
t.Helper()
|
||||
return writeTree(t, map[string]string{
|
||||
"docs/site.yaml": fixtureSite,
|
||||
"docs/index.md": page("Acme docs", "index", 0, indexBody),
|
||||
"docs/setup/start.md": page("Start", "setup", 10, "Text.\n"),
|
||||
"modules/fixture/fixture.go": fixtureModule,
|
||||
"modules/fixture/README.md": "# fixture\n\nFixture does one thing.\n\n" + readme,
|
||||
"modules/fixture/sub/sub.go": "package sub\n\n// Thing is exported.\ntype Thing struct{}\n",
|
||||
"modules/fixture/testdata/x.go": "package x\n\n// Hidden is never indexed.\nfunc Hidden() {}\n",
|
||||
})
|
||||
}
|
||||
|
||||
// writeFile writes one file under root, creating its directory.
|
||||
func writeFile(t *testing.T, root, name, body string) {
|
||||
t.Helper()
|
||||
p := filepath.Join(root, filepath.FromSlash(name))
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifierChecker(t *testing.T) {
|
||||
passing := strings.Join([]string{
|
||||
"- `fixture.New()` and `fixture.New`",
|
||||
"- `*fixture.Bus` and `fixture.Bus.Close`",
|
||||
"- `fixture.Bus.Fire[string](\"x\")` (generic method)",
|
||||
"- `fixture.Bus.Name` and `fixture.Bus.Base` (field, embedded)",
|
||||
"- `fixture.Bus.ID` and `fixture.Bus.Ping()` (promoted through Base)",
|
||||
"- `fixture.Handler.Handle` (interface method)",
|
||||
"- `fixture.Mode`, `fixture.Default`, `fixture.Bus.lowercase`",
|
||||
"- `sub.Thing` (sub-package)",
|
||||
"- `http.Handler`, `fields.yaml`, `acme.blog`, `summer.yaml`, `fixture.lower`",
|
||||
"",
|
||||
"```text",
|
||||
"fixture.NotChecked() // fenced blocks are skipped",
|
||||
"```",
|
||||
"",
|
||||
}, "\n")
|
||||
root := identFixture(t, passing, "## Usage\n\nCall `fixture.New()`.\n")
|
||||
problems, err := Check(Options{Root: root})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
t.Fatalf("passing fixture: %q", problemLines(problems))
|
||||
}
|
||||
|
||||
failing := passing + "Then `fixture.Missing` and `fixture.Bus.Nope`.\n\n`fixture.Hidden` lives in testdata.\n"
|
||||
root = identFixture(t, failing, "## Usage\n\nCall `fixture.Gone()`.\n")
|
||||
writeFile(t, root, "README.md", "# Root\n\nSee `x.Y`.\n\n`sub.Nothing`\n")
|
||||
problems, err = Check(Options{Root: root})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{
|
||||
"README.md:5: identifier: sub.Nothing does not exist in modules/fixture/sub",
|
||||
"docs/index.md:22: identifier: fixture.Missing does not exist in modules/fixture",
|
||||
"docs/index.md:22: identifier: fixture.Bus.Nope does not exist in modules/fixture",
|
||||
"docs/index.md:24: identifier: fixture.Hidden does not exist in modules/fixture",
|
||||
"modules/fixture/README.md:7: identifier: fixture.Gone does not exist in modules/fixture",
|
||||
}
|
||||
if got := problemLines(problems); !slices.Equal(got, want) {
|
||||
t.Fatalf("problems =\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifierIndexDuplicateName(t *testing.T) {
|
||||
root := writeTree(t, map[string]string{
|
||||
"modules/alpha/alpha.go": "package alpha\n",
|
||||
"modules/alpha/util/util.go": "package util\n",
|
||||
"modules/beta/util/util.go": "package util\n",
|
||||
})
|
||||
_, problems, err := buildIdentIndex(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `modules/beta/util: identifier: package name "util" is used by modules/alpha/util and modules/beta/util; spans cannot tell them apart`
|
||||
if got := problemLines(problems); !slices.Equal(got, []string{want}) {
|
||||
t.Fatalf("problems = %q, want [%q]", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user