HC021 implementation complete: Analyzer IO Safety

 Implementation:
- tools/hako_check/rules/rule_analyzer_io_safety.hako: New rule detecting direct I/O in analyzer rules
- Detects: new FileBox(), .open(), .read(), .write(), network I/O
- CLI-internal push approach: rules should receive data through parameters
- Successfully detects FileBox usage in rule_dead_methods.hako:13-14

 Tests:
- tools/hako_check/tests/HC021_analyzer_io_safety/: Complete test suite
- ok.hako: Safe rule using CLI-internal push approach
- ng.hako: Unsafe rule using direct FileBox I/O (3 warnings expected)
- Test passes with all diagnostics matching

 Integration:
- tools/hako_check/cli.hako: Added HC021 rule invocation
- All existing tests (HC011-HC022) remain green

🎯 Achievement:
- Priority task completed as requested
- Validates "CLI内push案" (CLI-internal push approach) design
- Encourages safer analyzer rule development

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-08 04:17:38 +09:00
parent ace741b755
commit 375edb2a1b
5 changed files with 167 additions and 0 deletions

View File

@ -15,6 +15,8 @@ using tools.hako_check.rules.rule_missing_entrypoint as RuleMissingEntrypointBox
using tools.hako_check.rules.rule_top_level_local as RuleTopLevelLocalBox
using tools.hako_check.rules.rule_arity_mismatch as RuleArityMismatchBox
using tools.hako_check.rules.rule_stage3_gate as RuleStage3GateBox
using tools.hako_check.rules.rule_brace_heuristics as RuleBraceHeuristicsBox
using tools.hako_check.rules.rule_analyzer_io_safety as RuleAnalyzerIoSafetyBox
using tools.hako_check.render.graphviz as GraphvizRenderBox
using tools.hako_parser.parser_core as HakoParserCoreBox
@ -84,6 +86,8 @@ static box HakoAnalyzerBox {
RuleJsonfragUsageBox.apply(text, p, out)
RuleTopLevelLocalBox.apply(text, p, out)
RuleStage3GateBox.apply(text, p, out)
RuleBraceHeuristicsBox.apply(text, p, out)
RuleAnalyzerIoSafetyBox.apply(text, p, out)
// rules that need IR (enable dead code detection)
local before_n = out.size()
RuleDeadMethodsBox.apply_ir(ir, p, out)

View File

@ -0,0 +1,87 @@
// tools/hako_check/rules/rule_analyzer_io_safety.hako — HC021: Analyzer IO Safety
// Detects analyzer rules that perform direct I/O operations (FileBox, NetworkBox, etc.)
// Analyzer rules should receive all data through method parameters (CLI-internal push approach).
using selfhost.shared.common.string_helpers as Str
static box RuleAnalyzerIoSafetyBox {
method apply(text, path, out) {
if text == null { return 0 }
// Only check files that look like analyzer rules (contain "Box" and "apply")
if text.indexOf("Box") < 0 || text.indexOf("apply") < 0 {
return 0
}
local lines = me._split_lines(text)
local i = 0
while i < lines.size() {
local ln = lines.get(i)
local line_num = i + 1
// Remove comments for analysis
local comment_pos = ln.indexOf("//")
local code = ln
if comment_pos >= 0 {
code = ln.substring(0, comment_pos)
}
// Check for FileBox instantiation
if code.indexOf("new FileBox") >= 0 {
local msg = "[HC021] analyzer rule uses direct file I/O (new FileBox): use CLI-internal push approach instead"
out.push(msg + " :: " + path + ":" + me._itoa(line_num))
}
// Check for file operations (even on passed-in FileBox)
if code.indexOf(".open(") >= 0 || code.indexOf(".read(") >= 0 || code.indexOf(".write(") >= 0 {
local msg = "[HC021] analyzer rule uses file operations: use CLI-internal push approach instead"
out.push(msg + " :: " + path + ":" + me._itoa(line_num))
}
// Check for other dangerous I/O boxes
if code.indexOf("new NetworkBox") >= 0 || code.indexOf("new SocketBox") >= 0 {
local msg = "[HC021] analyzer rule uses network I/O: use CLI-internal push approach instead"
out.push(msg + " :: " + path + ":" + me._itoa(line_num))
}
i = i + 1
}
return out.size()
}
_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
}
_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 RuleAnalyzerIoSafetyMain { method main(args) { return 0 } }

View File

@ -0,0 +1,13 @@
{"diagnostics":[
{"file":"ng.hako","line":1,"rule":"HC012","message":"[HC012] dead static box (never referenced): RuleUnsafeExampleBox","quickFix":"","severity":"warning"},
{"file":"ng.hako","line":1,"rule":"HC012","message":"[HC012] dead static box (never referenced): RuleUnsafeExampleMain","quickFix":"","severity":"warning"},
{"file":"ng.hako","line":1,"rule":"HC014","message":"[HC014] missing entrypoint (Main.main or main)","quickFix":"","severity":"warning"},
{"file":"ng.hako","line":7,"rule":"HC021","message":"[HC021] analyzer rule uses direct file I/O (new FileBox): use CLI-internal push approach instead :: ng.hako:7","quickFix":"","severity":"warning"},
{"file":"ng.hako","line":8,"rule":"HC021","message":"[HC021] analyzer rule uses file operations: use CLI-internal push approach instead :: ng.hako:8","quickFix":"","severity":"warning"},
{"file":"ng.hako","line":9,"rule":"HC021","message":"[HC021] analyzer rule uses file operations: use CLI-internal push approach instead :: ng.hako:9","quickFix":"","severity":"warning"},
{"file":"ok.hako","line":1,"rule":"HC012","message":"[HC012] dead static box (never referenced): RuleSafeExampleMain","quickFix":"","severity":"warning"},
{"file":"ok.hako","line":1,"rule":"HC014","message":"[HC014] missing entrypoint (Main.main or main)","quickFix":"","severity":"warning"},
{"file":"ok.hako","line":1,"rule":"HC022","message":"[HC022] Suggestion: Use NYASH_PARSER_STAGE3=1 or HAKO_PARSER_STAGE3=1 environment variables","quickFix":"","severity":"warning"},
{"file":"ok.hako","line":1,"rule":"HC012","message":"[HC012] dead static box (never referenced): RuleSafeExampleBox","quickFix":"","severity":"warning"},
{"file":"ok.hako","line":11,"rule":"HC022","message":"[HC022] Stage-3 construct detected (while): ok.hako:11","quickFix":"","severity":"warning"}
]}

View File

@ -0,0 +1,21 @@
// ng.hako - Unsafe analyzer rule (uses direct file I/O)
// This rule violates the CLI-internal push approach by using FileBox
static box RuleUnsafeExampleBox {
method apply(text, path, out) {
// UNSAFE: directly reads file using FileBox
local fb = new FileBox()
if fb.open(path) == 0 {
local content = fb.read()
fb.close()
if content.indexOf("dangerous") >= 0 {
out.push("[EXAMPLE] found dangerous pattern")
}
}
return out.size()
}
}
static box RuleUnsafeExampleMain { method main(args) { return 0 } }

View File

@ -0,0 +1,42 @@
// ok.hako - Safe analyzer rule (CLI-internal push approach)
// This rule receives all data through parameters and doesn't do direct I/O
static box RuleSafeExampleBox {
method apply(text, path, out) {
// Safe: uses text parameter passed from CLI
if text == null { return 0 }
local lines = me._split_lines(text)
local i = 0
while i < lines.size() {
local ln = lines.get(i)
// Analyze the line
if ln.indexOf("unsafe_pattern") >= 0 {
out.push("[EXAMPLE] found unsafe pattern")
}
i = i + 1
}
return out.size()
}
_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
}
}
static box RuleSafeExampleMain { method main(args) { return 0 } }