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:
nyash-codex
2025-11-08 03:14:22 +09:00
parent 541b6d386e
commit 3d366f5cb8
5 changed files with 154 additions and 0 deletions

View File

@ -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)

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

View File

@ -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"}
]}

View File

@ -0,0 +1,9 @@
// ng.hako — has top-level local declaration (not allowed)
local global_temp
static box Main {
method main() {
return 0
}
}

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