Repository: goplus/gop
Pull request #1820 (2024-03-15 15:57:39)

gop/doc.Transform


Patch:
diff --git a/doc/z_gop.go b/doc/z_gop.go
new file mode 100644
index 00000000..1dfc990f
--- /dev/null
+++ b/doc/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/doc/z_transform.go b/doc/z_transform.go
new file mode 100644
index 00000000..507a7f92
--- /dev/null
+++ b/doc/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 = &params
+		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
+}


Test patch:
diff --git a/doc/z_test.go b/doc/z_test.go
new file mode 100644
index 00000000..1d2cfa72
--- /dev/null
+++ b/doc/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)
+	}
+}