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 + } +}