review

review patchsets using your default editor
Log | Files | Refs

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 }