From 3d366f5cb8e3a8e63b0f142b135a01b2fca8e517 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Sat, 8 Nov 2025 03:14:22 +0900 Subject: [PATCH] Implement HC018: Top-level local declaration detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Overview Detects top-level `local` declarations (outside of methods/boxes), which are cleanup omissions in Hakorune code. ## Implementation Details - **Rule**: `rule_top_level_local.hako` following box principles - **Detection Method**: Text-based scanning with context tracking - Tracks box/method entry/exit via brace depth - Identifies `local` statements outside method scope - Filters out comments (lines starting with `//`) - **Integration**: Added to cli.hako in text-based rules section ## Technical Approach - **Context Tracking**: Maintains `in_box` and `in_method` flags - **Brace Depth Counter**: Tracks `{` and `}` to determine scope boundaries - **Line-by-line Analysis**: Checks each line for `local ` prefix when not in method - **Comment Filtering**: Ignores commented-out local declarations ## Test Cases - **ok.hako**: All `local` declarations inside methods → no warnings - Helper.calculate() and Helper.process() both referenced from Main.main() - Avoids HC011 (unreachable method) warnings - **ng.hako**: Top-level `local global_temp` outside any method → HC018 warning ## Test Results ``` [TEST/OK] HC011_dead_methods [TEST/OK] HC012_dead_static_box [TEST/OK] HC013_duplicate_method [TEST/OK] HC014_missing_entrypoint [TEST/OK] HC016_unused_alias [TEST/OK] HC017_non_ascii_quotes [TEST/OK] HC018_top_level_local ← NEW [TEST/SUMMARY] all green ``` ## Diagnostic Format ``` [HC018] top-level local declaration (not allowed): : ``` ## Architecture - Box-first design: RuleTopLevelLocalBox with single responsibility - Helper methods: _trim(), _is_comment(), _split_lines(), _itoa() - Clean separation of concerns: parsing, context tracking, reporting 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tools/hako_check/cli.hako | 2 + .../rules/rule_top_level_local.hako | 116 ++++++++++++++++++ .../tests/HC018_top_level_local/expected.json | 3 + .../tests/HC018_top_level_local/ng.hako | 9 ++ .../tests/HC018_top_level_local/ok.hako | 24 ++++ 5 files changed, 154 insertions(+) create mode 100644 tools/hako_check/rules/rule_top_level_local.hako create mode 100644 tools/hako_check/tests/HC018_top_level_local/expected.json create mode 100644 tools/hako_check/tests/HC018_top_level_local/ng.hako create mode 100644 tools/hako_check/tests/HC018_top_level_local/ok.hako diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index 1d08fcd1..cd7b3377 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -12,6 +12,7 @@ using tools.hako_check.rules.rule_non_ascii_quotes as RuleNonAsciiQuotesBox using tools.hako_check.rules.rule_dead_static_box as RuleDeadStaticBoxBox using tools.hako_check.rules.rule_duplicate_method as RuleDuplicateMethodBox 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.render.graphviz as GraphvizRenderBox using tools.hako_parser.parser_core as HakoParserCoreBox @@ -79,6 +80,7 @@ static box HakoAnalyzerBox { // HC017 must inspect original text prior to sanitize RuleNonAsciiQuotesBox.apply(text_raw, p, out) RuleJsonfragUsageBox.apply(text, p, out) + RuleTopLevelLocalBox.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_top_level_local.hako b/tools/hako_check/rules/rule_top_level_local.hako new file mode 100644 index 00000000..e30bfa2e --- /dev/null +++ b/tools/hako_check/rules/rule_top_level_local.hako @@ -0,0 +1,116 @@ +// tools/hako_check/rules/rule_top_level_local.hako — HC018: Top-level local in prelude +// Detects top-level `local` statements (outside of methods/boxes), which are cleanup omissions. + +using selfhost.shared.common.string_helpers as Str + +static box RuleTopLevelLocalBox { + method apply(text, path, out) { + if text == null { return 0 } + + local lines = me._split_lines(text) + local i = 0 + local in_box = 0 + local in_method = 0 + local brace_depth = 0 + + while i < lines.size() { + local ln = lines.get(i) + local trimmed = me._trim(ln) + + // Track context: are we inside a box or method? + if trimmed.indexOf("box ") == 0 || trimmed.indexOf("static box ") == 0 { + in_box = 1 + } + if in_box == 1 && (trimmed.indexOf("method ") >= 0 || trimmed.indexOf("birth(") >= 0) { + in_method = 1 + } + + // Track brace depth + local j = 0 + while j < trimmed.length() { + local ch = trimmed.substring(j, j+1) + if ch == "{" { brace_depth = brace_depth + 1 } + if ch == "}" { + brace_depth = brace_depth - 1 + if brace_depth == 0 { + in_method = 0 + in_box = 0 + } + } + j = j + 1 + } + + // Check for top-level local (not inside method, not a comment) + if trimmed.indexOf("local ") == 0 { + if in_method == 0 && me._is_comment(trimmed) == 0 { + out.push("[HC018] top-level local declaration (not allowed): " + path + ":" + me._itoa(i+1)) + } + } + + i = i + 1 + } + return out.size() + } + + _trim(s) { + if s == null { return "" } + local start = 0 + local end = s.length() + + // Trim leading whitespace + while start < end { + local ch = s.substring(start, start+1) + if ch == " " || ch == "\t" { start = start + 1 } else { break } + } + + // Trim trailing whitespace + while end > start { + local ch = s.substring(end-1, end) + if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { end = end - 1 } else { break } + } + + return s.substring(start, end) + } + + _is_comment(s) { + if s == null { return 0 } + local trimmed = me._trim(s) + if trimmed.indexOf("//") == 0 { return 1 } + return 0 + } + + _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 RuleTopLevelLocalMain { method main(args) { return 0 } } diff --git a/tools/hako_check/tests/HC018_top_level_local/expected.json b/tools/hako_check/tests/HC018_top_level_local/expected.json new file mode 100644 index 00000000..20ded33a --- /dev/null +++ b/tools/hako_check/tests/HC018_top_level_local/expected.json @@ -0,0 +1,3 @@ +{"diagnostics":[ + {"file":"ng.hako","line":3,"rule":"HC018","message":"[HC018] top-level local declaration (not allowed): ng.hako:3","quickFix":"","severity":"warning"} +]} diff --git a/tools/hako_check/tests/HC018_top_level_local/ng.hako b/tools/hako_check/tests/HC018_top_level_local/ng.hako new file mode 100644 index 00000000..3d145142 --- /dev/null +++ b/tools/hako_check/tests/HC018_top_level_local/ng.hako @@ -0,0 +1,9 @@ +// ng.hako — has top-level local declaration (not allowed) + +local global_temp + +static box Main { + method main() { + return 0 + } +} diff --git a/tools/hako_check/tests/HC018_top_level_local/ok.hako b/tools/hako_check/tests/HC018_top_level_local/ok.hako new file mode 100644 index 00000000..87b72252 --- /dev/null +++ b/tools/hako_check/tests/HC018_top_level_local/ok.hako @@ -0,0 +1,24 @@ +// ok.hako — all locals are inside methods (no top-level local) + +static box Helper { + method calculate(x) { + local result + result = x * 2 + return result + } + + method process() { + local temp + temp = 42 + return temp + } +} + +static box Main { + method main() { + local value + value = Helper.calculate(10) + Helper.process() + return 0 + } +}