Find and remove dead code and unused APIs in OCaml projects
at main 413 lines 14 kB view raw
1(* AST-based location finding using compiler-libs *) 2 3module T = Types 4open Parsetree 5module Log = (val Logs.src_log (Logs.Src.create "prune.locate") : Logs.LOG) 6 7(* Type definitions *) 8 9type field_info = { 10 field_name : string; 11 full_field_bounds : T.location; (* Full bounds including everything *) 12 enclosing_record : T.location; (* Location of the enclosing record {} *) 13 total_fields : int; (* Total number of fields in the record *) 14 context : [ `Type_definition | `Record_construction ]; 15} 16 17type type_def_info = { 18 type_name : string; 19 type_keyword_loc : T.location; 20 equals_loc : T.location option; 21 kind : [ `Abstract | `Record | `Variant | `Alias ]; 22 full_bounds : T.location; 23} 24 25(* Exceptions for early termination in visitors *) 26exception Found_field of field_info 27exception Found_type_def of type_def_info 28exception Found_location of T.location 29 30(* Error helper functions *) 31let err fmt = Fmt.kstr (fun e -> Error (`Msg e)) fmt 32let err_expected_impl file = err "Expected implementation file: %s" file 33let err_expected_intf file = err "Expected interface file: %s" file 34let err_field_not_found = err "Field not found at position" 35let err_no_type_def = err "No type definition found at position" 36let err_no_sig_item = err "No signature item found at position" 37let err_no_struct_item = err "No structure item found at position" 38let err_no_value_binding = err "No value binding found at position" 39let err_no_enclosing_record = err "Could not find enclosing record" 40 41(* Basic utilities *) 42 43let rec longident_last = function 44 | Longident.Lident s -> s 45 | Longident.Ldot (_, s) -> s.txt 46 | Longident.Lapply (_, l) -> longident_last l.txt 47 48let location_of_loc file (loc : Location.t) : T.location = 49 T.location file ~line:loc.loc_start.pos_lnum 50 ~start_col:(loc.loc_start.pos_cnum - loc.loc_start.pos_bol) 51 ~end_line:loc.loc_end.pos_lnum 52 ~end_col:(loc.loc_end.pos_cnum - loc.loc_end.pos_bol) 53 54(* AST cache access *) 55 56let ast_entry ~cache file = 57 match Cache.ast cache file with 58 | Ok (Implementation ast) -> Ok ast 59 | Ok (Interface _) -> err_expected_impl file 60 | Error e -> Error e 61 62let interface_ast ~cache file = 63 match Cache.ast cache file with 64 | Ok (Interface ast) -> Ok ast 65 | Ok (Implementation _) -> err_expected_intf file 66 | Error e -> Error e 67 68(* Generic AST traversal helpers *) 69 70let location_contains loc ~line ~col = 71 loc.T.start_line <= line && loc.T.end_line >= line 72 && (loc.T.start_line < line || loc.T.start_col <= col) 73 && (loc.T.end_line > line || loc.T.end_col >= col) 74 75(* Check if loc1 is contained within loc2 (loc1 is more specific) *) 76let is_loc1_contained_in_loc2 loc1 loc2 = 77 loc1.T.start_line > loc2.T.start_line 78 || loc1.T.start_line = loc2.T.start_line 79 && loc1.T.start_col >= loc2.T.start_col 80 || loc1.T.end_line < loc2.T.end_line 81 || (loc1.T.end_line = loc2.T.end_line && loc1.T.end_col <= loc2.T.end_col) 82 83let to_full_lines loc = 84 T.location loc.T.file ~line:loc.T.start_line ~end_line:loc.T.end_line 85 ~start_col:0 ~end_col:max_int 86 87(* Field handling *) 88 89let extend_field_bounds field_loc next_item_loc is_last_field = 90 if is_last_field then 91 (* For last field, extend to one character before the record end *) 92 T.extend field_loc ~end_line:next_item_loc.T.end_line 93 ~end_col:(next_item_loc.T.end_col - 1) 94 else 95 (* For other fields, extend to the start of the next field *) 96 T.extend field_loc ~end_line:next_item_loc.T.start_line 97 ~end_col:next_item_loc.T.start_col 98 99let field_in_type file type_decl ~line ~col ~field_name = 100 match type_decl.ptype_kind with 101 | Ptype_record label_decls -> 102 let record_loc = location_of_loc file type_decl.ptype_loc in 103 let total_fields = List.length label_decls in 104 105 (* Use List.find_mapi to avoid array allocation *) 106 let rec find_with_index i = function 107 | [] -> None 108 | ld :: rest -> 109 let name_loc = location_of_loc file ld.pld_name.loc in 110 if 111 ld.pld_name.txt = field_name 112 && location_contains name_loc ~line ~col 113 then 114 let full_loc = location_of_loc file ld.pld_loc in 115 let next_loc = 116 if i = total_fields - 1 then record_loc 117 else 118 match rest with 119 | next_ld :: _ -> location_of_loc file next_ld.pld_loc 120 | [] -> record_loc 121 in 122 let extended = 123 extend_field_bounds full_loc next_loc (i = total_fields - 1) 124 in 125 Some 126 { 127 field_name = ld.pld_name.txt; 128 full_field_bounds = extended; 129 enclosing_record = record_loc; 130 total_fields; 131 context = `Type_definition; 132 } 133 else find_with_index (i + 1) rest 134 in 135 find_with_index 0 label_decls 136 | _ -> None 137 138let field_in_record file expr ~line ~col ~field_name = 139 match expr.pexp_desc with 140 | Pexp_record (fields, _) -> 141 let record_loc = location_of_loc file expr.pexp_loc in 142 let total_fields = List.length fields in 143 144 (* Use recursive function to avoid array allocation *) 145 let rec find_with_index i = function 146 | [] -> None 147 | ((lid : Longident.t Asttypes.loc), expr) :: rest -> 148 let field_loc = location_of_loc file lid.loc in 149 let name = longident_last lid.txt in 150 if name = field_name && location_contains field_loc ~line ~col then 151 let value_loc = location_of_loc file expr.pexp_loc in 152 let full_loc = T.merge field_loc value_loc in 153 let next_loc = 154 if i = total_fields - 1 then record_loc 155 else 156 match rest with 157 | (next_lid, _) :: _ -> location_of_loc file next_lid.loc 158 | [] -> record_loc 159 in 160 let extended = 161 extend_field_bounds full_loc next_loc (i = total_fields - 1) 162 in 163 Some 164 { 165 field_name = name; 166 full_field_bounds = extended; 167 enclosing_record = record_loc; 168 total_fields; 169 context = `Record_construction; 170 } 171 else find_with_index (i + 1) rest 172 in 173 find_with_index 0 fields 174 | _ -> None 175 176(* Type definition handling *) 177 178(* Get type keyword location *) 179let type_keyword_loc file item = 180 let item_loc = location_of_loc file item.pstr_loc in 181 T.extend item_loc ~end_line:item_loc.start_line 182 ~end_col:(item_loc.start_col + 4) 183(* "type" *) 184 185(* Get equals location for type definition *) 186let equals_loc file td = 187 match (td.ptype_kind, td.ptype_manifest) with 188 | Ptype_abstract, Some _ | Ptype_record _, _ | Ptype_variant _, _ -> 189 let name_loc = location_of_loc file td.ptype_name.loc in 190 Some 191 (T.location file ~line:name_loc.end_line 192 ~start_col:(name_loc.end_col + 1) ~end_line:name_loc.end_line 193 ~end_col:(name_loc.end_col + 2)) 194 | _ -> None 195 196(* Get type definition kind *) 197let type_kind td = 198 match td.ptype_kind with 199 | Ptype_abstract -> if td.ptype_manifest <> None then `Alias else `Abstract 200 | Ptype_record _ -> `Record 201 | Ptype_variant _ -> `Variant 202 | Ptype_open -> `Abstract 203 204(* Process type declaration and create type_def_info *) 205let process_type_decl file item td loc = 206 let type_keyword_loc = type_keyword_loc file item in 207 let equals_loc = equals_loc file td in 208 let kind = type_kind td in 209 { 210 type_name = td.ptype_name.txt; 211 type_keyword_loc; 212 equals_loc; 213 kind; 214 full_bounds = loc; 215 } 216 217let type_definition file ast ~line ~col = 218 let iter = 219 { 220 Ast_iterator.default_iterator with 221 structure_item = 222 (fun self item -> 223 (match item.pstr_desc with 224 | Pstr_type (_, type_decls) -> 225 List.iter 226 (fun td -> 227 let loc = location_of_loc file td.ptype_loc in 228 if location_contains loc ~line ~col then 229 let type_def_info = process_type_decl file item td loc in 230 raise (Found_type_def type_def_info)) 231 type_decls 232 | _ -> ()); 233 Ast_iterator.default_iterator.structure_item self item); 234 } 235 in 236 237 try 238 iter.structure iter ast; 239 None 240 with Found_type_def info -> Some info 241 242(* Structure/signature item bounds *) 243 244let structure_item_bounds file ast ~line ~col = 245 List.find_map 246 (fun item -> 247 let loc = location_of_loc file item.pstr_loc in 248 if location_contains loc ~line ~col then 249 (* Return just the item bounds - comments will be added by 250 extend_location_with_comments *) 251 Some (to_full_lines loc) 252 else None) 253 ast 254 255let rec value_in_module file module_type ~line ~col = 256 match module_type.pmty_desc with 257 | Pmty_signature items -> 258 (* Look for value declarations inside this module signature *) 259 List.find_map 260 (fun item -> 261 match item.psig_desc with 262 | Psig_value vd -> 263 let loc = location_of_loc file vd.pval_loc in 264 if location_contains loc ~line ~col then 265 (* Found the value declaration *) 266 Some (to_full_lines loc) 267 else None 268 | Psig_module md -> 269 (* Recursively check inside nested modules *) 270 value_in_module file md.pmd_type ~line ~col 271 | _ -> None) 272 items 273 | _ -> None 274 275let signature_item_bounds file ast ~line ~col = 276 Log.debug (fun m -> 277 m "find_signature_item_bounds: looking for item at %s:%d:%d" file line col); 278 List.find_map 279 (fun item -> 280 let loc = location_of_loc file item.psig_loc in 281 Log.debug (fun m -> 282 m " Checking item at %d:%d-%d:%d" loc.start_line loc.start_col 283 loc.end_line loc.end_col); 284 if location_contains loc ~line ~col then ( 285 Log.debug (fun m -> m " Found matching item!"); 286 match item.psig_desc with 287 | Psig_value _ -> 288 (* For values, return just the value declaration bounds *) 289 Some (to_full_lines loc) 290 | Psig_module md -> ( 291 (* Check if we're inside a module - if so, find the specific 292 value *) 293 match value_in_module file md.pmd_type ~line ~col with 294 | Some bounds -> Some bounds 295 | None -> 296 (* Not inside a value, return the whole module *) 297 Some (to_full_lines loc)) 298 | _ -> 299 (* For other items (types, exceptions, etc.), return normal 300 bounds *) 301 Some (to_full_lines loc)) 302 else None) 303 ast 304 305(* Public API *) 306 307let field_info ~cache ~file ~line ~col ~field_name = 308 match ast_entry ~cache file with 309 | Error e -> Error e 310 | Ok ast -> ( 311 let iter = 312 { 313 Ast_iterator.default_iterator with 314 type_declaration = 315 (fun self td -> 316 (match field_in_type file td ~line ~col ~field_name with 317 | Some info -> raise (Found_field info) 318 | None -> ()); 319 Ast_iterator.default_iterator.type_declaration self td); 320 expr = 321 (fun self e -> 322 (match field_in_record file e ~line ~col ~field_name with 323 | Some info -> raise (Found_field info) 324 | None -> ()); 325 Ast_iterator.default_iterator.expr self e); 326 } 327 in 328 329 try 330 iter.structure iter ast; 331 err_field_not_found 332 with Found_field info -> Ok info) 333 334let type_definition_info ~cache ~file ~line ~col = 335 match ast_entry ~cache file with 336 | Error e -> Error e 337 | Ok ast -> ( 338 match type_definition file ast ~line ~col with 339 | None -> err_no_type_def 340 | Some info -> Ok info) 341 342let item_with_docs ~cache ~file ~line ~col = 343 if Filename.check_suffix file ".mli" then 344 match interface_ast ~cache file with 345 | Error e -> Error e 346 | Ok ast -> ( 347 match signature_item_bounds file ast ~line ~col with 348 | None -> err_no_sig_item 349 | Some bounds -> 350 (* Extend with doc comments if needed *) 351 Ok (Comments.extend_location_with_comments cache file bounds)) 352 else 353 match ast_entry ~cache file with 354 | Error e -> Error e 355 | Ok ast -> ( 356 match structure_item_bounds file ast ~line ~col with 357 | None -> err_no_struct_item 358 | Some bounds -> 359 Ok (Comments.extend_location_with_comments cache file bounds)) 360 361let value_binding ~cache ~file ~line ~col = 362 match ast_entry ~cache file with 363 | Error e -> Error e 364 | Ok ast -> ( 365 let iter = 366 { 367 Ast_iterator.default_iterator with 368 value_binding = 369 (fun self vb -> 370 let loc = location_of_loc file vb.pvb_loc in 371 if location_contains loc ~line ~col then 372 raise (Found_location loc) 373 else Ast_iterator.default_iterator.value_binding self vb); 374 } 375 in 376 try 377 iter.structure iter ast; 378 err_no_value_binding 379 with Found_location loc -> 380 Ok (Comments.extend_location_with_comments cache file loc)) 381 382let enclosing_record ~cache ~file ~line ~col = 383 match ast_entry ~cache file with 384 | Error e -> Error e 385 | Ok ast -> ( 386 let innermost = ref None in 387 let iter = 388 { 389 Ast_iterator.default_iterator with 390 expr = 391 (fun self e -> 392 (match e.pexp_desc with 393 | Pexp_record (_, _) -> ( 394 let loc = location_of_loc file e.pexp_loc in 395 if location_contains loc ~line ~col then 396 (* Update innermost if this record is smaller/more 397 specific *) 398 match !innermost with 399 | None -> innermost := Some loc 400 | Some prev_loc -> 401 (* If this location is contained within the previous 402 one, it's more specific *) 403 if is_loc1_contained_in_loc2 loc prev_loc then 404 innermost := Some loc) 405 | _ -> ()); 406 Ast_iterator.default_iterator.expr self e); 407 } 408 in 409 410 iter.structure iter ast; 411 match !innermost with 412 | None -> err_no_enclosing_record 413 | Some loc -> Ok loc)