Opinionated OCaml linter with Merlin integration for code quality, naming conventions, and style checks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

merlint: complete E8xx interop testing rules (13 rules total)

New rules enforce the /interop-testing skill standards:

Structure:
E800 Missing generate.sh wrapper
E801 Dir named after language instead of oracle tool
E802 Missing committed traces / (source_tree traces) dep
E803 Test shells out to external tool (not replay-only)

Reproducibility:
E805 Python oracle missing requirements.txt
E806 Go oracle missing go.mod
E807 Rust oracle missing Cargo.toml
E835 pip install --break-system-packages (no venv)

Dune integration:
E810 Missing regen-traces alias
E815 REGEN_TRACES env var sentinel

CSV hygiene:
E820 Hand-rolled CSV parsing (use csvt)
E825 CSV traces without csvt dependency in dune

Generator quality:
E830 Inlined algorithm in generator (not calling oracle API)

+230
+41
lib/rules/e802.ml
··· 1 + (** E802: Interop test missing committed traces *) 2 + 3 + type payload = { dir : string; reason : string } 4 + 5 + let check (ctx : Context.project) = 6 + let dirs = Interop.find_oracle_dirs ctx.project_root in 7 + List.filter_map 8 + (fun (d : Interop.oracle_dir) -> 9 + if not d.has_traces then 10 + Some 11 + (Issue.v 12 + { 13 + dir = d.path; 14 + reason = "missing traces/ directory — traces must be committed"; 15 + }) 16 + else if d.has_dune then 17 + let dune = Interop.dune_content d.path in 18 + if not (Astring.String.is_infix ~affix:"source_tree traces" dune) then 19 + Some 20 + (Issue.v 21 + { 22 + dir = d.path; 23 + reason = 24 + "dune test stanza missing (source_tree traces) dep — test \ 25 + must run from committed traces"; 26 + }) 27 + else None 28 + else None) 29 + dirs 30 + 31 + let pp ppf { dir; reason } = Fmt.pf ppf "Interop test %s: %s" dir reason 32 + 33 + let rule = 34 + Rule.v ~code:"E802" ~title:"Interop test not replay-only" 35 + ~category:Interop_testing 36 + ~hint: 37 + "Traces must be committed to git. The test stanza must depend on \ 38 + (source_tree traces) so tests run from traces alone — the external tool \ 39 + is NOT required at test time. This is the 'generate once, replay \ 40 + always' principle." 41 + ~examples:[] ~pp (Project check)
+45
lib/rules/e803.ml
··· 1 + (** E803: Interop test shells out to external tool *) 2 + 3 + type payload = { dir : string; pattern : string } 4 + 5 + let shell_patterns = 6 + [ 7 + "Sys.command"; 8 + "Unix.system"; 9 + "Unix.create_process"; 10 + "Unix.open_process"; 11 + "Eio.Process"; 12 + "Eio_process"; 13 + ] 14 + 15 + let check (ctx : Context.project) = 16 + let dirs = Interop.find_oracle_dirs ctx.project_root in 17 + List.filter_map 18 + (fun (d : Interop.oracle_dir) -> 19 + if d.has_test_ml then 20 + let content = Interop.test_content d.path in 21 + let found = 22 + List.find_opt 23 + (fun pat -> Astring.String.is_infix ~affix:pat content) 24 + shell_patterns 25 + in 26 + match found with 27 + | Some pattern -> Some (Issue.v { dir = d.path; pattern }) 28 + | None -> None 29 + else None) 30 + dirs 31 + 32 + let pp ppf { dir; pattern } = 33 + Fmt.pf ppf 34 + "Interop test %s/test.ml calls %s — test must run from traces alone" dir 35 + pattern 36 + 37 + let rule = 38 + Rule.v ~code:"E803" ~title:"Interop test requires external tool" 39 + ~category:Interop_testing 40 + ~hint: 41 + "Interop tests must run from committed traces without needing the \ 42 + external tool at test time. The test.ml should only read trace files, \ 43 + never shell out to run the oracle. If you need the oracle, put it in \ 44 + the generator script." 45 + ~examples:[] ~pp (Project check)
+28
lib/rules/e806.ml
··· 1 + (** E806: Go oracle missing go.mod *) 2 + 3 + type payload = { dir : string } 4 + 5 + let check (ctx : Context.project) = 6 + let dirs = Interop.find_oracle_dirs ctx.project_root in 7 + List.filter_map 8 + (fun (d : Interop.oracle_dir) -> 9 + let scripts = Filename.concat d.path "scripts" in 10 + let has_go = 11 + try 12 + Sys.readdir scripts |> Array.to_list 13 + |> List.exists (fun f -> Filename.check_suffix f ".go") 14 + with Sys_error _ -> false 15 + in 16 + let has_go_mod = Sys.file_exists (Filename.concat scripts "go.mod") in 17 + if has_go && not has_go_mod then Some (Issue.v { dir = d.path }) else None) 18 + dirs 19 + 20 + let pp ppf { dir } = Fmt.pf ppf "Go oracle %s/scripts/ missing go.mod" dir 21 + 22 + let rule = 23 + Rule.v ~code:"E806" ~title:"Missing go.mod" ~category:Interop_testing 24 + ~hint: 25 + "Go oracles must pin the upstream module in go.mod with a tagged version \ 26 + or pseudo-version. This ensures reproducible trace generation without \ 27 + depending on $GOPATH or local clones." 28 + ~examples:[] ~pp (Project check)
+37
lib/rules/e807.ml
··· 1 + (** E807: Rust oracle missing Cargo.toml *) 2 + 3 + type payload = { dir : string } 4 + 5 + let check (ctx : Context.project) = 6 + let dirs = Interop.find_oracle_dirs ctx.project_root in 7 + List.filter_map 8 + (fun (d : Interop.oracle_dir) -> 9 + let scripts = Filename.concat d.path "scripts" in 10 + let has_rust = 11 + try 12 + let files = Sys.readdir scripts |> Array.to_list in 13 + List.exists (fun f -> Filename.check_suffix f ".rs") files 14 + || 15 + let src = Filename.concat scripts "src" in 16 + Sys.file_exists src && Sys.is_directory src 17 + && 18 + try 19 + Sys.readdir src |> Array.to_list 20 + |> List.exists (fun f -> Filename.check_suffix f ".rs") 21 + with Sys_error _ -> false 22 + with Sys_error _ -> false 23 + in 24 + let has_cargo = Sys.file_exists (Filename.concat scripts "Cargo.toml") in 25 + if has_rust && not has_cargo then Some (Issue.v { dir = d.path }) 26 + else None) 27 + dirs 28 + 29 + let pp ppf { dir } = Fmt.pf ppf "Rust oracle %s/scripts/ missing Cargo.toml" dir 30 + 31 + let rule = 32 + Rule.v ~code:"E807" ~title:"Missing Cargo.toml" ~category:Interop_testing 33 + ~hint: 34 + "Rust oracles must pin the upstream crate in Cargo.toml with a tagged \ 35 + version or git rev. This ensures reproducible trace generation without \ 36 + depending on local checkouts." 37 + ~examples:[] ~pp (Project check)
+35
lib/rules/e825.ml
··· 1 + (** E825: Interop test uses CSV traces but dune lacks csvt dependency *) 2 + 3 + type payload = { dir : string } 4 + 5 + let check (ctx : Context.project) = 6 + let dirs = Interop.find_oracle_dirs ctx.project_root in 7 + List.filter_map 8 + (fun (d : Interop.oracle_dir) -> 9 + if d.has_traces && d.has_dune then 10 + let traces = Filename.concat d.path "traces" in 11 + let has_csv = 12 + try 13 + Sys.readdir traces |> Array.to_list 14 + |> List.exists (fun f -> Filename.check_suffix f ".csv") 15 + with Sys_error _ -> false 16 + in 17 + if has_csv then 18 + let dune = Interop.dune_content d.path in 19 + if not (Astring.String.is_infix ~affix:"csvt" dune) then 20 + Some (Issue.v { dir = d.path }) 21 + else None 22 + else None 23 + else None) 24 + dirs 25 + 26 + let pp ppf { dir } = 27 + Fmt.pf ppf "Interop test %s has CSV traces but dune lacks csvt dependency" dir 28 + 29 + let rule = 30 + Rule.v ~code:"E825" ~title:"Missing csvt dependency" ~category:Interop_testing 31 + ~hint: 32 + "Interop tests with CSV traces should use csvt for parsing. Add csvt to \ 33 + the (libraries ...) in the dune file and use Csvt.decode_file with a \ 34 + Row codec." 35 + ~examples:[] ~pp (Project check)
+44
lib/rules/e835.ml
··· 1 + (** E835: pip install --break-system-packages in interop script *) 2 + 3 + type payload = { dir : string; file : string } 4 + 5 + let check (ctx : Context.project) = 6 + let dirs = Interop.find_oracle_dirs ctx.project_root in 7 + List.filter_map 8 + (fun (d : Interop.oracle_dir) -> 9 + let scripts = Filename.concat d.path "scripts" in 10 + if not (Sys.file_exists scripts) then None 11 + else 12 + let files = 13 + try Sys.readdir scripts |> Array.to_list with Sys_error _ -> [] 14 + in 15 + let bad = 16 + List.filter_map 17 + (fun f -> 18 + if Filename.check_suffix f ".sh" || Filename.check_suffix f ".py" 19 + then 20 + let content = Interop.read_file (Filename.concat scripts f) in 21 + if 22 + Astring.String.is_infix ~affix:"--break-system-packages" 23 + content 24 + then Some f 25 + else None 26 + else None) 27 + files 28 + in 29 + match bad with 30 + | [] -> None 31 + | file :: _ -> Some (Issue.v { dir = d.path; file })) 32 + dirs 33 + 34 + let pp ppf { dir; file } = 35 + Fmt.pf ppf "%s/scripts/%s uses --break-system-packages" dir file 36 + 37 + let rule = 38 + Rule.v ~code:"E835" ~title:"pip install --break-system-packages" 39 + ~category:Interop_testing 40 + ~hint: 41 + "Python deps must live in a venv. Never use pip install \ 42 + --break-system-packages. The generate.sh wrapper should create/reuse a \ 43 + venv automatically." 44 + ~examples:[] ~pp (Project check)