Monorepo management for opam overlays
0
fork

Configure Feed

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

monopam/lint: flag subtrees missing the dune-warnings env stanza

Each subtree's top-level dune file should ship
(env (dev (flags :standard %{dune-warnings}))) so the warning set is
uniform across the monorepo. Without it, individual subtrees can
build with weaker checks than the rest of the tree, masking real
issues.

Add a dune_warning_issue type to the lint result, populated by
scanning each subtree's top-level dune file (only when a dune-project
exists, so foreign subtrees aren't flagged), and surface it in the
lint output alongside dependency and source-URL checks.

+163 -19
+59 -12
bin/cmd_lint.ml
··· 129 129 (fun (i : Monopam.Lint.source_issue) -> List.mem i.subtree dirs) 130 130 source_issues 131 131 132 + let filter_dune_warning_issues filter dune_warning_issues = 133 + match filter with 134 + | [] -> dune_warning_issues 135 + | dirs -> 136 + List.filter 137 + (fun (i : Monopam.Lint.dune_warning_issue) -> List.mem i.subtree dirs) 138 + dune_warning_issues 139 + 140 + let pp_dune_warning_issues dune_warning_issues = 141 + if dune_warning_issues = [] then () 142 + else begin 143 + Fmt.pr "%a %d subtree%s:@." 144 + Fmt.(styled `Bold string) 145 + "Dune warnings missing in:" 146 + (List.length dune_warning_issues) 147 + (if List.length dune_warning_issues = 1 then "" else "s"); 148 + List.iter 149 + (fun (i : Monopam.Lint.dune_warning_issue) -> 150 + let detail = 151 + match i.kind with 152 + | Monopam.Lint.Missing_file -> "no top-level dune file" 153 + | Monopam.Lint.Missing_flags -> 154 + "missing (env (dev (flags :standard %{dune-warnings})))" 155 + in 156 + Fmt.pr " %-24s %a@." i.subtree Fmt.(styled `Yellow string) detail) 157 + dune_warning_issues 158 + end 159 + 132 160 let scanned_label filter packages_scanned = 133 161 match filter with 134 162 | [] -> Fmt.str "%d scanned" packages_scanned ··· 138 166 List.filter (fun (i : Monopam.Lint.issue) -> i.kind = kind) issues 139 167 |> List.length 140 168 141 - let issue_subtrees issues source_issues = 169 + let issue_subtrees issues source_issues dune_warning_issues = 142 170 List.map (fun (i : Monopam.Lint.issue) -> i.subtree) issues 143 171 |> List.append 144 172 (List.map 145 173 (fun (i : Monopam.Lint.source_issue) -> i.subtree) 146 174 source_issues) 175 + |> List.append 176 + (List.map 177 + (fun (i : Monopam.Lint.dune_warning_issue) -> i.subtree) 178 + dune_warning_issues) 147 179 |> List.sort_uniq String.compare 148 180 149 - let summary_parts ~issues ~source_issues = 181 + let summary_parts ~issues ~source_issues ~dune_warning_issues = 150 182 let n_missing = count_kind Monopam.Lint.Missing issues in 151 183 let n_unused = count_kind Monopam.Lint.Unused issues in 152 184 let n_source = List.length source_issues in 185 + let n_warn = List.length dune_warning_issues in 153 186 List.filter_map Fun.id 154 187 [ 155 188 (if n_missing > 0 then Some (Fmt.str "%d missing" n_missing) else None); 156 189 (if n_unused > 0 then Some (Fmt.str "%d unused" n_unused) else None); 157 190 (if n_source > 0 then Some (Fmt.str "%d source" n_source) else None); 191 + (if n_warn > 0 then Some (Fmt.str "%d dune-warnings" n_warn) else None); 158 192 ] 159 193 160 - let print_summary ~issues ~source_issues ~label = 161 - let parts = summary_parts ~issues ~source_issues in 162 - let pkgs = issue_subtrees issues source_issues in 194 + let print_summary ~issues ~source_issues ~dune_warning_issues ~label = 195 + let parts = summary_parts ~issues ~source_issues ~dune_warning_issues in 196 + let pkgs = issue_subtrees issues source_issues dune_warning_issues in 163 197 Fmt.pr "%a %s in %d packages (%s)@." 164 198 Fmt.(styled (`Fg `Red) string) 165 199 "✗" (String.concat ", " parts) (List.length pkgs) label 166 200 167 - let print_issues issues source_issues = 201 + let print_issues issues source_issues dune_warning_issues = 168 202 if issues <> [] then 169 203 if Tty.is_tty () then pp_table issues else pp_plain issues; 170 - pp_source_issues source_issues 204 + pp_source_issues source_issues; 205 + pp_dune_warning_issues dune_warning_issues 171 206 172 207 let claude_dir_has_override ~fs ~monorepo = 173 208 let path = ··· 215 250 Common.with_config env @@ fun config -> 216 251 let fs = Eio.Stdenv.fs env in 217 252 let monorepo = Monopam.Config.Paths.monorepo config in 218 - let { Monopam.Lint.issues; source_issues; root_diffs; packages_scanned } = 253 + let { 254 + Monopam.Lint.issues; 255 + source_issues; 256 + dune_warning_issues; 257 + root_diffs; 258 + packages_scanned; 259 + } = 219 260 Monopam.Lint.run ~fs:(fs :> Eio.Fs.dir_ty Eio.Path.t) ~monorepo () 220 261 in 221 262 let issues = filter_dep_issues filter issues in 222 263 let source_issues = filter_source_issues filter source_issues in 264 + let dune_warning_issues = 265 + filter_dune_warning_issues filter dune_warning_issues 266 + in 223 267 let label = scanned_label filter packages_scanned in 224 - if issues = [] && source_issues = [] && root_diffs = [] then ( 268 + if 269 + issues = [] && source_issues = [] && dune_warning_issues = [] 270 + && root_diffs = [] 271 + then ( 225 272 Fmt.pr "%a All checks passed (%s).@." 226 273 Fmt.(styled (`Fg `Green) string) 227 274 "✓" label; 228 275 `Ok ()) 229 276 else ( 230 - print_issues issues source_issues; 277 + print_issues issues source_issues dune_warning_issues; 231 278 (if fix && root_diffs <> [] then run_fix ~env ~fs ~monorepo ~root_diffs 232 279 else 233 280 let migrated = claude_dir_has_override ~fs ~monorepo in 234 281 List.iter (print_root_diff ~migrated) root_diffs); 235 - if issues <> [] || source_issues <> [] then 236 - print_summary ~issues ~source_issues ~label 282 + if issues <> [] || source_issues <> [] || dune_warning_issues <> [] then 283 + print_summary ~issues ~source_issues ~dune_warning_issues ~label 237 284 else if fix && root_diffs <> [] then 238 285 Fmt.pr "%a All checks passed (%s).@." 239 286 Fmt.(styled (`Fg `Green) string)
+67 -1
lib/lint.ml
··· 395 395 sources_toml : string option; 396 396 } 397 397 398 + type dune_warning_kind = Missing_file | Missing_flags 399 + type dune_warning_issue = { subtree : string; kind : dune_warning_kind } 400 + 398 401 type result = { 399 402 issues : issue list; 400 403 source_issues : source_issue list; 404 + dune_warning_issues : dune_warning_issue list; 401 405 root_diffs : Root.diff list; 402 406 packages_scanned : int; 403 407 } ··· 469 473 else Some { subtree; dune_project = Some d; sources_toml = source }) 470 474 subdirs 471 475 472 - let sort_source_issues issues = 476 + let sort_source_issues (issues : source_issue list) = 477 + List.sort 478 + (fun (a : source_issue) (b : source_issue) -> 479 + String.compare a.subtree b.subtree) 480 + issues 481 + 482 + (* ---- Dune warnings consistency ---- *) 483 + 484 + (** Match the canonical stanza [(env (dev (flags :standard %{dune-warnings})))]. 485 + We do not require an exact textual match: the check passes if any stanza 486 + references [%{dune-warnings}] inside an [env] block, so per-subtree 487 + variations (extra fields, alternate profiles) still satisfy it. *) 488 + let rec mentions_dune_warnings (sexp : Sexp.t) = 489 + match sexp with 490 + | Sexp.Atom s -> 491 + let needle = "dune-warnings" in 492 + let len_s = String.length s in 493 + let len_n = String.length needle in 494 + let rec find i = 495 + if i + len_n > len_s then false 496 + else if String.sub s i len_n = needle then true 497 + else find (i + 1) 498 + in 499 + find 0 500 + | Sexp.List xs -> List.exists mentions_dune_warnings xs 501 + 502 + let dune_warning_status ~fs subtree_path = 503 + let dune_path = Fpath.(subtree_path / "dune") in 504 + match load_file fs dune_path with 505 + | None -> Some Missing_file 506 + | Some content -> 507 + let sexps = parse_sexps content in 508 + let env_stanzas = 509 + List.filter 510 + (function Sexp.List (Sexp.Atom "env" :: _) -> true | _ -> false) 511 + sexps 512 + in 513 + if List.exists mentions_dune_warnings env_stanzas then None 514 + else Some Missing_flags 515 + 516 + let compute_dune_warning_issues ~fs ~monorepo subdirs = 517 + List.filter_map 518 + (fun subtree -> 519 + let subtree_path = Fpath.(monorepo / subtree) in 520 + (* Only OCaml subtrees (those with a dune-project) are expected to 521 + configure warnings; foreign subtrees have no dune build to flag. *) 522 + let dune_project = 523 + Eio.Path.(fs / Fpath.to_string Fpath.(subtree_path / "dune-project")) 524 + in 525 + match Eio.Path.kind ~follow:false dune_project with 526 + | `Regular_file -> 527 + Option.map 528 + (fun kind -> { subtree; kind }) 529 + (dune_warning_status ~fs subtree_path) 530 + | _ | (exception Eio.Io _) -> None) 531 + subdirs 532 + 533 + let sort_dune_warning_issues (issues : dune_warning_issue list) = 473 534 List.sort (fun a b -> String.compare a.subtree b.subtree) issues 474 535 475 536 (* ---- Core algorithm ---- *) ··· 594 655 let source_issues = 595 656 compute_source_issues ~fs ~monorepo ~sources subdirs |> sort_source_issues 596 657 in 658 + let dune_warning_issues = 659 + compute_dune_warning_issues ~fs ~monorepo subdirs 660 + |> sort_dune_warning_issues 661 + in 597 662 let issues = ref [] in 598 663 let scanned = ref 0 in 599 664 List.iter ··· 633 698 { 634 699 issues = sort_issues !issues; 635 700 source_issues; 701 + dune_warning_issues; 636 702 root_diffs; 637 703 packages_scanned = !scanned; 638 704 }
+23 -4
lib/lint.mli
··· 1 1 (** Lint checks for monorepo packages. 2 2 3 - Two kinds of checks: 3 + Three kinds of checks: 4 4 - {b Dependency} — compare META [requires] against opam [depends] 5 5 bidirectionally: missing (needed by META but not declared) and unused 6 6 (declared but not needed by META). 7 7 - {b Source URL} — compare each subtree's dune-project [(source ...)] stanza 8 8 with the [source] field in [sources.toml] (or the derived default-origin 9 9 URL when there is no entry). Mismatches usually indicate a stale rename or 10 - a pending tangled-side rename. *) 10 + a pending tangled-side rename. 11 + - {b Dune warnings} — every subtree must ship a top-level [dune] file that 12 + enables all dune warnings via 13 + [(env (dev (flags :standard %{dune-warnings})))], so the warning set is 14 + uniform across the monorepo. *) 11 15 12 16 type kind = Missing | Unused (** The kind of dependency issue. *) 13 17 ··· 30 34 (** A single source-URL inconsistency. Emitted when the dune-project source and 31 35 the sources.toml source disagree on where the subtree lives. *) 32 36 37 + type dune_warning_kind = 38 + | Missing_file 39 + | Missing_flags 40 + (** Why the subtree's top-level [dune] file fails the warning check. *) 41 + 42 + type dune_warning_issue = { 43 + subtree : string; (** Monorepo subdirectory *) 44 + kind : dune_warning_kind; 45 + } 46 + (** A subtree whose top-level [dune] file does not enable [%{dune-warnings}]. *) 47 + 33 48 type result = { 34 49 issues : issue list; (** Dependency issues found *) 35 50 source_issues : source_issue list; (** Source-URL mismatches found *) 51 + dune_warning_issues : dune_warning_issue list; 52 + (** Subtrees missing the top-level [dune] file or its 53 + [(env (dev (flags :standard %{dune-warnings})))] stanza. *) 36 54 root_diffs : Root.diff list; 37 55 (** Root files ([dune-project], [README.md], [llms.txt], [CLAUDE.md]) 38 56 whose on-disk content differs from what {!Root.regenerate} would ··· 44 62 val run : fs:Eio.Fs.dir_ty Eio.Path.t -> monorepo:Fpath.t -> unit -> result 45 63 (** [run ~fs ~monorepo ()] scans every subtree under [monorepo], builds a 46 64 library-to-package index from META files, reports missing / unused 47 - dependencies, and reports subtrees whose dune-project source URL disagrees 48 - with the sources.toml entry (or default origin). *) 65 + dependencies, reports subtrees whose dune-project source URL disagrees with 66 + the sources.toml entry (or default origin), and reports subtrees whose 67 + top-level [dune] file does not enable [%{dune-warnings}]. *)
+14 -2
test/test_lint.ml
··· 21 21 22 22 let test_result_construction () = 23 23 let r : Monopam.Lint.result = 24 - { issues = []; source_issues = []; root_diffs = []; packages_scanned = 0 } 24 + { 25 + issues = []; 26 + source_issues = []; 27 + dune_warning_issues = []; 28 + root_diffs = []; 29 + packages_scanned = 0; 30 + } 25 31 in 26 32 Alcotest.(check int) "no issues" 0 (List.length r.issues); 27 33 Alcotest.(check int) "no packages" 0 r.packages_scanned ··· 34 40 ] 35 41 in 36 42 let r : Monopam.Lint.result = 37 - { issues; source_issues = []; root_diffs = []; packages_scanned = 2 } 43 + { 44 + issues; 45 + source_issues = []; 46 + dune_warning_issues = []; 47 + root_diffs = []; 48 + packages_scanned = 2; 49 + } 38 50 in 39 51 Alcotest.(check int) "two issues" 2 (List.length r.issues); 40 52 Alcotest.(check int) "two packages" 2 r.packages_scanned