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:
@ -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_top_level_local as RuleTopLevelLocalBox
|
||||||
using tools.hako_check.rules.rule_arity_mismatch as RuleArityMismatchBox
|
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_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_check.render.graphviz as GraphvizRenderBox
|
||||||
using tools.hako_parser.parser_core as HakoParserCoreBox
|
using tools.hako_parser.parser_core as HakoParserCoreBox
|
||||||
|
|
||||||
@ -84,6 +86,8 @@ static box HakoAnalyzerBox {
|
|||||||
RuleJsonfragUsageBox.apply(text, p, out)
|
RuleJsonfragUsageBox.apply(text, p, out)
|
||||||
RuleTopLevelLocalBox.apply(text, p, out)
|
RuleTopLevelLocalBox.apply(text, p, out)
|
||||||
RuleStage3GateBox.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)
|
// rules that need IR (enable dead code detection)
|
||||||
local before_n = out.size()
|
local before_n = out.size()
|
||||||
RuleDeadMethodsBox.apply_ir(ir, p, out)
|
RuleDeadMethodsBox.apply_ir(ir, p, out)
|
||||||
|
|||||||
87
tools/hako_check/rules/rule_analyzer_io_safety.hako
Normal file
87
tools/hako_check/rules/rule_analyzer_io_safety.hako
Normal 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 } }
|
||||||
@ -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"}
|
||||||
|
]}
|
||||||
21
tools/hako_check/tests/HC021_analyzer_io_safety/ng.hako
Normal file
21
tools/hako_check/tests/HC021_analyzer_io_safety/ng.hako
Normal 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 } }
|
||||||
42
tools/hako_check/tests/HC021_analyzer_io_safety/ok.hako
Normal file
42
tools/hako_check/tests/HC021_analyzer_io_safety/ok.hako
Normal 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 } }
|
||||||
Reference in New Issue
Block a user