552 lines
12 KiB
Plaintext
552 lines
12 KiB
Plaintext
|
|
// 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
|
||
|
|
}
|
||
|
|
}
|