Find and remove dead code and unused APIs in OCaml projects
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 196 lines 5.5 kB view raw
1(* Integration tests that use real merlin functionality *) 2open Alcotest 3open Prune 4open Prune.Removal 5module Cache = Prune.Cache 6 7(* Helper to check if content contains a line starting with substring *) 8let contains content sub = 9 match 10 String.split_on_char '\n' content 11 |> List.find_opt (fun line -> 12 String.length line >= String.length sub 13 && String.sub line 0 (String.length sub) = sub) 14 with 15 | Some _ -> true 16 | None -> false 17 18(* Helper to check removal results *) 19let check_removal_results content = 20 check bool "unused value removed" false (contains content "val unused"); 21 check bool "unused type removed" false (contains content "type unused_t"); 22 check bool "used value remains" true (contains content "val used"); 23 check bool "used type remains" true (contains content "type used_t") 24 25(* Helper to create a temporary OCaml project *) 26let with_temp_project test_name content_mli content_ml f = 27 let temp_dir = Filename.temp_file test_name "" in 28 Sys.remove temp_dir; 29 Unix.mkdir temp_dir 0o755; 30 31 let mli_file = Filename.concat temp_dir "test.mli" in 32 let ml_file = Filename.concat temp_dir "test.ml" in 33 34 let oc = open_out mli_file in 35 output_string oc content_mli; 36 close_out oc; 37 38 let oc = open_out ml_file in 39 output_string oc content_ml; 40 close_out oc; 41 42 (* Create a simple dune file *) 43 let dune_file = Filename.concat temp_dir "dune" in 44 let oc = open_out dune_file in 45 output_string oc "(library (name test))"; 46 close_out oc; 47 48 try 49 let result = f temp_dir mli_file ml_file in 50 (* Clean up *) 51 Sys.remove mli_file; 52 Sys.remove ml_file; 53 Sys.remove dune_file; 54 Unix.rmdir temp_dir; 55 result 56 with e -> 57 (* Clean up on error *) 58 (try Sys.remove mli_file with Sys_error _ -> ()); 59 (try Sys.remove ml_file with Sys_error _ -> ()); 60 (try Sys.remove dune_file with Sys_error _ -> ()); 61 (try Unix.rmdir temp_dir with Unix.Unix_error _ -> ()); 62 raise e 63 64(* Test the full removal flow with real files *) 65(* Helper to create unused symbols for test *) 66let unused_symbols mli_file = 67 [ 68 { 69 name = "unused"; 70 kind = Value; 71 location = 72 location mli_file ~line:5 ~start_col:0 (* Start of line *) 73 ~end_line:5 ~end_col:29 74 (* End of "val unused : string -> string" *); 75 }; 76 { 77 name = "unused_t"; 78 kind = Type; 79 location = 80 location mli_file ~line:11 ~start_col:0 (* Start of line *) 81 ~end_line:11 ~end_col:21 82 (* End of "type unused_t = float" *); 83 }; 84 ] 85 86let test_remove_unused_exports_real () = 87 let mli_content = 88 {|(** Used value *) 89val used : int -> int 90 91(** Unused value *) 92val unused : string -> string 93 94(** Used type *) 95type used_t = int 96 97(** Unused type *) 98type unused_t = float|} 99 in 100 101 let ml_content = 102 {|let used x = x * 2 103let unused s = s ^ "_test" 104type used_t = int 105type unused_t = float|} 106 in 107 108 with_temp_project "test_removal" mli_content ml_content 109 (fun root_dir mli_file _ml_file -> 110 let symbols = unused_symbols mli_file in 111 let cache = Cache.v () in 112 match remove_unused_exports ~cache root_dir mli_file symbols with 113 | Error e -> failf "Unexpected error: %a" pp_error e 114 | Ok () -> 115 (* Read the modified file *) 116 let ic = open_in mli_file in 117 let content = really_input_string ic (in_channel_length ic) in 118 close_in ic; 119 120 (* Check results *) 121 check_removal_results content) 122 123(* Helper to create test module data *) 124let module_test_data () = 125 let symbols = 126 [ 127 { 128 name = "M"; 129 kind = Module; 130 location = 131 location "test.mli" ~line:1 ~start_col:0 ~end_line:10 ~end_col:3; 132 }; 133 { 134 name = "foo"; 135 kind = Value; 136 location = 137 location "test.mli" ~line:3 ~start_col:2 ~end_line:3 ~end_col:20; 138 }; 139 { 140 name = "bar"; 141 kind = Value; 142 location = 143 location "test.mli" ~line:5 ~start_col:2 ~end_line:5 ~end_col:20; 144 }; 145 ] 146 in 147 let occurrence_data = 148 [ 149 { 150 symbol = List.nth symbols 0; 151 occurrences = 0; 152 locations = []; 153 usage_class = Unused; 154 }; 155 { 156 symbol = List.nth symbols 1; 157 occurrences = 2; 158 locations = []; 159 usage_class = Used; 160 }; 161 { 162 symbol = List.nth symbols 2; 163 occurrences = 0; 164 locations = []; 165 usage_class = Unused; 166 }; 167 ] 168 in 169 (symbols, occurrence_data) 170 171(* Test module filtering logic *) 172let test_module_filtering () = 173 let _symbols, occurrence_data = module_test_data () in 174 175 let unused = List.filter (fun occ -> occ.occurrences = 0) occurrence_data in 176 177 (* Apply module filtering *) 178 let filtered = Analysis.filter_modules_with_used unused occurrence_data in 179 180 (* Module M should be filtered out because it contains used symbol foo *) 181 check int "only bar should remain" 1 (List.length filtered); 182 check string "bar is the remaining symbol" "bar" 183 (List.hd filtered).symbol.name 184 185let suite = 186 let open Alcotest in 187 ( "Integration tests", 188 [ 189 (* Tests removed - mark_lines_for_removal is no longer public API *) 190 test_case "remove_unused_exports with real files" `Quick 191 test_remove_unused_exports_real; 192 test_case "module filtering preserves modules with used contents" `Quick 193 test_module_filtering; 194 ] ) 195 196let () = Alcotest.run "Prune integration tests" [ suite ]