diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index d0766003..265e2ea6 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -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) diff --git a/tools/hako_check/rules/rule_analyzer_io_safety.hako b/tools/hako_check/rules/rule_analyzer_io_safety.hako new file mode 100644 index 00000000..614a8b84 --- /dev/null +++ b/tools/hako_check/rules/rule_analyzer_io_safety.hako @@ -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 } } diff --git a/tools/hako_check/tests/HC021_analyzer_io_safety/expected.json b/tools/hako_check/tests/HC021_analyzer_io_safety/expected.json new file mode 100644 index 00000000..2ef2fcb2 --- /dev/null +++ b/tools/hako_check/tests/HC021_analyzer_io_safety/expected.json @@ -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"} +]} diff --git a/tools/hako_check/tests/HC021_analyzer_io_safety/ng.hako b/tools/hako_check/tests/HC021_analyzer_io_safety/ng.hako new file mode 100644 index 00000000..b237c748 --- /dev/null +++ b/tools/hako_check/tests/HC021_analyzer_io_safety/ng.hako @@ -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 } } diff --git a/tools/hako_check/tests/HC021_analyzer_io_safety/ok.hako b/tools/hako_check/tests/HC021_analyzer_io_safety/ok.hako new file mode 100644 index 00000000..bb40bb34 --- /dev/null +++ b/tools/hako_check/tests/HC021_analyzer_io_safety/ok.hako @@ -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 } }