analyser_test.odin (12534B)
1 package analyser 2 3 import "core:fmt" 4 import "core:os" 5 import "core:path/filepath" 6 import "core:strings" 7 import "core:testing" 8 import "jm:sh" 9 10 import "../change" 11 import "../finding" 12 import "../tree" 13 14 @(test) 15 parse_go_build_reads_the_errors :: proc(t: ^testing.T) { 16 out := "{\"ImportPath\":\"probe\",\"Action\":\"build-output\",\"Output\":\"# probe\\n\"}\n{\"ImportPath\":\"probe\",\"Action\":\"build-output\",\"Output\":\"./a.go:3:15: undefined: helper\\n\"}\n{\"ImportPath\":\"probe\",\"Action\":\"build-fail\"}\n" 17 got := parse_go_build("/tree", out, context.temp_allocator) 18 testing.expect_value(t, len(got), 1) 19 if len(got) == 1 { 20 testing.expect_value(t, got[0].file, "a.go") 21 testing.expect_value(t, got[0].line, 3) 22 testing.expect(t, got[0].fault) 23 testing.expect_value(t, got[0].message, "undefined: helper") 24 } 25 } 26 27 @(test) 28 parse_go_vet_reads_each_analyser :: proc(t: ^testing.T) { 29 out := "# probe\n{\n\t\"probe\": {\n\t\t\"printf\": [{\"posn\": \"/tree/a.go:6:14\", \"message\": \"wrong type\"}],\n\t\t\"shadow\": [{\"posn\": \"/tree/b.go:9:2\", \"message\": \"declaration of err shadows\"}]\n\t}\n}\n# other\nvet: other/x.go:3: undefined: y\n" 30 got := parse_go_vet("/tree", out, context.temp_allocator) 31 testing.expect_value(t, len(got), 2) 32 by := make(map[string]Diagnostic, context.temp_allocator) 33 for d in got { 34 by[d.code] = d 35 } 36 testing.expect_value(t, by["printf"].file, "a.go") 37 testing.expect_value(t, by["printf"].line, 6) 38 testing.expect_value(t, by["printf"].severity, finding.Severity.Must_Fix) 39 testing.expect_value(t, by["shadow"].severity, finding.Severity.Consider) 40 Case :: struct { 41 name: string, 42 want: finding.Severity, 43 } 44 for c in ([]Case{{"printf", .Must_Fix}, {"nilness", .Must_Fix}, {"shadow", .Consider}, {"unusedwrite", .Consider}, {"rangeint", .Note}, {"stringscut", .Note}}) { 45 testing.expectf(t, vet_severity(c.name) == c.want, "%s: %v", c.name, vet_severity(c.name)) 46 } 47 chunks := split_json_objects( 48 "# a\n{\"x\": {\"y\": \"}\"}}\nprose {not\n{\"z\": 1}\n", 49 context.temp_allocator, 50 ) 51 testing.expect_value(t, len(chunks), 2) 52 if len(chunks) == 2 { 53 testing.expect_value(t, chunks[0], `{"x": {"y": "}"}}`) 54 testing.expect_value(t, chunks[1], `{"z": 1}`) 55 } 56 file, line := split_position("/tree/a.go:6:14") 57 testing.expect_value(t, file, "/tree/a.go") 58 testing.expect_value(t, line, 6) 59 file, line = split_position("a.go:9") 60 testing.expect_value(t, file, "a.go") 61 testing.expect_value(t, line, 9) 62 } 63 64 @(test) 65 parse_odin_tells_vet_from_type_errors :: proc(t: ^testing.T) { 66 out := `{"error_count": 3, "errors": [ 67 {"type": "error", "pos": {"file": "/tree/lib/a.odin", "line": 6}, "msgs": ["'unused' declared but not used"]}, 68 {"type": "warning", "pos": {"file": "/tree/lib/a.odin", "line": 4}, "msgs": ["Syntax Error: With '-strict-style' the attached brace style (1TBS) is enforced"]}, 69 {"type": "error", "pos": {"file": "/tree/lib/a.odin", "line": 9}, "msgs": ["Undeclared name: foo"]} 70 ]}` 71 got := parse_odin("/tree", out, context.temp_allocator) 72 testing.expect_value(t, len(got), 3) 73 if len(got) == 3 { 74 testing.expect_value(t, got[0].code, "vet") 75 testing.expect_value(t, got[0].severity, finding.Severity.Consider) 76 testing.expect(t, !got[0].fault) 77 testing.expect_value(t, got[1].code, "style") 78 testing.expect_value(t, got[1].severity, finding.Severity.Note) 79 testing.expect_value(t, got[2].code, "") 80 testing.expect(t, got[2].fault) 81 testing.expect_value(t, got[2].file, "lib/a.odin") 82 } 83 } 84 85 @(test) 86 parse_tsc_ruff_mypy_cargo_semgrep :: proc(t: ^testing.T) { 87 ts := parse_tsc( 88 "/tree", 89 "/tree/web", 90 "src/a.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.\nnoise\n", 91 context.temp_allocator, 92 ) 93 testing.expect_value(t, len(ts), 1) 94 if len(ts) == 1 { 95 testing.expect_value(t, ts[0].file, "web/src/a.ts") 96 testing.expect_value(t, ts[0].line, 12) 97 testing.expect_value(t, ts[0].code, "TS2322") 98 testing.expect(t, ts[0].fault) 99 } 100 rf, rerr := parse_ruff( 101 "/tree", 102 `[{"code":"F401","message":"os imported but unused","filename":"/tree/a.py","location":{"row":1,"column":8}}, 103 {"code":"E501","message":"Line too long","filename":"/tree/a.py","location":{"row":9,"column":89}}]`, 104 context.temp_allocator, 105 ) 106 testing.expect_value(t, rerr, "") 107 testing.expect_value(t, len(rf), 2) 108 if len(rf) == 2 { 109 testing.expect_value(t, rf[0].severity, finding.Severity.Consider) 110 testing.expect_value(t, rf[0].file, "a.py") 111 testing.expect_value(t, rf[1].severity, finding.Severity.Note) 112 } 113 _, prose := parse_ruff("/tree", "not json", context.temp_allocator) 114 testing.expect(t, prose != "", "prose was read as a report") 115 my := parse_mypy( 116 "/tree", 117 `{"file": "a.py", "line": 4, "column": 4, "message": "Incompatible return value type", "hint": null, "code": "return-value", "severity": "error"} 118 {"file": "a.py", "line": 4, "column": 4, "message": "See the docs", "hint": null, "code": "return-value", "severity": "note"} 119 `, 120 context.temp_allocator, 121 ) 122 testing.expect_value(t, len(my), 2) 123 if len(my) == 2 { 124 testing.expect_value(t, my[0].severity, finding.Severity.Must_Fix) 125 testing.expect_value(t, my[0].code, "return-value") 126 testing.expect_value(t, my[1].severity, finding.Severity.Note) 127 } 128 cg := parse_cargo( 129 "/tree", 130 "/tree/crate", 131 `{"reason":"compiler-artifact","target":{}} 132 {"reason":"compiler-message","message":{"level":"error","message":"cannot find value x","code":{"code":"E0425"},"spans":[{"file_name":"src/main.rs","line_start":3,"is_primary":false},{"file_name":"src/main.rs","line_start":4,"is_primary":true}]}} 133 {"reason":"compiler-message","message":{"level":"warning","message":"unused variable","code":{"code":"unused_variables"},"spans":[{"file_name":"src/lib.rs","line_start":7,"is_primary":true}]}} 134 {"reason":"compiler-message","message":{"level":"note","message":"aborting","code":null,"spans":[]}} 135 `, 136 context.temp_allocator, 137 ) 138 testing.expect_value(t, len(cg), 2) 139 if len(cg) == 2 { 140 testing.expect_value(t, cg[0].file, "crate/src/main.rs") 141 testing.expect_value(t, cg[0].line, 4) 142 testing.expect_value(t, cg[0].code, "E0425") 143 testing.expect(t, cg[0].fault) 144 testing.expect_value(t, cg[1].severity, finding.Severity.Consider) 145 testing.expect(t, !cg[1].fault) 146 } 147 sg, serr := parse_semgrep( 148 "/tree", 149 `{"results":[ 150 {"check_id":"go.lang.security.audit.crypto.math_random","path":"a.go","start":{"line":12},"extra":{"message":"math/rand is not secure\n","severity":"WARNING"}}, 151 {"check_id":"python.lang.best-practice.open-never-closed","path":"/tree/b.py","start":{"line":3},"extra":{"message":"file never closed","severity":"ERROR"}}, 152 {"check_id":"generic.note","path":"c.js","start":{"line":1},"extra":{"message":"fyi","severity":"INFO"}} 153 ],"errors":[]}`, 154 context.temp_allocator, 155 ) 156 testing.expect_value(t, serr, "") 157 testing.expect_value(t, len(sg), 3) 158 if len(sg) == 3 { 159 testing.expect_value(t, sg[0].severity, finding.Severity.Consider) 160 testing.expect_value(t, sg[0].message, "math/rand is not secure") 161 testing.expect_value(t, sg[1].severity, finding.Severity.Must_Fix) 162 testing.expect_value(t, sg[1].file, "b.py") 163 testing.expect_value(t, sg[2].severity, finding.Severity.Note) 164 } 165 sc := parse_staticcheck( 166 "/tree", 167 `{"code":"SA4006","location":{"file":"/tree/a.go","line":5},"message":"never used"} 168 {"code":"compile","location":{"file":"/tree/a.go","line":1},"message":"broken"} 169 {"code":"ST1000","location":{"file":"/tree/a.go","line":1},"message":"style"} 170 `, 171 context.temp_allocator, 172 ) 173 testing.expect_value(t, len(sc), 2) 174 if len(sc) == 2 { 175 testing.expect_value(t, sc[0].severity, finding.Severity.Must_Fix) 176 testing.expect_value(t, sc[1].severity, finding.Severity.Note) 177 } 178 testing.expect_value(t, staticcheck_severity("S1002"), finding.Severity.Consider) 179 testing.expect_value(t, staticcheck_severity("U1000"), finding.Severity.Consider) 180 } 181 182 @(test) 183 a_run_past_the_timeout_is_not_waited_for :: proc(t: ^testing.T) { 184 saved := timeout_seconds_override 185 timeout_seconds_override = 1 186 defer timeout_seconds_override = saved 187 _, _, err := execute_both(".", "sleep", {"3"}, context.temp_allocator) 188 testing.expect(t, strings.contains(err, "not waited for"), err) 189 out, _, ok_err := execute_both(".", "echo", {"quick"}, context.temp_allocator) 190 testing.expect_value(t, ok_err, "") 191 testing.expect_value(t, out, "quick\n") 192 } 193 194 @(test) 195 collections_resolve_against_the_repository :: proc(t: ^testing.T) { 196 temp := os.temp_directory(context.temp_allocator) or_else "" 197 scratch, err := os.make_directory_temp(temp, "review-ols-*", context.temp_allocator) 198 testing.expect(t, err == nil) 199 defer os.remove_all(scratch) 200 testing.expect( 201 t, 202 os.write_entire_file( 203 join(scratch, "ols.json"), 204 transmute([]byte)string( 205 `{"collections":[{"name":"jm","path":"../jm"},{"name":"abs","path":"/opt/abs"},{"name":""}]}`, 206 ), 207 ) == 208 nil, 209 ) 210 got := collections(scratch, "/repo", context.temp_allocator) 211 testing.expect_value( 212 t, 213 fmt.tprint(got), 214 `["-collection:jm=/jm", "-collection:abs=/opt/abs"]`, 215 ) 216 testing.expect_value(t, len(collections("/nowhere", "/repo", context.temp_allocator)), 0) 217 } 218 219 @(test) 220 nearest_walks_up :: proc(t: ^testing.T) { 221 temp := os.temp_directory(context.temp_allocator) or_else "" 222 root, err := os.make_directory_temp(temp, "review-analyser-*", context.temp_allocator) 223 testing.expect(t, err == nil) 224 defer os.remove_all(root) 225 testing.expect(t, os.make_directory_all(join(root, "web/src/deep")) == nil) 226 testing.expect( 227 t, 228 os.write_entire_file(join(root, "web/tsconfig.json"), transmute([]byte)string("{}")) == 229 nil, 230 ) 231 testing.expect_value(t, nearest(root, "web/src/deep", "tsconfig.json"), "web/tsconfig.json") 232 testing.expect_value(t, nearest(root, "web/src/deep", "Cargo.toml"), "") 233 testing.expect_value(t, relative("/tree", "/tree/a/b.go"), "a/b.go") 234 testing.expect_value(t, relative("/tree", "./a.go"), "a.go") 235 testing.expect_value(t, relative("/tree", "/elsewhere/a.go"), "/elsewhere/a.go") 236 testing.expect_value(t, dir_of("a.go"), ".") 237 testing.expect_value(t, dir_of("x/y/a.go"), "x/y") 238 239 for path in ([]string{"a", "sidecar/govet", "sidecar/gofront"}) { 240 testing.expect(t, os.make_directory_all(join(root, path)) == nil) 241 } 242 for path in ([]string{"go.mod", "sidecar/govet/go.mod"}) { 243 testing.expect( 244 t, 245 os.write_entire_file(join(root, path), transmute([]byte)string("module x")) == nil, 246 ) 247 } 248 pkgs := go_packages( 249 root, 250 {"a/a.go", "sidecar/govet/main.go", "sidecar/gofront/main.go", "vendor/x/x.go"}, 251 ) 252 testing.expect_value(t, fmt.tprint(pkgs), `["./a", "./sidecar/gofront"]`) 253 } 254 255 // go_fixture is a module with one package: a printf fault on line 6 that 256 // vet sees, and nothing the compiler minds. 257 go_fixture :: proc(t: ^testing.T) -> (root: string, ok: bool) { 258 temp := os.temp_directory(context.temp_allocator) or_else "" 259 scratch, err := os.make_directory_temp(temp, "review-govet-*", context.temp_allocator) 260 if err != nil { 261 testing.fail_now(t, "no scratch directory") 262 } 263 root = scratch 264 git := proc(root: string, args: ..string) -> bool { 265 argv := make([dynamic]string, context.temp_allocator) 266 append( 267 &argv, 268 "git", 269 "-c", 270 "user.email=t@t", 271 "-c", 272 "user.name=t", 273 "-c", 274 "commit.gpgsign=false", 275 ) 276 append(&argv, ..args) 277 return sh.exec(argv[:], {dir = root}, context.temp_allocator).ok 278 } 279 ok = git(root, "init", "-q") 280 ok &&= 281 os.write_entire_file( 282 join(root, "go.mod"), 283 transmute([]byte)string("module probe\n\ngo 1.27.0\n"), 284 ) == 285 nil 286 ok &&= 287 os.write_entire_file( 288 join(root, "a.go"), 289 transmute([]byte)string( 290 "package probe\n\nimport \"fmt\"\n\nfunc F() {\n\tfmt.Printf(\"%d\", \"s\")\n}\n", 291 ), 292 ) == 293 nil 294 ok &&= git(root, "add", "go.mod", "a.go") 295 return root, ok 296 } 297 298 @(test) 299 go_vet_reads_the_change :: proc(t: ^testing.T) { 300 if !on_path("go") { 301 testing.fail_now(t, "go is not on the path") 302 } 303 root, made := go_fixture(t) 304 testing.expect(t, made) 305 defer os.remove_all(root) 306 c, gathered := change.gather("", root, context.temp_allocator) 307 testing.expect(t, gathered) 308 tr, at_ok := tree.at(root, "", context.temp_allocator) 309 testing.expect(t, at_ok) 310 findings := check(&c, tr, false, {go_build, go_vet}, context.temp_allocator) 311 testing.expect_value(t, len(findings), 1) 312 if len(findings) == 1 { 313 testing.expect_value(t, findings[0].rule, "go-vet/printf") 314 testing.expect_value(t, findings[0].file, "a.go") 315 testing.expect_value(t, findings[0].line, 6) 316 testing.expect_value(t, findings[0].severity, finding.Severity.Must_Fix) 317 testing.expect(t, findings[0].verified) 318 } 319 _ = fmt.tprint(filepath.SEPARATOR) 320 }