Files
five/compiler/pp/pp_test.go
CharlesKWON a8f6e53785 fix(pp): // line comment containing /* no longer eats subsequent lines
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) <noreply@anthropic.com>
2026-05-29 08:47:55 +09:00

296 lines
7.0 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
package pp
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestDefine(t *testing.T) {
p := New()
src := `#define VERSION "1.0"
? VERSION`
result, errs := p.Process("test.prg", src)
if len(errs) > 0 {
t.Fatal(errs)
}
if !strings.Contains(result, `"1.0"`) {
t.Errorf("define not substituted: %q", result)
}
}
func TestDefineFlag(t *testing.T) {
p := New()
src := `#define DEBUG
#ifdef DEBUG
? "Debug mode"
#else
? "Release mode"
#endif`
result, errs := p.Process("test.prg", src)
if len(errs) > 0 {
t.Fatal(errs)
}
if !strings.Contains(result, "Debug mode") {
t.Error("ifdef DEBUG should include Debug mode")
}
if strings.Contains(result, "Release mode") {
t.Error("should NOT include Release mode")
}
}
func TestIfndef(t *testing.T) {
p := New()
src := `#ifndef RELEASE
? "Not release"
#else
? "Release"
#endif`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, "Not release") {
t.Error("ifndef should include 'Not release'")
}
}
func TestNestedIfdef(t *testing.T) {
p := New()
p.Define("A", "")
src := `#ifdef A
? "A is defined"
#ifdef B
? "B is defined"
#else
? "B is not defined"
#endif
#endif`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, "A is defined") {
t.Error("A should be defined")
}
if !strings.Contains(result, "B is not defined") {
t.Error("B should not be defined")
}
if strings.Contains(result, "B is defined") {
t.Error("B should NOT appear as defined")
}
}
func TestUndef(t *testing.T) {
p := New()
src := `#define FOO "bar"
? FOO
#undef FOO
? FOO`
result, _ := p.Process("test.prg", src)
lines := strings.Split(result, "\n")
// First ? should have "bar", second should still have FOO (not substituted)
found := 0
for _, l := range lines {
l = strings.TrimSpace(l)
if strings.Contains(l, `"bar"`) {
found++
}
}
if found != 1 {
t.Errorf("expected FOO substituted once, found %d times", found)
}
}
func TestInclude(t *testing.T) {
dir := t.TempDir()
// Create header file
headerContent := `#define APP_NAME "Five Test"
#define APP_VERSION "1.0"`
os.WriteFile(filepath.Join(dir, "myapp.ch"), []byte(headerContent), 0644)
// Create main file
src := `#include "myapp.ch"
? APP_NAME
? APP_VERSION`
p := New()
p.AddIncludeDir(dir)
result, errs := p.Process(filepath.Join(dir, "main.prg"), src)
if len(errs) > 0 {
t.Fatal(errs)
}
if !strings.Contains(result, `"Five Test"`) {
t.Errorf("APP_NAME not substituted: %q", result)
}
if !strings.Contains(result, `"1.0"`) {
t.Error("APP_VERSION not substituted")
}
}
func TestIncludeNested(t *testing.T) {
dir := t.TempDir()
// base.ch includes sub.ch
os.WriteFile(filepath.Join(dir, "sub.ch"), []byte(`#define SUB_VAL 42`), 0644)
os.WriteFile(filepath.Join(dir, "base.ch"), []byte(`#include "sub.ch"
#define BASE_VAL 100`), 0644)
src := `#include "base.ch"
? SUB_VAL
? BASE_VAL`
p := New()
p.AddIncludeDir(dir)
result, _ := p.Process(filepath.Join(dir, "main.prg"), src)
if !strings.Contains(result, "42") {
t.Error("SUB_VAL from nested include should be 42")
}
if !strings.Contains(result, "100") {
t.Error("BASE_VAL should be 100")
}
}
func TestIncludeGuard(t *testing.T) {
dir := t.TempDir()
// Header with include guard
header := `#ifndef _MYHEADER_CH
#define _MYHEADER_CH
#define MY_CONST 999
#endif`
os.WriteFile(filepath.Join(dir, "myheader.ch"), []byte(header), 0644)
// Include twice — should work (guard prevents double processing)
src := `#include "myheader.ch"
#include "myheader.ch"
? MY_CONST`
p := New()
p.AddIncludeDir(dir)
result, _ := p.Process(filepath.Join(dir, "main.prg"), src)
if !strings.Contains(result, "999") {
t.Error("MY_CONST should be 999")
}
}
func TestHbclassChHandled(t *testing.T) {
dir := t.TempDir()
// Simulate hbclass.ch — #command CLASS maps to comments (Five handles natively)
hbclass := `#ifndef HB_CLASS_CH_
#define HB_CLASS_CH_
#command CLASS <name> => // class <name> handled natively
#endif`
os.WriteFile(filepath.Join(dir, "hbclass.ch"), []byte(hbclass), 0644)
src := `#include "hbclass.ch"
CLASS Person
FUNCTION Main()
? "OK"
RETURN NIL`
p := New()
p.AddIncludeDir(dir)
result, errs := p.Process(filepath.Join(dir, "main.prg"), src)
if len(errs) > 0 {
t.Fatal(errs)
}
// #command directives themselves should be removed
if strings.Contains(result, "#command") {
t.Error("preprocessor directives should be removed")
}
// CLASS Person should be expanded by #command rule
if !strings.Contains(result, "Person") {
t.Error("Person should appear in output")
}
// FUNCTION should still be there
if !strings.Contains(result, "FUNCTION Main") {
t.Error("FUNCTION Main should pass through")
}
}
func TestDefineInString(t *testing.T) {
p := New()
src := `#define FOO bar
? "FOO should not change"
? FOO`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, `"FOO should not change"`) {
t.Error("define should not replace inside strings")
}
// Outside string should be replaced
lines := strings.Split(result, "\n")
for _, l := range lines {
l = strings.TrimSpace(l)
if l == "? bar" {
return // found replacement outside string
}
}
t.Error("FOO should be replaced to bar outside strings")
}
func TestPragma(t *testing.T) {
p := New()
src := `#pragma compatibility(harbour)
? "test"`
result, _ := p.Process("test.prg", src)
if !strings.Contains(result, "// pragma") || !strings.Contains(result, "compatibility") {
t.Error("pragma should be converted to comment")
}
}
func TestMissingInclude(t *testing.T) {
p := New()
src := `#include "nonexistent.ch"
? "still works"`
result, _ := p.Process("test.prg", src)
// Missing include should not crash, just skip with comment
if !strings.Contains(result, "not found") {
t.Error("missing include should produce a comment")
}
if !strings.Contains(result, "still works") {
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)
}
}