├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── fields.go ├── fields_test.go ├── main.go ├── parser.go ├── parser_test.go ├── tag.go ├── tag_test.go └── testdata ├── const.go ├── func.go ├── import.go ├── interface.go ├── range.go ├── simple.go ├── struct.go ├── type.go └── var.go /.gitignore: -------------------------------------------------------------------------------- 1 | gotags 2 | *.png 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.1 5 | - 1.2 6 | - 1.3 7 | - tip 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Joel Stemmer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gotags 2 | 3 | gotags is a [ctags][]-compatible tag generator for [Go][]. 4 | 5 | [![Build Status][travis-badge]][travis-link] 6 | [![Report Card][report-badge]][report-link] 7 | 8 | ## Installation 9 | 10 | [Go][] version 1.1 or higher is required. Install or update gotags using the 11 | `go get` command: 12 | 13 | go get -u github.com/jstemmer/gotags 14 | 15 | Or using package manager `brew` on OS X 16 | 17 | brew install gotags 18 | 19 | ## Usage 20 | 21 | gotags [options] file(s) 22 | 23 | -L="": source file names are read from the specified file. If file is "-", input is read from standard in. 24 | -R=false: recurse into directories in the file list. 25 | -f="": write output to specified file. If file is "-", output is written to standard out. 26 | -silent=false: do not produce any output on error. 27 | -sort=true: sort tags. 28 | -tag-relative=false: file paths should be relative to the directory containing the tag file. 29 | -v=false: print version. 30 | 31 | ## Vim [Tagbar][] configuration 32 | 33 | Put the following configuration in your vimrc: 34 | 35 | let g:tagbar_type_go = { 36 | \ 'ctagstype' : 'go', 37 | \ 'kinds' : [ 38 | \ 'p:package', 39 | \ 'i:imports:1', 40 | \ 'c:constants', 41 | \ 'v:variables', 42 | \ 't:types', 43 | \ 'n:interfaces', 44 | \ 'w:fields', 45 | \ 'e:embedded', 46 | \ 'm:methods', 47 | \ 'r:constructor', 48 | \ 'f:functions' 49 | \ ], 50 | \ 'sro' : '.', 51 | \ 'kind2scope' : { 52 | \ 't' : 'ctype', 53 | \ 'n' : 'ntype' 54 | \ }, 55 | \ 'scope2kind' : { 56 | \ 'ctype' : 't', 57 | \ 'ntype' : 'n' 58 | \ }, 59 | \ 'ctagsbin' : 'gotags', 60 | \ 'ctagsargs' : '-sort -silent' 61 | \ } 62 | 63 | ### Vim+Tagbar Screenshot 64 | ![vim Tagbar gotags](https://stemmertech.com/images/gotags-1.0.0-screenshot.png) 65 | 66 | ## gotags with Emacs 67 | 68 | Gotags doesn't have support for generating etags yet, but 69 | [gotags-el](https://github.com/craig-ludington/gotags-el) allows you to use 70 | gotags directly in Emacs. 71 | 72 | [ctags]: http://ctags.sourceforge.net 73 | [go]: https://golang.org 74 | [tagbar]: https://majutsushi.github.com/tagbar/ 75 | [screenshot]: https://github.com/jstemmer/gotags/gotags-1.0.0-screenshot.png 76 | [travis-badge]: https://travis-ci.org/jstemmer/gotags.svg?branch=master 77 | [travis-link]: https://travis-ci.org/jstemmer/gotags 78 | [report-badge]: https://goreportcard.com/badge/github.com/jstemmer/gotags 79 | [report-link]: https://goreportcard.com/report/github.com/jstemmer/gotags 80 | -------------------------------------------------------------------------------- /fields.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | // FieldSet is a set of extension fields to include in a tag. 9 | type FieldSet map[TagField]bool 10 | 11 | // Includes tests whether the given field is included in the set. 12 | func (f FieldSet) Includes(field TagField) bool { 13 | b, ok := f[field] 14 | return ok && b 15 | } 16 | 17 | // ErrInvalidFields is an error returned when attempting to parse invalid 18 | // fields. 19 | type ErrInvalidFields struct { 20 | Fields string 21 | } 22 | 23 | func (e ErrInvalidFields) Error() string { 24 | return fmt.Sprintf("invalid fields: %s", e.Fields) 25 | } 26 | 27 | // currently only "+l" is supported 28 | var fieldsPattern = regexp.MustCompile(`^\+l$`) 29 | 30 | func parseFields(fields string) (FieldSet, error) { 31 | if fields == "" { 32 | return FieldSet{}, nil 33 | } 34 | if fieldsPattern.MatchString(fields) { 35 | return FieldSet{Language: true}, nil 36 | } 37 | return FieldSet{}, ErrInvalidFields{fields} 38 | } 39 | 40 | func parseExtraSymbols(symbols string) (FieldSet, error) { 41 | symbolsPattern := regexp.MustCompile(`^\+q$`) 42 | if symbols == "" { 43 | return FieldSet{}, nil 44 | } 45 | if symbolsPattern.MatchString(symbols) { 46 | return FieldSet{ExtraTags: true}, nil 47 | } 48 | return FieldSet{}, ErrInvalidFields{fields} 49 | } 50 | -------------------------------------------------------------------------------- /fields_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseFieldsEmpty(t *testing.T) { 8 | _, err := parseFields("") 9 | if err != nil { 10 | t.Fatalf("unexpected error from parseFields: %s", err) 11 | } 12 | } 13 | 14 | func TestParseFieldsLanguage(t *testing.T) { 15 | set, err := parseFields("+l") 16 | if err != nil { 17 | t.Fatalf("unexpected error from parseFields: %s", err) 18 | } 19 | if !set.Includes(Language) { 20 | t.Fatal("expected set to include Language") 21 | } 22 | } 23 | 24 | func TestParseFieldsInvalid(t *testing.T) { 25 | _, err := parseFields("junk") 26 | if err == nil { 27 | t.Fatal("expected parseFields to return error") 28 | } 29 | if _, ok := err.(ErrInvalidFields); !ok { 30 | t.Fatalf("expected parseFields to return error of type ErrInvalidFields, got %T", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "sort" 12 | "strings" 13 | ) 14 | 15 | // Contants used for the meta tags 16 | const ( 17 | Version = "1.4.1" 18 | Name = "gotags" 19 | URL = "https://github.com/jstemmer/gotags" 20 | AuthorName = "Joel Stemmer" 21 | AuthorEmail = "stemmertech@gmail.com" 22 | ) 23 | 24 | var ( 25 | printVersion bool 26 | inputFile string 27 | outputFile string 28 | recurse bool 29 | sortOutput bool 30 | silent bool 31 | relative bool 32 | listLangs bool 33 | fields string 34 | extraSymbols string 35 | ) 36 | 37 | // ignore unknown flags 38 | var flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 39 | 40 | // Initialize flags. 41 | func init() { 42 | flags.BoolVar(&printVersion, "v", false, "print version.") 43 | flags.StringVar(&inputFile, "L", "", `source file names are read from the specified file. If file is "-", input is read from standard in.`) 44 | flags.StringVar(&outputFile, "f", "", `write output to specified file. If file is "-", output is written to standard out.`) 45 | flags.BoolVar(&recurse, "R", false, "recurse into directories in the file list.") 46 | flags.BoolVar(&sortOutput, "sort", true, "sort tags.") 47 | flags.BoolVar(&silent, "silent", false, "do not produce any output on error.") 48 | flags.BoolVar(&relative, "tag-relative", false, "file paths should be relative to the directory containing the tag file.") 49 | flags.BoolVar(&listLangs, "list-languages", false, "list supported languages.") 50 | flags.StringVar(&fields, "fields", "", "include selected extension fields (only +l).") 51 | flags.StringVar(&extraSymbols, "extra", "", "include additional tags with package and receiver name prefixes (+q)") 52 | 53 | flags.Usage = func() { 54 | fmt.Fprintf(os.Stderr, "gotags version %s\n\n", Version) 55 | fmt.Fprintf(os.Stderr, "Usage: %s [options] file(s)\n\n", os.Args[0]) 56 | flags.PrintDefaults() 57 | } 58 | } 59 | 60 | func walkDir(names []string, dir string) ([]string, error) { 61 | e := filepath.Walk(dir, func(path string, finfo os.FileInfo, err error) error { 62 | if err != nil { 63 | return err 64 | } 65 | if strings.HasSuffix(path, ".go") && !finfo.IsDir() { 66 | names = append(names, path) 67 | } 68 | return nil 69 | }) 70 | 71 | return names, e 72 | } 73 | 74 | func recurseNames(names []string) ([]string, error) { 75 | var ret []string 76 | for _, name := range names { 77 | info, e := os.Stat(name) 78 | if e != nil || info == nil || !info.IsDir() { 79 | ret = append(ret, name) // defer the error handling to the scanner 80 | } else { 81 | ret, e = walkDir(ret, name) 82 | if e != nil { 83 | return names, e 84 | } 85 | } 86 | } 87 | return ret, nil 88 | } 89 | 90 | func readNames(names []string) ([]string, error) { 91 | if len(inputFile) == 0 { 92 | return names, nil 93 | } 94 | 95 | var scanner *bufio.Scanner 96 | if inputFile != "-" { 97 | in, err := os.Open(inputFile) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | defer in.Close() 103 | scanner = bufio.NewScanner(in) 104 | } else { 105 | scanner = bufio.NewScanner(os.Stdin) 106 | } 107 | 108 | for scanner.Scan() { 109 | names = append(names, scanner.Text()) 110 | } 111 | if err := scanner.Err(); err != nil { 112 | return nil, err 113 | } 114 | 115 | return names, nil 116 | } 117 | 118 | func getFileNames() ([]string, error) { 119 | var names []string 120 | 121 | names = append(names, flags.Args()...) 122 | names, err := readNames(names) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | if recurse { 128 | names, err = recurseNames(names) 129 | if err != nil { 130 | return nil, err 131 | } 132 | } 133 | 134 | return names, nil 135 | } 136 | 137 | func main() { 138 | if err := flags.Parse(os.Args[1:]); err == flag.ErrHelp { 139 | return 140 | } 141 | 142 | if printVersion { 143 | fmt.Printf("gotags version %s\n", Version) 144 | return 145 | } 146 | 147 | if listLangs { 148 | fmt.Println("Go") 149 | return 150 | } 151 | 152 | files, err := getFileNames() 153 | if err != nil { 154 | fmt.Fprintf(os.Stderr, "cannot get specified files\n\n") 155 | flags.Usage() 156 | os.Exit(1) 157 | } 158 | 159 | if len(files) == 0 && len(inputFile) == 0 { 160 | fmt.Fprintf(os.Stderr, "no file specified\n\n") 161 | flags.Usage() 162 | os.Exit(1) 163 | } 164 | 165 | var basedir string 166 | if relative { 167 | basedir, err = filepath.Abs(filepath.Dir(outputFile)) 168 | if err != nil { 169 | if !silent { 170 | fmt.Fprintf(os.Stderr, "could not determine absolute path: %s\n", err) 171 | } 172 | os.Exit(1) 173 | } 174 | } 175 | 176 | fieldSet, err := parseFields(fields) 177 | if err != nil { 178 | fmt.Fprintf(os.Stderr, "%s\n\n", err) 179 | flags.Usage() 180 | os.Exit(1) 181 | } 182 | 183 | symbolSet, err := parseExtraSymbols(extraSymbols) 184 | if err != nil { 185 | fmt.Fprintf(os.Stderr, "%s\n\n", err) 186 | flags.Usage() 187 | os.Exit(1) 188 | } 189 | 190 | tags := []Tag{} 191 | for _, file := range files { 192 | ts, err := Parse(file, relative, basedir, symbolSet) 193 | if err != nil { 194 | if !silent { 195 | fmt.Fprintf(os.Stderr, "parse error: %s\n\n", err) 196 | } 197 | continue 198 | } 199 | tags = append(tags, ts...) 200 | } 201 | 202 | output := createMetaTags() 203 | for _, tag := range tags { 204 | if fieldSet.Includes(Language) { 205 | tag.Fields[Language] = "Go" 206 | } 207 | output = append(output, tag.String()) 208 | } 209 | 210 | if sortOutput { 211 | sort.Sort(sort.StringSlice(output)) 212 | } 213 | 214 | var out io.Writer 215 | if len(outputFile) == 0 || outputFile == "-" { 216 | // For compatibility with older gotags versions, also write to stdout 217 | // when outputFile is not specified. 218 | out = os.Stdout 219 | } else { 220 | file, err := os.Create(outputFile) 221 | if err != nil { 222 | fmt.Fprintf(os.Stderr, "could not create output file: %s\n", err) 223 | os.Exit(1) 224 | } 225 | out = file 226 | defer file.Close() 227 | } 228 | 229 | for _, s := range output { 230 | fmt.Fprintln(out, s) 231 | } 232 | } 233 | 234 | // createMetaTags returns a list of meta tags. 235 | func createMetaTags() []string { 236 | var sorted int 237 | if sortOutput { 238 | sorted = 1 239 | } 240 | return []string{ 241 | "!_TAG_FILE_FORMAT\t2", 242 | fmt.Sprintf("!_TAG_FILE_SORTED\t%d\t/0=unsorted, 1=sorted/", sorted), 243 | fmt.Sprintf("!_TAG_PROGRAM_AUTHOR\t%s\t/%s/", AuthorName, AuthorEmail), 244 | fmt.Sprintf("!_TAG_PROGRAM_NAME\t%s", Name), 245 | fmt.Sprintf("!_TAG_PROGRAM_URL\t%s", URL), 246 | fmt.Sprintf("!_TAG_PROGRAM_VERSION\t%s\t/%s/", Version, runtime.Version()), 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // tagParser contains the data needed while parsing. 14 | type tagParser struct { 15 | fset *token.FileSet 16 | tags []Tag // list of created tags 17 | types []string // all types we encounter, used to determine the constructors 18 | relative bool // should filenames be relative to basepath 19 | basepath string // output file directory 20 | extraSymbols FieldSet // add the receiver and the package to function and method name 21 | } 22 | 23 | // Parse parses the source in filename and returns a list of tags. If relative 24 | // is true, the filenames in the list of tags are relative to basepath. 25 | func Parse(filename string, relative bool, basepath string, extra FieldSet) ([]Tag, error) { 26 | p := &tagParser{ 27 | fset: token.NewFileSet(), 28 | tags: []Tag{}, 29 | types: make([]string, 0), 30 | relative: relative, 31 | basepath: basepath, 32 | extraSymbols: extra, 33 | } 34 | 35 | f, err := parser.ParseFile(p.fset, filename, nil, 0) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // package 41 | pkgName := p.parsePackage(f) 42 | 43 | // imports 44 | p.parseImports(f) 45 | 46 | // declarations 47 | p.parseDeclarations(f, pkgName) 48 | 49 | return p.tags, nil 50 | } 51 | 52 | // parsePackage creates a package tag. 53 | func (p *tagParser) parsePackage(f *ast.File) string { 54 | p.tags = append(p.tags, p.createTag(f.Name.Name, f.Name.Pos(), Package)) 55 | return f.Name.Name 56 | } 57 | 58 | // parseImports creates an import tag for each import. 59 | func (p *tagParser) parseImports(f *ast.File) { 60 | for _, im := range f.Imports { 61 | name := strings.Trim(im.Path.Value, "\"") 62 | p.tags = append(p.tags, p.createTag(name, im.Path.Pos(), Import)) 63 | } 64 | } 65 | 66 | // parseDeclarations creates a tag for each function, type or value declaration. 67 | // On function symbol we will add 2 entries in the tag file, one with the function name only 68 | // and one with the belonging module name and the function name. 69 | // For method symbol we will add 3 entries: method, receiver.method, module.receiver.method 70 | func (p *tagParser) parseDeclarations(f *ast.File, pkgName string) { 71 | // first parse the type and value declarations, so that we have a list of all 72 | // known types before parsing the functions. 73 | for _, d := range f.Decls { 74 | if decl, ok := d.(*ast.GenDecl); ok { 75 | for _, s := range decl.Specs { 76 | switch ts := s.(type) { 77 | case *ast.TypeSpec: 78 | p.parseTypeDeclaration(ts, pkgName) 79 | case *ast.ValueSpec: 80 | p.parseValueDeclaration(ts, pkgName) 81 | } 82 | } 83 | } 84 | } 85 | 86 | // now parse all the functions 87 | for _, d := range f.Decls { 88 | if decl, ok := d.(*ast.FuncDecl); ok { 89 | p.parseFunction(decl, pkgName) 90 | } 91 | } 92 | } 93 | 94 | // parseFunction creates a tag for function declaration f. 95 | func (p *tagParser) parseFunction(f *ast.FuncDecl, pkgName string) { 96 | tag := p.createTag(f.Name.Name, f.Pos(), Function) 97 | 98 | tag.Fields[Access] = getAccess(tag.Name) 99 | tag.Fields[Signature] = fmt.Sprintf("(%s)", getTypes(f.Type.Params, true)) 100 | tag.Fields[TypeField] = getTypes(f.Type.Results, false) 101 | 102 | if f.Recv != nil && len(f.Recv.List) > 0 { 103 | // this function has a receiver, set the type to Method 104 | tag.Fields[ReceiverType] = getType(f.Recv.List[0].Type, false) 105 | tag.Type = Method 106 | } else if name, ok := p.belongsToReceiver(f.Type.Results); ok { 107 | // this function does not have a receiver, but it belongs to one based 108 | // on its return values; its type will be Function instead of Method. 109 | tag.Fields[ReceiverType] = name 110 | tag.Type = Function 111 | } 112 | 113 | p.tags = append(p.tags, tag) 114 | 115 | if p.extraSymbols.Includes(ExtraTags) { 116 | allNames := make([]string, 0, 10) 117 | allNames = append(allNames, fmt.Sprintf("%s.%s", pkgName, f.Name.Name)) 118 | if tag.Type == Method { 119 | allNames = append(allNames, 120 | fmt.Sprintf("%s.%s", tag.Fields[ReceiverType], f.Name.Name)) 121 | allNames = append(allNames, 122 | fmt.Sprintf("%s.%s.%s", 123 | pkgName, tag.Fields[ReceiverType], f.Name.Name)) 124 | } 125 | 126 | for _, n := range allNames { 127 | newTag := tag 128 | newTag.Name = n 129 | p.tags = append(p.tags, newTag) 130 | } 131 | } 132 | } 133 | 134 | // parseTypeDeclaration creates a tag for type declaration ts and for each 135 | // field in case of a struct, or each method in case of an interface. 136 | // The pkgName argument holds the name of the package the file currently parsed belongs to. 137 | func (p *tagParser) parseTypeDeclaration(ts *ast.TypeSpec, pkgName string) { 138 | tag := p.createTag(ts.Name.Name, ts.Pos(), Type) 139 | 140 | tag.Fields[Access] = getAccess(tag.Name) 141 | 142 | switch s := ts.Type.(type) { 143 | case *ast.StructType: 144 | tag.Fields[TypeField] = "struct" 145 | p.parseStructFields(tag.Name, s) 146 | p.types = append(p.types, tag.Name) 147 | case *ast.InterfaceType: 148 | tag.Fields[TypeField] = "interface" 149 | tag.Type = Interface 150 | p.parseInterfaceMethods(tag.Name, s) 151 | default: 152 | tag.Fields[TypeField] = getType(ts.Type, true) 153 | } 154 | 155 | p.tags = append(p.tags, tag) 156 | 157 | if p.extraSymbols.Includes(ExtraTags) { 158 | extraTag := tag 159 | extraTag.Name = fmt.Sprintf("%s.%s", pkgName, tag.Name) 160 | p.tags = append(p.tags, extraTag) 161 | } 162 | } 163 | 164 | // parseValueDeclaration creates a tag for each variable or constant declaration, 165 | // unless the declaration uses a blank identifier. 166 | func (p *tagParser) parseValueDeclaration(v *ast.ValueSpec, pkgName string) { 167 | for _, d := range v.Names { 168 | if d.Name == "_" { 169 | continue 170 | } 171 | 172 | tag := p.createTag(d.Name, d.Pos(), Variable) 173 | tag.Fields[Access] = getAccess(tag.Name) 174 | 175 | if v.Type != nil { 176 | tag.Fields[TypeField] = getType(v.Type, true) 177 | } 178 | 179 | switch d.Obj.Kind { 180 | case ast.Var: 181 | tag.Type = Variable 182 | case ast.Con: 183 | tag.Type = Constant 184 | } 185 | p.tags = append(p.tags, tag) 186 | if p.extraSymbols.Includes(ExtraTags) { 187 | otherTag := tag 188 | otherTag.Name = fmt.Sprintf("%s.%s", pkgName, tag.Name) 189 | p.tags = append(p.tags, otherTag) 190 | } 191 | } 192 | } 193 | 194 | // parseStructFields creates a tag for each field in struct s, using name as the 195 | // tags ctype. 196 | func (p *tagParser) parseStructFields(name string, s *ast.StructType) { 197 | for _, f := range s.Fields.List { 198 | var tag Tag 199 | if len(f.Names) > 0 { 200 | for _, n := range f.Names { 201 | tag = p.createTag(n.Name, n.Pos(), Field) 202 | tag.Fields[Access] = getAccess(tag.Name) 203 | tag.Fields[ReceiverType] = name 204 | tag.Fields[TypeField] = getType(f.Type, true) 205 | p.tags = append(p.tags, tag) 206 | } 207 | } else { 208 | // embedded field 209 | tag = p.createTag(getType(f.Type, true), f.Pos(), Embedded) 210 | tag.Fields[Access] = getAccess(tag.Name) 211 | tag.Fields[ReceiverType] = name 212 | tag.Fields[TypeField] = getType(f.Type, true) 213 | p.tags = append(p.tags, tag) 214 | } 215 | } 216 | } 217 | 218 | // parseInterfaceMethods creates a tag for each method in interface s, using name 219 | // as the tags ctype. 220 | func (p *tagParser) parseInterfaceMethods(name string, s *ast.InterfaceType) { 221 | for _, f := range s.Methods.List { 222 | var tag Tag 223 | if len(f.Names) > 0 { 224 | tag = p.createTag(f.Names[0].Name, f.Names[0].Pos(), Method) 225 | } else { 226 | // embedded interface 227 | tag = p.createTag(getType(f.Type, true), f.Pos(), Embedded) 228 | } 229 | 230 | tag.Fields[Access] = getAccess(tag.Name) 231 | 232 | if t, ok := f.Type.(*ast.FuncType); ok { 233 | tag.Fields[Signature] = fmt.Sprintf("(%s)", getTypes(t.Params, true)) 234 | tag.Fields[TypeField] = getTypes(t.Results, false) 235 | } 236 | 237 | tag.Fields[InterfaceType] = name 238 | 239 | p.tags = append(p.tags, tag) 240 | } 241 | } 242 | 243 | // createTag creates a new tag, using pos to find the filename and set the line number. 244 | func (p *tagParser) createTag(name string, pos token.Pos, tagType TagType) Tag { 245 | f := p.fset.File(pos).Name() 246 | if p.relative { 247 | if abs, err := filepath.Abs(f); err != nil { 248 | fmt.Fprintf(os.Stderr, "could not determine absolute path: %s\n", err) 249 | } else if rel, err := filepath.Rel(p.basepath, abs); err != nil { 250 | fmt.Fprintf(os.Stderr, "could not determine relative path: %s\n", err) 251 | } else { 252 | f = rel 253 | } 254 | } 255 | return NewTag(name, f, p.fset.Position(pos).Line, tagType) 256 | } 257 | 258 | // belongsToReceiver checks if a function with these return types belongs to 259 | // a receiver. If it belongs to a receiver, the name of that receiver will be 260 | // returned with ok set to true. Otherwise ok will be false. 261 | // Behavior should be similar to how go doc decides when a function belongs to 262 | // a receiver (gosrc/pkg/go/doc/reader.go). 263 | func (p *tagParser) belongsToReceiver(types *ast.FieldList) (name string, ok bool) { 264 | if types == nil || types.NumFields() == 0 { 265 | return "", false 266 | } 267 | 268 | // If the first return type has more than 1 result associated with 269 | // it, it should not belong to that receiver. 270 | // Similar behavior as go doc (go source/. 271 | if len(types.List[0].Names) > 1 { 272 | return "", false 273 | } 274 | 275 | // get name of the first return type 276 | t := getType(types.List[0].Type, false) 277 | 278 | // check if it exists in the current list of known types 279 | for _, knownType := range p.types { 280 | if t == knownType { 281 | return knownType, true 282 | } 283 | } 284 | 285 | return "", false 286 | } 287 | 288 | // getTypes returns a comma separated list of types in fields. If includeNames is 289 | // true each type is preceded by a comma separated list of parameter names. 290 | func getTypes(fields *ast.FieldList, includeNames bool) string { 291 | if fields == nil { 292 | return "" 293 | } 294 | 295 | types := make([]string, len(fields.List)) 296 | for i, param := range fields.List { 297 | if len(param.Names) > 0 { 298 | // there are named parameters, there may be multiple names for a single type 299 | t := getType(param.Type, true) 300 | 301 | if includeNames { 302 | // join all the names, followed by their type 303 | names := make([]string, len(param.Names)) 304 | for j, n := range param.Names { 305 | names[j] = n.Name 306 | } 307 | t = fmt.Sprintf("%s %s", strings.Join(names, ", "), t) 308 | } else { 309 | if len(param.Names) > 1 { 310 | // repeat t len(param.Names) times 311 | t = strings.Repeat(fmt.Sprintf("%s, ", t), len(param.Names)) 312 | 313 | // remove trailing comma and space 314 | t = t[:len(t)-2] 315 | } 316 | } 317 | 318 | types[i] = t 319 | } else { 320 | // no named parameters 321 | types[i] = getType(param.Type, true) 322 | } 323 | } 324 | 325 | return strings.Join(types, ", ") 326 | } 327 | 328 | // getType returns a string representation of the type of node. If star is true and the 329 | // type is a pointer, a * will be prepended to the string. 330 | func getType(node ast.Node, star bool) (paramType string) { 331 | switch t := node.(type) { 332 | case *ast.Ident: 333 | paramType = t.Name 334 | case *ast.StarExpr: 335 | if star { 336 | paramType = "*" + getType(t.X, star) 337 | } else { 338 | paramType = getType(t.X, star) 339 | } 340 | case *ast.SelectorExpr: 341 | paramType = getType(t.X, star) + "." + getType(t.Sel, star) 342 | case *ast.ArrayType: 343 | if l, ok := t.Len.(*ast.BasicLit); ok { 344 | paramType = fmt.Sprintf("[%s]%s", l.Value, getType(t.Elt, star)) 345 | } else { 346 | paramType = "[]" + getType(t.Elt, star) 347 | } 348 | case *ast.FuncType: 349 | fparams := getTypes(t.Params, true) 350 | fresult := getTypes(t.Results, false) 351 | 352 | if len(fresult) > 0 { 353 | paramType = fmt.Sprintf("func(%s) %s", fparams, fresult) 354 | } else { 355 | paramType = fmt.Sprintf("func(%s)", fparams) 356 | } 357 | case *ast.MapType: 358 | paramType = fmt.Sprintf("map[%s]%s", getType(t.Key, true), getType(t.Value, true)) 359 | case *ast.ChanType: 360 | paramType = fmt.Sprintf("chan %s", getType(t.Value, true)) 361 | case *ast.InterfaceType: 362 | paramType = "interface{}" 363 | case *ast.Ellipsis: 364 | paramType = fmt.Sprintf("...%s", getType(t.Elt, true)) 365 | } 366 | return 367 | } 368 | 369 | // getAccess returns the string "public" if name is considered an exported name, otherwise 370 | // the string "private" is returned. 371 | func getAccess(name string) (access string) { 372 | if idx := strings.LastIndex(name, "."); idx > -1 && idx < len(name) { 373 | name = name[idx+1:] 374 | } 375 | 376 | if ast.IsExported(name) { 377 | access = "public" 378 | } else { 379 | access = "private" 380 | } 381 | return 382 | } 383 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "runtime" 8 | "sort" 9 | "strconv" 10 | "testing" 11 | ) 12 | 13 | var goVersionRegexp = regexp.MustCompile(`^go1(?:\.(\d+))?`) 14 | 15 | // This type is used to implement the sort.Interface interface 16 | // in order to be able to sort an array of Tag 17 | type TagSlice []Tag 18 | 19 | // Return the len of the array 20 | func (t TagSlice) Len() int { 21 | return len(t) 22 | } 23 | 24 | // Compare two elements of a tag array 25 | func (t TagSlice) Less(i, j int) bool { 26 | return t[i].String() < t[j].String() 27 | } 28 | 29 | // Swap two elements of the underlying array 30 | func (t TagSlice) Swap(i, j int) { 31 | t[i], t[j] = t[j], t[i] 32 | } 33 | 34 | // Dump the names of the tags in a TagSlice 35 | func (t TagSlice) Dump() { 36 | for idx, val := range t { 37 | fmt.Println(idx, val.Name) 38 | } 39 | } 40 | 41 | type F map[TagField]string 42 | 43 | var testCases = []struct { 44 | filename string 45 | relative bool 46 | basepath string 47 | minversion int 48 | withExtraSymbols bool 49 | tags []Tag 50 | }{ 51 | {filename: "testdata/const.go", tags: []Tag{ 52 | tag("Test", 1, "p", F{}), 53 | tag("Constant", 3, "c", F{"access": "public", "type": "string"}), 54 | tag("OtherConst", 4, "c", F{"access": "public"}), 55 | tag("A", 7, "c", F{"access": "public"}), 56 | tag("B", 8, "c", F{"access": "public"}), 57 | tag("C", 8, "c", F{"access": "public"}), 58 | tag("D", 9, "c", F{"access": "public"}), 59 | }}, 60 | {filename: "testdata/const.go", withExtraSymbols: true, tags: []Tag{ 61 | tag("Test", 1, "p", F{}), 62 | tag("Constant", 3, "c", F{"access": "public", "type": "string"}), 63 | tag("OtherConst", 4, "c", F{"access": "public"}), 64 | tag("A", 7, "c", F{"access": "public"}), 65 | tag("B", 8, "c", F{"access": "public"}), 66 | tag("C", 8, "c", F{"access": "public"}), 67 | tag("D", 9, "c", F{"access": "public"}), 68 | tag("Test.Constant", 3, "c", F{"access": "public", "type": "string"}), 69 | tag("Test.OtherConst", 4, "c", F{"access": "public"}), 70 | tag("Test.A", 7, "c", F{"access": "public"}), 71 | tag("Test.B", 8, "c", F{"access": "public"}), 72 | tag("Test.C", 8, "c", F{"access": "public"}), 73 | tag("Test.D", 9, "c", F{"access": "public"}), 74 | }}, 75 | {filename: "testdata/func.go", tags: []Tag{ 76 | tag("Test", 1, "p", F{}), 77 | tag("Function1", 3, "f", F{"access": "public", "signature": "()", "type": "string"}), 78 | tag("function2", 6, "f", F{"access": "private", "signature": "(p1, p2 int, p3 *string)"}), 79 | tag("function3", 9, "f", F{"access": "private", "signature": "()", "type": "bool"}), 80 | tag("function4", 12, "f", F{"access": "private", "signature": "(p interface{})", "type": "interface{}"}), 81 | tag("function5", 15, "f", F{"access": "private", "signature": "()", "type": "string, string, error"}), 82 | tag("function6", 18, "f", F{"access": "private", "signature": "(v ...interface{})"}), 83 | tag("function7", 21, "f", F{"access": "private", "signature": "(s ...string)"}), 84 | }}, 85 | {filename: "testdata/func.go", withExtraSymbols: true, tags: []Tag{ 86 | tag("Test", 1, "p", F{}), 87 | tag("Test.Function1", 3, "f", F{"access": "public", "signature": "()", "type": "string"}), 88 | tag("Test.function2", 6, "f", F{"access": "private", "signature": "(p1, p2 int, p3 *string)"}), 89 | tag("Test.function3", 9, "f", F{"access": "private", "signature": "()", "type": "bool"}), 90 | tag("Test.function4", 12, "f", F{"access": "private", "signature": "(p interface{})", "type": "interface{}"}), 91 | tag("Test.function5", 15, "f", F{"access": "private", "signature": "()", "type": "string, string, error"}), 92 | tag("Test.function6", 18, "f", F{"access": "private", "signature": "(v ...interface{})"}), 93 | tag("Test.function7", 21, "f", F{"access": "private", "signature": "(s ...string)"}), 94 | tag("Function1", 3, "f", F{"access": "public", "signature": "()", "type": "string"}), 95 | tag("function2", 6, "f", F{"access": "private", "signature": "(p1, p2 int, p3 *string)"}), 96 | tag("function3", 9, "f", F{"access": "private", "signature": "()", "type": "bool"}), 97 | tag("function4", 12, "f", F{"access": "private", "signature": "(p interface{})", "type": "interface{}"}), 98 | tag("function5", 15, "f", F{"access": "private", "signature": "()", "type": "string, string, error"}), 99 | tag("function6", 18, "f", F{"access": "private", "signature": "(v ...interface{})"}), 100 | tag("function7", 21, "f", F{"access": "private", "signature": "(s ...string)"}), 101 | }}, 102 | {filename: "testdata/import.go", tags: []Tag{ 103 | tag("Test", 1, "p", F{}), 104 | tag("fmt", 3, "i", F{}), 105 | tag("go/ast", 6, "i", F{}), 106 | tag("go/parser", 7, "i", F{}), 107 | }}, 108 | {filename: "testdata/import.go", withExtraSymbols: true, tags: []Tag{ 109 | tag("Test", 1, "p", F{}), 110 | tag("fmt", 3, "i", F{}), 111 | tag("go/ast", 6, "i", F{}), 112 | tag("go/parser", 7, "i", F{}), 113 | }}, 114 | {filename: "testdata/interface.go", tags: []Tag{ 115 | tag("Test", 1, "p", F{}), 116 | tag("InterfaceMethod", 4, "m", F{"access": "public", "signature": "(int)", "ntype": "Interface", "type": "string"}), 117 | tag("OtherMethod", 5, "m", F{"access": "public", "signature": "()", "ntype": "Interface"}), 118 | tag("io.Reader", 6, "e", F{"access": "public", "ntype": "Interface"}), 119 | tag("Interface", 3, "n", F{"access": "public", "type": "interface"}), 120 | }}, 121 | {filename: "testdata/interface.go", withExtraSymbols: true, tags: []Tag{ 122 | tag("Test", 1, "p", F{}), 123 | tag("InterfaceMethod", 4, "m", F{"access": "public", "signature": "(int)", "ntype": "Interface", "type": "string"}), 124 | tag("OtherMethod", 5, "m", F{"access": "public", "signature": "()", "ntype": "Interface"}), 125 | tag("io.Reader", 6, "e", F{"access": "public", "ntype": "Interface"}), 126 | tag("Interface", 3, "n", F{"access": "public", "type": "interface"}), 127 | tag("Test.Interface", 3, "n", F{"access": "public", "type": "interface"}), 128 | }}, 129 | {filename: "testdata/struct.go", tags: []Tag{ 130 | tag("Test", 1, "p", F{}), 131 | tag("Field1", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), 132 | tag("Field2", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), 133 | tag("field3", 5, "w", F{"access": "private", "ctype": "Struct", "type": "string"}), 134 | tag("field4", 6, "w", F{"access": "private", "ctype": "Struct", "type": "*bool"}), 135 | tag("Struct", 3, "t", F{"access": "public", "type": "struct"}), 136 | tag("Struct", 20, "e", F{"access": "public", "ctype": "TestEmbed", "type": "Struct"}), 137 | tag("*io.Writer", 21, "e", F{"access": "public", "ctype": "TestEmbed", "type": "*io.Writer"}), 138 | tag("TestEmbed", 19, "t", F{"access": "public", "type": "struct"}), 139 | tag("Struct2", 27, "t", F{"access": "public", "type": "struct"}), 140 | tag("Connection", 36, "t", F{"access": "public", "type": "struct"}), 141 | tag("NewStruct", 9, "f", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "*Struct"}), 142 | tag("F1", 13, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "[]bool, [2]*string"}), 143 | tag("F2", 16, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "bool"}), 144 | tag("NewTestEmbed", 24, "f", F{"access": "public", "ctype": "TestEmbed", "signature": "()", "type": "TestEmbed"}), 145 | tag("NewStruct2", 30, "f", F{"access": "public", "ctype": "Struct2", "signature": "()", "type": "*Struct2, error"}), 146 | tag("Dial", 33, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, error"}), 147 | tag("Dial2", 39, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, *Struct2"}), 148 | tag("Dial3", 42, "f", F{"access": "public", "signature": "()", "type": "*Connection, *Connection"}), 149 | }}, 150 | {filename: "testdata/struct.go", withExtraSymbols: true, tags: []Tag{ 151 | tag("Test", 1, "p", F{}), 152 | tag("Field1", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), 153 | tag("Field2", 4, "w", F{"access": "public", "ctype": "Struct", "type": "int"}), 154 | tag("field3", 5, "w", F{"access": "private", "ctype": "Struct", "type": "string"}), 155 | tag("field4", 6, "w", F{"access": "private", "ctype": "Struct", "type": "*bool"}), 156 | tag("Struct", 3, "t", F{"access": "public", "type": "struct"}), 157 | tag("Test.Struct", 3, "t", F{"access": "public", "type": "struct"}), 158 | tag("Struct", 20, "e", F{"access": "public", "ctype": "TestEmbed", "type": "Struct"}), 159 | tag("*io.Writer", 21, "e", F{"access": "public", "ctype": "TestEmbed", "type": "*io.Writer"}), 160 | tag("TestEmbed", 19, "t", F{"access": "public", "type": "struct"}), 161 | tag("Test.TestEmbed", 19, "t", F{"access": "public", "type": "struct"}), 162 | tag("Struct2", 27, "t", F{"access": "public", "type": "struct"}), 163 | tag("Test.Struct2", 27, "t", F{"access": "public", "type": "struct"}), 164 | tag("Connection", 36, "t", F{"access": "public", "type": "struct"}), 165 | tag("Test.Connection", 36, "t", F{"access": "public", "type": "struct"}), 166 | tag("NewStruct", 9, "f", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "*Struct"}), 167 | tag("Test.NewStruct", 9, "f", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "*Struct"}), 168 | tag("F1", 13, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "[]bool, [2]*string"}), 169 | tag("Struct.F1", 13, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "[]bool, [2]*string"}), 170 | tag("Test.F1", 13, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "[]bool, [2]*string"}), 171 | tag("Test.Struct.F1", 13, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "[]bool, [2]*string"}), 172 | tag("F2", 16, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "bool"}), 173 | tag("Struct.F2", 16, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "bool"}), 174 | tag("Test.Struct.F2", 16, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "bool"}), 175 | tag("Test.F2", 16, "m", F{"access": "public", "ctype": "Struct", "signature": "()", "type": "bool"}), 176 | tag("NewTestEmbed", 24, "f", F{"access": "public", "ctype": "TestEmbed", "signature": "()", "type": "TestEmbed"}), 177 | tag("Test.NewTestEmbed", 24, "f", F{"access": "public", "ctype": "TestEmbed", "signature": "()", "type": "TestEmbed"}), 178 | tag("NewStruct2", 30, "f", F{"access": "public", "ctype": "Struct2", "signature": "()", "type": "*Struct2, error"}), 179 | tag("Test.NewStruct2", 30, "f", F{"access": "public", "ctype": "Struct2", "signature": "()", "type": "*Struct2, error"}), 180 | tag("Dial", 33, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, error"}), 181 | tag("Test.Dial", 33, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, error"}), 182 | tag("Dial2", 39, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, *Struct2"}), 183 | tag("Test.Dial2", 39, "f", F{"access": "public", "ctype": "Connection", "signature": "()", "type": "*Connection, *Struct2"}), 184 | tag("Dial3", 42, "f", F{"access": "public", "signature": "()", "type": "*Connection, *Connection"}), 185 | tag("Test.Dial3", 42, "f", F{"access": "public", "signature": "()", "type": "*Connection, *Connection"}), 186 | }}, 187 | {filename: "testdata/type.go", tags: []Tag{ 188 | tag("Test", 1, "p", F{}), 189 | tag("testType", 3, "t", F{"access": "private", "type": "int"}), 190 | tag("testArrayType", 4, "t", F{"access": "private", "type": "[4]int"}), 191 | tag("testSliceType", 5, "t", F{"access": "private", "type": "[]int"}), 192 | tag("testPointerType", 6, "t", F{"access": "private", "type": "*string"}), 193 | tag("testFuncType1", 7, "t", F{"access": "private", "type": "func()"}), 194 | tag("testFuncType2", 8, "t", F{"access": "private", "type": "func(int) string"}), 195 | tag("testMapType", 9, "t", F{"access": "private", "type": "map[string]bool"}), 196 | tag("testChanType", 10, "t", F{"access": "private", "type": "chan bool"}), 197 | }}, 198 | {filename: "testdata/type.go", withExtraSymbols: true, tags: []Tag{ 199 | tag("Test", 1, "p", F{}), 200 | tag("testType", 3, "t", F{"access": "private", "type": "int"}), 201 | tag("testArrayType", 4, "t", F{"access": "private", "type": "[4]int"}), 202 | tag("testSliceType", 5, "t", F{"access": "private", "type": "[]int"}), 203 | tag("testPointerType", 6, "t", F{"access": "private", "type": "*string"}), 204 | tag("testFuncType1", 7, "t", F{"access": "private", "type": "func()"}), 205 | tag("testFuncType2", 8, "t", F{"access": "private", "type": "func(int) string"}), 206 | tag("testMapType", 9, "t", F{"access": "private", "type": "map[string]bool"}), 207 | tag("testChanType", 10, "t", F{"access": "private", "type": "chan bool"}), 208 | tag("Test.testType", 3, "t", F{"access": "private", "type": "int"}), 209 | tag("Test.testArrayType", 4, "t", F{"access": "private", "type": "[4]int"}), 210 | tag("Test.testSliceType", 5, "t", F{"access": "private", "type": "[]int"}), 211 | tag("Test.testPointerType", 6, "t", F{"access": "private", "type": "*string"}), 212 | tag("Test.testFuncType1", 7, "t", F{"access": "private", "type": "func()"}), 213 | tag("Test.testFuncType2", 8, "t", F{"access": "private", "type": "func(int) string"}), 214 | tag("Test.testMapType", 9, "t", F{"access": "private", "type": "map[string]bool"}), 215 | tag("Test.testChanType", 10, "t", F{"access": "private", "type": "chan bool"}), 216 | }}, 217 | {filename: "testdata/var.go", tags: []Tag{ 218 | tag("Test", 1, "p", F{}), 219 | tag("variable1", 3, "v", F{"access": "private", "type": "int"}), 220 | tag("variable2", 4, "v", F{"access": "private", "type": "string"}), 221 | tag("A", 7, "v", F{"access": "public"}), 222 | tag("B", 8, "v", F{"access": "public"}), 223 | tag("C", 8, "v", F{"access": "public"}), 224 | tag("D", 9, "v", F{"access": "public"}), 225 | }}, 226 | {filename: "testdata/var.go", withExtraSymbols: true, tags: []Tag{ 227 | tag("Test", 1, "p", F{}), 228 | tag("variable1", 3, "v", F{"access": "private", "type": "int"}), 229 | tag("variable2", 4, "v", F{"access": "private", "type": "string"}), 230 | tag("A", 7, "v", F{"access": "public"}), 231 | tag("B", 8, "v", F{"access": "public"}), 232 | tag("C", 8, "v", F{"access": "public"}), 233 | tag("D", 9, "v", F{"access": "public"}), 234 | tag("Test.variable1", 3, "v", F{"access": "private", "type": "int"}), 235 | tag("Test.variable2", 4, "v", F{"access": "private", "type": "string"}), 236 | tag("Test.A", 7, "v", F{"access": "public"}), 237 | tag("Test.B", 8, "v", F{"access": "public"}), 238 | tag("Test.C", 8, "v", F{"access": "public"}), 239 | tag("Test.D", 9, "v", F{"access": "public"}), 240 | }}, 241 | {filename: "testdata/simple.go", relative: true, basepath: "dir", tags: []Tag{ 242 | {Name: "main", File: "../testdata/simple.go", Address: "1", Type: "p", Fields: F{"line": "1"}}, 243 | }}, 244 | {filename: "testdata/simple.go", withExtraSymbols: true, relative: true, basepath: "dir", tags: []Tag{ 245 | {Name: "main", File: "../testdata/simple.go", Address: "1", Type: "p", Fields: F{"line": "1"}}, 246 | }}, 247 | {filename: "testdata/range.go", minversion: 4, tags: []Tag{ 248 | tag("main", 1, "p", F{}), 249 | tag("fmt", 3, "i", F{}), 250 | tag("main", 5, "f", F{"access": "private", "signature": "()"}), 251 | }}, 252 | {filename: "testdata/range.go", withExtraSymbols: true, minversion: 4, tags: []Tag{ 253 | tag("main", 1, "p", F{}), 254 | tag("fmt", 3, "i", F{}), 255 | tag("main", 5, "f", F{"access": "private", "signature": "()"}), 256 | tag("main.main", 5, "f", F{"access": "private", "signature": "()"}), 257 | }}, 258 | } 259 | 260 | func TestParse(t *testing.T) { 261 | for _, testCase := range testCases { 262 | if testCase.minversion > 0 && extractVersionCode(runtime.Version()) < testCase.minversion { 263 | t.Skipf("[%s] skipping test. Version is %s, but test requires at least go1.%d", testCase.filename, runtime.Version(), testCase.minversion) 264 | continue 265 | } 266 | 267 | basepath, err := filepath.Abs(testCase.basepath) 268 | if err != nil { 269 | t.Errorf("[%s] could not determine base path: %s\n", testCase.filename, err) 270 | continue 271 | } 272 | 273 | var extra FieldSet 274 | if testCase.withExtraSymbols { 275 | extra = FieldSet{ExtraTags: true} 276 | } 277 | 278 | tags, err := Parse(testCase.filename, testCase.relative, basepath, extra) 279 | if err != nil { 280 | t.Errorf("[%s] Parse error: %s", testCase.filename, err) 281 | continue 282 | } 283 | 284 | sort.Sort(TagSlice(tags)) 285 | sort.Sort(TagSlice(testCase.tags)) 286 | 287 | if len(tags) != len(testCase.tags) { 288 | t.Errorf("[%s] len(tags) == %d, want %d", testCase.filename, len(tags), len(testCase.tags)) 289 | continue 290 | } 291 | 292 | for i, tag := range testCase.tags { 293 | if len(tag.File) == 0 { 294 | tag.File = testCase.filename 295 | } 296 | if tags[i].String() != tag.String() { 297 | t.Errorf("[%s] tag(%d)\n is:%s\nwant:%s", testCase.filename, i, tags[i].String(), tag.String()) 298 | } 299 | } 300 | } 301 | } 302 | 303 | func tag(n string, l int, t TagType, fields F) (tag Tag) { 304 | tag = Tag{ 305 | Name: n, 306 | File: "", 307 | Address: strconv.Itoa(l), 308 | Type: t, 309 | Fields: fields, 310 | } 311 | 312 | tag.Fields["line"] = tag.Address 313 | 314 | return 315 | } 316 | 317 | func extractVersionCode(version string) int { 318 | matches := goVersionRegexp.FindAllStringSubmatch(version, -1) 319 | if len(matches) == 0 || len(matches[0]) < 2 { 320 | return 0 321 | } 322 | n, _ := strconv.Atoi(matches[0][1]) 323 | return n 324 | } 325 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Tag represents a single tag. 12 | type Tag struct { 13 | Name string 14 | File string 15 | Address string 16 | Type TagType 17 | Fields map[TagField]string 18 | } 19 | 20 | // TagField represents a single field in a tag line. 21 | type TagField string 22 | 23 | // Tag fields. 24 | const ( 25 | Access TagField = "access" 26 | Signature TagField = "signature" 27 | TypeField TagField = "type" 28 | ReceiverType TagField = "ctype" 29 | Line TagField = "line" 30 | InterfaceType TagField = "ntype" 31 | Language TagField = "language" 32 | ExtraTags TagField = "extraTag" 33 | ) 34 | 35 | // TagType represents the type of a tag in a tag line. 36 | type TagType string 37 | 38 | // Tag types. 39 | const ( 40 | Package TagType = "p" 41 | Import TagType = "i" 42 | Constant TagType = "c" 43 | Variable TagType = "v" 44 | Type TagType = "t" 45 | Interface TagType = "n" 46 | Field TagType = "w" 47 | Embedded TagType = "e" 48 | Method TagType = "m" 49 | Constructor TagType = "r" 50 | Function TagType = "f" 51 | ) 52 | 53 | // NewTag creates a new Tag. 54 | func NewTag(name, file string, line int, tagType TagType) Tag { 55 | l := strconv.Itoa(line) 56 | return Tag{ 57 | Name: name, 58 | File: file, 59 | Address: l, 60 | Type: tagType, 61 | Fields: map[TagField]string{Line: l}, 62 | } 63 | } 64 | 65 | // The tags file format string representation of this tag. 66 | func (t Tag) String() string { 67 | var b bytes.Buffer 68 | 69 | b.WriteString(t.Name) 70 | b.WriteByte('\t') 71 | b.WriteString(t.File) 72 | b.WriteByte('\t') 73 | b.WriteString(t.Address) 74 | b.WriteString(";\"\t") 75 | b.WriteString(string(t.Type)) 76 | b.WriteByte('\t') 77 | 78 | fields := make([]string, 0, len(t.Fields)) 79 | i := 0 80 | for k, v := range t.Fields { 81 | if len(v) == 0 { 82 | continue 83 | } 84 | fields = append(fields, fmt.Sprintf("%s:%s", k, v)) 85 | i++ 86 | } 87 | 88 | sort.Sort(sort.StringSlice(fields)) 89 | b.WriteString(strings.Join(fields, "\t")) 90 | 91 | return b.String() 92 | } 93 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTagString(t *testing.T) { 8 | tag := NewTag("tagname", "filename", 2, "x") 9 | tag.Fields["access"] = "public" 10 | tag.Fields["type"] = "struct" 11 | tag.Fields["signature"] = "()" 12 | tag.Fields["empty"] = "" 13 | 14 | expected := "tagname\tfilename\t2;\"\tx\taccess:public\tline:2\tsignature:()\ttype:struct" 15 | 16 | s := tag.String() 17 | if s != expected { 18 | t.Errorf("Tag.String()\n is:%s\nwant:%s", s, expected) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/const.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | const Constant string = "const" 4 | const OtherConst = "const" 5 | 6 | const ( 7 | A = true 8 | B, C = "c", "d" 9 | _, D = 0, 0 10 | ) 11 | -------------------------------------------------------------------------------- /testdata/func.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | func Function1() string { 4 | } 5 | 6 | func function2(p1, p2 int, p3 *string) { 7 | } 8 | 9 | func function3() (result bool) { 10 | } 11 | 12 | func function4(p interface{}) interface{} { 13 | } 14 | 15 | func function5() (a, b string, c error) { 16 | } 17 | 18 | func function6(v ...interface{}) { 19 | } 20 | 21 | func function7(s ...string) { 22 | } 23 | -------------------------------------------------------------------------------- /testdata/import.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | import "fmt" 4 | 5 | import ( 6 | "go/ast" 7 | "go/parser" 8 | ) 9 | -------------------------------------------------------------------------------- /testdata/interface.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | type Interface interface { 4 | InterfaceMethod(int) string 5 | OtherMethod() 6 | io.Reader 7 | } 8 | -------------------------------------------------------------------------------- /testdata/range.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | for range [3]int{} { 7 | fmt.Println("testing") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /testdata/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /testdata/struct.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | type Struct struct { 4 | Field1, Field2 int 5 | field3 string 6 | field4 *bool 7 | } 8 | 9 | func NewStruct() *Struct { 10 | return &Struct{} 11 | } 12 | 13 | func (s Struct) F1() ([]bool, [2]*string) { 14 | } 15 | 16 | func (Struct) F2() (result bool) { 17 | } 18 | 19 | type TestEmbed struct { 20 | Struct 21 | *io.Writer 22 | } 23 | 24 | func NewTestEmbed() TestEmbed { 25 | } 26 | 27 | type Struct2 struct { 28 | } 29 | 30 | func NewStruct2() (*Struct2, error) { 31 | } 32 | 33 | func Dial() (*Connection, error) { 34 | } 35 | 36 | type Connection struct { 37 | } 38 | 39 | func Dial2() (*Connection, *Struct2) { 40 | } 41 | 42 | func Dial3() (a, b *Connection) { 43 | } 44 | -------------------------------------------------------------------------------- /testdata/type.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | type testType int 4 | type testArrayType [4]int 5 | type testSliceType []int 6 | type testPointerType *string 7 | type testFuncType1 func() 8 | type testFuncType2 func(int) string 9 | type testMapType map[string]bool 10 | type testChanType chan bool 11 | -------------------------------------------------------------------------------- /testdata/var.go: -------------------------------------------------------------------------------- 1 | package Test 2 | 3 | var variable1 int 4 | var variable2 string = "string" 5 | 6 | var ( 7 | A = true 8 | B, C = "c", "d" 9 | _, D = 0, 0 10 | ) 11 | --------------------------------------------------------------------------------