diff --git a/internal/_gopdoc/parse_overload_func.go b/internal/_gopdoc/parse_overload_func.go deleted file mode 100644 index bb94e84d5..000000000 --- a/internal/_gopdoc/parse_overload_func.go +++ /dev/null @@ -1,125 +0,0 @@ -package gopdoc - -import ( - "context" - "go/ast" - "go/doc" - "regexp" - "sort" - "strconv" - "strings" - - "golang.org/x/pkgsite/internal/log" -) - -var pattern string = `__\d+` - -// FindOverloadFuncThenAdd Find overloaded functions and update function names -func FindOverloadFuncThenAdd(d *doc.Package) { - if len(d.Funcs) == 0 { - return - } - var overloadFuncName = make(map[string]string) - for _, constO := range d.Consts { - for _, name := range constO.Names { - if strings.Contains(name, "Gopo_") { - for _, spec := range constO.Decl.Specs { - if vs, ok := spec.(*ast.ValueSpec); ok { - for _, name := range vs.Names { - if strings.Contains(name.Name, "Gopo_") { - n := strings.TrimPrefix(name.Name, "Gopo_") - for _, v := range vs.Values { - if bas, ok := v.(*ast.BasicLit); ok { - // ignore error - values, err := strconv.Unquote(bas.Value) - if err != nil { - continue - } - overloadFuncs := strings.Split(values, ",") - for i2 := range overloadFuncs { - overloadFuncName[overloadFuncs[i2]] = n - } - } - } - } - } - } - } - } - } - } - // even though overloadFuncName is empty, but the situation of funcName__N may occur, so it cannot be returned in advance - var overloadFunc = make([]*doc.Func, 0, len(overloadFuncName)) - for _, funcO := range d.Funcs { - match, err := isMatchName(funcO.Name) - if err != nil { - continue - } - if match { - restoreName(funcO) - } - if name, ok := overloadFuncName[funcO.Name]; ok { - newFunc := buildNewFunc(funcO, name) - overloadFunc = append(overloadFunc, newFunc) - } - } - for _, funs := range overloadFunc { - d.Funcs = append(d.Funcs, funs) - } - - sort.Slice(d.Funcs, func(i, j int) bool { - return d.Funcs[i].Name < d.Funcs[j].Name - }) -} - -// FindOverloadFuncTypeThenRestoreName func name: xxx_001 xxx_002 -func FindOverloadFuncTypeThenRestoreName(types []*doc.Type) { - for _, t := range types { - for _, f := range t.Methods { - match, err := isMatchName(f.Name) - if err != nil { - continue - } - if match { - restoreName(f) - } - } - } -} - -func isMatchName(name string) (bool, error) { - match, err := regexp.MatchString(pattern, name) - if err != nil { - log.Errorf(context.Background(), "match function name err", err.Error()) - return false, err - } - return match, nil -} - -// restoreName restore overload func name -func restoreName(funcO *doc.Func) { - re := regexp.MustCompile(pattern) - name := re.ReplaceAllString(funcO.Name, "") - funcO.Decl.Name.Name = name - funcO.Name = name -} - -func buildNewFunc(oldFunc *doc.Func, name string) *doc.Func { - newFunc := &doc.Func{} - newFunc.Doc = oldFunc.Doc - newFunc.Recv = oldFunc.Recv - newFunc.Orig = oldFunc.Orig - newFunc.Decl = &ast.FuncDecl{} - newFunc.Decl.Type = oldFunc.Decl.Type - newFunc.Decl.Doc = oldFunc.Decl.Doc - newFunc.Decl.Recv = oldFunc.Decl.Recv - newFunc.Decl.Body = oldFunc.Decl.Body - newFunc.Decl.Name = &ast.Ident{} - newFunc.Decl.Name.NamePos = oldFunc.Decl.Name.NamePos - newFunc.Decl.Name.Name = name - newFunc.Decl.Name.Obj = oldFunc.Decl.Name.Obj - newFunc.Level = oldFunc.Level - newFunc.Examples = oldFunc.Examples - newFunc.Name = name - return newFunc -} diff --git a/internal/_gopdoc/transform.go b/internal/_gopdoc/transform.go deleted file mode 100644 index 0afec8e40..000000000 --- a/internal/_gopdoc/transform.go +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package gopdoc - -func Transform() { -} diff --git a/internal/godoc/render.go b/internal/godoc/render.go index 87233b10b..f0e50ae3c 100644 --- a/internal/godoc/render.go +++ b/internal/godoc/render.go @@ -22,6 +22,8 @@ import ( "golang.org/x/pkgsite/internal/godoc/dochtml" "golang.org/x/pkgsite/internal/source" "golang.org/x/pkgsite/internal/stdlib" + + gopdoc "golang.org/x/pkgsite/internal/gopdoc" ) const ( @@ -125,6 +127,7 @@ func (p *Package) DocPackage(innerPath string, modInfo *ModuleInfo) (_ *doc.Pack if d.ImportPath != importPath { panic(fmt.Errorf("internal error: *doc.Package has an unexpected import path (%q != %q)", d.ImportPath, importPath)) } + if noTypeAssociation { for _, t := range d.Types { d.Consts, t.Consts = append(d.Consts, t.Consts...), nil @@ -138,7 +141,7 @@ func (p *Package) DocPackage(innerPath string, modInfo *ModuleInfo) (_ *doc.Pack if len(d.Imports) > maxImportsPerPackage { return nil, fmt.Errorf("%d imports found package %q; exceeds limit %d for maxImportsPerPackage", len(d.Imports), importPath, maxImportsPerPackage) } - return d, nil + return gopdoc.Transform(d), nil } // renderOptions returns a RenderOptions for p. diff --git a/internal/gopdoc/z_gop.go b/internal/gopdoc/z_gop.go new file mode 100644 index 000000000..1dfc990f4 --- /dev/null +++ b/internal/gopdoc/z_gop.go @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package doc + +import ( + "go/doc" + "strings" +) + +const ( + goptPrefix = "Gopt_" // template method + gopoPrefix = "Gopo_" // overload function/method + gopxPrefix = "Gopx_" // type as parameters function/method + gopPackage = "GopPackage" +) + +func isGopPackage(in *doc.Package) bool { + for _, v := range in.Consts { + for _, name := range v.Names { + if name == gopPackage { + return true + } + } + } + return false +} + +func isGopoConst(name string) bool { + return strings.HasPrefix(name, gopoPrefix) +} + +func hasGopoConst(in *doc.Value) bool { + for _, name := range in.Names { + if isGopoConst(name) { + return true + } + } + return false +} + +func isOverload(name string) bool { + n := len(name) + return n > 3 && name[n-3:n-1] == "__" +} + +// Func (no _ func name) +// _Func (with _ func name) +// TypeName_Method (no _ method name) +// _TypeName__Method (with _ method name) +func checkTypeMethod(name string) mthd { + if pos := strings.IndexByte(name, '_'); pos >= 0 { + if pos == 0 { + t := name[1:] + if pos = strings.Index(t, "__"); pos <= 0 { + return mthd{"", t} // _Func + } + return mthd{t[:pos], t[pos+2:]} // _TypeName__Method + } + return mthd{name[:pos], name[pos+1:]} // TypeName_Method + } + return mthd{"", name} // Func +} diff --git a/internal/gopdoc/z_test.go b/internal/gopdoc/z_test.go new file mode 100644 index 000000000..1d2cfa726 --- /dev/null +++ b/internal/gopdoc/z_test.go @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package doc + +import ( + "bytes" + "fmt" + "go/ast" + "go/doc" + "go/format" + "go/parser" + "go/token" + "strings" + "testing" +) + +func TestToIndex(t *testing.T) { + if ret := toIndex('a'); ret != 10 { + t.Fatal("toIndex:", ret) + } + defer func() { + if e := recover(); e != "invalid character out of [0-9,a-z]" { + t.Fatal("panic:", e) + } + }() + toIndex('A') +} + +func TestCheckTypeMethod(t *testing.T) { + if ret := checkTypeMethod("_Foo_a"); ret.typ != "" || ret.name != "Foo_a" { + t.Fatal("checkTypeMethod:", ret) + } + if ret := checkTypeMethod("Foo_a"); ret.typ != "Foo" || ret.name != "a" { + t.Fatal("checkTypeMethod:", ret) + } +} + +func TestIsGopPackage(t *testing.T) { + if isGopPackage(&doc.Package{}) { + t.Fatal("isGopPackage: true?") + } +} + +func TestDocRecv(t *testing.T) { + if _, ok := docRecv(&ast.Field{}); ok { + t.Fatal("docRecv: ok?") + } +} + +func TestGopoMethod(t *testing.T) { + testPkg(t, ` +package foo + +const GopPackage = true + +const Gopo__T__Gop_Add = ".AddInt,AddString" + +type T int + +// AddInt doc +func (p T) AddInt(b int) *T {} + +// AddString doc +func AddString(this *T, b string) *T {} +`, `== Type T == +- Func AddString - +Doc: AddString doc + +func AddString(this *T, b string) *T +- Method AddInt - +Recv: T +Doc: AddInt doc + +func (p T) AddInt(b int) *T +- Method Gop_Add - +Recv: T +Doc: AddInt doc + +func (p T) Gop_Add(b int) *T +- Method Gop_Add - +Recv: *T +Doc: AddString doc + +func (this *T) Gop_Add(b string) *T +`) +} + +func TestGopoFn(t *testing.T) { + testPkg(t, ` +package foo + +const GopPackage = true + +const Gopo_Add = "AddInt,,AddString" + +// Add doc +func Add__1(a, b float64) float64 {} + +// AddInt doc +func AddInt(a, b int) int {} + +// AddString doc +func AddString(a, b string) string {} +`, `== Func Add == +Doc: AddInt doc + +func Add(a, b int) int +== Func Add == +Doc: Add doc + +func Add(a, b float64) float64 +== Func Add == +Doc: AddString doc + +func Add(a, b string) string +== Func AddInt == +Doc: AddInt doc + +func AddInt(a, b int) int +== Func AddString == +Doc: AddString doc + +func AddString(a, b string) string +`) +} + +func TestOverloadFn(t *testing.T) { + testPkg(t, ` +package foo + +const GopPackage = true + +// Bar doc +func Bar__0() { +} +`, `== Func Bar == +Doc: Bar doc + +func Bar() +`) +} + +func TestOverloadMethod(t *testing.T) { + testPkg(t, ` +package foo + +const GopPackage = true + +type T int + +// Bar doc 1 +func (p *T) Bar__0() { +} + +// Bar doc 2 +func (t T) Bar__1() { +} +`, `== Type T == +- Method Bar - +Recv: *T +Doc: Bar doc 1 + +func (p *T) Bar() +- Method Bar - +Recv: T +Doc: Bar doc 2 + +func (t T) Bar() +`) +} + +func printVal(parts []string, format string, val any) []string { + return append(parts, fmt.Sprintf(format, val)) +} + +func printFuncDecl(parts []string, fset *token.FileSet, decl *ast.FuncDecl) []string { + var b bytes.Buffer + if e := format.Node(&b, fset, decl); e != nil { + panic(e) + } + return append(parts, b.String()) +} + +func printFunc(parts []string, fset *token.FileSet, format string, fn *doc.Func) []string { + parts = printVal(parts, format, fn.Name) + if fn.Recv != "" { + parts = printVal(parts, "Recv: %s", fn.Recv) + } + parts = printVal(parts, "Doc: %s", fn.Doc) + parts = printFuncDecl(parts, fset, fn.Decl) + return parts +} + +func printFuncs(parts []string, fset *token.FileSet, fns []*doc.Func) []string { + for _, fn := range fns { + parts = printFunc(parts, fset, "== Func %s ==", fn) + } + return parts +} + +func printType(parts []string, fset *token.FileSet, typ *doc.Type) []string { + parts = append(parts, fmt.Sprintf("== Type %s ==", typ.Name)) + for _, fn := range typ.Funcs { + parts = printFunc(parts, fset, "- Func %s -", fn) + } + for _, fn := range typ.Methods { + parts = printFunc(parts, fset, "- Method %s -", fn) + } + return parts +} + +func printTypes(parts []string, fset *token.FileSet, types []*doc.Type) []string { + for _, typ := range types { + parts = printType(parts, fset, typ) + } + return parts +} + +func printPkg(fset *token.FileSet, in *doc.Package) string { + var parts []string + parts = printFuncs(parts, fset, in.Funcs) + parts = printTypes(parts, fset, in.Types) + return strings.Join(append(parts, ""), "\n") +} + +func testPkg(t *testing.T, in, expected string) { + t.Helper() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "foo.go", in, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + pkg, err := doc.NewFromFiles(fset, []*ast.File{f}, "foo") + if err != nil { + t.Fatal(err) + } + pkg = Transform(pkg) + if ret := printPkg(fset, pkg); ret != expected { + t.Fatalf("got:\n%s\nexpected:\n%s\n", ret, expected) + } +} diff --git a/internal/gopdoc/z_transform.go b/internal/gopdoc/z_transform.go new file mode 100644 index 000000000..507a7f925 --- /dev/null +++ b/internal/gopdoc/z_transform.go @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package doc + +import ( + "go/ast" + "go/doc" + "go/token" + "sort" + "strconv" + "strings" +) + +type mthd struct { + typ string + name string +} + +type omthd struct { + mthd + idx int +} + +type typExtra struct { + t *doc.Type + funcs []*doc.Func + methods []*doc.Func +} + +type transformCtx struct { + overloadFuncs map[mthd][]omthd // realName => []overloadName + typs map[string]*typExtra + orders map[*doc.Func]int +} + +func (p *transformCtx) finish(in *doc.Package) { + for _, ex := range p.typs { + if t := ex.t; t != nil { + t.Funcs = p.mergeFuncs(t.Funcs, ex.funcs) + t.Methods = p.mergeFuncs(t.Methods, ex.methods) + } else { + in.Funcs = p.mergeFuncs(in.Funcs, ex.funcs) + } + } +} + +func (p *transformCtx) mergeFuncs(a, b []*doc.Func) []*doc.Func { + if len(b) == 0 { + return a + } + a = append(a, b...) + sort.Slice(a, func(i, j int) bool { + fa, fb := a[i], a[j] + aName, bName := fa.Name, fb.Name + if aName == bName { + return p.orders[fa] < p.orders[fb] + } + return aName < bName + }) + return a +} + +func newCtx(in *doc.Package) *transformCtx { + typs := make(map[string]*typExtra, len(in.Types)+1) + typs[""] = &typExtra{} // global functions + for _, t := range in.Types { + typs[t.Name] = &typExtra{t: t} + } + return &transformCtx{ + overloadFuncs: make(map[mthd][]omthd), + typs: typs, + orders: make(map[*doc.Func]int), + } +} + +func newIdent(name string, in *ast.Ident) *ast.Ident { + ret := *in + ret.Name = name + return &ret +} + +func newFuncDecl(name string, in *ast.FuncDecl) *ast.FuncDecl { + ret := *in + ret.Name = newIdent(name, ret.Name) + return &ret +} + +func newMethodDecl(name string, in *ast.FuncDecl) *ast.FuncDecl { + ret := *in + if ret.Recv == nil { + ft := *ret.Type + params := *ft.Params + ret.Recv = &ast.FieldList{List: params.List[:1]} + params.List = params.List[1:] + ft.Params = ¶ms + ret.Type = &ft + } + ret.Name = newIdent(name, ret.Name) + return &ret +} + +func docRecv(recv *ast.Field) (_ string, ok bool) { + switch v := recv.Type.(type) { + case *ast.Ident: + return v.Name, true + case *ast.StarExpr: + if t, ok := v.X.(*ast.Ident); ok { + return "*" + t.Name, true + } + } + return +} + +func newMethod(name string, in *doc.Func) *doc.Func { + ret := *in + ret.Name = name + ret.Decl = newMethodDecl(name, in.Decl) + if recv, ok := docRecv(ret.Decl.Recv.List[0]); ok { + ret.Recv = recv + } + // TODO(xsw): alias doc - ret.Doc + return &ret +} + +func newFunc(name string, in *doc.Func) *doc.Func { + ret := *in + ret.Name = name + ret.Decl = newFuncDecl(name, in.Decl) + // TODO(xsw): alias doc - ret.Doc + return &ret +} + +func setOrder(ctx *transformCtx, in *doc.Func, order int) *doc.Func { + ctx.orders[in] = order + return in +} + +func buildFunc(ctx *transformCtx, overload omthd, in *doc.Func) { + if ex, ok := ctx.typs[overload.typ]; ok { + if ex.t != nil { // method + ex.methods = append(ex.methods, setOrder(ctx, newMethod(overload.name, in), overload.idx)) + } else { + ex.funcs = append(ex.funcs, setOrder(ctx, newFunc(overload.name, in), overload.idx)) + } + } +} + +func toIndex(c byte) int { + if c >= '0' && c <= '9' { + return int(c - '0') + } + if c >= 'a' && c <= 'z' { + return int(c - ('a' - 10)) + } + panic("invalid character out of [0-9,a-z]") +} + +func transformFunc(ctx *transformCtx, t *doc.Type, in *doc.Func, method bool) { + var m mthd + if method { + m.typ = t.Name + } + m.name = in.Name + if overloads, ok := ctx.overloadFuncs[m]; ok { + for _, overload := range overloads { + buildFunc(ctx, overload, in) + } + } + if isOverload(in.Name) { + order := toIndex(in.Name[len(in.Name)-1]) + in.Name = in.Name[:len(in.Name)-3] + in.Decl.Name.Name = in.Name + ctx.orders[in] = order + } +} + +func transformFuncs(ctx *transformCtx, t *doc.Type, in []*doc.Func, method bool) { + for _, f := range in { + transformFunc(ctx, t, f, method) + } +} + +func transformTypes(ctx *transformCtx, in []*doc.Type) { + for _, t := range in { + transformFuncs(ctx, t, t.Funcs, false) + transformFuncs(ctx, t, t.Methods, true) + } +} + +func transformGopo(ctx *transformCtx, name, val string) { + overload := checkTypeMethod(name[len(gopoPrefix):]) + parts := strings.Split(val, ",") + for idx, part := range parts { + if part == "" { + continue + } + var real mthd + if part[0] == '.' { + real = mthd{overload.typ, part[1:]} + } else { + real = mthd{"", part} + } + ctx.overloadFuncs[real] = append(ctx.overloadFuncs[real], omthd{overload, idx}) + } +} + +func transformConstSpec(ctx *transformCtx, vspec *ast.ValueSpec) { + name := vspec.Names[0].Name + if isGopoConst(name) { + if lit, ok := vspec.Values[0].(*ast.BasicLit); ok { + if lit.Kind == token.STRING { + if val, e := strconv.Unquote(lit.Value); e == nil { + transformGopo(ctx, name, val) + } + } + } + } +} + +func transformConst(ctx *transformCtx, in *doc.Value) { + if hasGopoConst(in) { + for _, spec := range in.Decl.Specs { + vspec := spec.(*ast.ValueSpec) + transformConstSpec(ctx, vspec) + } + } +} + +func transformConsts(ctx *transformCtx, in []*doc.Value) { + for _, v := range in { + transformConst(ctx, v) + } +} + +// Transform converts a Go doc package to a Go+ doc package. +func Transform(in *doc.Package) *doc.Package { + if isGopPackage(in) { + ctx := newCtx(in) + transformConsts(ctx, in.Consts) + transformFuncs(ctx, nil, in.Funcs, false) + transformTypes(ctx, in.Types) + ctx.finish(in) + } + return in +}