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 }