Find and remove dead code and unused APIs in OCaml projects
at main 108 lines 3.6 kB view raw
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