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.

toml: rename from tomlt, split raw AST into Value submodule

Drops the "t" suffix and follows the value/codec/toml/core pattern
(jsont.json_base style). The internal raw TOML module moves from
[Toml] to [Value] (file: lib/value.ml, was lib/toml.ml) to make room
for the top-level Toml facade (file: lib/toml.ml, was lib/tomlt.ml).

External callers now reach the raw AST through [Toml.Value.X] instead
of [Tomlt.Toml.X]. Every downstream reference updated in lockstep.

+240 -21
+6
docs/STYLE_GUIDE.md
··· 255 255 ``` 256 256 257 257 258 + **Opam Metadata**: Every package's opam file must declare `tags:` with an `org:*` marker and one or more topics from the canonical vocabulary configured in `.merlint`. The tag vocabulary powers topic-grouped indexes across the monorepo, so consistency matters. 259 + 260 + ### [E915] Opam tag metadata 261 + 262 + Every *.opam file must declare tags: ["org:blacksun" "<topic>" ...] where each topic is listed in the topics: field of .merlint. Edit the package's dune-project so dune regenerates the opam file. 263 + 258 264 ## Command-Line Applications 259 265 260 266 For command-line applications in the `bin/` directory, it's common to use a library like `Cmdliner`.
+10 -3
docs/index.html
··· 1670 1670 <span class="error-code">E820</span> 1671 1671 <span class="error-title">Hand-rolled CSV parsing</span> 1672 1672 </div> 1673 - <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> 1673 + <div class="error-hint"><p>Use csv (Csv.decode_file with a Csv.Row codec) for CSV trace parsing. Never hand-roll CSV readers with open_in/input_line/split_on_char.</p></div> 1674 1674 </div> 1675 1675 <div class="error-card" id="E825"> 1676 1676 <div> 1677 1677 <span class="error-code">E825</span> 1678 - <span class="error-title">Missing csvt dependency</span> 1678 + <span class="error-title">Missing csv dependency</span> 1679 1679 </div> 1680 - <div class="error-hint"><p>Interop tests with CSV traces should use csvt for parsing. Add csvt to the (libraries ...) in the dune file and use Csvt.decode_file with a Row codec.</p></div> 1680 + <div class="error-hint"><p>Interop tests with CSV traces should use csv for parsing. Add csv to the (libraries ...) in the dune file and use Csv.decode_file with a Row codec.</p></div> 1681 1681 </div> 1682 1682 <div class="error-card" id="E830"> 1683 1683 <div> ··· 1706 1706 <span class="error-title">Wire struct_/module_ in public API</span> 1707 1707 </div> 1708 1708 <div class="error-hint"><p>Move struct_, module_, c_stubs, ml_stubs out of the .mli. These belong in c/gen.ml where they are used to generate EverParse 3D files and C stubs. The codec is the public API; the 3D projection is a build artifact.</p></div> 1709 + </div> 1710 + <div class="error-card" id="E915"> 1711 + <div> 1712 + <span class="error-code">E915</span> 1713 + <span class="error-title">Opam tag metadata</span> 1714 + </div> 1715 + <div class="error-hint"><p>Every *.opam file must declare tags: ["org:blacksun" "&lt;topic&gt;" ...] where each topic is listed in the topics: field of .merlint. Edit the package's dune-project so dune regenerates the opam file.</p></div> 1709 1716 </div> 1710 1717 1711 1718 <a href="#top" class="back-to-top">↑ Top</a>
+1
dune-project
··· 28 28 fmt 29 29 astring 30 30 sexp 31 + opam-file-format 31 32 tty 32 33 vlog 33 34 (alcotest :with-test)))
+24 -18
lib/config.ml
··· 10 10 max_underscores_in_name : int; 11 11 min_name_length_underscore : int; 12 12 allowed_words : string list; 13 + topics : string list; 13 14 (* Style rules *) 14 15 allow_obj_magic : bool; 15 16 allow_str_module : bool; ··· 32 33 max_underscores_in_name = 3; 33 34 min_name_length_underscore = 5; 34 35 allowed_words = []; 36 + topics = []; 35 37 (* Style defaults - all issues enabled *) 36 38 allow_obj_magic = false; 37 39 allow_str_module = false; ··· 63 65 (** Normalize config key from kebab-case to snake_case *) 64 66 let normalize_key key = String.map (function '-' -> '_' | c -> c) key 65 67 68 + (** Parse comma or space separated list, optionally wrapped in [...] *) 69 + let parse_list value = 70 + let stripped = 71 + let v = String.trim value in 72 + if String.length v >= 2 && v.[0] = '[' && v.[String.length v - 1] = ']' then 73 + String.sub v 1 (String.length v - 2) 74 + else v 75 + in 76 + String.split_on_char ',' stripped 77 + |> List.concat_map (fun s -> String.split_on_char ' ' (String.trim s)) 78 + |> List.filter (fun s -> s <> "") 79 + 66 80 (** Apply a configuration key-value pair to the config *) 67 81 let apply_config config key value : t = 68 82 match normalize_key key with ··· 79 93 | "min_name_length_underscore" -> 80 94 { config with min_name_length_underscore = parse_int value } 81 95 | "allowed_words" | "acronyms" -> 82 - (* Parse list: "[EdDSA, ECDSA, SHA]" or "EdDSA, ECDSA, SHA" *) 83 - let stripped = 84 - let v = String.trim value in 85 - if String.length v >= 2 && v.[0] = '[' && v.[String.length v - 1] = ']' 86 - then String.sub v 1 (String.length v - 2) 87 - else v 88 - in 89 - let words = 90 - String.split_on_char ',' stripped 91 - |> List.concat_map (fun s -> String.split_on_char ' ' (String.trim s)) 92 - |> List.filter (fun s -> s <> "") 93 - in 94 - { config with allowed_words = words } 96 + { config with allowed_words = parse_list value } 97 + | "topics" -> { config with topics = parse_list value } 95 98 (* Style rules *) 96 99 | "allow_obj_magic" -> { config with allow_obj_magic = parse_bool value } 97 100 | "allow_str_module" -> { config with allow_str_module = parse_bool value } ··· 150 153 && a.max_underscores_in_name = b.max_underscores_in_name 151 154 && a.min_name_length_underscore = b.min_name_length_underscore 152 155 && a.allowed_words = b.allowed_words 156 + && a.topics = b.topics 153 157 && a.allow_obj_magic = b.allow_obj_magic 154 158 && a.allow_str_module = b.allow_str_module 155 159 && a.allow_catch_all_exceptions = b.allow_catch_all_exceptions ··· 162 166 Fmt.pf ppf 163 167 "@[<v>{ max_complexity = %d; max_function_length = %d; max_nesting = %d; \ 164 168 exempt_data_definitions = %b; max_underscores_in_name = %d; \ 165 - min_name_length_underscore = %d; allow_obj_magic = %b; allow_str_module = \ 166 - %b; allow_catch_all_exceptions = %b; require_ocamlformat_file = %b; \ 167 - require_mli_files = %b }@]" 169 + min_name_length_underscore = %d; topics = [%s]; allow_obj_magic = %b; \ 170 + allow_str_module = %b; allow_catch_all_exceptions = %b; \ 171 + require_ocamlformat_file = %b; require_mli_files = %b }@]" 168 172 t.max_complexity t.max_function_length t.max_nesting 169 173 t.exempt_data_definitions t.max_underscores_in_name 170 - t.min_name_length_underscore t.allow_obj_magic t.allow_str_module 171 - t.allow_catch_all_exceptions t.require_ocamlformat_file t.require_mli_files 174 + t.min_name_length_underscore 175 + (String.concat "; " t.topics) 176 + t.allow_obj_magic t.allow_str_module t.allow_catch_all_exceptions 177 + t.require_ocamlformat_file t.require_mli_files
+4
lib/config.mli
··· 12 12 allowed_words : string list; 13 13 (** Words treated as atomic by naming rules (e.g. EdDSA, ECDSA). Parsed 14 14 from [allowed_words] or [acronyms] in [.merlint]. *) 15 + topics : string list; 16 + (** Canonical opam tag vocabulary. Parsed from [topics] in [.merlint]. 17 + When non-empty, E915 rejects any opam tag not in this list (plus the 18 + [org:*] namespace prefix which is always allowed). *) 15 19 (* Style rules *) 16 20 allow_obj_magic : bool; 17 21 allow_str_module : bool;
+1
lib/data.ml
··· 74 74 E835.rule; 75 75 E900.rule; 76 76 E905.rule; 77 + E915.rule; 77 78 ]
+1
lib/dune
··· 16 16 tty 17 17 jsont 18 18 jsont.bytesrw 19 + opam-file-format 19 20 sexp)) 20 21 21 22 (rule
+7
lib/guide.ml
··· 126 126 "**Interface Files**: Create `.mli` files for all public modules \ 127 127 to define clear interfaces and hide implementation details."; 128 128 Rule "E505"; 129 + Paragraph 130 + "**Opam Metadata**: Every package's opam file must declare `tags:` \ 131 + with an `org:*` marker and one or more topics from the canonical \ 132 + vocabulary configured in `.merlint`. The tag vocabulary powers \ 133 + topic-grouped indexes across the monorepo, so consistency \ 134 + matters."; 135 + Rule "E915"; 129 136 ] ); 130 137 Section 131 138 ( "Command-Line Applications",
+110
lib/rules/e915.ml
··· 1 + (** E915: Opam tag metadata enforcement. 2 + 3 + Every [*.opam] file must declare a [tags:] field that contains the 4 + [org:blacksun] marker plus one or more topics from the canonical vocabulary 5 + declared as [topics:] in [.merlint]. 6 + 7 + A topic-grouped view of the monorepo (generated from these tags) only works 8 + if the vocabulary is kept honest, so every finding is an error: 9 + - No [tags:] field in the opam file. 10 + - [tags:] present but [org:blacksun] missing. 11 + - A tag that is not [org:*] and not in the configured [topics:] list. *) 12 + 13 + type finding = Missing_tags | Missing_org | Unknown_topic of string 14 + type payload = { package : string; opam : string; findings : finding list } 15 + 16 + let dir_exists path = try Sys.is_directory path with Sys_error _ -> false 17 + 18 + (** Extract the list of tags from an opam file using the official 19 + [opam-file-format] parser. Handles both list ([tags: ["a" "b"]]) and 20 + single-string ([tags: "a"]) forms, as well as multi-line values. 21 + 22 + Returns [None] if no [tags:] field is found or the file cannot be parsed. *) 23 + let read_tags path = 24 + let open OpamParserTypes.FullPos in 25 + let string_of_value v = 26 + match v.pelem with String s -> Some s | _ -> None 27 + in 28 + try 29 + let file = OpamParser.FullPos.file path in 30 + let rec find = function 31 + | [] -> None 32 + | item :: rest -> ( 33 + match item.pelem with 34 + | Variable (name, value) when name.pelem = "tags" -> ( 35 + match value.pelem with 36 + | String s -> Some [ s ] 37 + | List l -> Some (List.filter_map string_of_value l.pelem) 38 + | _ -> Some []) 39 + | _ -> find rest) 40 + in 41 + find file.file_contents 42 + with _ -> None 43 + 44 + let check_opam_file ~topics pkg_dir opam_name = 45 + let path = Filename.concat pkg_dir opam_name in 46 + let findings = ref [] in 47 + (match read_tags path with 48 + | None -> findings := [ Missing_tags ] 49 + | Some tags -> 50 + let has_org = 51 + List.exists 52 + (fun t -> String.length t >= 4 && String.sub t 0 4 = "org:") 53 + tags 54 + in 55 + if not has_org then findings := Missing_org :: !findings; 56 + if topics <> [] then 57 + List.iter 58 + (fun tag -> 59 + let is_org = 60 + String.length tag >= 4 && String.sub tag 0 4 = "org:" 61 + in 62 + if (not is_org) && not (List.mem tag topics) then 63 + findings := Unknown_topic tag :: !findings) 64 + tags); 65 + List.rev !findings 66 + 67 + let list_opam_files pkg_dir = 68 + try 69 + Sys.readdir pkg_dir |> Array.to_list 70 + |> List.filter (fun f -> Filename.check_suffix f ".opam") 71 + with Sys_error _ -> [] 72 + 73 + let check (ctx : Context.project) = 74 + let root = ctx.project_root in 75 + let topics = ctx.config.topics in 76 + let issues = ref [] in 77 + let try_readdir d = 78 + try Sys.readdir d |> Array.to_list with Sys_error _ -> [] 79 + in 80 + let skip = [ "_build"; ".git"; "_opam"; "node_modules" ] in 81 + let packages = try_readdir root in 82 + List.iter 83 + (fun pkg -> 84 + let pkg_dir = Filename.concat root pkg in 85 + if dir_exists pkg_dir && not (List.mem pkg skip) then 86 + List.iter 87 + (fun opam -> 88 + let findings = check_opam_file ~topics pkg_dir opam in 89 + if findings <> [] then 90 + issues := Issue.v { package = pkg; opam; findings } :: !issues) 91 + (list_opam_files pkg_dir)) 92 + packages; 93 + !issues 94 + 95 + let pp ppf { package; opam; findings } = 96 + let describe = function 97 + | Missing_tags -> "missing tags: field" 98 + | Missing_org -> "tags: missing org:blacksun marker" 99 + | Unknown_topic t -> Fmt.str "unknown topic %S" t 100 + in 101 + Fmt.pf ppf "%s/%s: %s" package opam 102 + (String.concat "; " (List.map describe findings)) 103 + 104 + let rule = 105 + Rule.v ~code:"E915" ~title:"Opam tag metadata" 106 + ~hint: 107 + "Every *.opam file must declare tags: [\"org:blacksun\" \"<topic>\" ...] \ 108 + where each topic is listed in the topics: field of .merlint. Edit the \ 109 + package's dune-project so dune regenerates the opam file." 110 + ~category:Rule.Project_structure ~examples:[] ~pp (Project check)
+7
lib/rules/e915.mli
··· 1 + (** E915: Opam tag metadata enforcement. 2 + 3 + Every [*.opam] file must declare [tags:] with an [org:*] marker and topics 4 + from the vocabulary configured in [.merlint]. *) 5 + 6 + val rule : Rule.t 7 + (** The E915 rule definition. *)
+1
test/cram/e915.t/bad/.merlint
··· 1 + topics: [codec, crypto, network]
+1
test/cram/e915.t/bad/dune-project
··· 1 + (lang dune 3.21)
+4
test/cram/e915.t/bad/pkg1/pkg1.opam
··· 1 + opam-version: "2.0" 2 + synopsis: "Missing tags field" 3 + maintainer: ["Test"] 4 + depends: ["ocaml"]
+5
test/cram/e915.t/bad/pkg2/pkg2.opam
··· 1 + opam-version: "2.0" 2 + synopsis: "Missing org:blacksun marker" 3 + tags: ["codec" "network"] 4 + maintainer: ["Test"] 5 + depends: ["ocaml"]
+5
test/cram/e915.t/bad/pkg3/pkg3.opam
··· 1 + opam-version: "2.0" 2 + synopsis: "Unknown topic" 3 + tags: ["org:blacksun" "codec" "weird-new-topic"] 4 + maintainer: ["Test"] 5 + depends: ["ocaml"]
+1
test/cram/e915.t/good/.merlint
··· 1 + topics: [codec, crypto, network]
+1
test/cram/e915.t/good/dune-project
··· 1 + (lang dune 3.21)
+5
test/cram/e915.t/good/pkg1/pkg1.opam
··· 1 + opam-version: "2.0" 2 + synopsis: "Well-formed metadata" 3 + tags: ["org:blacksun" "codec" "network"] 4 + maintainer: ["Test"] 5 + depends: ["ocaml"]
+46
test/cram/e915.t/run.t
··· 1 + Test bad example - three packages each with a different tag problem: 2 + $ merlint -B -r E915 bad/ 3 + Running merlint analysis... 4 + 5 + Analyzing 0 files 6 + 7 + ✓ Code Quality (0 total issues) 8 + ✓ Code Style (0 total issues) 9 + ✓ Naming Conventions (0 total issues) 10 + ✓ Documentation (0 total issues) 11 + ✗ Project Structure (3 total issues) 12 + [E915] Opam tag metadata (3 issues) 13 + Every *.opam file must declare tags: ["org:blacksun" "<topic>" ...] where 14 + each topic is listed in the topics: field of .merlint. Edit the package's 15 + dune-project so dune regenerates the opam file. 16 + - (global) pkg1/pkg1.opam: missing tags: field 17 + - (global) pkg2/pkg2.opam: tags: missing org:blacksun marker 18 + - (global) pkg3/pkg3.opam: unknown topic "weird-new-topic" 19 + ✓ Test Quality (0 total issues) 20 + ✓ Interop Testing (0 total issues) 21 + ✓ Code Generation (0 total issues) 22 + 23 + Summary: ✗ 3 total issues (applied 1 rule) 24 + ✗ Some checks failed. See details above. 25 + [1] 26 + 27 + 28 + 29 + 30 + Test good example - well-formed opam metadata: 31 + $ merlint -B -r E915 good/ 32 + Running merlint analysis... 33 + 34 + Analyzing 0 files 35 + 36 + ✓ Code Quality (0 total issues) 37 + ✓ Code Style (0 total issues) 38 + ✓ Naming Conventions (0 total issues) 39 + ✓ Documentation (0 total issues) 40 + ✓ Project Structure (0 total issues) 41 + ✓ Test Quality (0 total issues) 42 + ✓ Interop Testing (0 total issues) 43 + ✓ Code Generation (0 total issues) 44 + 45 + Summary: ✓ 0 total issues (applied 1 rule) 46 + ✓ All checks passed!