diff --git a/compiler/analyzer/analyzer.go b/compiler/analyzer/analyzer.go index 0ca7303..9706111 100644 --- a/compiler/analyzer/analyzer.go +++ b/compiler/analyzer/analyzer.go @@ -111,11 +111,13 @@ func Analyze(file *ast.File, externalFuncs ...map[string]bool) []Diagnostic { } } - // Phase 2: Analyze each function + // Phase 2: Analyze each function and class method body for _, d := range file.Decls { switch decl := d.(type) { case *ast.FuncDecl: a.analyzeFunc(decl) + case *ast.MethodDecl: + a.analyzeMethod(decl) } } @@ -175,6 +177,63 @@ func (a *Analyzer) analyzeFunc(fn *ast.FuncDecl) { } } +// analyzeMethod walks a class-method body (`METHOD Foo() CLASS TBar`) +// applying the same undeclared-variable and unused-variable checks +// that analyzeFunc performs on standalone functions. Without this, +// unresolved identifiers inside CLASS methods silently fell through +// to gengo's memvar fallback (NIL at runtime) — e.g. a missing +// `#include "dbinfo.ch"` leaving DBI_FULLPATH undefined in a method. +func (a *Analyzer) analyzeMethod(m *ast.MethodDecl) { + a.scope = &Scope{ + Name: m.Name, + Declared: make(map[string]VarInfo), + Used: make(map[string]bool), + } + + // Module-level STATICs are visible to class methods too + for name, info := range a.moduleStatics { + a.scope.Declared[name] = info + } + + // Parameters + for _, p := range m.Params { + a.scope.Declared[strings.ToUpper(p.Name)] = VarInfo{ + Name: p.Name, + Pos: p.NamePos, + IsParam: true, + } + } + + // LOCAL / STATIC declarations inside the method + for _, d := range m.Decls { + if vd, ok := d.(*ast.VarDecl); ok { + for _, v := range vd.Vars { + a.scope.Declared[strings.ToUpper(v.Name)] = VarInfo{ + Name: v.Name, + Pos: v.NamePos, + Kind: vd.Scope, + } + } + } + } + + for _, stmt := range m.Body { + a.analyzeStmt(stmt) + } + + // Unused-variable hints (same exclusions as analyzeFunc) + for name, info := range a.scope.Declared { + if !a.scope.Used[name] && !info.IsParam { + lower := strings.ToLower(info.Name) + if lower == "i" || lower == "j" || lower == "k" || lower == "n" || + lower == "err" || lower == "_" { + continue + } + a.hint(info.Pos, "unused variable '%s'", info.Name) + } + } +} + func (a *Analyzer) analyzeStmt(stmt ast.Stmt) { if stmt == nil { return @@ -487,6 +546,8 @@ var rtlFunctions = map[string]bool{ "DBRECALL": true, "DBCOMMIT": true, "DBRLOCK": true, "DBRUNLOCK": true, "DBSEEK": true, "DBSELECTAREA": true, "DBPACK": true, "DBZAP": true, "DBCREATE": true, "DBINFO": true, "DBORDERINFO": true, "DBSETINDEX": true, + // FiveSql2 hybrid hot-path RTL (pcode + Go-native scan) + "PCCOMPILE": true, "PCEVAL": true, "SQLSCAN": true, "RECALL": true, "PACK": true, "ZAP": true, "FLOCK": true, "DBUNLOCK": true, "__DBPACK": true, "__DBZAP": true,