// tools/hako_check/rules/rule_dead_code.hako — HC019: Comprehensive Dead Code Detection // Unified dead code analyzer combining method-level, block-level, and box-level detection. // Phase 153: Revival of dead code detection mode with JoinIR integration. static box DeadCodeAnalyzerBox { // Main entry point for comprehensive dead code analysis // Input: ir (Analysis IR), path (file path), out (diagnostics array) // Returns: void (like other rules) method apply_ir(ir, path, out) { // Phase 153 MVP: Focus on method-level reachability // Block-level analysis deferred to Phase 154+ when MIR JSON integration is ready if ir == null { return } if out == null { return } // 1. Analyze unreachable methods (similar to HC011 but enhanced) me._analyze_dead_methods(ir, path, out) // 2. Analyze dead static boxes (similar to HC012 but integrated) me._analyze_dead_boxes(ir, path, out) // 3. TODO(Phase 154): Analyze unreachable blocks (requires MIR CFG) // me._analyze_unreachable_blocks(ir, path, out) return } // Analyze unreachable methods using DFS from entrypoints _analyze_dead_methods(ir, path, out) { local methods = ir.get("methods") if methods == null || methods.size() == 0 { // Fallback: scan from source text local src = ir.get("source") if src != null { methods = me._scan_methods_from_text(src) } else { return } } if methods == null || methods.size() == 0 { return } local calls = ir.get("calls") if calls == null || calls.size() == 0 { // Build minimal calls from source text local src = ir.get("source") if src != null { calls = me._scan_calls_from_text(src) } else { calls = new ArrayBox() } } local eps = ir.get("entrypoints") if eps == null { eps = new ArrayBox() } // Build adjacency graph local adj = new MapBox() local i = 0 while i < methods.size() { adj.set(methods.get(i), new ArrayBox()) i = i + 1 } // Add edges from calls i = 0 while i < calls.size() { local c = calls.get(i) local f = c.get("from") local t = c.get("to") // Normalize from: prefer exact, otherwise try adding "/0" suffix local ff = f if adj.has(ff) == 0 { local f0 = f + "/0" if adj.has(f0) == 1 { ff = f0 } } if adj.has(ff) == 1 { adj.get(ff).push(t) } i = i + 1 } // DFS from entrypoints local seen = new MapBox() local seeds = me._resolve_entrypoints(eps, adj, methods) local j = 0 while j < seeds.size() { me._dfs(adj, seeds.get(j), seen) j = j + 1 } // Report dead methods (not seen) local src_text = ir.get("source") i = 0 while i < methods.size() { local m = methods.get(i) if seen.has(m) == 0 { // Check if method is actually called via text heuristic (reduce false positives) local keep = 1 if src_text != null { local slash = m.lastIndexOf("/") local dotp = m.lastIndexOf(".") if dotp >= 0 { local meth = (slash > dotp) ? m.substring(dotp+1, slash) : m.substring(dotp+1) if src_text.indexOf("." + meth + "(") >= 0 { keep = 0 } } } if keep == 1 { out.push("[HC019] unreachable method (dead code): " + path + " :: " + m) } } i = i + 1 } return } // Analyze dead static boxes (never referenced) _analyze_dead_boxes(ir, path, out) { local boxes = ir.get("boxes") if boxes == null { return } local calls = ir.get("calls") if calls == null { return } // Collect all box names referenced in calls local referenced_boxes = new MapBox() local ci = 0 while ci < calls.size() { local call = calls.get(ci) if call == null { ci = ci + 1; continue } local to = call.get("to") if to != null { // Extract box name from qualified call (e.g., "Box.method/0" -> "Box") local dot = to.indexOf(".") if dot > 0 { local box_name = to.substring(0, dot) referenced_boxes.set(box_name, "1") } } ci = ci + 1 } // Check each box local findings = 0 local bi = 0 while bi < boxes.size() { local box_info = boxes.get(bi) if box_info == null { bi = bi + 1; continue } local name = box_info.get("name") if name == null { bi = bi + 1; continue } local is_static = box_info.get("is_static") // Skip Main box (entry point) if name == "Main" { bi = bi + 1; continue } // Only check static boxes if is_static != null { if is_static == 0 { bi = bi + 1; continue } } // Check if box is referenced local ref_check = referenced_boxes.get(name) if ref_check == null || ref_check != "1" { // Box is never referenced - report error local line_any = box_info.get("span_line") local line_s = "" if line_any == null { line_s = "1" } else { line_s = "" + line_any } // Parse leading digits local i = 0 local digits = "" while i < line_s.length() { local ch = line_s.substring(i, i+1) if ch >= "0" && ch <= "9" { digits = digits + ch i = i + 1 continue } break } if digits == "" { digits = "1" } out.push("[HC019] dead static box (never referenced): " + name + " :: " + path + ":" + digits) } bi = bi + 1 } return } // Resolve entrypoints to actual method names in adjacency graph _resolve_entrypoints(eps, adj, methods) { local seeds = new ArrayBox() // Collect keys from methods array local keys = new ArrayBox() local i = 0 while i < methods.size() { keys.push(methods.get(i)) i = i + 1 } // Try to match each entrypoint local j = 0 while j < eps.size() { local ep = eps.get(j) // Exact match if adj.has(ep) == 1 { seeds.push(ep) } // Prefix match: ep + "/" local pref = ep + "/" local k = 0 while k < keys.size() { local key = keys.get(k) if key.indexOf(pref) == 0 { seeds.push(key) } k = k + 1 } j = j + 1 } // Fallback: common Main.main/0 if still empty if seeds.size() == 0 { if adj.has("Main.main/0") == 1 { seeds.push("Main.main/0") } } return seeds } // DFS traversal for reachability _dfs(adj, node, seen) { if node == null { return } if seen.has(node) == 1 { return } seen.set(node, 1) if adj.has(node) == 0 { return } local arr = adj.get(node) local k = 0 while k < arr.size() { me._dfs(adj, arr.get(k), seen) k = k + 1 } } // Scan methods from source text (fallback when IR is incomplete) _scan_methods_from_text(text) { local res = new ArrayBox() if text == null { return res } local lines = me._split_lines(text) local cur = null local depth = 0 local i = 0 while i < lines.size() { local ln = me._ltrim(lines.get(i)) if ln.indexOf("static box ") == 0 { local rest = ln.substring("static box ".length()) local p = rest.indexOf("{") if p > 0 { cur = me._rstrip(rest.substring(0, p)) } else { cur = me._rstrip(rest) } depth = depth + 1 i = i + 1 continue } if cur != null && ln.indexOf("method ") == 0 { local rest = ln.substring("method ".length()) local p1 = rest.indexOf("(") local name = (p1 > 0) ? me._rstrip(rest.substring(0, p1)) : me._rstrip(rest) local ar = 0 local p2 = rest.indexOf(")", (p1 >= 0) ? (p1+1) : 0) if p1 >= 0 && p2 > p1+1 { local inside = rest.substring(p1+1, p2) // Count commas + 1 if any non-space local any = 0 local cnt = 1 local k = 0 while k < inside.length() { local c = inside.substring(k, k+1) if c == "," { cnt = cnt + 1 } if c != " " && c != "\t" { any = 1 } k = k + 1 } if any == 1 { ar = cnt } } res.push(cur + "." + name + "/" + me._itoa(ar)) } // Adjust depth by braces local j = 0 while j < ln.length() { local ch = ln.substring(j, j+1) if ch == "{" { depth = depth + 1 } else { if ch == "}" { depth = depth - 1 if depth < 0 { depth = 0 } } } j = j + 1 } if depth == 0 { cur = null } i = i + 1 } return res } // Scan calls from source text (fallback) _scan_calls_from_text(text) { local arr = new ArrayBox() if text == null { return arr } local lines = me._split_lines(text) local src_m = "Main.main/0" local i = 0 while i < lines.size() { local ln = lines.get(i) // Naive: detect patterns like "Main.foo(" local pos = 0 local n = ln.length() loop (pos < n) { local k = ln.indexOf(".", pos) if k < 0 { break } // Scan ident before '.' local lhs = me._scan_ident_rev(ln, k-1) // Scan ident after '.' local rhs = me._scan_ident_fwd(ln, k+1) if lhs != null && rhs != null { local to = lhs + "." + rhs + "/0" local rec = new MapBox() rec.set("from", src_m) rec.set("to", to) arr.push(rec) } pos = k + 1 } i = i + 1 } return arr } // Helper: scan identifier backwards from position _scan_ident_rev(s, i) { if i < 0 { return null } local n = i local start = 0 local rr = 0 while rr <= n { local j = i - rr local c = s.substring(j, j+1) if me._is_ident_char(c) == 0 { start = j+1 break } if j == 0 { start = 0 break } rr = rr + 1 } if start > i { return null } return s.substring(start, i+1) } // Helper: scan identifier forwards from position _scan_ident_fwd(s, i) { local n = s.length() if i >= n { return null } local endp = i local off = 0 while off < n { local j = i + off if j >= n { break } local c = s.substring(j, j+1) if me._is_ident_char(c) == 0 { endp = j break } if j == n-1 { endp = n break } off = off + 1 } if endp == i { return null } return s.substring(i, endp) } // Helper: check if character is valid identifier character _is_ident_char(c) { if c == "_" { return 1 } if c >= "A" && c <= "Z" { return 1 } if c >= "a" && c <= "z" { return 1 } if c >= "0" && c <= "9" { return 1 } return 0 } // Helper: split string into lines _split_lines(s) { local arr = new ArrayBox() if s == null { return arr } local n = s.length() local last = 0 local i = 0 loop (i < n) { local ch = s.substring(i, i+1) if ch == "\n" { arr.push(s.substring(last, i)) last = i+1 } i = i + 1 } if last <= n { arr.push(s.substring(last)) } return arr } // Helper: left trim _ltrim(s) { return me._ltrim_chars(s, " \t") } _ltrim_chars(s, cs) { local n = s.length() local head = 0 local idx = 0 while idx < n { local ch = s.substring(idx, idx+1) if ch != " " && ch != "\t" { head = idx break } if idx == n-1 { head = n } idx = idx + 1 } return s.substring(head) } // Helper: right strip _rstrip(s) { local n = s.length() local last = n local r = 0 while r < n { local i4 = n-1-r local c = s.substring(i4, i4+1) if c != " " && c != "\t" { last = i4+1 break } if r == n-1 { last = 0 } r = r + 1 } return s.substring(0, last) } // Helper: integer to string _itoa(n) { local v = 0 + n if v == 0 { return "0" } local out = "" local digits = "0123456789" local tmp = "" while v > 0 { local d = v % 10 tmp = digits.substring(d, d+1) + tmp v = v / 10 } out = tmp return out } } static box RuleDeadCodeMain { method main(args) { return 0 } }