Merlint Error Codes Reference

This document lists all error codes that Merlint can detect, along with their descriptions and fix hints.

Table of Contents

Complexity

E001-E099 • Code complexity and maintainability issues
E001 High Cyclomatic Complexity

This issue means your functions have too much conditional logic. Fix them by extracting complex logic into smaller helper functions with clear names that describe their purpose.

BAD

let process_data data user =
  if data.valid then
    if user.authenticated then
      if data.size < 1000 then
        if has_permission user data then
          (* complex processing logic *)
        else Error "No permission"
      else Error "Data too large"
    else Error "Not authenticated"
  else Error "Invalid data"

GOOD

let validate_data data = 
  if not data.valid then Error "Invalid data" else Ok ()

let check_auth user = 
  if not user.authenticated then Error "Not authenticated" else Ok ()

let check_size data = 
  if data.size >= 1000 then Error "Data too large" else Ok ()

let check_permission user data = 
  if not (has_permission user data) then Error "No permission" else Ok ()

let process_data data user =
  let open Result.Syntax in
  let* () = validate_data data in
  let* () = check_auth user in
  let* () = check_size data in
  let* () = check_permission user data in
  (* complex processing logic *)
E005 Long Functions

This issue means your functions are too long and hard to read. Fix them by extracting logical sections into separate functions with descriptive names. Note: Functions with pattern matching get additional allowance (2 lines per case). Pure data structures (lists, records) are also exempt from length checks. For better readability, consider using helper functions for complex logic. Aim for functions under 50 lines of actual logic.

BAD

let process_everything user data config =
  (* 100+ lines of mixed concerns: validation, processing, formatting *)
  let valid = check_user user && verify_data data in
  (* ... many more lines ... *)
  format_output result

GOOD

let validate_inputs user data = 
  check_user user && verify_data data

let process_data data config = 
  (* focused processing logic *)

let process_everything user data config =
  let valid = validate_inputs user data in
  let result = process_data data config in
  format_output result
E010 Deep Nesting

This issue means your code has too many nested conditions making it hard to follow. Fix it by using pattern matching, early returns with 'when' guards, or extracting nested logic into helper functions.

BAD

let process_order order user =
  if order.valid then
    if user.authenticated then
      if order.total > 0 then
        if check_inventory order then
          (* deeply nested logic *)
          process_payment order
        else Error "Out of stock"
      else Error "Invalid total"
    else Error "Not authenticated"
  else Error "Invalid order"

GOOD

let process_order order user =
  if not order.valid then Error "Invalid order" else
  if not user.authenticated then Error "Not authenticated" else
  if order.total <= 0 then Error "Invalid total" else
  if not (check_inventory order) then Error "Out of stock" else
  process_payment order

Security/Safety

E100-E199 • Potential security vulnerabilities and unsafe code patterns
E100 Unsafe Type Casting

This issue means you're using unsafe type casting that can crash your program. Fix it by replacing Obj.magic with proper type definitions, variant types, or GADTs to represent different cases safely.

BAD

let coerce x = Obj.magic x

GOOD

(* Use proper type conversions *)
let int_of_string_opt s =
  try Some (int_of_string s) with _ -> None

(* Or use variant types *)
type value = Int of int | String of string
let to_int = function Int i -> Some i | _ -> None
E105 Underscore Pattern Warning

WARNING: This rule currently detects ANY underscore (_) pattern, not just exception handlers. This is a known limitation. The rule is intended to catch dangerous patterns like 'try ... with _ ->' but currently flags all uses of _. To avoid this warning, use named bindings with underscore prefix (e.g., _unused) for intentionally unused values. This will be fixed in a future version.

BAD

try risky_operation () with _ -> default_value

GOOD

try risky_operation () with
| Not_found -> default_value  
| Invalid_argument _ -> error_value
E110 Silenced Compiler Warnings

This issue means you're hiding compiler warnings that indicate potential problems. Fix it by removing warning silencing attributes and addressing the underlying issues that trigger the warnings.

BAD

[@@@ocaml.warning "-32"] (* unused value *)
let unused_function x = x + 1

GOOD

(* Remove unused code or use it *)
let helper x = x + 1
let result = helper 42

Style/Modernization

E200-E299 • Code style and modernization recommendations
E200 Outdated Str Module

This issue means you're using the outdated Str module for regular expressions. Fix it by switching to the modern Re module: add 're' to your dune dependencies and replace Str functions with Re equivalents.

BAD

let is_email s = 
  Str.string_match (Str.regexp ".*@.*") s 0

GOOD

let email_re = Re.compile (Re.seq [Re.any; Re.char '@'; Re.any])
let is_email s = Re.execp email_re s
E205 Consider Using Fmt Module

This is a style suggestion. While Printf and Format are part of OCaml's standard library and perfectly fine to use, the Fmt library offers additional features like custom formatters and better composability. Consider using Fmt for new code, but Printf/Format remain valid choices for many use cases.

BAD

let error_msg = Printf.sprintf "Error: %s at line %d" msg line
let () = Printf.printf "Processing %d items...\n" count

GOOD

let error_msg = Fmt.str "Error: %s at line %d" msg line
let () = Fmt.pr "Processing %d items...@." count

(* Even better with custom formatters *)
let pp_error ppf (msg, line) = 
  Fmt.pf ppf "Error: %s at line %d" msg line

Naming Conventions

E300-E399 • Identifier naming convention violations
E300 Variant Naming Convention

This issue means your variant constructors don't follow OCaml naming conventions. Fix them by renaming to Snake_case (e.g., MyVariant → My_variant).

BAD

type status = 
  | WaitingForInput    (* CamelCase *)
  | ProcessingData
  | errorOccurred      (* lowerCamelCase *)

GOOD

type status = 
  | Waiting_for_input  (* Snake_case *)
  | Processing_data
  | Error_occurred
E305 Module Naming Convention

This issue means your module names don't follow OCaml naming conventions. Fix them by using underscores between words while keeping the first letter capitalized (e.g., MyModule → My_module).

BAD

module UserProfile = struct ... end

GOOD

module User_profile = struct ... end
E310 Value Naming Convention

This issue means your value names don't follow OCaml naming conventions. Fix them by renaming to snake_case (e.g., myValue → my_value).

BAD

let myValue = 42
let getUserName user = user.name

GOOD

let my_value = 42
let get_user_name user = user.name
E315 Type Naming Convention

This issue means your type names don't follow OCaml naming conventions. Fix them by renaming to snake_case (e.g., myType → my_type).

BAD

type userProfile = { name: string }
type HTTPResponse = Ok | Error

GOOD

type user_profile = { name: string }
type http_response = Ok | Error
E320 Long Identifier Names

This issue means your identifier has too many underscores (more than 4) making it hard to read. Fix it by removing redundant prefixes and suffixes: • In test files: remove 'test_' prefix (e.g., test_check_foo → check_foo or just foo) • In hint files: remove '_hint' suffix (e.g., complexity_hint → complexity) • In modules: remove '_module' suffix (e.g., parser_module → parser) • Remove redundant words that repeat the context (e.g., check_mli_doc → check_mli) The file/module context already makes the purpose clear.

BAD

let get_user_profile_data_from_database_by_id id = ...

GOOD

let get_user_by_id id = ...
E325 Function Naming Pattern

This issue means your function names don't match their return types. Fix them by using consistent naming: get_* for extraction (returns value directly), find_* for search (returns option type).

BAD

let get_user id = List.find_opt (fun u -> u.id = id) users
let find_name user = user.name

GOOD

let find_user id = List.find_opt (fun u -> u.id = id) users  
let get_name user = user.name
E330 Redundant Module Names

This issue means your function or type name redundantly includes the module name. Fix it by removing the redundant prefix since the module context is already clear from usage.

BAD

(* In process.ml *)
let process_start () = ...
let process_stop () = ...
type process_config = ...

GOOD

(* In process.ml *)
let start () = ...
let stop () = ...
type config = ...
(* Usage: Process.start (), Process.config *)
E335 Used Underscore-Prefixed Binding

This issue means a binding prefixed with underscore (indicating it should be unused) is actually used in the code. Fix it by removing the underscore prefix to clearly indicate the binding is intentionally used.

BAD

let _debug_mode = true in
if _debug_mode then
  print_endline "Debug mode enabled"

GOOD

let debug_mode = true in
if debug_mode then
  print_endline "Debug mode enabled"
E340 Inline Error Construction

This issue means you're constructing errors inline instead of using helper functions. Fix by defining err_* functions at the top of your file for each error case. This promotes consistency, enables easy error message updates, and makes error handling patterns clearer.

BAD

let process_data data =
  match validate data with
  | None -> Error (Fmt.str "Invalid data: %s" data.id)
  | Some v -> 
      if v.size > max_size then
        Error (Fmt.str "Data too large: %d" v.size)
      else Ok v

GOOD

(* Define error helpers at the top of the file *)
let err_invalid_data id = Error (`Invalid_data id)
let err_fmt fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt

let process_data data =
  match validate data with
  | None -> err_invalid_data data.id
  | Some v -> 
      if v.size > max_size then
        err_fmt "Data too large: %d bytes" v.size
      else Ok v
E350 Boolean Blindness

This issue means your function has multiple boolean parameters, making call sites ambiguous and error-prone. Fix it by using explicit variant types that leverage OCaml's type system for clarity and safety.

BAD

let create_window visible resizable fullscreen =
  (* What do these booleans mean at the call site? *)
  ...
  
let w = create_window true false true

GOOD

type visibility = Visible | Hidden
type window_mode = Windowed | Fullscreen
type resizable = Resizable | Fixed_size

let create_window ~visibility ~mode ~resizable =
  ...
  
let w = create_window ~visibility:Visible ~mode:Fullscreen ~resizable:Fixed_size
E351 Global Mutable State

This issue warns about global mutable state which makes code harder to test and reason about. Local mutable state within functions is perfectly acceptable in OCaml. Fix by either using local refs within functions, or preferably by using functional approaches with explicit state passing.

BAD

(* Global mutable state - avoid this *)
let counter = ref 0
let incr_counter () = counter := !counter + 1

let global_cache = Array.make 100 None
let cached_results = Hashtbl.create 100

GOOD

(* Local mutable state is fine *)
let compute_sum lst =
  let sum = ref 0 in
  List.iter (fun x -> sum := !sum + x) lst;
  !sum

(* Or better, use functional approach *)
let compute_sum lst = List.fold_left (+) 0 lst

(* Pass state explicitly *)
let incr_counter counter = counter + 1

Documentation

E400-E499 • Missing or incorrect documentation
E400 Missing Module Documentation

This issue means your modules lack documentation making them hard to understand. Fix it by adding module documentation at the top of .mli files with a brief summary and description of the module's purpose.

BAD

(* user.mli - no module doc *)
val create : string -> t

GOOD

(** User management module 
    
    Handles user creation and authentication. *)
val create : string -> t
E405 Missing Value Documentation

This issue means your public functions and values lack documentation making them hard to use. Fix it by adding documentation comments that explain what each function does, its parameters, and return value.

BAD

val parse : string -> t

GOOD

(** [parse str] converts a string to type [t].
    @raise Invalid_argument if [str] is malformed. *)
val parse : string -> t
E410 Documentation Style Issues

This issue means your documentation doesn't follow OCaml conventions making it inconsistent. Fix it by following the standard OCaml documentation format with proper syntax and structure.

BAD

(* this function parses strings *)
val parse : string -> t

GOOD

(** [parse str] parses a string into type [t]. *)
val parse : string -> t
E415 Missing Standard Functions

This issue means your types lack standard functions making them hard to use in collections and debugging. Fix it by implementing equal, compare, pp (pretty-printer), and to_string functions for your types.

BAD

type user = { id: int; name: string }
(* No standard functions *)

GOOD

type user = { id: int; name: string }
val equal : user -> user -> bool
val compare : user -> user -> int
val pp : Format.formatter -> user -> unit

Project Structure

E500-E599 • Project organization and configuration issues
E500 Missing Code Formatter

This issue means your project lacks consistent code formatting. Fix it by creating a .ocamlformat file in your project root with 'profile = default' and a version number to ensure consistent formatting.

BAD

(* No .ocamlformat file in project root *)

GOOD

(* .ocamlformat *)
profile = default
version = 0.26.2
E505 Missing Interface Files

This issue means your modules lack interface files making their public API unclear. Fix it by creating .mli files that document which functions and types should be public. Copy public signatures from the .ml file and remove private ones.

BAD

(* Only user.ml exists, no user.mli *)

GOOD

(* user.mli *)
type t
val create : string -> int -> t
val name : t -> string

Testing

E600-E699 • Test coverage and test quality issues
E600 Test Module Convention

This issue means your test files don't follow the expected convention for test organization. Fix it by exporting a 'suite' value instead of running tests directly, allowing better test composition and organization.

BAD

(* test_user.ml *)
let () = Alcotest.run "tests" [("user", tests)]

GOOD

(* test_user.ml *)
let suite = ("user", tests)
E605 Missing Test Coverage

This issue means some of your library modules lack test coverage making bugs more likely. Fix it by creating corresponding test files for each library module to ensure your code works correctly.

BAD

(* lib/parser.ml exists but no test/test_parser.ml *)

GOOD

(* test/test_parser.ml *)
let suite = ("parser", [test_parse; test_errors])
E610 Orphaned Test Files

This issue means you have test files that don't correspond to any library module making your test organization confusing. Fix it by either removing orphaned test files or creating the corresponding library modules.

BAD

(* test/test_old_feature.ml exists but lib/old_feature.ml was removed *)

GOOD

(* Remove test/test_old_feature.ml or restore lib/old_feature.ml *)
E615 Excluded Test Suites

This issue means some test suites aren't included in your main test runner so they never get executed. Fix it by adding them to the main test runner to ensure all tests are run during development.

BAD

(* test/test.ml *)
let () = Alcotest.run "all" [Test_user.suite] 
(* Missing Test_parser.suite *)

GOOD

(* test/test.ml *)
let () = Alcotest.run "all" [
  Test_user.suite;
  Test_parser.suite
]
↑ Top