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>
296 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|