review

review patchsets using your default editor
Log | Files | Refs

frontend.odin (4083B)


      1 /*
      2 Package frontend reads source files through the language sidecars this
      3 repository ships: review-go for Go and odin-review-extract for Odin. Each
      4 sidecar is that language's own parser printing one JSON answer, so the
      5 reviewer here does not own a grammar. The answer's shape is shared; a field
      6 one language lacks stays zero.
      7 */
      8 package frontend
      9 
     10 import "core:encoding/json"
     11 import "core:strings"
     12 import "jm:sh"
     13 
     14 // Decl is one declaration as a sidecar reports it. Kinds are func, type,
     15 // field, const, var and value. A test is what the language's test runner
     16 // runs. A local declaration sits inside a function body.
     17 Decl :: struct {
     18 	name:     string,
     19 	kind:     string,
     20 	line:     int,
     21 	end_line: int,
     22 	exported: bool,
     23 	test:     bool,
     24 	local:    bool,
     25 	text:     string,
     26 	doc:      string,
     27 	// body is the declaration's whole text where the reader has it as
     28 	// one match; empty where the caller reads it from the file by lines.
     29 	body:     string,
     30 }
     31 
     32 // Comment is one comment, located by its first line, marker stripped.
     33 Comment :: struct {
     34 	line: int,
     35 	text: string,
     36 }
     37 
     38 // File is what one source file declares. An error is a file the sidecar
     39 // could not parse; its lists are then empty rather than missing.
     40 File :: struct {
     41 	name:     string,
     42 	pkg:      string `json:"package"`,
     43 	imports:  []string,
     44 	decls:    []Decl,
     45 	comments: []Comment,
     46 	error:    string,
     47 }
     48 
     49 Output :: struct {
     50 	files: []File,
     51 }
     52 
     53 // Sidecar names the parser a language is read through: a program built
     54 // on the language's own parser for Go and Odin, and ast-grep's grammars
     55 // for the rest.
     56 Sidecar :: enum {
     57 	Go,
     58 	Odin,
     59 	Script,
     60 	Python,
     61 	Rust,
     62 }
     63 
     64 // Scan_Error says why a scan returned nothing: the sidecar is not on the
     65 // path, it exited without an answer, or its answer was not the JSON expected.
     66 Scan_Error :: enum {
     67 	None,
     68 	Not_Installed,
     69 	Failed,
     70 	Unreadable,
     71 }
     72 
     73 // binary is the executable a sidecar is found on the path as.
     74 binary :: proc(sidecar: Sidecar) -> string {
     75 	switch sidecar {
     76 	case .Go:
     77 		return "review-go"
     78 	case .Odin:
     79 		return "odin-review-extract"
     80 	case .Script, .Python, .Rust:
     81 		return "ast-grep"
     82 	}
     83 	return ""
     84 }
     85 
     86 // sidecar_for is the sidecar that reads a path, by its extension. The
     87 // ast-grep languages are covered only while ast-grep is on the path; their
     88 // files fall to the comment reader otherwise.
     89 sidecar_for :: proc(path: string) -> (sidecar: Sidecar, covered: bool) {
     90 	switch {
     91 	case strings.has_suffix(path, ".go"):
     92 		return .Go, true
     93 	case strings.has_suffix(path, ".odin"):
     94 		return .Odin, true
     95 	case grammar_of(path) != "":
     96 		return .Script, installed(.Script)
     97 	case strings.has_suffix(path, ".py"):
     98 		return .Python, installed(.Python)
     99 	case strings.has_suffix(path, ".rs"):
    100 		return .Rust, installed(.Rust)
    101 	}
    102 	return .Go, false
    103 }
    104 
    105 // installed reports whether a sidecar can be run.
    106 installed :: proc(sidecar: Sidecar) -> bool {
    107 	_, found := sh.which(binary(sidecar), context.temp_allocator)
    108 	return found
    109 }
    110 
    111 // scan asks a sidecar for what the files declare. The files are paths the
    112 // sidecar can read; the answer names each by the path given. Everything
    113 // the answer holds is allocated from allocator.
    114 scan :: proc(
    115 	sidecar: Sidecar,
    116 	files: []string,
    117 	allocator := context.allocator,
    118 ) -> (
    119 	out: Output,
    120 	err: Scan_Error,
    121 ) {
    122 	if len(files) == 0 {
    123 		return
    124 	}
    125 	if !installed(sidecar) {
    126 		return out, .Not_Installed
    127 	}
    128 	if sidecar == .Script || sidecar == .Python || sidecar == .Rust {
    129 		return grep_scan(sidecar, files, allocator)
    130 	}
    131 	argv := make([]string, len(files) + 1, context.temp_allocator)
    132 	argv[0] = binary(sidecar)
    133 	copy(argv[1:], files)
    134 	r := sh.exec(argv, allocator = allocator)
    135 	if !r.ok && len(r.stdout) == 0 {
    136 		return out, .Failed
    137 	}
    138 	if e := json.unmarshal_string(r.stdout, &out, allocator = allocator); e != nil {
    139 		return out, .Unreadable
    140 	}
    141 	return out, .None
    142 }
    143 
    144 // tests are the declarations a file's test runner would run.
    145 tests :: proc(file: File, allocator := context.allocator) -> []Decl {
    146 	found := make([dynamic]Decl, allocator)
    147 	for decl in file.decls {
    148 		if decl.test {
    149 			append(&found, decl)
    150 		}
    151 	}
    152 	return found[:]
    153 }