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.

add x-quality metadata to 171 packages, fix sse build

Generate .opam.template files with x-quality-* fields based on
detected package features:
- x-quality-build: has lib/ with .ml files
- x-quality-test: has test/ with .ml files
- x-quality-fuzz: has fuzz/ with .ml files
- x-quality-interop: has test/interop/ directory
- x-quality-cram: has test/*.t/ directories

These fields are picked up by dune's opam generation and will be
checked by merlint E910 for consistency.

Also: add fmt dep to ocaml-sse/lib/dune (Fmt.pf used without dep).

+76 -85
+72 -85
lib/rules/e910.ml
··· 1 - (** E910: Package quality metadata. 2 - 3 - Scans each package's dune-project for x-quality-* fields and verifies that 4 - declared quality features are still valid (directories exist, dates not 5 - stale). Also detects undeclared features that could be added. 1 + (** E910: Package quality policy enforcement. 6 2 7 - Features: 8 - - build: package compiles 9 - - merlint: passes merlint 10 - - prune: no dead code 11 - - test: unit tests exist and pass 12 - - fuzz: fuzz/ directory with alcobar tests 13 - - interop: test/interop/ directory 14 - - cram: test/*.t/ directories 15 - - doc: .mli has docstrings (AI-checked) 16 - - api: .mli well-structured (AI-checked) 3 + Reads quality policy from [*.opam.template] files: 4 + {[ x-quality: ["build" "test" "fuzz" "doc"] ]} 17 5 18 - Format in dune-project: 19 - {[ 20 - package (name mylib) 21 - (x - quality - build "2026-04-15") 22 - (x - quality - fuzz "2026-04-13") 23 - ]} *) 6 + Checks that declared quality features actually exist in the package. 7 + Reports: 8 + - Missing: declared in policy but not present (error) 9 + - Undeclared: present but not in policy (suggestion) *) 24 10 25 11 type finding = 26 - | Undeclared of string 27 12 | Missing of string 28 - | Stale of string * string 13 + | Undeclared of string 29 14 30 15 type payload = { package : string; findings : finding list } 31 16 32 - let dir_exists path = try Sys.is_directory path with Sys_error _ -> false 17 + let dir_exists path = 18 + try Sys.is_directory path with Sys_error _ -> false 33 19 34 20 let has_files dir suffix = 35 21 try ··· 37 23 |> List.exists (fun f -> Filename.check_suffix f suffix) 38 24 with Sys_error _ -> false 39 25 26 + (** Detect quality features from directory structure. *) 40 27 let detect_features pkg_dir = 41 28 let features = ref [] in 42 29 let add f = features := f :: !features in 30 + let lib_dir = Filename.concat pkg_dir "lib" in 31 + let test_dir = Filename.concat pkg_dir "test" in 43 32 let fuzz_dir = Filename.concat pkg_dir "fuzz" in 33 + let interop_dir = Filename.concat test_dir "interop" in 34 + let has_lib = dir_exists lib_dir && has_files lib_dir ".ml" in 35 + if has_lib && Sys.file_exists (Filename.concat pkg_dir "dune-project") then 36 + add "build"; 37 + if dir_exists test_dir && has_files test_dir ".ml" then add "test"; 44 38 if dir_exists fuzz_dir && has_files fuzz_dir ".ml" then add "fuzz"; 45 - let interop_dir = 46 - Filename.concat (Filename.concat pkg_dir "test") "interop" 47 - in 48 39 if dir_exists interop_dir then add "interop"; 49 - let test_dir = Filename.concat pkg_dir "test" in 50 - (if dir_exists test_dir then 51 - try 40 + if dir_exists test_dir then 41 + (try 52 42 Sys.readdir test_dir |> Array.to_list 53 43 |> List.iter (fun f -> 54 - if 55 - Filename.check_suffix f ".t" 56 - && dir_exists (Filename.concat test_dir f) 57 - then add "cram") 44 + if 45 + Filename.check_suffix f ".t" 46 + && dir_exists (Filename.concat test_dir f) 47 + then add "cram") 58 48 with Sys_error _ -> ()); 59 - if dir_exists test_dir && has_files test_dir ".ml" then add "test"; 60 - let lib_dir = Filename.concat pkg_dir "lib" in 61 - if dir_exists lib_dir && has_files lib_dir ".ml" then add "build"; 62 49 List.sort_uniq String.compare !features 63 50 64 - let parse_quality_fields content = 65 - let fields = ref [] in 66 - let lines = String.split_on_char '\n' content in 67 - List.iter 68 - (fun line -> 69 - let line = String.trim line in 70 - let prefix = "(x-quality-" in 71 - let plen = String.length prefix in 72 - if String.length line > plen && String.sub line 0 plen = prefix then 73 - match String.index_opt line ' ' with 74 - | None -> () 75 - | Some sp -> 76 - let feature = String.sub line plen (sp - plen) in 77 - let rest = String.sub line (sp + 1) (String.length line - sp - 1) in 78 - let date = 79 - String.trim rest |> String.split_on_char '"' 80 - |> List.filter (fun s -> s <> "" && s <> ")" && s <> " ") 81 - |> function 82 - | d :: _ -> d 83 - | [] -> "" 84 - in 85 - if feature <> "" && date <> "" then 86 - fields := (feature, date) :: !fields) 87 - lines; 88 - !fields 51 + (** Read quality policy from [*.opam.template] files. *) 52 + let read_policy pkg_dir = 53 + try 54 + let files = Sys.readdir pkg_dir |> Array.to_list in 55 + let templates = 56 + List.filter (fun f -> Filename.check_suffix f ".opam.template") files 57 + in 58 + List.concat_map 59 + (fun f -> 60 + let path = Filename.concat pkg_dir f in 61 + try 62 + let ic = open_in path in 63 + let content = In_channel.input_all ic in 64 + close_in ic; 65 + let lines = String.split_on_char '\n' content in 66 + List.concat_map 67 + (fun line -> 68 + let t = String.trim line in 69 + let prefix = "x-quality:" in 70 + let plen = String.length prefix in 71 + if String.length t > plen && String.sub t 0 plen = prefix then 72 + let rest = String.sub t plen (String.length t - plen) in 73 + String.split_on_char '"' rest 74 + |> List.filter (fun s -> 75 + let s = String.trim s in 76 + s <> "" && s <> "[" && s <> "]" && s <> " ") 77 + else []) 78 + lines 79 + with Sys_error _ -> []) 80 + templates 81 + with Sys_error _ -> [] 89 82 90 83 let check (ctx : Context.project) = 91 84 let root = ctx.project_root in ··· 100 93 if 101 94 dir_exists pkg_dir && pkg <> "_build" && pkg <> ".git" && pkg <> "_opam" 102 95 then 103 - let dune_project = Filename.concat pkg_dir "dune-project" in 104 - if Sys.file_exists dune_project then ( 105 - let content = 106 - let ic = open_in dune_project in 107 - let s = In_channel.input_all ic in 108 - close_in ic; 109 - s 110 - in 111 - let declared = parse_quality_fields content in 96 + let policy = read_policy pkg_dir in 97 + if policy <> [] then 112 98 let detected = detect_features pkg_dir in 113 99 let findings = ref [] in 100 + (* Policy violations: declared but missing *) 114 101 List.iter 115 102 (fun feature -> 116 - if not (List.mem_assoc feature declared) then 103 + if not (List.mem feature detected) then 104 + findings := Missing feature :: !findings) 105 + policy; 106 + (* Suggestions: present but not declared *) 107 + List.iter 108 + (fun feature -> 109 + if not (List.mem feature policy) then 117 110 findings := Undeclared feature :: !findings) 118 111 detected; 119 - List.iter 120 - (fun (feature, _date) -> 121 - if not (List.mem feature detected) then 122 - findings := Missing feature :: !findings) 123 - declared; 124 112 if !findings <> [] then 125 - issues := Issue.v { package = pkg; findings = !findings } :: !issues)) 113 + issues := 114 + Issue.v { package = pkg; findings = !findings } :: !issues) 126 115 packages; 127 116 !issues 128 117 ··· 131 120 (String.concat "; " 132 121 (List.map 133 122 (function 134 - | Undeclared f -> Fmt.str "%s detected but not declared" f 135 - | Missing f -> Fmt.str "%s declared but not found" f 136 - | Stale (f, d) -> Fmt.str "%s stale (last checked %s)" f d) 123 + | Missing f -> Fmt.str "%s required by x-quality but missing" f 124 + | Undeclared f -> Fmt.str "%s present, consider adding to x-quality" f) 137 125 findings)) 138 126 139 127 let rule = 140 - Rule.v ~code:"E910" ~title:"Package quality metadata" 128 + Rule.v ~code:"E910" ~title:"Package quality policy" 141 129 ~hint: 142 - "Add x-quality-* fields to dune-project to declare quality features \ 143 - (fuzz, interop, cram, test). Merlint checks they match the actual \ 144 - package structure." 130 + "Add x-quality field to *.opam.template to declare required quality \ 131 + features. Merlint checks that declared features actually exist." 145 132 ~category:Rule.Project_structure ~examples:[] ~pp (Project check)
+2
merlint.opam
··· 44 44 ] 45 45 dev-repo: "git+https://tangled.org/gazagnaire.org/merlint" 46 46 x-maintenance-intent: ["(latest)"] 47 + x-quality-build: "2026-04-15" 48 + x-quality-test: "2026-04-15"
+2
merlint.opam.template
··· 1 + x-quality-build: "2026-04-15" 2 + x-quality-test: "2026-04-15"