Find and remove dead code and unused APIs in OCaml projects
1(* Module alias detection for OCaml code *)
2
3module Log =
4 (val Logs.src_log (Logs.Src.create "prune.module_alias") : Logs.LOG)
5
6(* Helper for matching OCaml whitespace (space, tab, newline) *)
7let ws = Re.(rep (alt [ space; char '\n'; char '\r' ]))
8let ws1 = Re.(rep1 (alt [ space; char '\n'; char '\r' ]))
9
10(* Helper for matching OCaml module names - start with uppercase or underscore,
11 then alphanumeric/underscore/apostrophe *)
12let module_name =
13 Re.(
14 seq
15 [ alt [ rg 'A' 'Z'; char '_' ]; rep (alt [ alnum; char '_'; char '\'' ]) ])
16
17(* Regular expressions for module alias detection *)
18let module_type_alias_re =
19 Re.compile
20 Re.(
21 seq
22 [
23 (* "module type" at the start of the line *)
24 bos;
25 str "module";
26 ws1;
27 str "type";
28 ws1;
29 (* Module type name *)
30 group module_name;
31 ws;
32 str "=";
33 ws;
34 (* The aliased module *)
35 group module_name;
36 (* Optional .T or similar *)
37 opt (seq [ char '.'; module_name ]);
38 ])
39
40let module_include_alias_re =
41 Re.compile
42 Re.(
43 seq
44 [
45 (* Can be "sig" or start of line *)
46 alt [ str "sig"; bos ];
47 ws;
48 (* "include module type of" *)
49 str "include";
50 ws1;
51 str "module";
52 ws1;
53 str "type";
54 ws1;
55 str "of";
56 ws1;
57 (* The module being included *)
58 group module_name;
59 ])
60
61(* Check if a module declaration in a .mli file is a module type alias *)
62let is_module_type_alias content col =
63 let lines = String.split_on_char '\n' content in
64 (* Find the line containing this column position *)
65 let rec find_line lines_before col_offset = function
66 | [] -> None
67 | line :: rest ->
68 let line_length = String.length line + 1 in
69 (* +1 for newline *)
70 if col_offset < line_length then
71 (* Found the line - check if it's an alias *)
72 Some
73 (Re.execp module_type_alias_re line
74 || Re.execp module_include_alias_re line)
75 else find_line (line :: lines_before) (col_offset - line_length) rest
76 in
77 match find_line [] col lines with None -> false | Some result -> result
78
79(* Check if a multi-line module signature contains 'include module type of' *)
80let is_multiline_alias ~cache file start_line end_line_opt =
81 let max_lines_to_check = 20 in
82 (* Reasonable limit for module signatures *)
83 let end_line =
84 match end_line_opt with
85 | Some el -> min el (start_line + max_lines_to_check)
86 | None -> start_line + max_lines_to_check
87 in
88 (* Collect lines and check for the pattern *)
89 let rec collect_lines acc line_num =
90 if line_num > end_line then String.concat " " (List.rev acc)
91 else
92 match Cache.line cache file line_num with
93 | None -> String.concat " " (List.rev acc)
94 | Some line -> collect_lines (String.trim line :: acc) (line_num + 1)
95 in
96 let content = collect_lines [] start_line in
97 Re.execp module_include_alias_re content
98
99(* Check if a module is an alias (interface files only) *)
100let is_module_alias ~cache file symbol_kind loc content =
101 match symbol_kind with
102 | Types.Module when Filename.check_suffix file ".mli" ->
103 (* For .mli files, check if it's a module type alias or uses 'include
104 module type of' *)
105 is_module_type_alias content loc.Types.start_col
106 || is_multiline_alias ~cache file loc.Types.start_line
107 (Some loc.Types.end_line)
108 | _ -> false