Find and remove dead code and unused APIs in OCaml projects
at main 397 lines 13 kB view raw
1(* Warning parsing and handling for prune *) 2 3module Log = (val Logs.src_log (Logs.Src.create "prune.warning") : Logs.LOG) 4 5(* Create regex patterns for warning location parsing *) 6let single_line_pattern = 7 Re.( 8 compile 9 (seq 10 [ 11 bos; 12 str "File \""; 13 group (rep1 (compl [ char '"' ])); 14 str "\", line "; 15 group (rep1 digit); 16 str ", characters "; 17 group (rep1 digit); 18 str "-"; 19 group (rep1 digit); 20 str ":"; 21 ])) 22 23let multi_line_pattern = 24 Re.( 25 compile 26 (seq 27 [ 28 bos; 29 str "File \""; 30 group (rep1 (compl [ char '"' ])); 31 str "\", lines "; 32 group (rep1 digit); 33 str "-"; 34 group (rep1 digit); 35 str ", characters "; 36 group (rep1 digit); 37 str "-"; 38 group (rep1 digit); 39 str ":"; 40 ])) 41 42(* Parse single line warning format *) 43let parse_single_line line = 44 try 45 let groups = Re.exec ~pos:0 single_line_pattern line in 46 let file = Re.Group.get groups 1 in 47 let line = int_of_string (Re.Group.get groups 2) in 48 let start_col = int_of_string (Re.Group.get groups 3) in 49 let end_col = int_of_string (Re.Group.get groups 4) in 50 Some (Types.location file ~line ~start_col ~end_col) 51 with Not_found -> None 52 53(* Parse multi-line warning format *) 54let parse_multi_line line = 55 try 56 let groups = Re.exec ~pos:0 multi_line_pattern line in 57 let file = Re.Group.get groups 1 in 58 let start_line = int_of_string (Re.Group.get groups 2) in 59 let end_line = int_of_string (Re.Group.get groups 3) in 60 let start_col = int_of_string (Re.Group.get groups 4) in 61 let end_col = int_of_string (Re.Group.get groups 5) in 62 Some (Types.location file ~line:start_line ~start_col ~end_line ~end_col) 63 with Not_found -> None 64 65(* Parse warning 32/34 location from build output line *) 66let parse_warning_line line = 67 (* Examples: File "lib/prune.ml", line 15, characters 4-17: File 68 "lib/brui.mli", lines 5-6, characters 2-80: *) 69 let line = String.trim line in 70 match parse_single_line line with 71 | Some location -> Some location 72 | None -> parse_multi_line line 73 74(* Parse warning 32/33/34/69 symbol name and type from warning message *) 75(* Helper to create regex for standard unused pattern *) 76let unused_pattern prefix = 77 (* For warnings that end with a dot, capture everything up to the final dot *) 78 Re.( 79 seq 80 [ 81 str prefix; 82 group (rep1 (alt [ alnum; char '_'; char '.'; char '\'' ])); 83 char '.'; 84 ]) 85 86(* Helper to create regex for unused field pattern *) 87let unused_field_pattern () = 88 Re.( 89 alt 90 [ 91 (* Pattern for regular unused fields *) 92 seq 93 [ 94 rep (compl [ char ':' ]); 95 (* Skip everything before colon *) 96 str ":"; 97 space; 98 str "record field "; 99 group (rep1 (alt [ alnum; char '_' ])); 100 str " is never read"; 101 ]; 102 (* Pattern for mutable fields that are never mutated *) 103 seq 104 [ 105 rep (compl [ char ':' ]); 106 (* Skip everything before colon *) 107 str ":"; 108 space; 109 str "mutable record field "; 110 group (rep1 (alt [ alnum; char '_' ])); 111 str " is never mutated"; 112 ]; 113 ]) 114 115(* Helper to create regex for warning name extraction *) 116let name_regex warning_num = 117 Re.compile 118 (match warning_num with 119 | "32" -> unused_pattern ": unused value " 120 | "33" -> unused_pattern ": unused open " 121 | "34" -> unused_pattern ": unused type " 122 | "37" -> unused_pattern ": unused constructor " 123 | "38" -> 124 Re.( 125 seq 126 [ 127 str ": unused exception "; 128 group (rep1 (compl [ char '.' ])); 129 opt (char '.'); 130 ]) 131 | "69" -> unused_field_pattern () 132 | _ -> failwith "Unexpected warning number") 133 134(* Helper to extract module name from qualified name *) 135let extract_module_name warning_num raw_name = 136 match warning_num with 137 | "33" -> 138 (* Extract the last component of a qualified module name *) 139 let parts = String.split_on_char '.' raw_name in 140 List.hd (List.rev parts) 141 | _ -> raw_name 142 143(* Helper to get warning type from number *) 144let type_of_number = function 145 | "32" -> Types.Unused_value 146 | "33" -> Types.Unused_open 147 | "34" -> Types.Unused_type 148 | "37" -> Types.Unused_constructor 149 | "38" -> Types.Unused_exception 150 | "69" -> Types.Unused_field 151 | n -> Fmt.failwith "Unexpected warning number: %s" n 152 153let parse_warning_name line = 154 (* Parse name and type from warning messages *) 155 (* First extract the warning number using Re DSL *) 156 let num_re = 157 Re.( 158 compile 159 (seq 160 [ 161 alt [ str "Warning"; seq [ str "Error"; space; str "(warning" ] ]; 162 space; 163 group 164 (alt 165 [ str "32"; str "33"; str "34"; str "37"; str "38"; str "69" ]); 166 (* Match optional space, bracket, or other characters after warning 167 number *) 168 rep (compl [ char ':' ]); 169 ])) 170 in 171 172 try 173 let num_groups = Re.exec num_re line in 174 let warning_num = Re.Group.get num_groups 1 in 175 176 (* Then extract the name based on warning type *) 177 let name_re = name_regex warning_num in 178 let name_groups = Re.exec name_re line in 179 (* For warning 69, we need to find which group has the field name *) 180 let raw_name = 181 if warning_num = "69" then 182 (* Try group 1 first (regular field), then group 2 (mutable field) *) 183 try Re.Group.get name_groups 1 184 with Not_found -> Re.Group.get name_groups 2 185 else Re.Group.get name_groups 1 186 in 187 188 (* Extract final name and warning type *) 189 let name = extract_module_name warning_num raw_name in 190 let warning_type = 191 if warning_num = "69" then 192 (* Check if it's "never mutated" vs "never read" *) 193 if Re.execp (Re.compile (Re.str "is never mutated")) line then 194 Types.Unnecessary_mutable 195 else Types.Unused_field 196 else type_of_number warning_num 197 in 198 199 Some (name, warning_type) 200 with Not_found -> None 201 202(* Create warning info from parsed components *) 203let v location name warning_type = 204 { 205 Types.location; 206 name; 207 warning_type; 208 location_precision = Types.precision_of_warning_type warning_type; 209 } 210 211(* Parse signature mismatch errors from build output *) 212(* Extract signature name from error line *) 213let extract_signature_name line = 214 let value_required_re = 215 Re.( 216 compile 217 (seq 218 [ 219 str "The value "; 220 opt (char '"'); 221 group (rep1 (compl [ space; char '"' ])); 222 opt (char '"'); 223 str " is required but not provided"; 224 ])) 225 in 226 try 227 let groups = Re.exec value_required_re line in 228 Some (Re.Group.get groups 1) 229 with Not_found -> None 230 231(* Find location in the next few lines *) 232let mli_location lines_to_check = 233 let rec search = function 234 | [] -> None 235 | loc_line :: more -> ( 236 match parse_warning_line loc_line with 237 | Some location when String.ends_with ~suffix:".mli" location.file -> 238 Some location 239 | _ -> search more) 240 in 241 search lines_to_check 242 243(* Get next few lines to search for location *) 244let next_lines rest = 245 match rest with l1 :: l2 :: l3 :: _ -> [ l1; l2; l3 ] | lines -> lines 246 247(* Create pairs of (line, remaining_lines) *) 248let line_pairs lines = 249 let rec make_pairs = function 250 | [] -> [] 251 | line :: rest -> (line, rest) :: make_pairs rest 252 in 253 make_pairs lines 254 255(* Process a single line for signature mismatch *) 256let process_signature_line line rest = 257 match extract_signature_name line with 258 | None -> [] 259 | Some name -> ( 260 let next_lines = next_lines rest in 261 match mli_location next_lines with 262 | Some location -> 263 let warning = v location name Types.Signature_mismatch in 264 [ warning ] 265 | None -> []) 266 267let parse_signature_mismatch_error lines = 268 (* Look for pattern: Error: The implementation "lib/base.ml" does not match 269 the interface "lib/base.ml": The value "missing_func" is required but not 270 provided File "lib/base.mli", line 2, characters 0-35: Expected 271 declaration *) 272 let rec find_error = function 273 | [] -> [] 274 | (line, rest) :: remaining_pairs -> 275 let warnings = process_signature_line line rest in 276 warnings @ find_error remaining_pairs 277 in 278 find_error (line_pairs lines) 279 280(* Parse warnings using a simpler approach - scan for all warning messages 281 first, then match with locations *) 282let parse_all_from_lines lines = 283 let rec find_warnings acc lines_with_idx = 284 match lines_with_idx with 285 | [] -> List.rev acc 286 | (line, idx) :: rest -> ( 287 (* Look for warning pattern in current line *) 288 match parse_warning_name line with 289 | Some (name, warning_type) -> ( 290 Log.debug (fun m -> 291 m "Found warning '%s' type %s on line %d: %s" name 292 (Fmt.str "%a" Types.pp_warning_type warning_type) 293 idx line); 294 (* Found a warning, now search backwards for the corresponding 295 location *) 296 let rec find_location search_idx = 297 if search_idx < 0 then None 298 else 299 let search_line = List.nth lines search_idx in 300 match parse_warning_line search_line with 301 | Some location -> Some location 302 | None -> 303 if search_idx > 0 then find_location (search_idx - 1) 304 else None 305 in 306 match find_location (idx - 1) with 307 | Some location -> 308 let warning = v location name warning_type in 309 find_warnings (warning :: acc) rest 310 | None -> 311 Log.debug (fun m -> 312 m 313 "Warning %s (type %s) found at line %d but no location \ 314 found before it" 315 name 316 (Fmt.str "%a" Types.pp_warning_type warning_type) 317 idx); 318 find_warnings acc rest) 319 | None -> find_warnings acc rest) 320 in 321 let indexed_lines = List.mapi (fun i line -> (line, i)) lines in 322 find_warnings [] indexed_lines 323 324(* Parse unbound record field errors from build output *) 325let parse_unbound_field_error lines = 326 (* Look for pattern: Error: Unbound record field address *) 327 let unbound_field_re = 328 Re.( 329 compile 330 (seq 331 [ 332 str "Error: Unbound record field"; 333 rep (alt [ space; char '\t' ]); 334 opt (char '"'); 335 group 336 (seq 337 [ 338 alt [ alpha; char '_' ]; 339 rep (alt [ alnum; char '_'; char '\'' ]); 340 ]); 341 opt (char '"'); 342 ])) 343 in 344 let rec find_error line_and_idx_pairs acc = 345 match line_and_idx_pairs with 346 | [] -> acc 347 | (line, idx) :: remaining_pairs -> ( 348 try 349 let groups = Re.exec unbound_field_re line in 350 let field_name = Re.Group.get groups 1 in 351 352 (* Look backwards for the file location (it comes before the error) *) 353 let rec find_location i = 354 if i <= 0 then None 355 else 356 let prev_line = List.nth lines (i - 1) in 357 match parse_warning_line prev_line with 358 | Some location -> 359 let warning = v location field_name Types.Unbound_field in 360 Some warning 361 | None -> if i > 1 then find_location (i - 1) else None 362 in 363 match find_location idx with 364 | Some warning -> find_error remaining_pairs (warning :: acc) 365 | None -> find_error remaining_pairs acc 366 with Not_found -> find_error remaining_pairs acc) 367 in 368 (* Create pairs of (line, index) for easier processing *) 369 let indexed_lines = List.mapi (fun i line -> (line, i)) lines in 370 find_error indexed_lines [] 371 372(* Parse all warning 32/33/34/69 messages and signature mismatches from build 373 output *) 374let parse output = 375 let lines = String.split_on_char '\n' output in 376 (* Parse all types of warnings/errors *) 377 let warnings = parse_all_from_lines lines in 378 let sig_mismatches = parse_signature_mismatch_error lines in 379 let unbound_fields = parse_unbound_field_error lines in 380 381 let all_warnings = warnings @ sig_mismatches @ unbound_fields in 382 383 (* Single summary log if we found anything *) 384 if all_warnings <> [] then ( 385 Log.debug (fun m -> 386 m "Parsed %d warnings/errors from %d lines" (List.length all_warnings) 387 (List.length lines)); 388 List.iter 389 (fun (w : Types.warning_info) -> 390 Log.debug (fun m -> 391 m " %a: %a %s" Types.pp_location w.location Types.pp_warning_type 392 w.warning_type w.name)) 393 all_warnings) 394 else 395 Log.debug (fun m -> 396 m "No warnings found in %d lines of output" (List.length lines)); 397 all_warnings