Find and remove dead code and unused APIs in OCaml projects
at main 142 lines 5.9 kB view raw
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