Find and remove dead code and unused APIs in OCaml projects
1(* Source comment scanning utilities for comments not in the AST
2
3 This module handles detection of source-level comments (* ... *) that are not
4 part of the OCaml AST. Doc comments (** ... *) attached to items become
5 attributes and are handled through the AST, but floating doc comments and
6 regular comments need this scanner.
7
8 TODO: Remove this module once OCaml parser includes all comments in AST *)
9
10module Log = (val Logs.src_log (Logs.Src.create "prune.comments") : Logs.LOG)
11
12(* Check if a string starts with a prefix *)
13let starts_with s prefix =
14 String.length s >= String.length prefix
15 && String.sub s 0 (String.length prefix) = prefix
16
17(* Check if a line contains comment end marker *)
18let line_contains_comment_end line =
19 let rec check i =
20 if i >= String.length line - 1 then false
21 else if line.[i] = '*' && line.[i + 1] = ')' then true
22 else check (i + 1)
23 in
24 check 0
25
26(* Check if a line is a comment line *)
27let is_comment_line line =
28 let trimmed = String.trim line in
29 starts_with trimmed "(*"
30
31(* Check if a line is empty *)
32let is_empty_line line = String.trim line = ""
33
34(* Find the start of comments (both doc and regular) preceding a given line *)
35let preceding_comment_start cache file start_line_idx =
36 if start_line_idx <= 0 then start_line_idx
37 else
38 let rec scan_backwards line_idx in_comment_block =
39 if line_idx < 0 then 0
40 else
41 match Cache.line cache file (line_idx + 1) with
42 | None -> line_idx + 1
43 | Some line ->
44 if is_empty_line line then
45 (* Empty line - might be separator or part of comment block *)
46 if in_comment_block then scan_backwards (line_idx - 1) true
47 else line_idx + 1 (* Stop here - this is a separator *)
48 else if is_comment_line line then
49 (* Found a comment - keep scanning *)
50 scan_backwards (line_idx - 1) true
51 else if line_contains_comment_end line && not (is_comment_line line)
52 then
53 (* Multi-line comment end - need to find start *)
54 let rec find_multi_start idx depth =
55 if idx < 0 then 0
56 else
57 match Cache.line cache file (idx + 1) with
58 | None -> idx + 1
59 | Some l ->
60 let rec count_markers i starts ends =
61 if i >= String.length l - 1 then (starts, ends)
62 else if l.[i] = '(' && l.[i + 1] = '*' then
63 count_markers (i + 2) (starts + 1) ends
64 else if l.[i] = '*' && l.[i + 1] = ')' then
65 count_markers (i + 2) starts (ends + 1)
66 else count_markers (i + 1) starts ends
67 in
68 let starts, ends = count_markers 0 0 0 in
69 let new_depth = depth + ends - starts in
70 if new_depth <= 0 && starts > 0 then idx
71 else find_multi_start (idx - 1) new_depth
72 in
73 find_multi_start (line_idx - 1) 0
74 else
75 (* Hit code - stop *)
76 line_idx + 1
77 in
78 scan_backwards (start_line_idx - 1) false
79
80(* Find trailing comments after a given line *)
81let trailing_comment_end cache file end_line_idx =
82 Log.debug (fun m ->
83 m "trailing_comment_end: file=%s end_line_idx=%d" file end_line_idx);
84 match Cache.line_count cache file with
85 | None -> end_line_idx
86 | Some max_lines ->
87 if end_line_idx >= max_lines then end_line_idx
88 else
89 let scan_forward line_idx =
90 if line_idx >= max_lines then line_idx
91 else
92 match Cache.line cache file (line_idx + 1) with
93 | None ->
94 Log.debug (fun m ->
95 m " scan_forward: no line at %d, returning %d"
96 (line_idx + 1) line_idx);
97 line_idx
98 | Some line ->
99 Log.debug (fun m ->
100 m " scan_forward: line %d = '%s'" (line_idx + 1) line);
101 if is_empty_line line then
102 (* Blank line - stop here, comments after blank lines belong
103 to next item *)
104 line_idx
105 else if is_comment_line line then
106 (* Comment immediately follows - this is a trailing comment *)
107 if line_contains_comment_end line then
108 line_idx + 1 (* Single-line comment *)
109 else
110 (* Multi-line comment - find where it ends *)
111 let rec find_end idx =
112 if idx >= max_lines - 1 then max_lines - 1
113 else
114 match Cache.line cache file (idx + 1) with
115 | None -> idx
116 | Some l ->
117 if line_contains_comment_end l then idx + 1
118 else find_end (idx + 1)
119 in
120 find_end line_idx
121 else
122 (* Non-comment line - stop here *)
123 line_idx
124 in
125 scan_forward end_line_idx
126
127(* Extend location bounds to include source-level comments *)
128let extend_location_with_comments cache file location =
129 (* Find preceding comments *)
130 let start_with_comments =
131 preceding_comment_start cache file (location.Types.start_line - 1) + 1
132 in
133 (* Find trailing comments *)
134 let end_with_comments =
135 trailing_comment_end cache file location.Types.end_line
136 in
137 Log.debug (fun m ->
138 m "extend_location_with_comments: file=%s original %d-%d, extended %d-%d"
139 file location.Types.start_line location.Types.end_line
140 start_with_comments end_with_comments);
141 Types.extend location ~start_line:start_with_comments
142 ~end_line:end_with_comments