package main import ( "bytes" "go/ast" "go/doc" "go/format" "go/token" "html/template" "log" "net/http" "sort" "strings" ) // IdentIndex is the precomputed identifier index across all targets. type IdentIndex struct { Entries []*IdentEntry ByKey map[string]*IdentEntry Total int // total number of targets } // IdentEntry is one identifier (type, func, method, const, or var) across all targets. type IdentEntry struct { Name string Kind string // "type", "func", "method", "const", "var" Count int Key string // URL key: "kind/name" Groups []IdentGroup } // IdentGroup is a set of targets with identical declaration for an identifier. type IdentGroup struct { Decl string // representative formatted declaration Targets []string // sorted target names } func buildIdentIndex(results []Result) *IdentIndex { type occ struct { target string decl string // grouping key + display } type ikey struct { name, kind string } idx := make(map[ikey][]occ) addFunc := func(target string, fset *token.FileSet, f *doc.Func, prefix string) { name := f.Name if prefix != "" { name = prefix + "." + f.Name } kind := "func" if f.Recv != "" { kind = "method" } decl := fmtNode(fset, f.Decl) idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, decl}) } addValues := func(target string, fset *token.FileSet, vals []*doc.Value, kind string) { for _, v := range vals { for _, name := range v.Names { typeKey := resolveValueType(fset, v.Decl, name) idx[ikey{name, kind}] = append(idx[ikey{name, kind}], occ{target, typeKey}) } } } for _, r := range results { t := r.Target.Name fset := r.Fset pkg := r.Pkg for _, ty := range pkg.Types { decl := fmtNode(fset, ty.Decl) idx[ikey{ty.Name, "type"}] = append(idx[ikey{ty.Name, "type"}], occ{t, decl}) for _, m := range ty.Methods { addFunc(t, fset, m, ty.Name) } for _, f := range ty.Funcs { addFunc(t, fset, f, "") } addValues(t, fset, ty.Consts, "const") addValues(t, fset, ty.Vars, "var") } for _, f := range pkg.Funcs { addFunc(t, fset, f, "") } addValues(t, fset, pkg.Consts, "const") addValues(t, fset, pkg.Vars, "var") } index := &IdentIndex{ ByKey: make(map[string]*IdentEntry), Total: len(results), } for key, occs := range idx { groups := make(map[string][]string) // decl -> targets for _, o := range occs { groups[o.decl] = append(groups[o.decl], o.target) } var gs []IdentGroup for decl, targets := range groups { sort.Strings(targets) gs = append(gs, IdentGroup{Decl: decl, Targets: targets}) } sort.Slice(gs, func(i, j int) bool { return len(gs[i].Targets) > len(gs[j].Targets) }) urlKey := key.kind + "/" + key.name entry := &IdentEntry{ Name: key.name, Kind: key.kind, Count: len(occs), Key: urlKey, Groups: gs, } index.Entries = append(index.Entries, entry) index.ByKey[urlKey] = entry } sort.Slice(index.Entries, func(i, j int) bool { if index.Entries[i].Count != index.Entries[j].Count { return index.Entries[i].Count > index.Entries[j].Count } return index.Entries[i].Name < index.Entries[j].Name }) return index } // resolveValueType returns the effective type string for a const/var name // within a GenDecl, handling iota type inheritance for constants. func resolveValueType(fset *token.FileSet, gd *ast.GenDecl, name string) string { isConst := gd.Tok == token.CONST var lastType ast.Expr for _, spec := range gd.Specs { vs := spec.(*ast.ValueSpec) if vs.Type != nil { lastType = vs.Type } for _, n := range vs.Names { if n.Name != name { continue } effType := vs.Type if effType == nil && isConst { effType = lastType } if effType != nil { return fmtNode(fset, effType) } return "(untyped)" } } return "(unknown)" } // fmtNode formats an AST node to Go source. Separate from formatNode in main.go // to make it clear this is the server's version (same logic). func fmtNode(fset *token.FileSet, node ast.Node) string { var buf bytes.Buffer format.Node(&buf, fset, node) return buf.String() } // --- HTTP Server --- func serve(addr string, results []Result) { index := buildIdentIndex(results) byTarget := make(map[string]Result, len(results)) for _, r := range results { byTarget[r.Target.Name] = r } mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } data := pageData{Index: index, Results: results} pageTmpl.Execute(w, data) }) mux.HandleFunc("/id/", func(w http.ResponseWriter, r *http.Request) { key := strings.TrimPrefix(r.URL.Path, "/id/") entry, ok := index.ByKey[key] if !ok { http.NotFound(w, r) return } data := pageData{Index: index, Results: results, Selected: entry, SelectedKey: key} pageTmpl.Execute(w, data) }) mux.HandleFunc("/target/", func(w http.ResponseWriter, r *http.Request) { name := strings.TrimPrefix(r.URL.Path, "/target/") res, ok := byTarget[name] if !ok { http.NotFound(w, r) return } api := convertPackage(res.Target.Name, res.Pkg, res.Fset) targetTmpl.Execute(w, api) }) log.Printf("serving documentation on http://localhost%s", addr) log.Fatal(http.ListenAndServe(addr, mux)) } type pageData struct { Index *IdentIndex Results []Result Selected *IdentEntry SelectedKey string } // --- Templates --- var pageTmpl = template.Must(template.New("page").Funcs(template.FuncMap{ "kindBadge": func(kind string) string { switch kind { case "type": return "T" case "func": return "F" case "method": return "M" case "const": return "C" case "var": return "V" } return "?" }, "pre": func(s string) template.HTML { return template.HTML("
" + template.HTMLEscapeString(s) + "
") }, "kindClass": func(kind string) string { return "kind-" + kind }, }).Parse(` tgdoc{{if .Selected}} — {{.Selected.Name}}{{end}}
{{if .Selected}}

{{.Selected.Kind}} {{.Selected.Name}}

Present in {{.Selected.Count}} / {{.Index.Total}} targets {{if eq (len .Selected.Groups) 1}}— identical across all targets{{else}}— {{len .Selected.Groups}} distinct signatures{{end}}

{{range .Selected.Groups}}
{{len .Targets}} target{{if ne (len .Targets) 1}}s{{end}}
{{pre .Decl}}
{{range .Targets}}{{.}} {{end}}
{{end}} {{else}}

tgdoc

{{len .Index.Entries}} identifiers across {{.Index.Total}} targets

Select an identifier from the sidebar to see its appearances across targets.

Browse by target:

{{end}}
`)) var targetTmpl = template.Must(template.New("target").Funcs(template.FuncMap{ "pre": func(s string) template.HTML { return template.HTML("
" + template.HTMLEscapeString(s) + "
") }, }).Parse(` {{.Target}} — tgdoc

← identifier index

{{.Target}}

package {{.Package}}

{{if .Doc}}
{{.Doc}}
{{end}} {{if .Constants}}

Constants

{{range .Constants}} {{if .Doc}}
{{.Doc}}
{{end}} {{pre .Decl}} {{end}} {{end}} {{if .Variables}}

Variables

{{range .Variables}} {{if .Doc}}
{{.Doc}}
{{end}} {{pre .Decl}} {{end}} {{end}} {{if .Functions}}

Functions

{{range .Functions}}

func {{.Name}}

{{if .Doc}}
{{.Doc}}
{{end}} {{pre .Decl}} {{end}} {{end}} {{if .Types}}

Types

{{range .Types}}

type {{.Name}}

{{if .Doc}}
{{.Doc}}
{{end}} {{pre .Decl}} {{if .Constants}}

Associated Constants

{{range .Constants}}{{pre .Decl}}{{end}}
{{end}} {{if .Variables}}

Associated Variables

{{range .Variables}}{{pre .Decl}}{{end}}
{{end}} {{if .Functions}}

Constructors

{{range .Functions}} {{if .Doc}}
{{.Doc}}
{{end}} {{pre .Decl}} {{end}}
{{end}} {{if .Methods}}

Methods

{{range .Methods}} {{if .Doc}}
{{.Doc}}
{{end}} {{pre .Decl}} {{end}}
{{end}} {{end}} {{end}} `))