Implement HC018: Top-level local declaration detection
## 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): <path>:<line>
```
## 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 <noreply@anthropic.com>
This commit is contained in:
@ -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_dead_static_box as RuleDeadStaticBoxBox
|
||||||
using tools.hako_check.rules.rule_duplicate_method as RuleDuplicateMethodBox
|
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_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_check.render.graphviz as GraphvizRenderBox
|
||||||
using tools.hako_parser.parser_core as HakoParserCoreBox
|
using tools.hako_parser.parser_core as HakoParserCoreBox
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ static box HakoAnalyzerBox {
|
|||||||
// HC017 must inspect original text prior to sanitize
|
// HC017 must inspect original text prior to sanitize
|
||||||
RuleNonAsciiQuotesBox.apply(text_raw, p, out)
|
RuleNonAsciiQuotesBox.apply(text_raw, p, out)
|
||||||
RuleJsonfragUsageBox.apply(text, p, out)
|
RuleJsonfragUsageBox.apply(text, p, out)
|
||||||
|
RuleTopLevelLocalBox.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)
|
||||||
|
|||||||
116
tools/hako_check/rules/rule_top_level_local.hako
Normal file
116
tools/hako_check/rules/rule_top_level_local.hako
Normal file
@ -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 } }
|
||||||
@ -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"}
|
||||||
|
]}
|
||||||
9
tools/hako_check/tests/HC018_top_level_local/ng.hako
Normal file
9
tools/hako_check/tests/HC018_top_level_local/ng.hako
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// ng.hako — has top-level local declaration (not allowed)
|
||||||
|
|
||||||
|
local global_temp
|
||||||
|
|
||||||
|
static box Main {
|
||||||
|
method main() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
24
tools/hako_check/tests/HC018_top_level_local/ok.hako
Normal file
24
tools/hako_check/tests/HC018_top_level_local/ok.hako
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user