diff --git a/tools/hako_check/cli.hako b/tools/hako_check/cli.hako index beb04ea9..1d08fcd1 100644 --- a/tools/hako_check/cli.hako +++ b/tools/hako_check/cli.hako @@ -11,6 +11,7 @@ using tools.hako_check.rules.rule_unused_alias as RuleUnusedAliasBox 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.render.graphviz as GraphvizRenderBox using tools.hako_parser.parser_core as HakoParserCoreBox @@ -101,6 +102,13 @@ static box HakoAnalyzerBox { local added = after_n - before_n print("[hako_check/HC013] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n)) } + before_n = out.size() + RuleMissingEntrypointBox.apply_ir(ir, p, out) + if debug == 1 { + local after_n = out.size() + local added = after_n - before_n + print("[hako_check/HC014] file=" + p + " added=" + me._itoa(added) + " total_out=" + me._itoa(after_n)) + } // suppression: HC012(dead box) > HC011(unreachable method) local filtered = me._suppress_overlap(out) // flush (text only) diff --git a/tools/hako_check/rules/rule_missing_entrypoint.hako b/tools/hako_check/rules/rule_missing_entrypoint.hako new file mode 100644 index 00000000..3426b1ce --- /dev/null +++ b/tools/hako_check/rules/rule_missing_entrypoint.hako @@ -0,0 +1,64 @@ +// tools/hako_check/rules/rule_missing_entrypoint.hako — HC014: Missing Entrypoint +// Detect when no entrypoint (Main.main or main) exists in the analyzed code. + +using selfhost.shared.common.string_helpers as Str + +static box RuleMissingEntrypointBox { + method apply_ir(ir, path, out) { + local entrypoints = ir.get("entrypoints") + if entrypoints == null { return 0 } + + local methods = ir.get("methods") + if methods == null || methods.size() == 0 { + // No methods at all - definitely missing entrypoint + out.push("[HC014] missing entrypoint (Main.main or main)") + return out.size() + } + + // Check if any entrypoint exists + local found = 0 + local ei = 0 + while ei < entrypoints.size() { + local ep = entrypoints.get(ei) + if me._has_entrypoint_method(methods, ep) == 1 { + found = 1 + break + } + ei = ei + 1 + } + + // If no entrypoint found, report error + if found == 0 { + out.push("[HC014] missing entrypoint (Main.main or main)") + } + + return out.size() + } + + _has_entrypoint_method(methods, ep_name) { + // Check if method starting with ep_name exists + // e.g., "Main.main" matches "Main.main/0", "Main.main/1", etc. + // "main" matches "main/0", etc. + local mi = 0 + while mi < methods.size() { + local m = methods.get(mi) + if m == null { mi = mi + 1; continue } + + // Check exact match with arity suffix + // ep_name can be "Main.main" or "main" + local len = ep_name.length() + if m.length() >= len + 2 { + local prefix = m.substring(0, len) + if prefix == ep_name { + // Check if followed by "/" (arity separator) + local next_char = m.substring(len, len+1) + if next_char == "/" { return 1 } + } + } + mi = mi + 1 + } + return 0 + } +} + +static box RuleMissingEntrypointMain { method main(args) { return 0 } } diff --git a/tools/hako_check/tests/HC014_missing_entrypoint/expected.json b/tools/hako_check/tests/HC014_missing_entrypoint/expected.json new file mode 100644 index 00000000..d75181a2 --- /dev/null +++ b/tools/hako_check/tests/HC014_missing_entrypoint/expected.json @@ -0,0 +1,4 @@ +{"diagnostics":[ + {"file":"ng.hako","line":1,"rule":"HC011","message":"[HC011] unreachable method (dead code): PLACEHOLDER :: Main.run/0","quickFix":"Remove or reference the dead method from an entrypoint","severity":"error"}, + {"file":"ng.hako","line":1,"rule":"HC014","message":"[HC014] missing entrypoint (Main.main or main)","quickFix":"","severity":"warning"} +]} diff --git a/tools/hako_check/tests/HC014_missing_entrypoint/ng.hako b/tools/hako_check/tests/HC014_missing_entrypoint/ng.hako new file mode 100644 index 00000000..23c464c8 --- /dev/null +++ b/tools/hako_check/tests/HC014_missing_entrypoint/ng.hako @@ -0,0 +1,7 @@ +// ng.hako — missing Main.main entrypoint + +static box Main { + method run() { + return 0 + } +} diff --git a/tools/hako_check/tests/HC014_missing_entrypoint/ok.hako b/tools/hako_check/tests/HC014_missing_entrypoint/ok.hako new file mode 100644 index 00000000..bdff0ea9 --- /dev/null +++ b/tools/hako_check/tests/HC014_missing_entrypoint/ok.hako @@ -0,0 +1,14 @@ +// ok.hako — has Main.main entrypoint + +static box Helper { + method calculate(x) { + return x * 2 + } +} + +static box Main { + method main() { + Helper.calculate(42) + return 0 + } +}