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: add E830 inlined-algorithm detection for interop generators

Completes the E8xx interop testing rule series. E830 flags generator
scripts that define encode/decode/compute functions locally instead of
calling the upstream oracle's public API — the core principle of interop
testing.

+180
+49
docs/index.html
··· 1588 1588 </div> 1589 1589 </div></div> 1590 1590 </div> 1591 + <div class="error-card" id="E800"> 1592 + <div> 1593 + <span class="error-code">E800</span> 1594 + <span class="error-title">Missing generate.sh</span> 1595 + </div> 1596 + <div class="error-hint"><p>Every interop test must have scripts/generate.sh as the single entry point for trace regeneration via `dune build @regen-traces`.</p></div> 1597 + </div> 1598 + <div class="error-card" id="E801"> 1599 + <div> 1600 + <span class="error-code">E801</span> 1601 + <span class="error-title">Interop dir named after language</span> 1602 + </div> 1603 + <div class="error-hint"><p>Interop test directories should be named after the oracle tool (e.g. spacepackets, dariol83, crcmod), not the language (e.g. python, go). This makes it clear which external implementation is the reference.</p></div> 1604 + </div> 1605 + <div class="error-card" id="E805"> 1606 + <div> 1607 + <span class="error-code">E805</span> 1608 + <span class="error-title">Missing requirements.txt</span> 1609 + </div> 1610 + <div class="error-hint"><p>Python oracles must pin dependencies in requirements.txt with exact versions (e.g. crcmod==1.7). This ensures reproducible trace generation without depending on local installs.</p></div> 1611 + </div> 1612 + <div class="error-card" id="E810"> 1613 + <div> 1614 + <span class="error-code">E810</span> 1615 + <span class="error-title">Missing regen-traces alias</span> 1616 + </div> 1617 + <div class="error-hint"><p>Every interop test dune file must define a regen-traces alias as the single trigger for refreshing traces: `(rule (alias regen-traces) ...)`.</p></div> 1618 + </div> 1619 + <div class="error-card" id="E815"> 1620 + <div> 1621 + <span class="error-code">E815</span> 1622 + <span class="error-title">REGEN_TRACES sentinel in dune</span> 1623 + </div> 1624 + <div class="error-hint"><p>The regen-traces alias should be the single entry point — no REGEN_TRACES=1 env var sentinel. Remove the (enabled_if ...) guard so `dune build @regen-traces` works directly.</p></div> 1625 + </div> 1626 + <div class="error-card" id="E820"> 1627 + <div> 1628 + <span class="error-code">E820</span> 1629 + <span class="error-title">Hand-rolled CSV parsing</span> 1630 + </div> 1631 + <div class="error-hint"><p>Use csvt (Csvt.decode_file with a Csvt.Row codec) for CSV trace parsing. Never hand-roll CSV readers with open_in/input_line/split_on_char.</p></div> 1632 + </div> 1633 + <div class="error-card" id="E830"> 1634 + <div> 1635 + <span class="error-code">E830</span> 1636 + <span class="error-title">Inlined algorithm in generator</span> 1637 + </div> 1638 + <div class="error-hint"><p>The generator MUST call the upstream tool's public API. Never reimplement the algorithm being verified — this defeats the purpose of interop testing. If the public API doesn't expose what you need, drop the test rather than inlining.</p></div> 1639 + </div> 1591 1640 1592 1641 <a href="#top" class="back-to-top">↑ Top</a> 1593 1642 </body>
+7
lib/data.ml
··· 57 57 E724.rule; 58 58 E725.rule; 59 59 E726.rule; 60 + E800.rule; 61 + E801.rule; 62 + E805.rule; 63 + E810.rule; 64 + E815.rule; 65 + E820.rule; 66 + E830.rule; 60 67 ]
+124
lib/rules/e830.ml
··· 1 + (** E830: Inlined algorithm in interop generator *) 2 + 3 + type payload = { dir : string; file : string; reason : string } 4 + 5 + (* Heuristic: a generator that defines functions with algorithmic names 6 + (encode, decode, compute, calculate, process, transform, convert) 7 + is likely reimplementing the algorithm instead of calling the oracle. *) 8 + let suspicious_defs = 9 + [ 10 + (* Python: def <name>( *) 11 + (fun line -> 12 + let l = String.trim line in 13 + String.length l > 4 14 + && String.sub l 0 4 = "def " 15 + && 16 + let name = 17 + try 18 + let i = String.index l '(' in 19 + String.sub l 4 (i - 4) |> String.trim 20 + with Not_found -> "" 21 + in 22 + List.exists 23 + (fun prefix -> 24 + Astring.String.is_prefix ~affix:prefix (String.lowercase_ascii name)) 25 + [ "encode"; "decode"; "compute"; "calculate"; "process"; "transform" ] 26 + && 27 + (* Exclude test/verification helpers *) 28 + not 29 + (Astring.String.is_prefix ~affix:"test" (String.lowercase_ascii name) 30 + || Astring.String.is_prefix ~affix:"verify" 31 + (String.lowercase_ascii name))); 32 + (* Rust: fn <name>( or pub fn <name>( *) 33 + (fun line -> 34 + let l = String.trim line in 35 + let after_fn = 36 + if Astring.String.is_prefix ~affix:"fn " l then 37 + Some (String.sub l 3 (String.length l - 3)) 38 + else if Astring.String.is_prefix ~affix:"pub fn " l then 39 + Some (String.sub l 7 (String.length l - 7)) 40 + else None 41 + in 42 + match after_fn with 43 + | None -> false 44 + | Some rest -> 45 + let name = 46 + try 47 + let i = String.index rest '(' in 48 + String.sub rest 0 i |> String.trim 49 + with Not_found -> "" 50 + in 51 + List.exists 52 + (fun prefix -> Astring.String.is_prefix ~affix:prefix name) 53 + [ "encode"; "decode"; "compute"; "calculate"; "process" ]); 54 + ] 55 + 56 + let scan_file path = 57 + try 58 + let ic = open_in path in 59 + let found = ref false in 60 + (try 61 + while true do 62 + let line = input_line ic in 63 + if List.exists (fun check -> check line) suspicious_defs then 64 + found := true 65 + done 66 + with End_of_file -> ()); 67 + close_in ic; 68 + !found 69 + with Sys_error _ -> false 70 + 71 + let check (ctx : Context.project) = 72 + let dirs = Interop.find_oracle_dirs ctx.project_root in 73 + List.filter_map 74 + (fun (d : Interop.oracle_dir) -> 75 + let scripts = Filename.concat d.path "scripts" in 76 + if not (Sys.file_exists scripts) then None 77 + else 78 + let files = 79 + try Sys.readdir scripts |> Array.to_list with Sys_error _ -> [] 80 + in 81 + let generator_files = 82 + List.filter 83 + (fun f -> 84 + let f = String.lowercase_ascii f in 85 + Astring.String.is_prefix ~affix:"generate" f 86 + && (Filename.check_suffix f ".py" 87 + || Filename.check_suffix f ".rs" 88 + || Filename.check_suffix f ".go")) 89 + files 90 + in 91 + let inlined = 92 + List.filter_map 93 + (fun f -> 94 + let path = Filename.concat scripts f in 95 + if scan_file path then Some f else None) 96 + generator_files 97 + in 98 + match inlined with 99 + | [] -> None 100 + | file :: _ -> 101 + Some 102 + (Issue.v 103 + { 104 + dir = d.path; 105 + file; 106 + reason = 107 + "defines encode/decode/compute functions — may be \ 108 + reimplementing the algorithm instead of calling the \ 109 + oracle's API"; 110 + })) 111 + dirs 112 + 113 + let pp ppf { dir; file; reason } = 114 + Fmt.pf ppf "Interop generator %s/scripts/%s: %s" dir file reason 115 + 116 + let rule = 117 + Rule.v ~code:"E830" ~title:"Inlined algorithm in generator" 118 + ~category:Interop_testing 119 + ~hint: 120 + "The generator MUST call the upstream tool's public API. Never \ 121 + reimplement the algorithm being verified — this defeats the purpose of \ 122 + interop testing. If the public API doesn't expose what you need, drop \ 123 + the test rather than inlining." 124 + ~examples:[] ~pp (Project check)