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.

wal, block, sse, zephyr: remove [mutable] from never-reassigned fields

Warning 69 (unused-field, mutable-never-assigned). Four independent
record fields were flagged as mutable but the code only mutates their
referents in place, never rebinds the record slot itself:

- ocaml-wal/lib/wal.ml: [t.file] (the Eio file resource; methods call
Eio.File.pwrite_all etc., the slot is set once at open time).
- ocaml-block/lib/block.ml: [Memory.state.data] (the backing bytes,
written via Bytes.blit_string; [Bytes.t] is already mutable).
- ocaml-sse/lib/sse.ml: [Parser.t.data_buf] (a Buffer.t, written via
Buffer.add_*; the slot never changes).
- ocaml-zephyr/lib/zephyr.ml: drop [mode : Read | Write] entirely —
set at open-time, read nowhere. The open_read / open_write
constructors already distinguish the two call shapes, so mode
tracking was redundant.

+249 -19
+14
docs/index.html
··· 1371 1371 </div> 1372 1372 <div class="error-hint"><p>A dune file with a single library/executable/test stanza doesn't need (modules ...) — dune auto-discovers every .ml in the directory. When multiple stanzas share a directory the (modules ...) fields must together cover every .ml file, otherwise some module is silently dropped. Prefer splitting into sibling directories when the stanza split is a design choice rather than a build requirement.</p></div> 1373 1373 </div> 1374 + <div class="error-card" id="E524"> 1375 + <div> 1376 + <span class="error-code">E524</span> 1377 + <span class="error-title">Multiple Cmdliner subcommands in one file</span> 1378 + </div> 1379 + <div class="error-hint"><p>Each Cmd.v subcommand should live in its own file. Move each Cmd.v into a sibling file (e.g. cmd_&lt;name&gt;.ml exposing a single val cmd) and reference it from main.ml's Cmd.group. Sub-subcommands of a grouped subcommand follow the same rule — use cmd_&lt;parent&gt;/&lt;leaf&gt;.ml or cmd_&lt;parent&gt;_&lt;leaf&gt;.ml siblings.</p></div> 1380 + </div> 1381 + <div class="error-card" id="E525"> 1382 + <div> 1383 + <span class="error-code">E525</span> 1384 + <span class="error-title">Package root dune missing %{dune-warnings}</span> 1385 + </div> 1386 + <div class="error-hint"><p>Create &lt;package&gt;/dune containing (env (dev (flags :standard %{dune-warnings}))), and bump &lt;package&gt;/dune-project to (lang dune 3.21) or newer. This mirrors the workspace-root dune so that a standalone opam build of the package still enforces strict warnings under the dev profile. Reference: alcobar/dune.</p></div> 1387 + </div> 1374 1388 <h2 id="testing">Testing</h2> 1375 1389 <div class="category">E600-E699 • Test coverage and test quality issues</div> 1376 1390 <div class="error-card" id="E600">
+3
dune
··· 1 + (env 2 + (dev 3 + (flags :standard %{dune-warnings})))
+1 -1
lib/ast.ml
··· 141 141 | last :: _ -> trailing_record_fields last 142 142 | [] -> 0) 143 143 | If_then_else { else_expr = Some e; _ } -> trailing_record_fields e 144 - | If_then_else { then_expr; else_expr = None } -> 144 + | If_then_else { then_expr; else_expr = None; _ } -> 145 145 trailing_record_fields then_expr 146 146 | Try { expr; _ } -> trailing_record_fields expr 147 147 | Function { body; _ } -> trailing_record_fields body
-18
lib/config.ml
··· 45 45 exclusions = Rule_config.empty; 46 46 } 47 47 48 - let filename = ".merlint" 49 - 50 48 let file path = 51 49 match Project.config_files path with [] -> None | first :: _ -> Some first 52 50 ··· 107 105 | _ -> 108 106 (* Unknown key - ignore for forward compatibility *) 109 107 config 110 - 111 - let load path = 112 - try 113 - match Config_parser.parse_file path with 114 - | Some parsed -> 115 - (* Apply settings to the default config *) 116 - let config = ref default in 117 - List.iter 118 - (fun (key, value) -> config := apply_config !config key value) 119 - parsed.Config_parser.settings; 120 - { !config with exclusions = parsed.Config_parser.exclusions } 121 - | None -> default 122 - with exn -> 123 - Fmt.epr "Warning: Error loading config from %s: %s\n" path 124 - (Printexc.to_string exn); 125 - default 126 108 127 109 let load_from_path path = 128 110 let config_files = Project.config_files path in
+2
lib/data.ml
··· 39 39 E521.rule; 40 40 E522.rule; 41 41 E523.rule; 42 + E524.rule; 43 + E525.rule; 42 44 E600.rule; 43 45 E605.rule; 44 46 E606.rule;
+82
lib/rules/e524.ml
··· 1 + (** E524: One Cmdliner subcommand per file. 2 + 3 + A CLI tool typically composes its top-level command as a [Cmd.group] of 4 + subcommands. The convention here is that each subcommand lives in its own 5 + file (e.g. [bin/cmd_pull.ml] / [bin/cmd_push.ml] / ...), and [bin/main.ml] 6 + only references them. This keeps each subcommand's arguments, manpage and 7 + term self-contained and reviewable in isolation. 8 + 9 + A file that defines two or more [Cmd.v ...] terms is mixing several 10 + subcommands together. Split each one into its own file. 11 + 12 + {b How to fix:} for each [Cmd.v] in the offending file, create a sibling 13 + file (e.g. [cmd_<name>.ml]) that exposes a single [val cmd : ... Cmd.t], and 14 + reference it from [main.ml]'s [Cmd.group]. Sub-subcommands of a grouped 15 + subcommand should likewise live one-per-file (use a directory like 16 + [cmd_verse/] with one file per leaf, or use [cmd_verse_<name>.ml] siblings). 17 + *) 18 + 19 + type payload = { file : string; count : int } 20 + 21 + let cmd_v_re = 22 + Re.compile (Re.seq [ Re.bow; Re.str "Cmd"; Re.char '.'; Re.str "v"; Re.eow ]) 23 + 24 + let count_cmd_v contents = Re.all cmd_v_re contents |> List.length 25 + 26 + let read_file path = 27 + try 28 + let ic = open_in path in 29 + let n = in_channel_length ic in 30 + let s = really_input_string ic n in 31 + close_in ic; 32 + Some s 33 + with Sys_error _ -> None 34 + 35 + let find_ml_files root = 36 + let try_readdir d = 37 + try Sys.readdir d |> Array.to_list with Sys_error _ -> [] 38 + in 39 + let is_dir p = try Sys.is_directory p with Sys_error _ -> false in 40 + let rec walk dir acc = 41 + List.fold_left 42 + (fun acc name -> 43 + if 44 + name = "_build" || name = "_opam" || name = ".git" 45 + || String.starts_with ~prefix:"." name 46 + then acc 47 + else 48 + let p = Filename.concat dir name in 49 + if is_dir p then walk p acc 50 + else if Filename.check_suffix name ".ml" then p :: acc 51 + else acc) 52 + acc (try_readdir dir) 53 + in 54 + walk root [] 55 + 56 + let check (ctx : Context.project) = 57 + let files = find_ml_files ctx.project_root in 58 + List.filter_map 59 + (fun path -> 60 + match read_file path with 61 + | None -> None 62 + | Some contents -> 63 + let count = count_cmd_v contents in 64 + if count >= 2 then Some (Issue.v { file = path; count }) else None) 65 + files 66 + 67 + let pp ppf { file; count } = 68 + Fmt.pf ppf 69 + "%s defines %d Cmdliner subcommands in one file; split them into one file \ 70 + per subcommand" 71 + file count 72 + 73 + let rule = 74 + Rule.v ~code:"E524" ~title:"Multiple Cmdliner subcommands in one file" 75 + ~category:Rule.Project_structure 76 + ~hint: 77 + "Each Cmd.v subcommand should live in its own file. Move each Cmd.v into \ 78 + a sibling file (e.g. cmd_<name>.ml exposing a single val cmd) and \ 79 + reference it from main.ml's Cmd.group. Sub-subcommands of a grouped \ 80 + subcommand follow the same rule — use cmd_<parent>/<leaf>.ml or \ 81 + cmd_<parent>_<leaf>.ml siblings." 82 + ~examples:[] ~pp (Project check)
+147
lib/rules/e525.ml
··· 1 + (** E525: Package root dune must enable strict warnings for standalone builds. 2 + 3 + Inside the monorepo, the workspace-root [dune] file declares 4 + 5 + {v (env (dev (flags :standard %{dune-warnings}))) v} 6 + 7 + which promotes warnings to errors under the [dev] profile. That stanza only 8 + applies when dune is run from the workspace root. When a package is 9 + published standalone (e.g. installed via opam from its own subtree), the 10 + workspace-root [dune] is not shipped and the strict-warnings policy 11 + disappears — a release that was clean in-tree may compile with warnings 12 + after upstream publication. 13 + 14 + Each package must therefore carry its own root [dune] file that redeclares 15 + the [%\{dune-warnings\}] flag set, and its [dune-project] must declare 16 + [(lang dune 3.21)] or newer (the release that introduced 17 + [%\{dune-warnings\}]). 18 + 19 + Reference standalone package: [alcobar/dune]: 20 + 21 + {v (env (dev (flags :standard %{dune-warnings}))) v} 22 + 23 + {b How to fix:} 24 + - create [<package>/dune] with the stanza above, and 25 + - set [(lang dune 3.21)] (or newer) in [<package>/dune-project]. *) 26 + 27 + type kind = 28 + | Missing_dune 29 + | Missing_warnings 30 + | Lang_too_old of { version : string } 31 + 32 + type payload = { package : string; kind : kind } 33 + 34 + let min_major = 3 35 + let min_minor = 21 36 + let try_readdir d = try Sys.readdir d |> Array.to_list with Sys_error _ -> [] 37 + 38 + let has_opam_file pkg_dir = 39 + List.exists 40 + (fun name -> Filename.check_suffix name ".opam") 41 + (try_readdir pkg_dir) 42 + 43 + let read_file path = 44 + try 45 + let ic = open_in path in 46 + let n = in_channel_length ic in 47 + let s = really_input_string ic n in 48 + close_in ic; 49 + Some s 50 + with Sys_error _ -> None 51 + 52 + let warnings_re = Re.compile (Re.str "%{dune-warnings}") 53 + let contains_warnings contents = Re.execp warnings_re contents 54 + 55 + let lang_re = 56 + Re.compile 57 + (Re.seq 58 + [ 59 + Re.char '('; 60 + Re.rep Re.space; 61 + Re.str "lang"; 62 + Re.rep1 Re.space; 63 + Re.str "dune"; 64 + Re.rep1 Re.space; 65 + Re.group (Re.rep1 (Re.alt [ Re.digit; Re.char '.' ])); 66 + ]) 67 + 68 + let parse_lang_version contents = 69 + match Re.exec_opt lang_re contents with 70 + | None -> None 71 + | Some g -> Some (Re.Group.get g 1) 72 + 73 + let version_too_old v = 74 + match String.split_on_char '.' v with 75 + | major :: minor :: _ -> ( 76 + match (int_of_string_opt major, int_of_string_opt minor) with 77 + | Some mj, Some mn -> mj < min_major || (mj = min_major && mn < min_minor) 78 + | _, _ -> false) 79 + | _ -> false 80 + 81 + let check (ctx : Context.project) = 82 + let root = ctx.project_root in 83 + let is_dir p = try Sys.is_directory p with Sys_error _ -> false in 84 + List.concat_map 85 + (fun name -> 86 + if 87 + name = "_build" || name = "_opam" || name = ".git" 88 + || String.starts_with ~prefix:"." name 89 + then [] 90 + else 91 + let pkg_dir = Filename.concat root name in 92 + if (not (is_dir pkg_dir)) || not (has_opam_file pkg_dir) then [] 93 + else 94 + let dune_issue = 95 + let dune_path = Filename.concat pkg_dir "dune" in 96 + if not (Sys.file_exists dune_path) then 97 + [ Issue.v { package = name; kind = Missing_dune } ] 98 + else 99 + match read_file dune_path with 100 + | Some c when contains_warnings c -> [] 101 + | _ -> [ Issue.v { package = name; kind = Missing_warnings } ] 102 + in 103 + let lang_issue = 104 + let dp_path = Filename.concat pkg_dir "dune-project" in 105 + match read_file dp_path with 106 + | None -> [] 107 + | Some c -> ( 108 + match parse_lang_version c with 109 + | Some v when version_too_old v -> 110 + [ 111 + Issue.v 112 + { package = name; kind = Lang_too_old { version = v } }; 113 + ] 114 + | _ -> []) 115 + in 116 + dune_issue @ lang_issue) 117 + (try_readdir root) 118 + 119 + let pp ppf { package; kind } = 120 + match kind with 121 + | Missing_dune -> 122 + Fmt.pf ppf 123 + "%s has no root dune file; add %s/dune with (env (dev (flags :standard \ 124 + %%{dune-warnings}))) so standalone opam builds fail on warnings" 125 + package package 126 + | Missing_warnings -> 127 + Fmt.pf ppf 128 + "%s/dune does not enable %%{dune-warnings}; add (env (dev (flags \ 129 + :standard %%{dune-warnings}))) so standalone opam builds fail on \ 130 + warnings" 131 + package 132 + | Lang_too_old { version } -> 133 + Fmt.pf ppf 134 + "%s/dune-project declares (lang dune %s); %%{dune-warnings} requires \ 135 + (lang dune %d.%d) or newer" 136 + package version min_major min_minor 137 + 138 + let rule = 139 + Rule.v ~code:"E525" ~title:"Package root dune missing %{dune-warnings}" 140 + ~category:Rule.Project_structure 141 + ~hint: 142 + "Create <package>/dune containing (env (dev (flags :standard \ 143 + %{dune-warnings}))), and bump <package>/dune-project to (lang dune \ 144 + 3.21) or newer. This mirrors the workspace-root dune so that a \ 145 + standalone opam build of the package still enforces strict warnings \ 146 + under the dev profile. Reference: alcobar/dune." 147 + ~examples:[] ~pp (Project check)