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.

irmin: O(1) child lookup — Hashtbl for named, Array for indexed

+252
+2
lib/rule.ml
··· 8 8 | Documentation 9 9 | Project_structure 10 10 | Testing 11 + | Interop_testing 11 12 12 13 type example = { is_good : bool; code : string } 13 14 ··· 45 46 | Documentation -> "Documentation" 46 47 | Project_structure -> "Project Structure" 47 48 | Testing -> "Test Quality" 49 + | Interop_testing -> "Interop Testing" 48 50 49 51 let is_file_scoped (T desc) = 50 52 match desc.check with File _ -> true | Project _ -> false
+1
lib/rule.mli
··· 9 9 | Documentation 10 10 | Project_structure 11 11 | Testing 12 + | Interop_testing 12 13 13 14 type example = { 14 15 is_good : bool; (** true for good examples, false for bad examples. *)
+25
lib/rules/e800.ml
··· 1 + (** E800: Missing generate.sh in interop test *) 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_scripts then 10 + let generate_sh = Filename.concat d.path "scripts/generate.sh" in 11 + if not (Sys.file_exists generate_sh) then 12 + Some (Issue.v { dir = d.path }) 13 + else None 14 + else None) 15 + dirs 16 + 17 + let pp ppf { dir } = 18 + Fmt.pf ppf "Interop test %s/scripts/ is missing generate.sh" dir 19 + 20 + let rule = 21 + Rule.v ~code:"E800" ~title:"Missing generate.sh" ~category:Interop_testing 22 + ~hint: 23 + "Every interop test must have scripts/generate.sh as the single entry \ 24 + point for trace regeneration via `dune build @regen-traces`." 25 + ~examples:[] ~pp (Project check)
+36
lib/rules/e801.ml
··· 1 + (** E801: Interop test not under test/interop/<tool>/ *) 2 + 3 + type payload = { path : string; reason : string } 4 + 5 + let known_languages = 6 + [ "python"; "java"; "go"; "rust"; "c"; "cpp"; "javascript"; "typescript" ] 7 + 8 + let check (ctx : Context.project) = 9 + let dirs = Interop.find_oracle_dirs ctx.project_root in 10 + List.filter_map 11 + (fun (d : Interop.oracle_dir) -> 12 + (* Check if tool name is a language instead of a tool *) 13 + if List.mem (String.lowercase_ascii d.tool) known_languages then 14 + Some 15 + (Issue.v 16 + { 17 + path = d.path; 18 + reason = 19 + Printf.sprintf 20 + "directory named after language %S, should be named after \ 21 + the oracle tool" 22 + d.tool; 23 + }) 24 + else None) 25 + dirs 26 + 27 + let pp ppf { path; reason } = Fmt.pf ppf "Interop dir %s: %s" path reason 28 + 29 + let rule = 30 + Rule.v ~code:"E801" ~title:"Interop dir named after language" 31 + ~category:Interop_testing 32 + ~hint: 33 + "Interop test directories should be named after the oracle tool (e.g. \ 34 + spacepackets, dariol83, crcmod), not the language (e.g. python, go). \ 35 + This makes it clear which external implementation is the reference." 36 + ~examples:[] ~pp (Project check)
+33
lib/rules/e805.ml
··· 1 + (** E805: Python oracle missing requirements.txt *) 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_python = 11 + try 12 + Sys.readdir scripts |> Array.to_list 13 + |> List.exists (fun f -> Filename.check_suffix f ".py") 14 + with Sys_error _ -> false 15 + in 16 + let has_requirements = 17 + Sys.file_exists (Filename.concat scripts "requirements.txt") 18 + in 19 + if has_python && not has_requirements then Some (Issue.v { dir = d.path }) 20 + else None) 21 + dirs 22 + 23 + let pp ppf { dir } = 24 + Fmt.pf ppf "Python oracle %s/scripts/ missing requirements.txt" dir 25 + 26 + let rule = 27 + Rule.v ~code:"E805" ~title:"Missing requirements.txt" 28 + ~category:Interop_testing 29 + ~hint: 30 + "Python oracles must pin dependencies in requirements.txt with exact \ 31 + versions (e.g. crcmod==1.7). This ensures reproducible trace generation \ 32 + without depending on local installs." 33 + ~examples:[] ~pp (Project check)
+30
lib/rules/e810.ml
··· 1 + (** E810: Missing regen-traces alias in dune *) 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_dune then 10 + let content = Interop.dune_content d.path in 11 + if 12 + not 13 + (Astring.String.is_infix ~affix:"regen-traces" content 14 + || Astring.String.is_infix ~affix:"regen_traces" content) 15 + then Some (Issue.v { dir = d.path }) 16 + else None 17 + else None) 18 + dirs 19 + 20 + let pp ppf { dir } = 21 + Fmt.pf ppf "Interop test %s/dune missing regen-traces alias" dir 22 + 23 + let rule = 24 + Rule.v ~code:"E810" ~title:"Missing regen-traces alias" 25 + ~category:Interop_testing 26 + ~hint: 27 + "Every interop test dune file must define a regen-traces alias as the \ 28 + single trigger for refreshing traces: `(rule (alias regen-traces) \ 29 + ...)`." 30 + ~examples:[] ~pp (Project check)
+27
lib/rules/e815.ml
··· 1 + (** E815: REGEN_TRACES sentinel in dune *) 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_dune then 10 + let content = Interop.dune_content d.path in 11 + if Astring.String.is_infix ~affix:"REGEN_TRACES" content then 12 + Some (Issue.v { dir = d.path }) 13 + else None 14 + else None) 15 + dirs 16 + 17 + let pp ppf { dir } = 18 + Fmt.pf ppf "Interop test %s/dune uses REGEN_TRACES sentinel" dir 19 + 20 + let rule = 21 + Rule.v ~code:"E815" ~title:"REGEN_TRACES sentinel in dune" 22 + ~category:Interop_testing 23 + ~hint: 24 + "The regen-traces alias should be the single entry point — no \ 25 + REGEN_TRACES=1 env var sentinel. Remove the (enabled_if ...) guard so \ 26 + `dune build @regen-traces` works directly." 27 + ~examples:[] ~pp (Project check)
+37
lib/rules/e820.ml
··· 1 + (** E820: Hand-rolled CSV parsing in interop test *) 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_test_ml then 10 + let content = Interop.test_content d.path in 11 + (* Detect the common hand-rolled pattern: open_in + input_line + 12 + split_on_char ',' *) 13 + let has_open_in = Astring.String.is_infix ~affix:"open_in" content in 14 + let has_input_line = 15 + Astring.String.is_infix ~affix:"input_line" content 16 + in 17 + let has_split_comma = 18 + Astring.String.is_infix ~affix:"split_on_char" content 19 + && Astring.String.is_infix ~affix:"','" content 20 + in 21 + if has_open_in && has_input_line && has_split_comma then 22 + Some (Issue.v { dir = d.path }) 23 + else None 24 + else None) 25 + dirs 26 + 27 + let pp ppf { dir } = 28 + Fmt.pf ppf "Interop test %s/test.ml hand-rolls CSV parsing instead of csvt" 29 + dir 30 + 31 + let rule = 32 + Rule.v ~code:"E820" ~title:"Hand-rolled CSV parsing" ~category:Interop_testing 33 + ~hint: 34 + "Use csvt (Csvt.decode_file with a Csvt.Row codec) for CSV trace \ 35 + parsing. Never hand-roll CSV readers with \ 36 + open_in/input_line/split_on_char." 37 + ~examples:[] ~pp (Project check)
+61
lib/rules/interop.ml
··· 1 + (** Shared helpers for E8xx interop testing rules. *) 2 + 3 + type oracle_dir = { 4 + path : string; 5 + package : string; 6 + tool : string; 7 + has_scripts : bool; 8 + has_traces : bool; 9 + has_test_ml : bool; 10 + has_dune : bool; 11 + } 12 + 13 + let find_oracle_dirs project_root = 14 + (* Walk <pkg>/test/interop/<tool>/ directories *) 15 + let results = ref [] in 16 + let try_readdir d = 17 + try Sys.readdir d |> Array.to_list with Sys_error _ -> [] 18 + in 19 + let packages = try_readdir project_root in 20 + List.iter 21 + (fun pkg -> 22 + let pkg_dir = Filename.concat project_root pkg in 23 + if Sys.is_directory pkg_dir && pkg <> "_build" && pkg <> ".git" then 24 + let interop = 25 + Filename.concat (Filename.concat pkg_dir "test") "interop" 26 + in 27 + if Sys.file_exists interop && Sys.is_directory interop then 28 + let tools = try_readdir interop in 29 + List.iter 30 + (fun tool -> 31 + let path = Filename.concat interop tool in 32 + if Sys.is_directory path then 33 + results := 34 + { 35 + path; 36 + package = pkg; 37 + tool; 38 + has_scripts = 39 + Sys.file_exists (Filename.concat path "scripts"); 40 + has_traces = Sys.file_exists (Filename.concat path "traces"); 41 + has_test_ml = 42 + Sys.file_exists (Filename.concat path "test.ml"); 43 + has_dune = Sys.file_exists (Filename.concat path "dune"); 44 + } 45 + :: !results) 46 + tools) 47 + packages; 48 + !results 49 + 50 + let read_file path = 51 + try 52 + let ic = open_in path in 53 + Fun.protect 54 + ~finally:(fun () -> close_in ic) 55 + (fun () -> 56 + let n = in_channel_length ic in 57 + really_input_string ic n) 58 + with Sys_error _ -> "" 59 + 60 + let dune_content dir = read_file (Filename.concat dir "dune") 61 + let test_content dir = read_file (Filename.concat dir "test.ml")