main.go (7388B)
1 // review-go prints the declarations, imports and comments of the Go files it 2 // is given, as JSON, using Go's own parser. It is the Go frontend as a 3 // sidecar, so a reviewer written in another language reads Go the way this 4 // one does. One line per invocation: 5 // 6 // review-go file.go ... > decls.json 7 // 8 // where the JSON is {"files":[{"name":"a.go","package":"main", 9 // "imports":["fmt"],"decls":[{"name":"main","kind":"func","line":7, 10 // "end_line":9,"exported":false,"test":false,"local":false, 11 // "text":"func main() {","doc":"..."}],"comments":[{"line":5,"text":"..."}]}]} 12 // 13 // Kinds are func, type, field, const and var. A method is a func under its 14 // own name. A test is a func the testing package would run. A local 15 // declaration sits inside a function body. A file that does not parse is 16 // reported with its error and nothing else, so the caller can tell a file 17 // with no declarations from one it could not read. Build it and put it on 18 // the path: 19 // 20 // go build -o ~/go/bin/review-go ./sidecar/gofront 21 package main 22 23 import ( 24 "encoding/json" 25 "fmt" 26 "go/ast" 27 "go/parser" 28 "go/token" 29 "os" 30 "strings" 31 ) 32 33 // Decl is one declaration as the sidecar reports it. The shape is the Odin 34 // sidecar's, with what Go adds: a kind that tells const from var, and 35 // whether the declaration is local to a function. 36 type Decl struct { 37 Name string `json:"name"` 38 Kind string `json:"kind"` 39 Line int `json:"line"` 40 EndLine int `json:"end_line"` 41 Exported bool `json:"exported"` 42 Test bool `json:"test"` 43 Local bool `json:"local"` 44 Text string `json:"text"` 45 Doc string `json:"doc"` 46 } 47 48 // Comment is one comment, located by its first line, with the marker 49 // stripped and the text trimmed. 50 type Comment struct { 51 Line int `json:"line"` 52 Text string `json:"text"` 53 } 54 55 // File is what one Go file declares. 56 type File struct { 57 Name string `json:"name"` 58 Package string `json:"package,omitempty"` 59 Imports []string `json:"imports"` 60 Decls []Decl `json:"decls"` 61 Comments []Comment `json:"comments"` 62 Error string `json:"error,omitempty"` 63 } 64 65 type Output struct { 66 Files []File `json:"files"` 67 } 68 69 func main() { 70 out := Output{Files: []File{}} 71 for _, arg := range os.Args[1:] { 72 source, err := os.ReadFile(arg) 73 if err != nil { 74 out.Files = append(out.Files, File{Name: arg, Imports: []string{}, Decls: []Decl{}, Comments: []Comment{}, Error: err.Error()}) 75 continue 76 } 77 out.Files = append(out.Files, readFile(arg, source)) 78 } 79 data, err := json.MarshalIndent(out, "", " ") 80 if err != nil { 81 fmt.Fprintf(os.Stderr, "review-go: %v\n", err) 82 os.Exit(1) 83 } 84 os.Stdout.Write(data) 85 } 86 87 // readFile parses one file and lists everything in it a reviewer asks 88 // about. Every declaration is reported, tests and locals included; what to 89 // keep is the caller's decision. 90 func readFile(name string, source []byte) File { 91 result := File{Name: name, Imports: []string{}, Decls: []Decl{}, Comments: []Comment{}} 92 fset := token.NewFileSet() 93 file, err := parser.ParseFile(fset, name, source, parser.ParseComments) 94 if err != nil { 95 result.Error = err.Error() 96 return result 97 } 98 result.Package = file.Name.Name 99 lines := strings.Split(string(source), "\n") 100 line := func(pos token.Pos) int { return fset.Position(pos).Line } 101 textAt := func(pos token.Pos) string { 102 n := line(pos) 103 if n < 1 || n > len(lines) { 104 return "" 105 } 106 return strings.TrimSpace(lines[n-1]) 107 } 108 for _, imported := range file.Imports { 109 result.Imports = append(result.Imports, importName(imported)) 110 } 111 112 // A declaration is local when it sits inside a function body. Inspect 113 // gives no leaving event, so the bodies still open are kept by their 114 // end, and closed as soon as a node starts past it. 115 var open []token.Pos 116 ast.Inspect(file, func(n ast.Node) bool { 117 if n == nil { 118 return false 119 } 120 for len(open) > 0 && n.Pos() >= open[len(open)-1] { 121 open = open[:len(open)-1] 122 } 123 switch d := n.(type) { 124 case *ast.FuncDecl: 125 name := d.Name.Name 126 result.Decls = append(result.Decls, Decl{ 127 Name: name, Kind: "func", Line: line(d.Pos()), EndLine: line(d.End()), 128 Exported: ast.IsExported(name), Test: isTest(name), 129 Text: textAt(d.Pos()), Doc: doc(d.Doc), 130 }) 131 if d.Body != nil { 132 open = append(open, d.Body.End()) 133 } 134 case *ast.FuncLit: 135 open = append(open, d.Body.End()) 136 case *ast.GenDecl: 137 result.Decls = append(result.Decls, specs(d, len(open) > 0, line, textAt)...) 138 } 139 return true 140 }) 141 142 for _, group := range file.Comments { 143 for _, comment := range group.List { 144 result.Comments = append(result.Comments, Comment{ 145 Line: line(comment.Pos()), 146 Text: strings.TrimSpace(strings.TrimPrefix(comment.Text, "//")), 147 }) 148 } 149 } 150 return result 151 } 152 153 // specs lists what a const, var or type declaration declares. A struct's 154 // fields are declarations too, and a field is where a restated fact often 155 // sits. 156 func specs(d *ast.GenDecl, local bool, line func(token.Pos) int, textAt func(token.Pos) string) []Decl { 157 var decls []Decl 158 for _, spec := range d.Specs { 159 switch s := spec.(type) { 160 case *ast.TypeSpec: 161 decls = append(decls, Decl{ 162 Name: s.Name.Name, Kind: "type", Line: line(s.Pos()), EndLine: line(s.End()), 163 Exported: ast.IsExported(s.Name.Name), Local: local, 164 Text: textAt(s.Pos()), Doc: docOf(s.Doc, d), 165 }) 166 structure, ok := s.Type.(*ast.StructType) 167 if !ok { 168 continue 169 } 170 for _, field := range structure.Fields.List { 171 for _, ident := range field.Names { 172 decls = append(decls, Decl{ 173 Name: ident.Name, Kind: "field", Line: line(ident.Pos()), EndLine: line(field.End()), 174 Exported: ast.IsExported(ident.Name), Local: local, 175 Text: textAt(ident.Pos()), Doc: doc(field.Doc), 176 }) 177 } 178 } 179 case *ast.ValueSpec: 180 for _, ident := range s.Names { 181 decls = append(decls, Decl{ 182 Name: ident.Name, Kind: kindOf(d.Tok), Line: line(ident.Pos()), EndLine: line(s.End()), 183 Exported: ast.IsExported(ident.Name), Local: local, 184 Text: textAt(ident.Pos()), Doc: docOf(s.Doc, d), 185 }) 186 } 187 } 188 } 189 return decls 190 } 191 192 // importName is the name an import binds in the file: its alias where it 193 // has one, else the last element of its path. 194 // 195 //review:ignore duplicate-body the sidecar is one self-contained package main, and the Go tool keeps its own reading until it reads Go through this one 196 func importName(spec *ast.ImportSpec) string { 197 if spec.Name != nil { 198 return spec.Name.Name 199 } 200 path := strings.Trim(spec.Path.Value, `"`) 201 if i := strings.LastIndex(path, "/"); i >= 0 { 202 path = path[i+1:] 203 } 204 return path 205 } 206 207 // isTest reports whether a function is one the testing package runs. 208 func isTest(name string) bool { 209 return strings.HasPrefix(name, "Test") || strings.HasPrefix(name, "Fuzz") || strings.HasPrefix(name, "Benchmark") 210 } 211 212 func kindOf(tok token.Token) string { 213 switch tok { 214 case token.CONST: 215 return "const" 216 case token.VAR: 217 return "var" 218 } 219 return "value" 220 } 221 222 func doc(group *ast.CommentGroup) string { 223 if group == nil { 224 return "" 225 } 226 return strings.TrimSpace(group.Text()) 227 } 228 229 // docOf is a spec's own doc, or its group's when the group declares only 230 // it: `// Doc` above `type X struct` documents X, not a parenthesis. 231 func docOf(own *ast.CommentGroup, group *ast.GenDecl) string { 232 if text := doc(own); text != "" { 233 return text 234 } 235 if len(group.Specs) == 1 { 236 return doc(group.Doc) 237 } 238 return "" 239 }