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 allowed_words/acronyms support to merlint (E300)

Words listed in .merlint as 'acronyms:' or 'allowed_words:' are
treated as atomic by the variant naming rule (E300). They are not
split at case boundaries.

Example .merlint:
acronyms: [EdDSA, ECDSA, COSE]

This resolves the false positive for Cose.Algorithm.EdDSA.

ocaml-scitt: 61 → 1 merlint issues (the 1 remaining is a first-class
module doc parsing limitation).

+26 -5
+17
lib/config.ml
··· 9 9 (* Naming rules *) 10 10 max_underscores_in_name : int; 11 11 min_name_length_underscore : int; 12 + allowed_words : string list; 12 13 (* Style rules *) 13 14 allow_obj_magic : bool; 14 15 allow_str_module : bool; ··· 30 31 (* Naming defaults *) 31 32 max_underscores_in_name = 3; 32 33 min_name_length_underscore = 5; 34 + allowed_words = []; 33 35 (* Style defaults - all issues enabled *) 34 36 allow_obj_magic = false; 35 37 allow_str_module = false; ··· 76 78 { config with max_underscores_in_name = parse_int value } 77 79 | "min_name_length_underscore" -> 78 80 { config with min_name_length_underscore = parse_int value } 81 + | "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 } 79 95 (* Style rules *) 80 96 | "allow_obj_magic" -> { config with allow_obj_magic = parse_bool value } 81 97 | "allow_str_module" -> { config with allow_str_module = parse_bool value } ··· 133 149 && a.exempt_data_definitions = b.exempt_data_definitions 134 150 && a.max_underscores_in_name = b.max_underscores_in_name 135 151 && a.min_name_length_underscore = b.min_name_length_underscore 152 + && a.allowed_words = b.allowed_words 136 153 && a.allow_obj_magic = b.allow_obj_magic 137 154 && a.allow_str_module = b.allow_str_module 138 155 && a.allow_catch_all_exceptions = b.allow_catch_all_exceptions
+3
lib/config.mli
··· 9 9 (* Naming rules *) 10 10 max_underscores_in_name : int; 11 11 min_name_length_underscore : int; 12 + allowed_words : string list; 13 + (** Words treated as atomic by naming rules (e.g. EdDSA, ECDSA). 14 + Parsed from [allowed_words] or [acronyms] in [.merlint]. *) 12 15 (* Style rules *) 13 16 allow_obj_magic : bool; 14 17 allow_str_module : bool;
+6 -5
lib/rules/e300.ml
··· 4 4 5 5 let check (ctx : Context.file) = 6 6 let filename = ctx.filename in 7 + let allowed = ctx.config.allowed_words in 7 8 Merlin.Dump.check_elements ~full_path:filename (Context.dump ctx).variants 8 9 (fun name -> 9 10 (* For qualified names, only check the basename *) 10 11 let name_to_check = 11 12 if String.contains name '.' then 12 - (* Get everything after the last dot *) 13 13 let parts = String.split_on_char '.' name in 14 14 List.hd (List.rev parts) 15 15 else name 16 16 in 17 - (* Check if the name needs to be converted *) 18 - let expected = Naming.to_capitalized_snake_case name_to_check in 19 - (* Only report if the conversion actually changes the name *) 20 - if expected <> name_to_check then Some expected else None) 17 + (* Skip names that match an allowed word exactly *) 18 + if List.mem name_to_check allowed then None 19 + else 20 + let expected = Naming.to_capitalized_snake_case name_to_check in 21 + if expected <> name_to_check then Some expected else None) 21 22 (fun variant_name loc expected -> 22 23 Issue.v ~loc { variant = variant_name; expected }) 23 24