Repository: goplus/gop (go)

Issue:
Proposal for official Docker image support for Go+
### Proposal

1. **Base image**: I propose using `docker.io/library/golang`'s Debian image variant as the base for the first official Go+ Docker image. This base image is selected for its comprehensive inclusion of essential tools such as Go, GCC, and Git, which are indispensable for Go+ application development. Future iterations might explore alternative base images, like the Alpine variant, to offer more lightweight options.
2. **Automation**: To streamline the build and release process of the image, I propose expanding Go+'s existing integration with GoReleaser to support Docker image building. This setup can be efficiently implemented to ensure consistent and timely updates to the Docker image.
3. **Hosting**: Considering Docker Hub's pull rate limitations and accessibility challenges in certain regions, including mainland China, I proposal hosting the official Go+ Docker images exclusively on GitHub Packages (ghcr.io). This choice not only circumvents the limitations of Docker Hub but also simplifies integration with GitHub's ecosystem, where the Go+ project already resides.

### Background

This proposal aims to introduce official Docker image support for the Go+ programming language, providing a standardized environment for building and running Go+ applications. Utilizing a Go+ container for both the build and runtime environment streamlines development workflows, aligning with modern software practices.

### Workarounds

N/A

Pull request description:
Introduce official Docker image
Fixes #1818

---

Note that this feature requires the [Workflow permissions](https://github.com/goplus/gop/settings/actions) to be set to `Read and write permissions`.

Patch:
diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index 1b88a592..92e8181d 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -59,6 +59,16 @@ jobs:
       with:
         go-version: "1.21"
 
+    - name: Set up QEMU
+      uses: docker/setup-qemu-action@v3
+
+    - name: Log in to GitHub Container Registry
+      uses: docker/login-action@v3
+      with:
+        registry: ghcr.io
+        username: ${{ github.actor }}
+        password: ${{ github.token }}
+
     - name: Release with goreleaser
       uses: goreleaser/goreleaser-action@v5
       with:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 056f1ef7..6fb54a1f 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -68,6 +68,60 @@ changelog:
       - "^docs:"
       - "^test:"
 
+dockers:
+  - goarch: "386"
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-i386
+    use: buildx
+    build_flag_templates:
+      - --platform=linux/386
+      - --build-arg=USE_GORELEASER_ARTIFACTS=1
+    extra_files:
+      - ./
+  - goarch: amd64
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-x86_64
+    use: buildx
+    build_flag_templates:
+      - --platform=linux/amd64
+      - --build-arg=USE_GORELEASER_ARTIFACTS=1
+    extra_files:
+      - ./
+  - goarch: arm64
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-arm64
+    use: buildx
+    build_flag_templates:
+      - --platform=linux/arm64
+      - --build-arg=USE_GORELEASER_ARTIFACTS=1
+    extra_files:
+      - ./
+
+docker_manifests:
+  - name_template: ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-i386
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-x86_64
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-arm64
+  - name_template: ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Major }}.{{ .Minor }}
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-i386
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-x86_64
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-arm64
+    skip_push: auto
+  - name_template: ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Major }}
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-i386
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-x86_64
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-arm64
+    skip_push: auto
+  - name_template: ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:latest
+    image_templates:
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-i386
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-x86_64
+      - ghcr.io/{{ envOrDefault "GITHUB_REPOSITORY" "goplus/gop" }}:v{{ .Version }}-arm64
+    skip_push: auto
+
 winget:
   - name: goplus
     homepage: "https://goplus.org/"
@@ -147,6 +201,8 @@ nfpms:
     bindir: /usr/lib/{{ .ProjectName }}
     contents:
       # source folder
+      - src: "Dockerfile"
+        dst: "/usr/lib/{{ .ProjectName }}/Dockerfile"
       - src: "LICENSE"
         dst: "/usr/lib/{{ .ProjectName }}/LICENSE"
       - src: "Makefile"
@@ -236,6 +292,8 @@ snapcrafts:
       {{- if .Arm }}v{{ .Arm }}{{ end }}
     extra_files:
       # source folder
+      - source: "Dockerfile"
+        destination: "Dockerfile"
       - source: "LICENSE"
         destination: "LICENSE"
       - source: "Makefile"
@@ -310,3 +368,6 @@ snapcrafts:
 
 checksum:
   name_template: "{{ .ProjectName }}_v{{ .Version }}_checksums.txt"
+
+release:
+  prerelease: auto
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..740cab6d
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+ARG BASE_IMAGE=golang:1.22-bookworm
+
+FROM $BASE_IMAGE AS build
+ARG USE_GORELEASER_ARTIFACTS=0
+WORKDIR /usr/local/src/gop
+COPY . .
+ENV GOPROOT=/usr/local/gop
+RUN set -eux; \
+	mkdir -p $GOPROOT/bin; \
+	git ls-tree --full-tree --name-only -r HEAD | grep -vE "^\." | xargs -I {} cp --parents {} $GOPROOT/; \
+	if [ $USE_GORELEASER_ARTIFACTS -eq 1 ]; then \
+		GOARCH=$(go env GOARCH); \
+		BIN_DIR_SUFFIX=linux_$GOARCH; \
+		[ $GOARCH = "amd64" ] && BIN_DIR_SUFFIX=${BIN_DIR_SUFFIX}_v1; \
+		[ $GOARCH = "arm" ] && BIN_DIR_SUFFIX=${BIN_DIR_SUFFIX}_$(go env GOARM | cut -d , -f 1); \
+		cp .dist/gop_$BIN_DIR_SUFFIX/bin/gop .dist/gopfmt_$BIN_DIR_SUFFIX/bin/gopfmt $GOPROOT/bin/; \
+	else \
+		./all.bash; \
+		cp bin/gop bin/gopfmt $GOPROOT/bin/; \
+	fi
+
+FROM $BASE_IMAGE
+ENV GOPROOT=/usr/local/gop
+COPY --from=build $GOPROOT/ $GOPROOT/
+ENV PATH=$GOPROOT/bin:$PATH
+WORKDIR /gop
diff --git a/doc/z_gop.go b/doc/z_gop.go
deleted file mode 100644
index 1dfc990f..00000000
--- a/doc/z_gop.go
+++ /dev/null
@@ -1,76 +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 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
deleted file mode 100644
index 507a7f92..00000000
--- a/doc/z_transform.go
+++ /dev/null
@@ -1,259 +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 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
deleted file mode 100644
index 1d2cfa72..00000000
--- a/doc/z_test.go
+++ /dev/null
@@ -1,255 +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 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)
-	}
-}