Files
hakorune/tools/hako_check/rules/rule_dead_methods.hako
nyash-codex 50ac8af2b8 Phase 21.4 Complete: FileBox SSOT + Analyzer Stabilization (7 Tasks)
 Task 1: Fallback Guarantee (create_box failure → ring1/core-ro auto fallback)
- Three-tier fallback system: plugin → builtin → core-ro
- Mode control: auto/plugin-only/core-ro
- New: src/box_factory/builtin_impls/file_box.rs
- New: tools/test_filebox_fallback_smoke.sh

 Task 2: Provider Registration SSOT (static/dynamic/core-ro unified)
- ProviderFactory trait with priority-based selection
- Global registry PROVIDER_FACTORIES implementation
- Priority: dynamic(100) > builtin(10) > core-ro(0)
- New: src/boxes/file/builtin_factory.rs
- New: tools/smoke_provider_modes.sh

 Task 3: FileBox Publication Unification
- Verified: basic/file_box.rs already minimized (11 lines)
- Perfect re-export pattern maintained

 Task 4: ENV Unification (FILEBOX_MODE/DISABLE_PLUGINS priority)
- Removed auto-setting of NYASH_USE_PLUGIN_BUILTINS
- Removed auto-setting of NYASH_PLUGIN_OVERRIDE_TYPES
- Added deprecation warnings with migration guide
- ENV hierarchy: DISABLE_PLUGINS > BOX_FACTORY_POLICY > FILEBOX_MODE

 Task 5: Error Log Visibility (Analyzer rule execution errors to stderr)
- Added [rule/exec] logging before IR-based rule execution
- Format: [rule/exec] HC012 (dead_static_box) <filepath>
- VM errors now traceable via stderr output

 Task 6: Unnecessary Using Removal (14 rules Str alias cleanup)
- Removed unused `using ... as Str` from 14 rule files
- All rules use local _itoa() helper instead
- 14 lines of dead code eliminated

 Task 7: HC017 Skip & TODO Documentation (UTF-8 support required)
- Enhanced run_tests.sh with clear skip message
- Added "Known Limitations" section to README.md
- Technical requirements documented (3 implementation options)
- Re-enable timeline: Phase 22 (Unicode Support Phase)

📊 Test Results:
- Analyzer: 10 tests PASS, 1 skipped (HC017)
- FileBox fallback: All 3 modes PASS
- Provider modes: All 4 modes PASS
- Build: Success (0 errors, 0 warnings)

🎯 Key Achievements:
- 28 files modified/created
- Three-Tier Fallback System (stability)
- SSOT Provider Registry (extensibility)
- ENV unification (operational clarity)
- Error visibility (debugging efficiency)
- Code cleanup (maintainability)
- Comprehensive documentation (Phase 22 ready)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 17:04:21 +09:00

218 lines
8.2 KiB
Plaintext

static box RuleDeadMethodsBox {
// IR expects: methods(Array<String>), calls(Array<Map{from,to}>), entrypoints(Array<String>)
apply_ir(ir, path, out) {
local methods = ir.get("methods")
// If IR has no methods, or methods is empty, rebuild from source file.
if methods == null || methods.size() == 0 {
// Prefer in-memory source if provided (avoids FileBox/plugin dependency)
local src = ir.get("source")
if src != null { methods = me._scan_methods_from_text(src) } else {
// Fallback to FileBox only when no source text provided
local fb = new FileBox()
if fb.open(path) == 0 { local text = fb.read(); fb.close(); methods = me._scan_methods_from_text(text) } else { methods = new ArrayBox() }
}
}
if methods == null || methods.size() == 0 { return }
local calls = ir.get("calls");
if (calls == null || calls.size() == 0) {
// build minimal calls from source text (avoid plugin)
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 graph
local adj = new MapBox()
local i = 0; while i < methods.size() { adj.set(methods.get(i), new ArrayBox()); i = i + 1 }
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();
// resolve seeds: accept exact or prefix ("name/arity") matches for entrypoint names
local seeds = new ArrayBox()
// collect keys
local keys = new ArrayBox(); i = 0; while i < methods.size() { keys.push(methods.get(i)); i = i + 1 }
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") }
}
// run DFS from seeds
j = 0; while j < seeds.size() { me._dfs(adj, seeds.get(j), seen); j = j + 1 }
// report dead = methods not seen (filter with simple call-text heuristic)
local src_text = ir.get("source")
local cands = new ArrayBox()
i = 0; while i < methods.size() { local m=methods.get(i); if seen.has(m)==0 { cands.push(m) }; i = i + 1 }
i = 0; while i < cands.size() {
local m = cands.get(i)
local keep = 1
if src_text != null {
// If source text contains a call like ".methodName(", consider it reachable
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("[HC011] unreachable method (dead code): PLACEHOLDER :: " + m) }
i = i + 1
}
}
_scan_methods_from_text(text) {
local res = new ArrayBox()
if text == null { return res }
// use local implementation to avoid external static calls
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 on the line
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
}
_ltrim(s) { return me._ltrim_chars(s, " \t") }
_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)
}
_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)
}
_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 }
_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
}
_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
}
_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)
}
_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)
}
_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
}
_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 }
}
}
static box RuleDeadMethodsMain { method main(args) { return 0 } }