odin.odin (4209B)
1 package analyser 2 3 import "core:encoding/json" 4 import "core:os" 5 import "core:strings" 6 7 import "../txt" 8 9 // odin_check type-checks each package the change touched with every vet 10 // switch and the compiler's own style, and reads its JSON errors. The 11 // compiler writes its JSON to stderr and exits non-zero when it has 12 // errors, which is the answer, not a failure. 13 odin_check := Analyser { 14 name = "odin-check", 15 covers = proc(path: string) -> bool {return strings.has_suffix(path, ".odin")}, 16 ready = proc(tree_dir: string) -> (bool, string) {return on_path("odin"), ""}, 17 run = proc(tree_dir, root: string, files: []string) -> ([]Diagnostic, string) { 18 dirs := make(map[string]bool, context.temp_allocator) 19 for f in files { 20 dirs[dir_of(f)] = true 21 } 22 found := make([dynamic]Diagnostic) 23 for dir in sorted(dirs) { 24 args := make([dynamic]string, context.temp_allocator) 25 append(&args, "check", dir, "-vet", "-strict-style", "-json-errors", "-no-entry-point") 26 append(&args, ..collections(tree_dir, root)) 27 stdout, stderr, err := execute_both(tree_dir, "odin", args[:], context.temp_allocator) 28 out := strings.concatenate({stdout, stderr}, context.temp_allocator) 29 if err != "" && !strings.contains(out, "error_count") { 30 return found[:], strings.concatenate({"odin: ", tail(out, 200)}) 31 } 32 append(&found, ..parse_odin(tree_dir, out, context.temp_allocator)) 33 } 34 return found[:], "" 35 }, 36 } 37 38 // collections are the -collection flags the ols.json at the end of the 39 // change declares, which is where an Odin project names the collections 40 // its imports resolve through; a relative path is relative to the 41 // repository the change is in. 42 collections :: proc(tree_dir, root: string, allocator := context.temp_allocator) -> []string { 43 Config :: struct { 44 collections: []struct { 45 name: string `json:"name"`, 46 path: string `json:"path"`, 47 } `json:"collections"`, 48 } 49 data, err := os.read_entire_file_from_path(join(tree_dir, "ols.json"), context.temp_allocator) 50 if err != nil { 51 return nil 52 } 53 config: Config 54 if json.unmarshal(data, &config, allocator = context.temp_allocator) != nil { 55 return nil 56 } 57 flags := make([dynamic]string, allocator) 58 for c in config.collections { 59 if c.name == "" || c.path == "" { 60 continue 61 } 62 // A relative path is relative to the repository, not to the 63 // scratch copy a range is read from, which has no neighbours. 64 path := c.path 65 if !strings.has_prefix(path, "/") { 66 path = join(root, path, allocator) 67 } 68 append(&flags, strings.concatenate({"-collection:", c.name, "=", path}, allocator)) 69 } 70 return flags[:] 71 } 72 73 // Odin_Report is the compiler's -json-errors. 74 Odin_Report :: struct { 75 errors: []struct { 76 type: string `json:"type"`, 77 pos: struct { 78 file: string `json:"file"`, 79 line: int `json:"line"`, 80 } `json:"pos"`, 81 msgs: []string `json:"msgs"`, 82 } `json:"errors"`, 83 } 84 85 // parse_odin reads the compiler's -json-errors: a type error is a fault, 86 // a vet failure the vet's opinion, a style failure a note. The compiler 87 // prints its own prose before the JSON when it cannot even start; the 88 // object is taken from wherever it sits. 89 parse_odin :: proc(tree_dir, out: string, allocator := context.allocator) -> []Diagnostic { 90 found := make([dynamic]Diagnostic, allocator) 91 raw, ok := txt.object(out) 92 if !ok { 93 return found[:] 94 } 95 report: Odin_Report 96 if json.unmarshal_string(raw, &report, allocator = context.temp_allocator) != nil { 97 return found[:] 98 } 99 for e in report.errors { 100 message := strings.join(e.msgs, "; ", allocator) 101 d := Diagnostic { 102 file = strings.clone(relative(tree_dir, e.pos.file), allocator), 103 line = e.pos.line, 104 message = message, 105 severity = .Must_Fix, 106 fault = true, 107 } 108 switch { 109 case strings.contains(message, "-strict-style"): 110 d.code, d.severity, d.fault = "style", .Note, false 111 case e.type == "warning": 112 d.severity, d.fault = .Consider, false 113 case strings.contains(message, "declared but not used"), 114 strings.contains(message, "shadow"): 115 // A vet failure fails the build under -vet, but it is the 116 // vet's opinion, not a type error. 117 d.code, d.severity, d.fault = "vet", .Consider, false 118 } 119 append(&found, d) 120 } 121 return found[:] 122 }