├── .gitignore ├── testdata ├── invalid_file.go ├── vendor │ └── main.go ├── generic.go ├── empty_var_block.go ├── issue7.go ├── main.go ├── parenthesis.go └── existed.go ├── go.mod ├── helper_test.go ├── doc.go ├── .github └── workflows │ └── ci.yml ├── go.sum ├── LICENSE ├── helper.go ├── README.md ├── main.go ├── parse.go └── parse_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /testdata/invalid_file.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | var i := 1 4 | -------------------------------------------------------------------------------- /testdata/vendor/main.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | var I = 1 4 | -------------------------------------------------------------------------------- /testdata/generic.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | type G[T any] struct{} 4 | -------------------------------------------------------------------------------- /testdata/empty_var_block.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | var ( 4 | // Error 5 | ) 6 | -------------------------------------------------------------------------------- /testdata/issue7.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | import "log" 4 | 5 | // I ... 6 | var I = 1 7 | 8 | func a() { 9 | 10 | // LogAll ... 11 | var LogAll map[string]struct{} 12 | log.Println(LogAll) 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cuonglm/gocmt 2 | 3 | go 1.19 4 | 5 | require github.com/stretchr/testify v1.6.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /testdata/main.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | var i = 0 4 | 5 | var I = 1 6 | 7 | var c = "constant un-exported" 8 | 9 | const C = "constant exported" 10 | 11 | type t struct{} 12 | 13 | type T struct{} 14 | 15 | func main() { 16 | } 17 | 18 | func unexport(s string) { 19 | } 20 | func Export(s string) { 21 | } 22 | 23 | func ExportWithComment(s string) { 24 | } 25 | 26 | // ExistedComment 27 | func ExistedComment() {} 28 | -------------------------------------------------------------------------------- /testdata/parenthesis.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | type Summon string 4 | 5 | const ( 6 | DarkOmega Summon = "celeste" 7 | // LightOmega best summon 8 | LightOmega Summon = "luminineria" 9 | // WindOmega 10 | WindOmega Summon = "tiamat" 11 | ) 12 | 13 | const FireUtility Summon = "the sun" 14 | 15 | const ( 16 | // Light best summon 17 | Light Summon = "lucifer" 18 | ) 19 | 20 | // Light2 best summon 21 | const ( 22 | Light2 Summon = "lucifer" 23 | ) 24 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func Test_isGoFile(t *testing.T) { 9 | isGoFileTest := []struct { 10 | path string 11 | expected bool 12 | }{ 13 | {"main.go", true}, 14 | {"README.md", false}, 15 | } 16 | 17 | for _, tt := range isGoFileTest { 18 | fi, err := os.Stat(tt.path) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if got := isGoFile(fi); got != tt.expected { 24 | t.Fatalf("isGoFile(%+v): expected %v, got %v", fi, tt.expected, got) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // gocmt adds missing comments on exported identifiers in Go source files. 2 | // 3 | // Usage: 4 | // 5 | // gocmt [-i] [-p] [-t "comment template"] [-d dir] 6 | // 7 | // This tools exists because I have to work with some existed code base, which 8 | // is lack of documentation for many exported identifiers. Iterating over them 9 | // is time consuming and maybe not suitable at a stage of the project. So I 10 | // wrote this tool to quickly bypassing CI system. Once thing is settle, we can 11 | // lookback and fix missing comments. 12 | // 13 | // You SHOULD always write documentation for all your exported identifiers. 14 | package main 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-go@v2 16 | with: 17 | go-version: 1.19 18 | - name: Cache 19 | uses: actions/cache@v2 20 | with: 21 | path: | 22 | ~/go/bin 23 | ~/go/src 24 | ~/go/pkg/mod 25 | ~/.cache/go-build 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | - run: go version 30 | - run: go test -race -v ./... 31 | lint: 32 | name: Lint 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: golangci/golangci-lint-action@v2 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 7 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Author:: LE Manh Cuong 2 | Copyright:: Copyright (c) 2016, Cuong Manh Le 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * Neither the name of the @organization@ nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG 25 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 28 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 29 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 30 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 31 | IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | "go/scanner" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | func isGoFile(f os.FileInfo) bool { 11 | name := f.Name() 12 | return !f.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".go") 13 | } 14 | 15 | func printError(err error) { 16 | scanner.PrintError(os.Stderr, err) 17 | } 18 | 19 | func walkFunc(path string, fi os.FileInfo, err error) error { 20 | if err == nil && isGoFile(fi) { 21 | err = processFile(path, *template, *inPlace) 22 | } 23 | 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func isLineComment(comment *ast.CommentGroup) bool { 32 | if comment == nil { 33 | return false 34 | } 35 | if len(comment.List) == 0 { 36 | return false 37 | } 38 | head := comment.List[0].Text 39 | head = strings.TrimSpace(head) 40 | return strings.HasPrefix(head, "//") 41 | } 42 | 43 | func hasCommentPrefix(comment *ast.CommentGroup, prefix string) bool { 44 | return strings.HasPrefix(strings.TrimSpace(comment.Text()), prefix) 45 | } 46 | 47 | func appendCommentGroup(list []*ast.CommentGroup, item *ast.CommentGroup) []*ast.CommentGroup { 48 | ret := []*ast.CommentGroup{} 49 | hasInsert := false 50 | for _, group := range list { 51 | if group.Pos() < item.Pos() { 52 | ret = append(ret, group) 53 | continue 54 | } 55 | if group.Pos() == item.Pos() { 56 | ret = append(ret, item) 57 | hasInsert = true 58 | continue 59 | } 60 | if group.Pos() > item.Pos() { 61 | if !hasInsert { 62 | ret = append(ret, item) 63 | hasInsert = true 64 | } 65 | ret = append(ret, group) 66 | continue 67 | } 68 | } 69 | if !hasInsert { 70 | ret = append(ret, item) 71 | } 72 | return ret 73 | } 74 | -------------------------------------------------------------------------------- /testdata/existed.go: -------------------------------------------------------------------------------- 1 | package p 2 | 3 | // global comments should never be deleted 4 | // something 5 | 6 | import "embed" 7 | 8 | // global comment 1 9 | 10 | // global comment 2 11 | // global comment 3 12 | 13 | // ============= function ============= 14 | 15 | // FuncWithExistedComment1 16 | func FuncWithExistedComment1() { 17 | } 18 | 19 | func FuncWithExistedComment2() { 20 | // this comments should never be deleted 21 | } 22 | 23 | // something 24 | func FuncWithExistedComment3() { 25 | } 26 | 27 | // multi-line comments 28 | // something 29 | func FuncWithExistedComment4() { 30 | } 31 | 32 | /* 33 | something 34 | */ 35 | func FuncWithExistedComment5() { 36 | } 37 | 38 | // ============= value ============= 39 | 40 | // existed comment 41 | var ValueWithExistedComment1 = 1 42 | 43 | // existed comment with spaces 44 | var ValueWithExistedComment2 = 1 45 | 46 | // multi-line comments 47 | // something 48 | var ValueWithExistedComment3 = 1 49 | 50 | /* 51 | should't change C style comment 52 | */ 53 | var ValueWithExistedComment4 = 1 54 | 55 | // ============= paren value ============= 56 | 57 | // existed comment 58 | const ( 59 | ParenValueWithExistedComment1 = 1 60 | // ParenValueWithExistedComment2 something 61 | ParenValueWithExistedComment2 = 1 62 | ) 63 | 64 | // multi-line comments 65 | // something 66 | const ( 67 | ParenValueWithExistedComment3 = 1 68 | // ParenValueWithExistedComment2 something 69 | ParenValueWithExistedComment4 = 1 70 | ) 71 | 72 | // ============= type ============= 73 | 74 | // existed comment 75 | type TypeWithExistedComment1 int 76 | 77 | // existed comment with spaces 78 | type TypeWithExistedComment2 int 79 | 80 | // multi-line comments 81 | // something 82 | type TypeWithExistedComment3 int 83 | 84 | /* 85 | should't change C style comment 86 | */ 87 | type TypeWithExistedComment4 int 88 | 89 | // ============= marker comment ============= 90 | 91 | //go:embed dont_modify_this_comment.txt 92 | //go:embed image/* 93 | var Embed embed.FS 94 | 95 | // something 96 | //go:embed dont_modify_this_comment.txt 97 | var Embed embed.FS 98 | 99 | //go:generate goyacc -o gopher.go -p parser gopher.y 100 | func Generate() { 101 | } 102 | 103 | // ============= end ============= 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocmt - Add missing comment on exported function, method, type, constant, variable in go file 2 | 3 | ![Build status](https://github.com/cuonglm/gocmt/actions/workflows/ci.yml/badge.svg?branch=main) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/cuonglm/gocmt.svg)](https://pkg.go.dev/github.com/cuonglm/gocmt) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/cuonglm/gocmt)](https://goreportcard.com/report/github.com/cuonglm/gocmt) 6 | 7 | # Installation 8 | 9 | For go1.15 and below: 10 | 11 | ```sh 12 | go get -u github.com/cuonglm/gocmt 13 | ``` 14 | 15 | For go1.16 and above: 16 | 17 | ```sh 18 | go install github.com/cuonglm/gocmt@latest 19 | ``` 20 | 21 | # Why gocmt 22 | 23 | Some of my projects have many files with exported fields, variables, functions missing comment, so lint tools will complain. 24 | 25 | I find a way to auto add missing comment to them, just to pass the lint tools but nothing existed. 26 | 27 | So `gocmt` comes in. 28 | 29 | # Usage 30 | ```sh 31 | $ gocmt -h 32 | usage: gocmt [flags] [file ...] 33 | -d string 34 | Directory to process 35 | -i Make in-place editing 36 | -t string 37 | Comment template (default "...") 38 | ``` 39 | 40 | # Example 41 | ```sh 42 | $ cat testdata/main.go 43 | package p 44 | 45 | var i = 0 46 | 47 | var I = 1 48 | 49 | var c = "constant un-exported" 50 | 51 | const C = "constant exported" 52 | 53 | type t struct{} 54 | 55 | type T struct{} 56 | 57 | func main() { 58 | } 59 | 60 | func unexport(s string) { 61 | } 62 | func Export(s string) { 63 | } 64 | 65 | func ExportWithComment(s string) { 66 | } 67 | ``` 68 | 69 | Using `gocmt` give you: 70 | ```sh 71 | $ gocmt testdata/main.go 72 | package p 73 | 74 | var i = 0 75 | 76 | // I ... 77 | var I = 1 78 | 79 | var c = "constant un-exported" 80 | 81 | // C ... 82 | const C = "constant exported" 83 | 84 | type t struct{} 85 | 86 | // T ... 87 | type T struct{} 88 | 89 | func main() { 90 | } 91 | 92 | func unexport(s string) { 93 | } 94 | // Export ... 95 | func Export(s string) { 96 | } 97 | 98 | // ExportWithComment ... 99 | func ExportWithComment(s string) { 100 | } 101 | ``` 102 | 103 | Default template is `...`, you can change it using `-t` option. 104 | 105 | # Author 106 | 107 | Cuong Manh Le 108 | 109 | # License 110 | 111 | See [LICENSE](https://github.com/cuonglm/gocmt/blob/main/LICENSE) 112 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "go/format" 8 | "go/token" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | // ensure that the comment starts on a newline (without the \n, sometimes it starts on the previous } 17 | commentBase = "\n// %s " 18 | // if it's in an indented block, this makes sure that the indentation is correct 19 | commentIndentedBase = "// %s " 20 | fset = token.NewFileSet() 21 | defaultMode = os.FileMode(0644) 22 | tralingWsRegex = regexp.MustCompile(`(?m)[\t ]+$`) 23 | newlinesRegex = regexp.MustCompile(`(?m)\n{3,}`) 24 | ) 25 | 26 | var ( 27 | inPlace = flag.Bool("i", false, "Make in-place editing") 28 | template = flag.String("t", "...", "Comment template") 29 | dir = flag.String("d", "", "Directory to process") 30 | parenComment = flag.Bool("p", false, "Add comments to all const inside the parens if true") 31 | ) 32 | 33 | func main() { 34 | os.Exit(gocmtRun()) 35 | } 36 | 37 | func usage() { 38 | fmt.Fprintf(os.Stderr, "usage: gocmt [flags] [file ...]\n") 39 | flag.PrintDefaults() 40 | } 41 | 42 | func gocmtRun() int { 43 | flag.Parse() 44 | 45 | if *dir != "" { 46 | if err := filepath.Walk(*dir, walkFunc); err != nil { 47 | printError(err) 48 | return 1 49 | } 50 | return 0 51 | } 52 | 53 | if flag.NArg() == 0 { 54 | usage() 55 | } 56 | 57 | for i := 0; i < flag.NArg(); i++ { 58 | path := flag.Arg(i) 59 | switch fi, err := os.Stat(path); { 60 | case err != nil: 61 | printError(err) 62 | case fi.IsDir(): 63 | printError(fmt.Errorf("%s is a directory", path)) 64 | default: 65 | if err := processFile(path, *template, *inPlace); err != nil { 66 | printError(err) 67 | return 1 68 | } 69 | } 70 | } 71 | 72 | return 0 73 | } 74 | 75 | func processFile(filename, template string, inPlace bool) error { 76 | // skip test files and files in vendor/ 77 | if strings.HasSuffix(filename, "_test.go") || strings.Contains(filename, "/vendor/") { 78 | return nil 79 | } 80 | 81 | af, modified, err := parseFile(fset, filename, template) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | var buf bytes.Buffer 87 | if err := format.Node(&buf, fset, af); err != nil { 88 | panic(err) 89 | } 90 | 91 | newBuf := buf.Bytes() 92 | if modified { 93 | newBuf = tralingWsRegex.ReplaceAll(newBuf, []byte("")) 94 | newBuf = newlinesRegex.ReplaceAll(newBuf, []byte("\n\n")) 95 | if inPlace { 96 | return os.WriteFile(filename, newBuf, defaultMode) 97 | } 98 | 99 | fmt.Fprintf(os.Stdout, "%s", newBuf) 100 | return nil 101 | } 102 | 103 | fmt.Fprintf(os.Stderr, "%s no changes\n", filename) 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "strings" 9 | ) 10 | 11 | // parseFile parses and modifies the input file if necessary. Returns AST represents of (new) source, a boolean 12 | // to report whether the source file was modified, and any error if occurred. 13 | func parseFile(fset *token.FileSet, filePath, template string) (af *ast.File, modified bool, err error) { 14 | af, err = parser.ParseFile(fset, filePath, nil, parser.ParseComments|parser.AllErrors) 15 | if err != nil { 16 | return 17 | } 18 | 19 | // Inject first comment to prevent nil comment map 20 | if len(af.Comments) == 0 { 21 | af.Comments = []*ast.CommentGroup{{List: []*ast.Comment{{Slash: -1, Text: "// gocmt"}}}} 22 | defer func() { 23 | // Remove the injected comment 24 | af.Comments = af.Comments[1:] 25 | }() 26 | } 27 | 28 | commentTemplate := commentBase + template 29 | 30 | originalCommentSign := "" 31 | for _, c := range af.Comments { 32 | originalCommentSign += c.Text() 33 | } 34 | 35 | cmap := ast.NewCommentMap(fset, af, af.Comments) 36 | 37 | skipped := make(map[ast.Node]bool) 38 | ast.Inspect(af, func(n ast.Node) bool { 39 | switch typ := n.(type) { 40 | case *ast.FuncDecl: 41 | if skipped[typ] || !typ.Name.IsExported() { 42 | return true 43 | } 44 | addFuncDeclComment(typ, commentTemplate) 45 | cmap[typ] = appendCommentGroup(cmap[typ], typ.Doc) 46 | 47 | case *ast.DeclStmt: 48 | skipped[typ.Decl] = true 49 | 50 | case *ast.GenDecl: 51 | switch typ.Tok { 52 | case token.CONST, token.VAR: 53 | if !(typ.Lparen == token.NoPos && typ.Rparen == token.NoPos) { 54 | // if there's a () and parenComment is true, add comment for each sub entry 55 | if *parenComment { 56 | for _, spec := range typ.Specs { 57 | vs := spec.(*ast.ValueSpec) 58 | if !vs.Names[0].IsExported() { 59 | continue 60 | } 61 | addParenValueSpecComment(vs, commentTemplate) 62 | cmap[vs] = appendCommentGroup(cmap[vs], vs.Doc) 63 | } 64 | return true 65 | } 66 | } 67 | 68 | // empty var block 69 | if len(typ.Specs) == 0 { 70 | return true 71 | } 72 | 73 | vs := typ.Specs[0].(*ast.ValueSpec) 74 | if skipped[typ] || !vs.Names[0].IsExported() { 75 | return true 76 | } 77 | addValueSpecComment(typ, vs, commentTemplate) 78 | 79 | case token.TYPE: 80 | ts := typ.Specs[0].(*ast.TypeSpec) 81 | if skipped[typ] || !ts.Name.IsExported() { 82 | return true 83 | } 84 | addTypeSpecComment(typ, ts, commentTemplate) 85 | default: 86 | return true 87 | } 88 | cmap[typ] = appendCommentGroup(cmap[typ], typ.Doc) 89 | } 90 | return true 91 | }) 92 | 93 | // Rebuild comments 94 | af.Comments = cmap.Filter(af).Comments() 95 | 96 | currentCommentSign := "" 97 | for _, c := range af.Comments { 98 | currentCommentSign += c.Text() 99 | } 100 | 101 | modified = currentCommentSign != originalCommentSign 102 | return 103 | } 104 | 105 | func addFuncDeclComment(fd *ast.FuncDecl, commentTemplate string) { 106 | if fd.Doc == nil || strings.TrimSpace(fd.Doc.Text()) == fd.Name.Name { 107 | text := fmt.Sprintf(commentTemplate, fd.Name) 108 | pos := fd.Pos() - token.Pos(1) 109 | if fd.Doc != nil { 110 | pos = fd.Doc.Pos() 111 | } 112 | fd.Doc = &ast.CommentGroup{List: []*ast.Comment{{Slash: pos, Text: text}}} 113 | return 114 | } 115 | if fd.Doc != nil && isLineComment(fd.Doc) && !hasCommentPrefix(fd.Doc, fd.Name.Name) { 116 | modifyComment(fd.Doc, fd.Name.Name) 117 | return 118 | } 119 | } 120 | 121 | func addValueSpecComment(gd *ast.GenDecl, vs *ast.ValueSpec, commentTemplate string) { 122 | if gd.Doc == nil || strings.TrimSpace(gd.Doc.Text()) == vs.Names[0].Name { 123 | text := fmt.Sprintf(commentTemplate, vs.Names[0].Name) 124 | pos := gd.Pos() - token.Pos(1) 125 | if gd.Doc != nil { 126 | pos = gd.Doc.Pos() 127 | } 128 | gd.Doc = &ast.CommentGroup{List: []*ast.Comment{{Slash: pos, Text: text}}} 129 | return 130 | } 131 | if gd.Doc != nil && isLineComment(gd.Doc) && !hasCommentPrefix(gd.Doc, vs.Names[0].Name) { 132 | modifyComment(gd.Doc, vs.Names[0].Name) 133 | return 134 | } 135 | } 136 | 137 | func addParenValueSpecComment(vs *ast.ValueSpec, commentTemplate string) { 138 | if vs.Doc == nil || strings.TrimSpace(vs.Doc.Text()) == vs.Names[0].Name { 139 | commentTemplate = strings.Replace(commentTemplate, commentBase, commentIndentedBase, 1) 140 | text := fmt.Sprintf(commentTemplate, vs.Names[0].Name) 141 | pos := vs.Pos() - token.Pos(1) 142 | if vs.Doc != nil { 143 | pos = vs.Doc.Pos() 144 | } 145 | vs.Doc = &ast.CommentGroup{List: []*ast.Comment{{Slash: pos, Text: text}}} 146 | return 147 | } 148 | if vs.Doc != nil && isLineComment(vs.Doc) && !hasCommentPrefix(vs.Doc, vs.Names[0].Name) { 149 | modifyComment(vs.Doc, vs.Names[0].Name) 150 | return 151 | } 152 | } 153 | 154 | func addTypeSpecComment(gd *ast.GenDecl, ts *ast.TypeSpec, commentTemplate string) { 155 | if gd.Doc == nil || strings.TrimSpace(gd.Doc.Text()) == ts.Name.Name { 156 | text := fmt.Sprintf(commentTemplate, ts.Name.Name) 157 | pos := gd.Pos() - token.Pos(1) 158 | if gd.Doc != nil { 159 | pos = gd.Doc.Pos() 160 | } 161 | gd.Doc = &ast.CommentGroup{List: []*ast.Comment{{Slash: pos, Text: text}}} 162 | return 163 | } 164 | if gd.Doc != nil && isLineComment(gd.Doc) && !hasCommentPrefix(gd.Doc, ts.Name.Name) { 165 | modifyComment(gd.Doc, ts.Name.Name) 166 | return 167 | } 168 | } 169 | 170 | func modifyComment(comment *ast.CommentGroup, prefix string) { 171 | commentTemplate := commentBase + *template 172 | first := comment.List[0].Text 173 | if strings.HasPrefix(first, "//") && !strings.HasPrefix(first, "// ") { 174 | text := fmt.Sprintf(commentTemplate, prefix) 175 | comment.List = append([]*ast.Comment{{Text: text, Slash: comment.Pos()}}, comment.List...) 176 | return 177 | } 178 | first = strings.TrimPrefix(first, "// ") 179 | first = fmt.Sprintf(commentBase+"%s", prefix, first) 180 | comment.List[0].Text = first 181 | } 182 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "go/format" 6 | "go/token" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const baseSrc = `package p 14 | 15 | var i = 0 16 | 17 | // I ... 18 | var I = 1 19 | 20 | var c = "constant un-exported" 21 | 22 | // C ... 23 | const C = "constant exported" 24 | 25 | type t struct{} 26 | 27 | // T ... 28 | type T struct{} 29 | 30 | func main() { 31 | } 32 | 33 | func unexport(s string) { 34 | } 35 | // Export ... 36 | func Export(s string) { 37 | } 38 | 39 | // ExportWithComment ... 40 | func ExportWithComment(s string) { 41 | } 42 | 43 | // ExistedComment ... 44 | func ExistedComment() {} 45 | ` 46 | 47 | const parenSrc = `package p 48 | 49 | // Summon ... 50 | type Summon string 51 | 52 | // DarkOmega ... 53 | const ( 54 | DarkOmega Summon = "celeste" 55 | // LightOmega best summon 56 | LightOmega Summon = "luminineria" 57 | // WindOmega 58 | WindOmega Summon = "tiamat" 59 | ) 60 | 61 | // FireUtility ... 62 | const FireUtility Summon = "the sun" 63 | 64 | // Light ... 65 | const ( 66 | // Light best summon 67 | Light Summon = "lucifer" 68 | ) 69 | 70 | // Light2 best summon 71 | const ( 72 | Light2 Summon = "lucifer" 73 | ) 74 | ` 75 | const parenSrc2 = `package p 76 | 77 | // Summon ... 78 | type Summon string 79 | 80 | const ( 81 | // DarkOmega ... 82 | DarkOmega Summon = "celeste" 83 | // LightOmega best summon 84 | LightOmega Summon = "luminineria" 85 | // WindOmega ... 86 | WindOmega Summon = "tiamat" 87 | ) 88 | 89 | // FireUtility ... 90 | const FireUtility Summon = "the sun" 91 | 92 | const ( 93 | // Light best summon 94 | Light Summon = "lucifer" 95 | ) 96 | 97 | // Light2 best summon 98 | const ( 99 | // Light2 ... 100 | Light2 Summon = "lucifer" 101 | ) 102 | ` 103 | 104 | const issue7 = `package p 105 | 106 | import "log" 107 | 108 | // I ... 109 | var I = 1 110 | 111 | func a() { 112 | 113 | // LogAll ... 114 | var LogAll map[string]struct{} 115 | log.Println(LogAll) 116 | }` 117 | 118 | const existed = `package p 119 | 120 | // global comments should never be deleted 121 | // something 122 | 123 | import "embed" 124 | 125 | // global comment 1 126 | 127 | // global comment 2 128 | // global comment 3 129 | 130 | // ============= function ============= 131 | 132 | // FuncWithExistedComment1 ... 133 | func FuncWithExistedComment1() { 134 | } 135 | 136 | // FuncWithExistedComment2 ... 137 | func FuncWithExistedComment2() { 138 | // this comments should never be deleted 139 | } 140 | 141 | // FuncWithExistedComment3 something 142 | func FuncWithExistedComment3() { 143 | } 144 | 145 | // FuncWithExistedComment4 multi-line comments 146 | // something 147 | func FuncWithExistedComment4() { 148 | } 149 | 150 | /* 151 | something 152 | */ 153 | func FuncWithExistedComment5() { 154 | } 155 | 156 | // ============= value ============= 157 | 158 | // ValueWithExistedComment1 existed comment 159 | var ValueWithExistedComment1 = 1 160 | 161 | // ValueWithExistedComment2 existed comment with spaces 162 | var ValueWithExistedComment2 = 1 163 | 164 | // ValueWithExistedComment3 multi-line comments 165 | // something 166 | var ValueWithExistedComment3 = 1 167 | 168 | /* 169 | should't change C style comment 170 | */ 171 | var ValueWithExistedComment4 = 1 172 | 173 | // ============= paren value ============= 174 | 175 | // ParenValueWithExistedComment1 existed comment 176 | const ( 177 | ParenValueWithExistedComment1 = 1 178 | // ParenValueWithExistedComment2 something 179 | ParenValueWithExistedComment2 = 1 180 | ) 181 | 182 | // ParenValueWithExistedComment3 multi-line comments 183 | // something 184 | const ( 185 | ParenValueWithExistedComment3 = 1 186 | // ParenValueWithExistedComment2 something 187 | ParenValueWithExistedComment4 = 1 188 | ) 189 | 190 | // ============= type ============= 191 | 192 | // TypeWithExistedComment1 existed comment 193 | type TypeWithExistedComment1 int 194 | 195 | // TypeWithExistedComment2 existed comment with spaces 196 | type TypeWithExistedComment2 int 197 | 198 | // TypeWithExistedComment3 multi-line comments 199 | // something 200 | type TypeWithExistedComment3 int 201 | 202 | /* 203 | should't change C style comment 204 | */ 205 | type TypeWithExistedComment4 int 206 | 207 | // ============= marker comment ============= 208 | 209 | // Embed ... 210 | //go:embed dont_modify_this_comment.txt 211 | //go:embed image/* 212 | var Embed embed.FS 213 | 214 | // Embed something 215 | //go:embed dont_modify_this_comment.txt 216 | var Embed embed.FS 217 | 218 | // Generate ... 219 | //go:generate goyacc -o gopher.go -p parser gopher.y 220 | func Generate() { 221 | } 222 | 223 | // ============= end ============= 224 | ` 225 | 226 | const generic = `package p 227 | 228 | // G ... 229 | type G[T any] struct{} 230 | ` 231 | 232 | func Test_parseFile(t *testing.T) { 233 | parseFileTests := []struct { 234 | path string 235 | expectedSrc string 236 | modified bool 237 | wantErr bool 238 | }{ 239 | {"testdata/main.go", baseSrc, true, false}, 240 | {"testdata/parenthesis.go", parenSrc, true, false}, 241 | {"testdata/invalid_file.go", "", false, true}, 242 | {"testdata/issue7.go", issue7, false, false}, 243 | {"testdata/existed.go", existed, true, false}, 244 | {"testdata/generic.go", generic, true, false}, 245 | } 246 | 247 | for _, tc := range parseFileTests { 248 | tc := tc 249 | t.Run(tc.path, func(t *testing.T) { 250 | t.Parallel() 251 | fset := token.NewFileSet() 252 | af, modified, err := parseFile(fset, tc.path, "...") 253 | assert.True(t, tc.wantErr == (err != nil)) 254 | assert.Equal(t, tc.modified, modified) 255 | 256 | if tc.modified { 257 | buf := new(bytes.Buffer) 258 | assert.NoError(t, format.Node(buf, fset, af)) 259 | newBuf := buf.Bytes() 260 | newBuf = tralingWsRegex.ReplaceAll(newBuf, []byte("")) 261 | newBuf = newlinesRegex.ReplaceAll(newBuf, []byte("\n\n")) 262 | assert.Equal(t, tc.expectedSrc, string(newBuf)) 263 | } 264 | }) 265 | 266 | } 267 | } 268 | func Test_parseFileWithParenComment(t *testing.T) { 269 | *parenComment = true 270 | parseFileTests := []struct { 271 | path string 272 | expectedSrc string 273 | modified bool 274 | wantErr bool 275 | }{ 276 | {"testdata/parenthesis.go", parenSrc2, true, false}, 277 | } 278 | 279 | for _, tc := range parseFileTests { 280 | tc := tc 281 | t.Run(tc.path, func(t *testing.T) { 282 | t.Parallel() 283 | fset := token.NewFileSet() 284 | af, modified, err := parseFile(fset, tc.path, "...") 285 | assert.True(t, tc.wantErr == (err != nil)) 286 | assert.Equal(t, tc.modified, modified) 287 | 288 | if tc.modified { 289 | buf := new(bytes.Buffer) 290 | assert.NoError(t, format.Node(buf, fset, af)) 291 | newBuf := buf.Bytes() 292 | newBuf = tralingWsRegex.ReplaceAll(newBuf, []byte("")) 293 | newBuf = newlinesRegex.ReplaceAll(newBuf, []byte("\n\n")) 294 | assert.Equal(t, tc.expectedSrc, string(newBuf)) 295 | } 296 | }) 297 | 298 | } 299 | } 300 | 301 | func TestSkipVendor(t *testing.T) { 302 | filePath := "testdata/vendor/main.go" 303 | origBuf, err := os.ReadFile(filePath) 304 | if err != nil { 305 | t.Fatal(err) 306 | } 307 | if err := processFile(filePath, "...", true); err != nil { 308 | t.Fatal(err) 309 | } 310 | buf, err := os.ReadFile(filePath) 311 | if err != nil { 312 | t.Fatal(err) 313 | } 314 | if !bytes.Equal(buf, origBuf) { 315 | t.Fatal("file in vendor/ directory was edited") 316 | } 317 | } 318 | --------------------------------------------------------------------------------