Find and remove dead code and unused APIs in OCaml projects
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 ]