From a8f6e537853ec00e23e810ce6c4f680bb65d9582 Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Fri, 29 May 2026 08:47:55 +0900 Subject: [PATCH] fix(pp): // line comment containing /* no longer eats subsequent lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stripBlockComments scanned each line for a /* block-comment opener while tracking string literals but had no notion of // or && line comments. A line like `// see app/api/*.prg` would open a block comment from /*.prg that ran until EOF or the next */, silently dropping every FUNCTION declaration in between. The compiled file ended up with an empty symbols slice, and callers in other files panicked at runtime with "no function symbol for call". Hit while writing app/lib/text.prg in solmade — its `// build's \`app/api/*.prg\` glob doesn't pick it up` line dropped all three of QueryParamRaw / UrlDecodeBytes / IsAllDigits. Fix: detect // and && line-comment markers before the /* check. When one is seen, copy the rest of the line through verbatim (the lexer and #command machinery still need it) and stop scanning so the embedded /* can't open a block comment. Two regression tests cover both markers. Full mandatory test suite (go test ./..., FiveSql2 43/43, compat 56/56, std.ch 17/17, FRB 7/7, pgserver 11/11) still passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- compiler/pp/pp.go | 18 ++++++++++++++++++ compiler/pp/pp_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/compiler/pp/pp.go b/compiler/pp/pp.go index d689899..bc1cef2 100644 --- a/compiler/pp/pp.go +++ b/compiler/pp/pp.go @@ -659,6 +659,10 @@ func (pp *Preprocessor) applyRules(line string) string { // stripBlockComments removes /* ... */ comments from a line. // If a /* is found without closing */, sets inBlock to true. +// +// `//` and `&&` line-comment markers are detected first so a `/*` +// substring inside one of them (e.g. `// see app/api/*.prg`) doesn't +// start a runaway block comment that eats subsequent lines. func stripBlockComments(line string, inBlock *bool) string { var out strings.Builder i := 0 @@ -679,6 +683,20 @@ func stripBlockComments(line string, inBlock *bool) string { i++ continue } + // // line comment — copy the rest of the line through verbatim + // (the lexer/`#command` machinery still needs to see it) but + // don't scan it for `/*` so an embedded `/*` substring (e.g. + // `// see app/api/*.prg`) can't open a runaway block comment + // that eats subsequent lines. + if i+1 < len(line) && line[i] == '/' && line[i+1] == '/' { + out.WriteString(line[i:]) + return out.String() + } + // && Harbour-style line comment — same rule. + if i+1 < len(line) && line[i] == '&' && line[i+1] == '&' { + out.WriteString(line[i:]) + return out.String() + } // Block comment start if i+1 < len(line) && line[i] == '/' && line[i+1] == '*' { // Find closing */ diff --git a/compiler/pp/pp_test.go b/compiler/pp/pp_test.go index 99e2660..c3ea659 100644 --- a/compiler/pp/pp_test.go +++ b/compiler/pp/pp_test.go @@ -262,3 +262,34 @@ func TestMissingInclude(t *testing.T) { t.Error("code after missing include should continue") } } + +// TestSlashStarInsideLineComment locks in that a `/*` substring inside +// a `//` line comment does NOT open a runaway block comment. Before +// the fix, a comment like "// see app/api/*.prg" would start a block +// comment from `/*.prg` that ate every subsequent line until EOF or +// the next `*/`, silently dropping FUNCTION declarations. +func TestSlashStarInsideLineComment(t *testing.T) { + p := New() + src := `// see app/api/*.prg for the glob +FUNCTION Foo() +RETURN 1 +` + result, _ := p.Process("test.prg", src) + if !strings.Contains(result, "FUNCTION Foo") { + t.Errorf("FUNCTION Foo() should survive a // line comment that contains /*; got:\n%s", result) + } +} + +// TestDoubleAmpInsideLineComment — same protection for Harbour's `&&` +// line comment marker. +func TestDoubleAmpInsideLineComment(t *testing.T) { + p := New() + src := `&& glob /*.prg +FUNCTION Foo() +RETURN 1 +` + result, _ := p.Process("test.prg", src) + if !strings.Contains(result, "FUNCTION Foo") { + t.Errorf("FUNCTION Foo() should survive a && line comment that contains /*; got:\n%s", result) + } +}