• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Kakadu / zanuda / 93

04 Jul 2026 12:24PM UTC coverage: 85.254% (+0.1%) from 85.105%
93

push

github

Kakadu
CHANGES

Signed-off-by: Kakadu <Kakadu@pm.me>

2301 of 2699 relevant lines covered (85.25%)

603.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.68
/src/utils.ml
1
(** Various helper functions. *)
2

3
[@@@ocaml.text "/*"]
4

5
(** Copyright 2021-2026, Kakadu. *)
6

7
(** SPDX-License-Identifier: LGPL-3.0-or-later *)
8

9
[@@@ocaml.text "/*"]
10

11
type json = Yojson.Safe.t
12

13
let make_sarif_message ~loc ~filename ~msg arg : json =
14
  let jmsg = `Assoc [ "text", `String (Format.asprintf "%a" msg arg) ] in
3✔
15
  let jloc : Yojson.Safe.t =
16
    let physloc : Yojson.Safe.t =
17
      `Assoc
18
        [ "artifactLocation", `Assoc [ "uri", `String filename ]
19
        ; "region", `Assoc [ "startLine", `Int loc.Location.loc_start.Lexing.pos_lnum ]
20
        ]
21
    in
22
    `Assoc [ "physicalLocation", physloc ]
23
  in
24
  `Assoc [ "message", jmsg; "locations", `List [ jloc ] ]
25
;;
26

27
open Format
28

29
let printfn fmt = kfprintf (fun ppf -> fprintf ppf "\n%!") std_formatter fmt
3✔
30

31
module ErrorFormat = struct
32
  let pp ppf ~filename ~line ~col:_ msg x =
33
    fprintf ppf "%s:%d:%d:%a\n%!" filename line (* col *) 0 msg x
×
34
  ;;
35
end
36

37
type rdjsonl_code = string * string option
38

39
module RDJsonl : sig
40
  val pp
41
    :  formatter
42
    -> filename:string
43
    -> line:int
44
    -> ?code:rdjsonl_code
45
    -> (formatter -> 'a -> unit)
46
    -> 'a
47
    -> unit
48
end = struct
49
  let pp ppf ~filename ~line ?code msg x =
50
    let location file ~line ~col =
×
51
      `Assoc
×
52
        [ "path", `String file
53
        ; "range", `Assoc [ "start", `Assoc [ "line", `Int line; "column", `Int col ] ]
54
        ]
55
    in
56
    let j =
57
      `Assoc
58
        ([ "message", `String (asprintf "%a" msg x)
×
59
         ; "location", location filename ~line ~col:1
×
60
         ; "severity", `String "INFO"
61
         ]
62
         @
63
         match code with
64
         | None -> []
×
65
         | Some (desc, None) -> [ "code", `Assoc [ "value", `String desc ] ]
×
66
         | Some (desc, Some url) ->
×
67
           [ "code", `Assoc [ "value", `String desc; "url", `String url ] ])
68
    in
69
    fprintf ppf "%s\n%!" (Yojson.to_string j)
×
70
  ;;
71
  (* { "message": "Constructor 'XXX' has no documentation attribute",  "location": {    "path": "Lambda/lib/ast.mli",    "range": {      "start": { "line": 12, "column": 13 }, "end": { "line": 12, "column": 15      }    }  },  "severity": "INFO",  "code": {  "value": "RULE1",    "url": "https://example.com/url/to/super-lint/RULE1"  }}*)
72
end
73

74
let cut_build_dir s =
75
  let prefix = "_build/default/" in
142✔
76
  if String.starts_with ~prefix s
77
  then Base.String.drop_prefix s (String.length prefix)
34✔
78
  else s
108✔
79
;;
80

81
include (
82
struct
83
  open Location
84
  open Lexing
85

86
  type input_line =
87
    { text : string
88
    ; start_pos : int
89
    }
90

91
  let infer_line_numbers (lines : (int option * input_line) list)
92
    : (int option * input_line) list
93
    =
94
    let _, offset, consistent =
141✔
95
      List.fold_left
96
        (fun (i, offset, consistent) (lnum, _) ->
97
          match lnum, offset with
247✔
98
          | None, _ -> i + 1, offset, consistent
73✔
99
          | Some n, None -> i + 1, Some (n - i), consistent
141✔
100
          | Some n, Some m -> i + 1, offset, consistent && n = m + i)
33✔
101
        (0, None, true)
102
        lines
103
    in
104
    match offset, consistent with
141✔
105
    | Some m, true -> List.mapi (fun i (_, line) -> Some (m + i), line) lines
141✔
106
    | _, _ -> lines
×
107
  ;;
108

109
  module ISet : sig
110
    type 'a bound = 'a * int
111
    type 'a t
112

113
    (* bounds are included *)
114
    val of_intervals : ('a bound * 'a bound) list -> 'a t
115
    val mem : 'a t -> pos:int -> bool
116
    val find_bound_in : 'a t -> range:int * int -> 'a bound option
117
    val is_start : 'a t -> pos:int -> 'a option
118
    val is_end : 'a t -> pos:int -> 'a option
119
    val extrema : 'a t -> ('a bound * 'a bound) option
120
  end = struct
121
    type 'a bound = 'a * int
122

123
    (* non overlapping intervals *)
124
    type 'a t = ('a bound * 'a bound) list
125

126
    let of_intervals intervals =
127
      let pos =
141✔
128
        List.map
129
          (fun ((a, x), (b, y)) -> if x > y then [] else [ (a, x), `S; (b, y), `E ])
×
130
          intervals
131
        |> List.flatten
141✔
132
        |> List.sort (fun ((_, x), k) ((_, y), k') ->
141✔
133
          (* Make `S come before `E so that consecutive intervals get merged
134
             together in the fold below *)
135
          let kn = function
141✔
136
            | `S -> 0
141✔
137
            | `E -> 1
141✔
138
          in
139
          compare (x, kn k) (y, kn k'))
141✔
140
      in
141
      let nesting, acc =
141✔
142
        List.fold_left
143
          (fun (nesting, acc) (a, kind) ->
144
            match kind, nesting with
282✔
145
            | `S, `Outside -> `Inside (a, 0), acc
141✔
146
            | `S, `Inside (s, n) -> `Inside (s, n + 1), acc
×
147
            | `E, `Outside -> assert false
148
            | `E, `Inside (s, 0) -> `Outside, (s, a) :: acc
141✔
149
            | `E, `Inside (s, n) -> `Inside (s, n - 1), acc)
×
150
          (`Outside, [])
151
          pos
152
      in
153
      assert (nesting = `Outside);
141✔
154
      List.rev acc
155
    ;;
156

157
    let mem iset ~pos = List.exists (fun ((_, s), (_, e)) -> s <= pos && pos <= e) iset
5,065✔
158

159
    let find_bound_in iset ~range:(start, end_) =
160
      List.find_map
247✔
161
        (fun ((a, x), (b, y)) ->
162
          if start <= x && x <= end_
141✔
163
          then Some (a, x)
141✔
164
          else if start <= y && y <= end_
106✔
165
          then Some (b, y)
33✔
166
          else None)
73✔
167
        iset
168
    ;;
169

170
    let is_start iset ~pos =
171
      List.find_map (fun ((a, x), _) -> if pos = x then Some a else None) iset
108✔
172
    ;;
173

174
    let is_end iset ~pos =
175
      List.find_map (fun (_, (b, y)) -> if pos = y then Some b else None) iset
108✔
176
    ;;
177

178
    let extrema iset =
179
      if iset = [] then None else Some (fst (List.hd iset), snd (List.hd (List.rev iset)))
×
180
    ;;
181
  end
182

183
  (* The number of lines already printed after input.
184

185
     This is used by [highlight_terminfo] to identify the current position of the
186
     input in the terminal. This would not be possible without this information,
187
     since printing several warnings/errors adds text between the user input and
188
     the bottom of the terminal.
189

190
     We also use for {!is_first_report}, see below.
191
  *)
192
  let num_loc_lines = ref 0
193

194
  (* We use [num_loc_lines] to determine if the report about to be
195
     printed is the first or a follow-up report of the current
196
     "batch" -- contiguous reports without user input in between, for
197
     example for the current toplevel phrase. We use this to print
198
     a blank line between messages of the same batch.
199
  *)
200
  let is_first_message () = !num_loc_lines = 0
×
201

202
  (* This is used by the toplevel to reset [num_loc_lines] before each phrase *)
203
  let reset () = num_loc_lines := 0
×
204

205
  (* This is used by the toplevel *)
206
  let echo_eof () =
207
    print_newline ();
×
208
    incr num_loc_lines
×
209
  ;;
210

211
  (* Code printing errors and warnings must be wrapped using this function, in
212
     order to update [num_loc_lines].
213

214
     [print_updating_num_loc_lines ppf f arg] is equivalent to calling [f ppf
215
   arg], and additionally updates [num_loc_lines]. *)
216
  let print_updating_num_loc_lines ppf f arg =
217
    let open Format in
×
218
    let out_functions = pp_get_formatter_out_functions ppf () in
219
    let out_string str start len =
×
220
      let rec count i c =
×
221
        if i = start + len
×
222
        then c
×
223
        else if String.get str i = '\n'
×
224
        then count (succ i) (succ c)
×
225
        else count (succ i) c
×
226
      in
227
      num_loc_lines := !num_loc_lines + count start 0;
×
228
      out_functions.out_string str start len
229
    in
230
    pp_set_formatter_out_functions ppf { out_functions with out_string };
231
    f ppf arg;
×
232
    pp_print_flush ppf ();
×
233
    pp_set_formatter_out_functions ppf out_functions
×
234
  ;;
235

236
  (* let setup_tags () = Misc.Style.setup !Clflags.color *)
237

238
  (* module Fmt = Format_doc *)
239
  module Fmt = Format
240

241
  let pp_two_columns ?(sep = "|") ?max_lines ppf (lines : (string * string) list) =
×
242
    let left_column_size =
33✔
243
      List.fold_left (fun acc (s, _) -> Int.max acc (String.length s)) 0 lines
139✔
244
    in
245
    let lines_nb = List.length lines in
33✔
246
    let ellipsed_first, ellipsed_last =
33✔
247
      match max_lines with
248
      | Some max_lines when lines_nb > max_lines ->
33✔
249
        let printed_lines = max_lines - 1 in
2✔
250
        (* the ellipsis uses one line *)
251
        let lines_before = (printed_lines / 2) + (printed_lines mod 2) in
252
        let lines_after = printed_lines / 2 in
253
        lines_before, lines_nb - lines_after - 1
254
      | _ -> -1, -1
31✔
255
    in
256
    Format.fprintf ppf "@[<v>";
257
    List.iteri
33✔
258
      (fun k (line_l, line_r) ->
259
        if k = ellipsed_first then Format.fprintf ppf "...@,";
2✔
260
        if ellipsed_first <= k && k <= ellipsed_last
129✔
261
        then ()
16✔
262
        else Format.fprintf ppf "%*s %s %s@," left_column_size line_l sep line_r)
123✔
263
      lines;
264
    Format.fprintf ppf "@]"
33✔
265
  ;;
266

267
  (* [get_lines] must return the lines to highlight, given starting and ending
268
     positions.
269

270
     See [lines_around_from_current_input] below for an instantiation of
271
     [get_lines] that reads from the current input.
272
  *)
273
  let highlight_quote
274
        ppf
275
        ~(get_lines : start_pos:position -> end_pos:position -> input_line list)
276
        ?(max_lines = 10)
141✔
277
        highlight_tag
278
        locs
279
    =
280
    let iset =
141✔
281
      ISet.of_intervals
282
      @@ List.filter_map
141✔
283
           (fun loc ->
284
             let s, e = loc.loc_start, loc.loc_end in
141✔
285
             if s.pos_cnum = -1 || e.pos_cnum = -1
×
286
             then None
×
287
             else Some ((s, s.pos_cnum), (e, e.pos_cnum - 1)))
141✔
288
           locs
289
    in
290
    match ISet.extrema iset with
141✔
291
    | None -> ()
×
292
    | Some ((leftmost, _), (rightmost, _)) ->
141✔
293
      let lines =
294
        get_lines ~start_pos:leftmost ~end_pos:rightmost
295
        |> List.map (fun ({ text; start_pos } as line) ->
296
          let end_pos = start_pos + String.length text - 1 in
247✔
297
          let line_nb =
298
            match ISet.find_bound_in iset ~range:(start_pos, end_pos) with
299
            | None -> None
73✔
300
            | Some (p, _) -> Some p.pos_lnum
174✔
301
          in
302
          line_nb, line)
303
        |> infer_line_numbers
141✔
304
        |> List.map (fun (lnum, { text; start_pos }) ->
141✔
305
          text, Option.fold ~some:Int.to_string ~none:"" lnum, start_pos)
247✔
306
      in
307
      Fmt.fprintf ppf "@[<v>";
141✔
308
      (match lines with
141✔
309
       | [] | [ ("", _, _) ] -> ()
×
310
       | [ (line, line_nb, line_start_cnum) ] ->
108✔
311
         (* Single-line error *)
312
         Fmt.fprintf ppf "%s | %s@," line_nb line;
313
         Fmt.fprintf ppf "%*s   " (String.length line_nb) "";
108✔
314
         (* Iterate up to [rightmost], which can be larger than the length of
315
            the line because we may point to a location after the end of the
316
            last token on the line, for instance:
317
            {[
318
              token
319
                        ^
320
              Did you forget ...
321
            ]} *)
322
         for i = 0 to rightmost.pos_cnum - line_start_cnum - 1 do
108✔
323
           let pos = line_start_cnum + i in
3,531✔
324
           if ISet.is_start iset ~pos <> None then Fmt.fprintf ppf "@{<%s>" highlight_tag;
108✔
325
           if ISet.mem iset ~pos
3,531✔
326
           then Fmt.pp_print_char ppf '^'
2,479✔
327
           else if i < String.length line
1,052✔
328
           then
329
             (* For alignment purposes, align using a tab for each tab in the
330
                source code *)
331
             if line.[i] = '\t'
1,052✔
332
             then Fmt.pp_print_char ppf '\t'
×
333
             else Fmt.pp_print_char ppf ' ';
1,052✔
334
           if ISet.is_end iset ~pos <> None then Fmt.fprintf ppf "@}"
108✔
335
         done;
336
         Fmt.fprintf ppf "@}@,"
108✔
337
       | _ ->
33✔
338
         (* Printf.eprintf "%s. %s %d\n%!" __FUNCTION__ __FILE__ __LINE__; *)
339
         (* Multi-line error *)
340
         pp_two_columns ~sep:"|" ~max_lines ppf
33✔
341
         @@ List.map
33✔
342
              (fun (line, line_nb, line_start_cnum) ->
343
                let line =
139✔
344
                  String.mapi
345
                    (fun i car ->
346
                      if ISet.mem iset ~pos:(line_start_cnum + i) then car else '.')
176✔
347
                    line
348
                in
349
                line_nb, line)
139✔
350
              lines);
351
      Fmt.fprintf ppf "@]"
352
  ;;
353

354
  let lines_around
355
        ~(start_pos : position)
356
        ~(end_pos : position)
357
        ~(seek : int -> unit)
358
        ~(read_char : unit -> char option)
359
    : input_line list
360
    =
361
    seek start_pos.pos_bol;
141✔
362
    let lines = ref [] in
141✔
363
    let bol = ref start_pos.pos_bol in
364
    let cur = ref start_pos.pos_bol in
365
    let b = Buffer.create 80 in
366
    let add_line () =
141✔
367
      if !bol < !cur
247✔
368
      then (
247✔
369
        let text = Buffer.contents b in
370
        Buffer.clear b;
247✔
371
        lines := { text; start_pos = !bol } :: !lines;
247✔
372
        bol := !cur)
373
    in
374
    let rec loop () =
375
      if !bol >= end_pos.pos_cnum
6,972✔
376
      then ()
137✔
377
      else (
6,835✔
378
        match read_char () with
379
        | None ->
4✔
380
          (* end of input *)
381
          add_line ()
382
        | Some c ->
6,831✔
383
          incr cur;
384
          (match c with
6,831✔
385
           | '\r' -> loop ()
×
386
           | '\n' ->
243✔
387
             add_line ();
388
             loop ()
243✔
389
           | _ ->
6,588✔
390
             Buffer.add_char b c;
391
             loop ()))
6,588✔
392
    in
393
    loop ();
394
    List.rev !lines
141✔
395
  ;;
396

397
  (* Attempt to get lines from the lexing buffer. *)
398
  let lines_around_from_lexbuf ~(start_pos : position) ~(end_pos : position) (lb : lexbuf)
399
    : input_line list
400
    =
401
    (* Printf.eprintf "%s lexbuf.len = %d\n%!" __FUNCTION__ lb.Lexing.lex_buffer_len; *)
402
    (* Converts a global position to one that is relative to the lexing buffer *)
403
    let rel n = n - lb.lex_abs_pos in
141✔
404
    if rel start_pos.pos_bol < 0
141✔
405
    then
406
      (* Do nothing if the buffer does not contain the input (because it has been
407
         refilled while lexing it) *)
408
      []
×
409
    else (
141✔
410
      let pos = ref 0 in
411
      (* relative position *)
412
      let seek n = pos := rel n in
141✔
413
      let read_char () =
414
        if !pos >= lb.lex_buffer_len
6,835✔
415
        then (* end of buffer *) None
4✔
416
        else (
6,831✔
417
          let c = Bytes.get lb.lex_buffer !pos in
418
          incr pos;
6,831✔
419
          Some c)
6,831✔
420
      in
421
      lines_around ~start_pos ~end_pos ~seek ~read_char)
422
  ;;
423

424
  (* Attempt to get lines from the phrase buffer *)
425
  let lines_around_from_phrasebuf
426
        ~(start_pos : position)
427
        ~(end_pos : position)
428
        (pb : Buffer.t)
429
    : input_line list
430
    =
431
    let pos = ref 0 in
×
432
    let seek n = pos := n in
×
433
    let read_char () =
434
      if !pos >= Buffer.length pb
×
435
      then None
×
436
      else (
×
437
        let c = Buffer.nth pb !pos in
438
        incr pos;
×
439
        Some c)
×
440
    in
441
    lines_around ~start_pos ~end_pos ~seek ~read_char
442
  ;;
443

444
  (* A [get_lines] function for [highlight_quote] that reads from the current
445
     input. *)
446
  let lines_around_from_current_input ~start_pos ~end_pos =
447
    match !Location.input_lexbuf, !Location.input_phrase_buffer, !Location.input_name with
141✔
448
    | _, Some pb, "//toplevel//" -> lines_around_from_phrasebuf pb ~start_pos ~end_pos
×
449
    | Some lb, _, _ ->
141✔
450
      let xs = lines_around_from_lexbuf lb ~start_pos ~end_pos in
451
      (* Printf.eprintf "lines_around_from_lexbuf return list len %d\n%!" (List.length xs); *)
452
      xs
141✔
453
    | None, _, _ -> []
×
454
  ;;
455

456
  let is_dummy_loc loc =
457
    (* Fixme: this should be just [loc.loc_ghost] and the function should be
458
       inlined below. However, currently, the compiler emits in some places ghost
459
       locations with valid ranges that should still be printed. These locations
460
       should be made non-ghost -- in the meantime we just check if the ranges are
461
       valid. *)
462
    loc.loc_start.pos_cnum = -1 || loc.loc_end.pos_cnum = -1
×
463
  ;;
464

465
  let is_quotable_loc loc =
466
    (not (is_dummy_loc loc))
142✔
467
    && loc.loc_start.pos_fname = !input_name
141✔
468
    && loc.loc_end.pos_fname = !input_name
141✔
469
  ;;
470

471
  let report_printer ppf loc f x =
472
    let highlight ppf loc =
142✔
473
      if is_quotable_loc loc
142✔
474
      then
475
        highlight_quote ppf ~get_lines:lines_around_from_current_input "warning" [ loc ]
141✔
476
    in
477
    Format.fprintf ppf "@[<v>%a:@ %a@]" print_loc loc highlight loc;
478
    Format.fprintf ppf "@[Alert zanuda-linter: @[%a@]@]@," f x;
142✔
479
    Format.fprintf ppf "%!"
142✔
480
  ;;
481
end :
482
sig
483
  val report_printer : formatter -> Location.t -> (formatter -> 'a -> unit) -> 'a -> unit
484
end)
485

486
module Report = struct
487
  let txt ~loc ~filename ppf msg msg_arg =
488
    if Sys.file_exists filename
143✔
489
    then (
142✔
490
      Location.input_name := cut_build_dir filename;
142✔
491
      Clflags.error_style := Some Misc.Error_style.Contextual;
492
      let file_contents = In_channel.with_open_text filename In_channel.input_all in
493
      Location.input_lexbuf := Some (Lexing.from_string file_contents);
142✔
494
      let loc =
495
        let open Location in
496
        { loc with
497
          loc_start = { loc.loc_start with pos_fname = !input_name }
498
        ; loc_end = { loc.loc_end with pos_fname = !input_name }
499
        }
500
      in
501
      report_printer ppf loc msg msg_arg)
502
    else (
1✔
503
      Format.fprintf ppf "@[Alert zanuda-linter: @[%a@]@]@," msg msg_arg;
504
      Format.fprintf ppf "%!")
1✔
505
  ;;
506

507
  let rdjsonl ~loc ~filename ~code ppf msg msg_arg =
508
    let code = code, Some "https://kakadu.github.io/zanuda/" in
×
509
    RDJsonl.pp ppf ~filename ~line:loc.Location.loc_start.pos_lnum ~code msg msg_arg
510
  ;;
511

512
  let sarif ~loc ~filename ~msg arg = Some (make_sarif_message ~loc ~filename ~msg arg)
3✔
513
end
514

515
let make_reporter ?(is_good = fun _ -> true) lint_id msg ~loc ~filename info =
133✔
516
  (* TODO: make filename optional and reconstruct it from [loc] *)
517
  let module M = struct
150✔
518
    let txt ppf () = if is_good filename then Report.txt ~loc ~filename ppf msg info
143✔
519
    let is_valid () = is_good filename
3✔
520

521
    let rdjsonl ppf () =
522
      if is_good filename
×
523
      then
524
        Report.rdjsonl
×
525
          ~loc
526
          ~code:lint_id
527
          ppf
528
          ~filename:(Config.recover_filepath loc.loc_start.pos_fname)
×
529
          msg
530
          info
531
    ;;
532

533
    let sarif () =
534
      Report.sarif
3✔
535
        ~loc
536
        ~filename:(Config.recover_filepath loc.loc_start.pos_fname)
3✔
537
        ~msg
538
        info
539
    ;;
540
  end
541
  in
542
  (module M : LINT.REPORTER)
543
;;
544

545
let string_of_group : LINT.group -> string = function
546
  | LINT.Correctness -> "correctness"
6✔
547
  | Style -> "style"
22✔
548
  | Perf -> "perf"
4✔
549
  | Restriction -> "restriction"
×
550
  | Deprecated -> "deprecated"
×
551
  | Pedantic -> "pedantic"
×
552
  | Complexity -> "complexity"
×
553
  | Suspicious -> "suspicious"
3✔
554
  | Nursery -> "nursery"
1✔
555
;;
556

557
let string_of_level : LINT.level -> string = function
558
  | LINT.Allow -> "allow"
1✔
559
  | Warn -> "warn"
15✔
560
  | Deny -> "deny"
20✔
561
  | Deprecated -> "deprecated"
×
562
;;
563

564
let string_of_impl = function
565
  | LINT.Typed -> "typed"
26✔
566
  | _ -> "untyped"
10✔
567
;;
568

569
let describe_as_clippy_json
570
      ?(group = LINT.Correctness)
6✔
571
      ?(level = LINT.Deny)
19✔
572
      ?(impl = LINT.Typed)
26✔
573
      id
574
      ~docs
575
  : Yojson.Safe.t
576
  =
577
  (* List if clippy lints https://github.com/rust-lang/rust-clippy/blob/gh-pages/master/lints.json *)
578
  `Assoc
36✔
579
    [ "id", `String id
580
    ; "group", `String (string_of_group group)
36✔
581
    ; "level", `String (string_of_level level)
36✔
582
    ; "impl", `String (string_of_impl impl)
36✔
583
    ; "docs", `String docs
584
    ; ( "applicability"
585
      , `Assoc
586
          [ "is_multi_part_suggestion", `Bool false
587
          ; "applicability", `String "Unresolved"
588
          ] )
589
    ]
590
;;
591

592
exception Ident_is_found of Location.t
593

594
let no_ident_iterator ident =
595
  let open Tast_iterator in
76✔
596
  let open Typedtree in
597
  { default_iterator with
598
    expr =
599
      (fun self e ->
600
        let rec ident_in_list = function
1,348✔
601
          | [] -> false
157✔
602
          | (_, (id, _)) :: _ when Ident.equal id ident -> true
1✔
603
          | _ :: tl -> ident_in_list tl
210✔
604
        in
605
        Tast_pattern.(
606
          let p1 =
607
            map2 (texp_function_body __ __) ~f:(fun args rhs -> `Function (args, rhs))
158✔
608
          in
609
          let p2 = map1 (texp_ident __) ~f:(fun x -> `Ident x) in
532✔
610
          parse
1,348✔
611
            (p1 ||| p2)
1,348✔
612
            (* TODO: should we check other patterns? *)
613
            e.exp_loc
614
            e
615
            ~on_error:(fun _ -> default_iterator.expr self e))
658✔
616
          (function
617
          | `Function (args, _rhs) when ident_in_list args -> ()
1✔
618
          | `Function (_, rhs) -> self.expr self rhs
157✔
619
          | `Ident (Pident id) when Ident.same id ident ->
347✔
620
            raise_notrace (Ident_is_found e.exp_loc)
12✔
621
          | _ -> default_iterator.expr self e))
520✔
622
  ; case =
623
      (fun (type a) self (c : a case) ->
624
        match c.c_lhs.pat_desc with
74✔
625
        | Tpat_value v ->
12✔
626
          (match (v :> pattern) with
627
           | p ->
12✔
628
             Tast_pattern.(
629
               parse
630
                 (tpat_id __)
12✔
631
                 Location.none
632
                 p
633
                 ~on_error:(fun _ -> default_iterator.case self c)
12✔
634
                 (fun id ->
635
                   if Ident.equal ident id then () else default_iterator.case self c)))
×
636
        | _ -> default_iterator.case self c)
62✔
637
  }
638
;;
639

640
(* Checks that identifier is not used *)
641
let no_ident ident f =
642
  try
31✔
643
    f (no_ident_iterator ident);
31✔
644
    true
30✔
645
  with
646
  | Ident_is_found _loc -> false
1✔
647
;;
648

649
let has_ident ident f = not (no_ident ident f)
×
650

651
[%%if ocaml_version < (5, 0, 0)]
652

653
type intf_or_impl =
654
  | Intf
655
  | Impl
656

657
let with_info _kind ~source_file f =
658
  Compile_common.with_info
147✔
659
    ~native:false
660
    ~source_file
661
    ~tool_name:"asdf" (* TODO: pass right tool name *)
662
    ~output_prefix:"asdf"
663
    ~dump_ext:"asdf"
664
    f
665
;;
666

667
[%%else]
668

669
type intf_or_impl = Unit_info.intf_or_impl
670

671
let with_info kind ~source_file =
672
  Compile_common.with_info
673
    ~native:false
674
    ~tool_name:"asdf" (* TODO: pass right tool name *)
675
    ~dump_ext:"asdf"
676
    (Unit_info.make ~source_file kind "")
677
;;
678

679
[%%endif]
680
[%%if ocaml_version < (5, 0, 0)]
681

682
let pp_path = Path.print
683

684
[%%else]
685

686
let pp_path = Format_doc.compat Path.print
687

688
[%%endif]
689
[%%if ocaml_version < (5, 0, 0)]
690

691
let source_of_info info = info.Compile_common.source_file
145✔
692

693
[%%else]
694

695
let source_of_info info = Unit_info.source_file info.Compile_common.target
696

697
[%%endif]
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc