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