mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +01:00
feat(extgen): make the generator idempotent and avoid touching the original source (#2011)
This commit is contained in:
committed by
GitHub
parent
db59edb590
commit
fba79a6ac8
@@ -4,8 +4,9 @@ import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"go/format"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
@@ -21,7 +22,7 @@ type GoFileGenerator struct {
|
||||
type goTemplateData struct {
|
||||
PackageName string
|
||||
BaseName string
|
||||
Imports []string
|
||||
SanitizedBaseName string
|
||||
Constants []phpConstant
|
||||
Variables []string
|
||||
InternalFunctions []string
|
||||
@@ -30,16 +31,7 @@ type goTemplateData struct {
|
||||
}
|
||||
|
||||
func (gg *GoFileGenerator) generate() error {
|
||||
filename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+".go")
|
||||
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
backupFilename := filename + ".bak"
|
||||
if err := os.Rename(filename, backupFilename); err != nil {
|
||||
return fmt.Errorf("backing up existing Go file: %w", err)
|
||||
}
|
||||
|
||||
gg.generator.SourceFile = backupFilename
|
||||
}
|
||||
filename := filepath.Join(gg.generator.BuildDir, gg.generator.BaseName+"_generated.go")
|
||||
|
||||
content, err := gg.buildContent()
|
||||
if err != nil {
|
||||
@@ -51,38 +43,18 @@ func (gg *GoFileGenerator) generate() error {
|
||||
|
||||
func (gg *GoFileGenerator) buildContent() (string, error) {
|
||||
sourceAnalyzer := SourceAnalyzer{}
|
||||
imports, variables, internalFunctions, err := sourceAnalyzer.analyze(gg.generator.SourceFile)
|
||||
packageName, variables, internalFunctions, err := sourceAnalyzer.analyze(gg.generator.SourceFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("analyzing source file: %w", err)
|
||||
}
|
||||
|
||||
filteredImports := make([]string, 0, len(imports))
|
||||
for _, imp := range imports {
|
||||
if imp != `"C"` && imp != `"unsafe"` && imp != `"github.com/dunglas/frankenphp"` && imp != `"runtime/cgo"` {
|
||||
filteredImports = append(filteredImports, imp)
|
||||
}
|
||||
}
|
||||
|
||||
classes := make([]phpClass, len(gg.generator.Classes))
|
||||
copy(classes, gg.generator.Classes)
|
||||
|
||||
if len(classes) > 0 {
|
||||
hasCgo := false
|
||||
for _, imp := range imports {
|
||||
if imp == `"runtime/cgo"` {
|
||||
hasCgo = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasCgo {
|
||||
filteredImports = append(filteredImports, `"runtime/cgo"`)
|
||||
}
|
||||
}
|
||||
|
||||
templateContent, err := gg.getTemplateContent(goTemplateData{
|
||||
PackageName: SanitizePackageName(gg.generator.BaseName),
|
||||
PackageName: packageName,
|
||||
BaseName: gg.generator.BaseName,
|
||||
Imports: filteredImports,
|
||||
SanitizedBaseName: SanitizePackageName(gg.generator.BaseName),
|
||||
Constants: gg.generator.Constants,
|
||||
Variables: variables,
|
||||
InternalFunctions: internalFunctions,
|
||||
@@ -94,7 +66,12 @@ func (gg *GoFileGenerator) buildContent() (string, error) {
|
||||
return "", fmt.Errorf("executing template: %w", err)
|
||||
}
|
||||
|
||||
return templateContent, nil
|
||||
fc, err := format.Source([]byte(templateContent))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("formatting source: %w", err)
|
||||
}
|
||||
|
||||
return string(fc), nil
|
||||
}
|
||||
|
||||
func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, error) {
|
||||
@@ -106,6 +83,10 @@ func (gg *GoFileGenerator) getTemplateContent(data goTemplateData) (string, erro
|
||||
funcMap["isVoid"] = func(t phpType) bool {
|
||||
return t == phpVoid
|
||||
}
|
||||
funcMap["extractGoFunctionName"] = extractGoFunctionName
|
||||
funcMap["extractGoFunctionSignatureParams"] = extractGoFunctionSignatureParams
|
||||
funcMap["extractGoFunctionSignatureReturn"] = extractGoFunctionSignatureReturn
|
||||
funcMap["extractGoFunctionCallParams"] = extractGoFunctionCallParams
|
||||
|
||||
tmpl := template.Must(template.New("gofile").Funcs(funcMap).Parse(goFileContent))
|
||||
|
||||
@@ -128,7 +109,7 @@ type GoParameter struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
var phpToGoTypeMap= map[phpType]string{
|
||||
var phpToGoTypeMap = map[phpType]string{
|
||||
phpString: "string",
|
||||
phpInt: "int64",
|
||||
phpFloat: "float64",
|
||||
@@ -146,3 +127,119 @@ func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
|
||||
|
||||
return "any"
|
||||
}
|
||||
|
||||
// extractGoFunctionName extracts the Go function name from a Go function signature string.
|
||||
func extractGoFunctionName(goFunction string) string {
|
||||
idx := strings.Index(goFunction, "func ")
|
||||
if idx == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
start := idx + len("func ")
|
||||
|
||||
end := start
|
||||
for end < len(goFunction) && goFunction[end] != '(' {
|
||||
end++
|
||||
}
|
||||
|
||||
if end >= len(goFunction) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(goFunction[start:end])
|
||||
}
|
||||
|
||||
// extractGoFunctionSignatureParams extracts the parameters from a Go function signature.
|
||||
func extractGoFunctionSignatureParams(goFunction string) string {
|
||||
start := strings.IndexByte(goFunction, '(')
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
start++
|
||||
|
||||
depth := 1
|
||||
end := start
|
||||
for end < len(goFunction) && depth > 0 {
|
||||
switch goFunction[end] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
}
|
||||
if depth > 0 {
|
||||
end++
|
||||
}
|
||||
}
|
||||
|
||||
if end >= len(goFunction) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(goFunction[start:end])
|
||||
}
|
||||
|
||||
// extractGoFunctionSignatureReturn extracts the return type from a Go function signature.
|
||||
func extractGoFunctionSignatureReturn(goFunction string) string {
|
||||
start := strings.IndexByte(goFunction, '(')
|
||||
if start == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
depth := 1
|
||||
pos := start + 1
|
||||
for pos < len(goFunction) && depth > 0 {
|
||||
switch goFunction[pos] {
|
||||
case '(':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
}
|
||||
pos++
|
||||
}
|
||||
|
||||
if pos >= len(goFunction) {
|
||||
return ""
|
||||
}
|
||||
|
||||
end := strings.IndexByte(goFunction[pos:], '{')
|
||||
if end == -1 {
|
||||
return ""
|
||||
}
|
||||
end += pos
|
||||
|
||||
returnType := strings.TrimSpace(goFunction[pos:end])
|
||||
return returnType
|
||||
}
|
||||
|
||||
// extractGoFunctionCallParams extracts just the parameter names for calling a function.
|
||||
func extractGoFunctionCallParams(goFunction string) string {
|
||||
params := extractGoFunctionSignatureParams(goFunction)
|
||||
if params == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var names []string
|
||||
parts := strings.Split(params, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if len(part) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
words := strings.Fields(part)
|
||||
if len(words) > 0 {
|
||||
names = append(names, words[0])
|
||||
}
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
for i, name := range names {
|
||||
if i > 0 {
|
||||
result.WriteString(", ")
|
||||
}
|
||||
|
||||
result.WriteString(name)
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -69,16 +71,20 @@ func anotherHelper() {
|
||||
goGen := GoFileGenerator{generator}
|
||||
require.NoError(t, goGen.generate())
|
||||
|
||||
expectedFile := filepath.Join(tmpDir, "test.go")
|
||||
require.FileExists(t, expectedFile)
|
||||
sourceStillExists := filepath.Join(tmpDir, "test.go")
|
||||
require.FileExists(t, sourceStillExists)
|
||||
sourceStillContent, err := readFile(sourceStillExists)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, sourceContent, sourceStillContent, "Source file should not be modified")
|
||||
|
||||
content, err := readFile(expectedFile)
|
||||
generatedFile := filepath.Join(tmpDir, "test_generated.go")
|
||||
require.FileExists(t, generatedFile)
|
||||
|
||||
generatedContent, err := readFile(generatedFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
testGoFileBasicStructure(t, content, "test")
|
||||
testGoFileImports(t, content)
|
||||
testGoFileExportedFunctions(t, content, generator.Functions)
|
||||
testGoFileInternalFunctions(t, content)
|
||||
testGeneratedFileBasicStructure(t, generatedContent, "main", "test")
|
||||
testGeneratedFileWrappers(t, generatedContent, generator.Functions)
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_BuildContent(t *testing.T) {
|
||||
@@ -87,6 +93,7 @@ func TestGoFileGenerator_BuildContent(t *testing.T) {
|
||||
baseName string
|
||||
sourceFile string
|
||||
functions []phpFunction
|
||||
classes []phpClass
|
||||
contains []string
|
||||
notContains []string
|
||||
}{
|
||||
@@ -107,13 +114,14 @@ func test() {
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"package simple",
|
||||
"package main",
|
||||
`#include "simple.h"`,
|
||||
`import "C"`,
|
||||
"func init()",
|
||||
"frankenphp.RegisterExtension(",
|
||||
"//export test",
|
||||
"func test()",
|
||||
"//export go_test",
|
||||
"func go_test()",
|
||||
"test()", // wrapper calls original function
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -142,12 +150,10 @@ func process(data *go_string) *go_value {
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"package complex",
|
||||
`"fmt"`,
|
||||
`"strings"`,
|
||||
`"encoding/json"`,
|
||||
"//export process",
|
||||
"package main",
|
||||
"//export go_process",
|
||||
`"C"`,
|
||||
"process(", // wrapper calls original function
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -173,9 +179,81 @@ func internalFunc2(data string) {
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"//export go_publicFunc",
|
||||
"func go_publicFunc()",
|
||||
"publicFunc()", // wrapper calls original function
|
||||
},
|
||||
notContains: []string{
|
||||
"func internalFunc1() string",
|
||||
"func internalFunc2(data string)",
|
||||
"//export publicFunc",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "runtime/cgo blank import without classes",
|
||||
baseName: "no_classes",
|
||||
sourceFile: createTempSourceFile(t, `package main
|
||||
|
||||
//export_php: getValue(): string
|
||||
func getValue() string {
|
||||
return "test"
|
||||
}`),
|
||||
functions: []phpFunction{
|
||||
{
|
||||
Name: "getValue",
|
||||
ReturnType: phpString,
|
||||
GoFunction: `func getValue() string {
|
||||
return "test"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
classes: nil,
|
||||
contains: []string{
|
||||
`_ "runtime/cgo"`,
|
||||
"func init()",
|
||||
"frankenphp.RegisterExtension(",
|
||||
},
|
||||
notContains: []string{
|
||||
"cgo.NewHandle",
|
||||
"registerGoObject",
|
||||
"getGoObject",
|
||||
"removeGoObject",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "runtime/cgo normal import with classes",
|
||||
baseName: "with_classes",
|
||||
sourceFile: createTempSourceFile(t, `package main
|
||||
|
||||
//export_php:class TestClass
|
||||
type TestStruct struct {
|
||||
value string
|
||||
}
|
||||
|
||||
//export_php:method TestClass::getValue(): string
|
||||
func (ts *TestStruct) GetValue() string {
|
||||
return ts.value
|
||||
}`),
|
||||
functions: []phpFunction{},
|
||||
classes: []phpClass{
|
||||
{
|
||||
Name: "TestClass",
|
||||
GoStruct: "TestStruct",
|
||||
Methods: []phpClassMethod{
|
||||
{
|
||||
Name: "GetValue",
|
||||
ReturnType: phpString,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
`"runtime/cgo"`,
|
||||
"cgo.NewHandle",
|
||||
"func registerGoObject",
|
||||
"func getGoObject",
|
||||
},
|
||||
notContains: []string{
|
||||
`_ "runtime/cgo"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -186,6 +264,7 @@ func internalFunc2(data string) {
|
||||
BaseName: tt.baseName,
|
||||
SourceFile: tt.sourceFile,
|
||||
Functions: tt.functions,
|
||||
Classes: tt.classes,
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
@@ -195,6 +274,10 @@ func internalFunc2(data string) {
|
||||
for _, expected := range tt.contains {
|
||||
assert.Contains(t, content, expected, "Generated Go content should contain %q", expected)
|
||||
}
|
||||
|
||||
for _, notExpected := range tt.notContains {
|
||||
assert.NotContains(t, content, notExpected, "Generated Go content should NOT contain %q", notExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -204,11 +287,11 @@ func TestGoFileGenerator_PackageNameSanitization(t *testing.T) {
|
||||
baseName string
|
||||
expectedPackage string
|
||||
}{
|
||||
{"simple", "simple"},
|
||||
{"my-extension", "my_extension"},
|
||||
{"ext.with.dots", "ext_with_dots"},
|
||||
{"123invalid", "_123invalid"},
|
||||
{"valid_name", "valid_name"},
|
||||
{"simple", "main"},
|
||||
{"my-extension", "main"},
|
||||
{"ext.with.dots", "main"},
|
||||
{"123invalid", "main"},
|
||||
{"valid_name", "main"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -275,57 +358,6 @@ func TestGoFileGenerator_ErrorHandling(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_ImportFiltering(t *testing.T) {
|
||||
sourceContent := `package main
|
||||
|
||||
import (
|
||||
"C"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/dunglas/frankenphp/internal/extensions/types"
|
||||
"github.com/other/package"
|
||||
originalPkg "github.com/test/original"
|
||||
)
|
||||
|
||||
//export_php: test(): void
|
||||
func test() {}`
|
||||
|
||||
sourceFile := createTempSourceFile(t, sourceContent)
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "importtest",
|
||||
SourceFile: sourceFile,
|
||||
Functions: []phpFunction{
|
||||
{Name: "test", ReturnType: phpVoid, GoFunction: "func test() {}"},
|
||||
},
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
content, err := goGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedImports := []string{
|
||||
`"fmt"`,
|
||||
`"strings"`,
|
||||
`"github.com/other/package"`,
|
||||
}
|
||||
|
||||
for _, imp := range expectedImports {
|
||||
assert.Contains(t, content, imp, "Generated content should contain import: %s", imp)
|
||||
}
|
||||
|
||||
forbiddenImports := []string{
|
||||
`"C"`,
|
||||
}
|
||||
|
||||
cImportCount := strings.Count(content, `"C"`)
|
||||
assert.Equal(t, 1, cImportCount, "Expected exactly 1 occurrence of 'import \"C\"'")
|
||||
|
||||
for _, imp := range forbiddenImports[1:] {
|
||||
assert.NotContains(t, content, imp, "Generated content should NOT contain import: %s", imp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_ComplexScenario(t *testing.T) {
|
||||
sourceContent := `package example
|
||||
|
||||
@@ -398,26 +430,12 @@ func debugPrint(msg string) {
|
||||
goGen := GoFileGenerator{generator}
|
||||
content, err := goGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, content, "package complex_example", "Package name should be sanitized")
|
||||
|
||||
internalFuncs := []string{
|
||||
"func internalProcess(data string) string",
|
||||
"func validateFormat(input string) bool",
|
||||
"func jsonHelper(data any) ([]byte, error)",
|
||||
"func debugPrint(msg string)",
|
||||
}
|
||||
|
||||
for _, fn := range internalFuncs {
|
||||
assert.Contains(t, content, fn, "Generated content should contain internal function: %s", fn)
|
||||
}
|
||||
assert.Contains(t, content, "package example", "Package name should match source package")
|
||||
|
||||
for _, fn := range functions {
|
||||
exportDirective := "//export " + fn.Name
|
||||
exportDirective := "//export go_" + fn.Name
|
||||
assert.Contains(t, content, exportDirective, "Generated content should contain export directive: %s", exportDirective)
|
||||
}
|
||||
|
||||
assert.False(t, strings.Contains(content, "types.Array") || strings.Contains(content, "types.Bool"), "Types should be replaced (types.* should not appear)")
|
||||
assert.True(t, strings.Contains(content, "return Array(") && strings.Contains(content, "return Bool("), "Replaced types should appear without types prefix")
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_MethodWrapperWithNullableParams(t *testing.T) {
|
||||
@@ -602,6 +620,434 @@ func (as *ArrayStruct) FilterData(data frankenphp.AssociativeArray, filter strin
|
||||
assert.Contains(t, content, "//export FilterData_wrapper", "Generated content should contain FilterData export directive")
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_Idempotency(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export_php: greet(name string): string
|
||||
func greet(name *go_string) *go_value {
|
||||
return String("Hello " + CStringToGoString(name))
|
||||
}
|
||||
|
||||
func internalHelper(data string) string {
|
||||
return strings.ToUpper(data)
|
||||
}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: tmpDir,
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "greet",
|
||||
ReturnType: phpString,
|
||||
GoFunction: `func greet(name *go_string) *go_value {
|
||||
return String("Hello " + CStringToGoString(name))
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
require.NoError(t, goGen.generate(), "First generation should succeed")
|
||||
|
||||
generatedFile := filepath.Join(tmpDir, "test_generated.go")
|
||||
require.FileExists(t, generatedFile, "Generated file should exist after first run")
|
||||
|
||||
firstRunContent, err := os.ReadFile(generatedFile)
|
||||
require.NoError(t, err)
|
||||
firstRunSourceContent, err := os.ReadFile(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, goGen.generate(), "Second generation should succeed")
|
||||
|
||||
secondRunContent, err := os.ReadFile(generatedFile)
|
||||
require.NoError(t, err)
|
||||
secondRunSourceContent, err := os.ReadFile(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, bytes.Equal(firstRunContent, secondRunContent), "Generated file content should be identical between runs")
|
||||
assert.True(t, bytes.Equal(firstRunSourceContent, secondRunSourceContent), "Source file should remain unchanged after both runs")
|
||||
assert.Equal(t, sourceContent, string(secondRunSourceContent), "Source file content should match original")
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_HeaderComments(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
//export_php: test(): void
|
||||
func test() {
|
||||
// simple function
|
||||
}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: tmpDir,
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "test",
|
||||
ReturnType: phpVoid,
|
||||
GoFunction: "func test() {\n\t// simple function\n}",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
require.NoError(t, goGen.generate())
|
||||
|
||||
generatedFile := filepath.Join(tmpDir, "test_generated.go")
|
||||
require.FileExists(t, generatedFile)
|
||||
|
||||
assertContainsHeaderComment(t, generatedFile)
|
||||
}
|
||||
|
||||
func TestExtractGoFunctionName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple function",
|
||||
input: "func test() {}",
|
||||
expected: "test",
|
||||
},
|
||||
{
|
||||
name: "function with params",
|
||||
input: "func calculate(a int, b int) int {}",
|
||||
expected: "calculate",
|
||||
},
|
||||
{
|
||||
name: "function with complex params",
|
||||
input: "func process(data *go_string, opts *go_nullable) *go_value {}",
|
||||
expected: "process",
|
||||
},
|
||||
{
|
||||
name: "function with whitespace",
|
||||
input: "func spacedName () {}",
|
||||
expected: "spacedName",
|
||||
},
|
||||
{
|
||||
name: "no func keyword",
|
||||
input: "test() {}",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractGoFunctionName(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGoFunctionSignatureParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no parameters",
|
||||
input: "func test() {}",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single parameter",
|
||||
input: "func test(name string) {}",
|
||||
expected: "name string",
|
||||
},
|
||||
{
|
||||
name: "multiple parameters",
|
||||
input: "func test(a int, b string, c bool) {}",
|
||||
expected: "a int, b string, c bool",
|
||||
},
|
||||
{
|
||||
name: "pointer parameters",
|
||||
input: "func test(data *go_string) {}",
|
||||
expected: "data *go_string",
|
||||
},
|
||||
{
|
||||
name: "nested parentheses",
|
||||
input: "func test(fn func(int) string) {}",
|
||||
expected: "fn func(int) string",
|
||||
},
|
||||
{
|
||||
name: "variadic parameters",
|
||||
input: "func test(args ...string) {}",
|
||||
expected: "args ...string",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractGoFunctionSignatureParams(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGoFunctionSignatureReturn(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no return type",
|
||||
input: "func test() {}",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single return type",
|
||||
input: "func test() string {}",
|
||||
expected: "string",
|
||||
},
|
||||
{
|
||||
name: "pointer return type",
|
||||
input: "func test() *go_value {}",
|
||||
expected: "*go_value",
|
||||
},
|
||||
{
|
||||
name: "multiple return types",
|
||||
input: "func test() (string, error) {}",
|
||||
expected: "(string, error)",
|
||||
},
|
||||
{
|
||||
name: "named return values",
|
||||
input: "func test() (result string, err error) {}",
|
||||
expected: "(result string, err error)",
|
||||
},
|
||||
{
|
||||
name: "complex return type",
|
||||
input: "func test() unsafe.Pointer {}",
|
||||
expected: "unsafe.Pointer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractGoFunctionSignatureReturn(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractGoFunctionCallParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no parameters",
|
||||
input: "func test() {}",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single parameter",
|
||||
input: "func test(name string) {}",
|
||||
expected: "name",
|
||||
},
|
||||
{
|
||||
name: "multiple parameters",
|
||||
input: "func test(a int, b string, c bool) {}",
|
||||
expected: "a, b, c",
|
||||
},
|
||||
{
|
||||
name: "pointer parameters",
|
||||
input: "func test(data *go_string, opts *go_nullable) {}",
|
||||
expected: "data, opts",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractGoFunctionCallParams(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_SourceFilePreservation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
//export_php: greet(name string): string
|
||||
func greet(name *go_string) *go_value {
|
||||
return String(fmt.Sprintf("Hello, %s!", CStringToGoString(name)))
|
||||
}
|
||||
|
||||
func internalHelper() {
|
||||
fmt.Println("internal")
|
||||
}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
hashBefore := computeFileHash(t, sourceFile)
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: tmpDir,
|
||||
Functions: []phpFunction{
|
||||
{
|
||||
Name: "greet",
|
||||
ReturnType: phpString,
|
||||
GoFunction: `func greet(name *go_string) *go_value {
|
||||
return String(fmt.Sprintf("Hello, %s!", CStringToGoString(name)))
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
require.NoError(t, goGen.generate())
|
||||
|
||||
hashAfter := computeFileHash(t, sourceFile)
|
||||
|
||||
assert.Equal(t, hashBefore, hashAfter, "Source file hash should remain unchanged after generation")
|
||||
|
||||
contentAfter, err := os.ReadFile(sourceFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, sourceContent, string(contentAfter), "Source file content should be byte-for-byte identical")
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_WrapperParameterForwarding(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
//export_php: process(name string, count int): string
|
||||
func process(name *go_string, count long) *go_value {
|
||||
n := CStringToGoString(name)
|
||||
return String(fmt.Sprintf("%s: %d", n, count))
|
||||
}
|
||||
|
||||
//export_php: simple(): void
|
||||
func simple() {}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
functions := []phpFunction{
|
||||
{
|
||||
Name: "process",
|
||||
ReturnType: phpString,
|
||||
GoFunction: `func process(name *go_string, count long) *go_value {
|
||||
n := CStringToGoString(name)
|
||||
return String(fmt.Sprintf("%s: %d", n, count))
|
||||
}`,
|
||||
},
|
||||
{
|
||||
Name: "simple",
|
||||
ReturnType: phpVoid,
|
||||
GoFunction: "func simple() {}",
|
||||
},
|
||||
}
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "wrapper_test",
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: tmpDir,
|
||||
Functions: functions,
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
content, err := goGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, content, "//export go_process", "Should have wrapper export directive")
|
||||
assert.Contains(t, content, "func go_process(", "Should have wrapper function")
|
||||
assert.Contains(t, content, "process(", "Wrapper should call original function")
|
||||
|
||||
assert.Contains(t, content, "//export go_simple", "Should have simple wrapper export directive")
|
||||
assert.Contains(t, content, "func go_simple()", "Should have simple wrapper function")
|
||||
assert.Contains(t, content, "simple()", "Simple wrapper should call original function")
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_MalformedSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sourceContent string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "missing package declaration",
|
||||
sourceContent: "func test() {}",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "syntax error",
|
||||
sourceContent: "package main\nfunc test( {}",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "incomplete function",
|
||||
sourceContent: "package main\nfunc test() {",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "valid minimal source",
|
||||
sourceContent: "package main\nfunc test() {}",
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(tt.sourceContent), 0644))
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "test",
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: tmpDir,
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
_, err := goGen.buildContent()
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "Expected error for malformed source")
|
||||
} else {
|
||||
assert.NoError(t, err, "Should not error for valid source")
|
||||
}
|
||||
|
||||
contentAfter, readErr := os.ReadFile(sourceFile)
|
||||
require.NoError(t, readErr)
|
||||
assert.Equal(t, tt.sourceContent, string(contentAfter), "Source file should remain unchanged even on error")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_MethodWrapperWithNullableArrayParams(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
@@ -672,37 +1118,6 @@ func createTempSourceFile(t *testing.T, content string) string {
|
||||
return tmpFile
|
||||
}
|
||||
|
||||
func testGoFileBasicStructure(t *testing.T, content, baseName string) {
|
||||
requiredElements := []string{
|
||||
"package " + SanitizePackageName(baseName),
|
||||
"// #include <stdlib.h>",
|
||||
`// #include "` + baseName + `.h"`,
|
||||
`import "C"`,
|
||||
"func init() {",
|
||||
"frankenphp.RegisterExtension(",
|
||||
"}",
|
||||
}
|
||||
|
||||
for _, element := range requiredElements {
|
||||
assert.Contains(t, content, element, "Go file should contain: %s", element)
|
||||
}
|
||||
}
|
||||
|
||||
func testGoFileImports(t *testing.T, content string) {
|
||||
cImportCount := strings.Count(content, `"C"`)
|
||||
assert.Equal(t, 1, cImportCount, "Expected exactly 1 C import")
|
||||
}
|
||||
|
||||
func testGoFileExportedFunctions(t *testing.T, content string, functions []phpFunction) {
|
||||
for _, fn := range functions {
|
||||
exportDirective := "//export " + fn.Name
|
||||
assert.Contains(t, content, exportDirective, "Go file should contain export directive: %s", exportDirective)
|
||||
|
||||
funcStart := "func " + fn.Name + "("
|
||||
assert.Contains(t, content, funcStart, "Go file should contain function definition: %s", funcStart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
@@ -822,22 +1237,54 @@ func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testGoFileInternalFunctions(t *testing.T, content string) {
|
||||
internalIndicators := []string{
|
||||
"func internalHelper",
|
||||
"func anotherHelper",
|
||||
func testGeneratedFileBasicStructure(t *testing.T, content, expectedPackage, baseName string) {
|
||||
requiredElements := []string{
|
||||
"package " + expectedPackage,
|
||||
"// #include <stdlib.h>",
|
||||
`// #include "` + baseName + `.h"`,
|
||||
`import "C"`,
|
||||
"func init() {",
|
||||
"frankenphp.RegisterExtension(",
|
||||
"}",
|
||||
}
|
||||
|
||||
foundInternal := false
|
||||
for _, indicator := range internalIndicators {
|
||||
if strings.Contains(content, indicator) {
|
||||
foundInternal = true
|
||||
for _, element := range requiredElements {
|
||||
assert.Contains(t, content, element, "Generated file should contain: %s", element)
|
||||
}
|
||||
|
||||
break
|
||||
assert.NotContains(t, content, "func internalHelper", "Generated file should not contain internal functions from source")
|
||||
assert.NotContains(t, content, "func anotherHelper", "Generated file should not contain internal functions from source")
|
||||
}
|
||||
|
||||
func testGeneratedFileWrappers(t *testing.T, content string, functions []phpFunction) {
|
||||
for _, fn := range functions {
|
||||
exportDirective := "//export go_" + fn.Name
|
||||
assert.Contains(t, content, exportDirective, "Generated file should contain export directive: %s", exportDirective)
|
||||
|
||||
wrapperFunc := "func go_" + fn.Name + "("
|
||||
assert.Contains(t, content, wrapperFunc, "Generated file should contain wrapper function: %s", wrapperFunc)
|
||||
|
||||
funcName := extractGoFunctionName(fn.GoFunction)
|
||||
if funcName != "" {
|
||||
assert.Contains(t, content, funcName+"(", "Generated wrapper should call original function: %s", funcName)
|
||||
}
|
||||
}
|
||||
|
||||
if !foundInternal {
|
||||
t.Log("No internal functions found (this may be expected)")
|
||||
}
|
||||
}
|
||||
|
||||
// computeFileHash returns SHA256 hash of file
|
||||
func computeFileHash(t *testing.T, filename string) string {
|
||||
content, err := os.ReadFile(filename)
|
||||
require.NoError(t, err)
|
||||
hash := sha256.Sum256(content)
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// assertContainsHeaderComment verifies file has autogenerated header
|
||||
func assertContainsHeaderComment(t *testing.T, filename string) {
|
||||
content, err := os.ReadFile(filename)
|
||||
require.NoError(t, err)
|
||||
|
||||
headerSection := string(content[:min(len(content), 500)])
|
||||
assert.Contains(t, headerSection, "AUTOGENERATED FILE - DO NOT EDIT", "File should contain autogenerated header comment")
|
||||
assert.Contains(t, headerSection, "FrankenPHP extension generator", "File should mention FrankenPHP extension generator")
|
||||
}
|
||||
|
||||
@@ -37,24 +37,25 @@ func (pfg *PHPFuncGenerator) generate(fn phpFunction) string {
|
||||
|
||||
func (pfg *PHPFuncGenerator) generateGoCall(fn phpFunction) string {
|
||||
callParams := pfg.paramParser.generateGoCallParams(fn.Params)
|
||||
goFuncName := "go_" + fn.Name
|
||||
|
||||
if fn.ReturnType == phpVoid {
|
||||
return fmt.Sprintf(" %s(%s);", fn.Name, callParams)
|
||||
return fmt.Sprintf(" %s(%s);", goFuncName, callParams)
|
||||
}
|
||||
|
||||
if fn.ReturnType == phpString {
|
||||
return fmt.Sprintf(" zend_string *result = %s(%s);", fn.Name, callParams)
|
||||
return fmt.Sprintf(" zend_string *result = %s(%s);", goFuncName, callParams)
|
||||
}
|
||||
|
||||
if fn.ReturnType == phpArray {
|
||||
return fmt.Sprintf(" zend_array *result = %s(%s);", fn.Name, callParams)
|
||||
return fmt.Sprintf(" zend_array *result = %s(%s);", goFuncName, callParams)
|
||||
}
|
||||
|
||||
if fn.ReturnType == phpMixed {
|
||||
return fmt.Sprintf(" zval *result = %s(%s);", fn.Name, callParams)
|
||||
return fmt.Sprintf(" zval *result = %s(%s);", goFuncName, callParams)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(" %s result = %s(%s);", pfg.getCReturnType(fn.ReturnType), fn.Name, callParams)
|
||||
return fmt.Sprintf(" %s result = %s(%s);", pfg.getCReturnType(fn.ReturnType), goFuncName, callParams)
|
||||
}
|
||||
|
||||
func (pfg *PHPFuncGenerator) getCReturnType(returnType phpType) string {
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
|
||||
"PHP_FUNCTION(greet)",
|
||||
"zend_string *name = NULL;",
|
||||
"Z_PARAM_STR(name)",
|
||||
"zend_string *result = greet(name);",
|
||||
"zend_string *result = go_greet(name);",
|
||||
"RETURN_STR(result)",
|
||||
},
|
||||
},
|
||||
@@ -61,7 +61,7 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
|
||||
},
|
||||
contains: []string{
|
||||
"PHP_FUNCTION(doSomething)",
|
||||
"doSomething(action);",
|
||||
"go_doSomething(action);",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -109,7 +109,7 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
|
||||
"PHP_FUNCTION(process_array)",
|
||||
"zend_array *input = NULL;",
|
||||
"Z_PARAM_ARRAY_HT(input)",
|
||||
"zend_array *result = process_array(input);",
|
||||
"zend_array *result = go_process_array(input);",
|
||||
"RETURN_ARR(result)",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,33 +10,24 @@ import (
|
||||
|
||||
type SourceAnalyzer struct{}
|
||||
|
||||
func (sa *SourceAnalyzer) analyze(filename string) (imports []string, variables []string, internalFunctions []string, err error) {
|
||||
func (sa *SourceAnalyzer) analyze(filename string) (packageName string, variables []string, internalFunctions []string, err error) {
|
||||
fset := token.NewFileSet()
|
||||
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("parsing file: %w", err)
|
||||
return "", nil, nil, fmt.Errorf("parsing file: %w", err)
|
||||
}
|
||||
|
||||
for _, imp := range node.Imports {
|
||||
if imp.Path != nil {
|
||||
importPath := imp.Path.Value
|
||||
if imp.Name != nil {
|
||||
imports = append(imports, fmt.Sprintf("%s %s", imp.Name.Name, importPath))
|
||||
} else {
|
||||
imports = append(imports, importPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
packageName = node.Name.Name
|
||||
|
||||
sourceContent, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("reading source file: %w", err)
|
||||
return "", nil, nil, fmt.Errorf("reading source file: %w", err)
|
||||
}
|
||||
|
||||
variables = sa.extractVariables(string(sourceContent))
|
||||
internalFunctions = sa.extractInternalFunctions(string(sourceContent))
|
||||
|
||||
return imports, variables, internalFunctions, nil
|
||||
return packageName, variables, internalFunctions, nil
|
||||
}
|
||||
|
||||
func (sa *SourceAnalyzer) extractVariables(content string) []string {
|
||||
|
||||
@@ -227,7 +227,7 @@ func testFunction() {
|
||||
require.NoError(t, os.WriteFile(filename, []byte(tt.sourceContent), 0644))
|
||||
|
||||
analyzer := &SourceAnalyzer{}
|
||||
imports, variables, functions, err := analyzer.analyze(filename)
|
||||
_, variables, functions, err := analyzer.analyze(filename)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "expected error")
|
||||
@@ -236,10 +236,6 @@ func testFunction() {
|
||||
|
||||
assert.NoError(t, err, "unexpected error")
|
||||
|
||||
if len(imports) != 0 && len(tt.expectedImports) != 0 {
|
||||
assert.Equal(t, tt.expectedImports, imports, "imports mismatch")
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedVariables, variables, "variables mismatch")
|
||||
assert.Len(t, functions, len(tt.expectedFunctions), "function count mismatch")
|
||||
|
||||
@@ -385,6 +381,110 @@ var x = 10`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceAnalyzer_InternalFunctionPreservation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//export_php: exported1(): string
|
||||
func exported1() *go_value {
|
||||
return String(internal1())
|
||||
}
|
||||
|
||||
func internal1() string {
|
||||
return "helper1"
|
||||
}
|
||||
|
||||
//export_php: exported2(): void
|
||||
func exported2() {
|
||||
internal2()
|
||||
}
|
||||
|
||||
func internal2() {
|
||||
fmt.Println("helper2")
|
||||
}
|
||||
|
||||
func internal3(data string) string {
|
||||
return strings.ToUpper(data)
|
||||
}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
analyzer := &SourceAnalyzer{}
|
||||
packageName, variables, internalFuncs, err := analyzer.analyze(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "main", packageName)
|
||||
|
||||
assert.Len(t, internalFuncs, 3, "Should extract exactly 3 internal functions")
|
||||
|
||||
expectedInternalFuncs := []string{
|
||||
`func internal1() string {
|
||||
return "helper1"
|
||||
}`,
|
||||
`func internal2() {
|
||||
fmt.Println("helper2")
|
||||
}`,
|
||||
`func internal3(data string) string {
|
||||
return strings.ToUpper(data)
|
||||
}`,
|
||||
}
|
||||
|
||||
for i, expected := range expectedInternalFuncs {
|
||||
assert.Equal(t, expected, internalFuncs[i], "Internal function %d should match", i)
|
||||
}
|
||||
|
||||
assert.Empty(t, variables, "Should not have variables")
|
||||
}
|
||||
|
||||
func TestSourceAnalyzer_VariableBlockPreservation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
cache = make(map[string]string)
|
||||
)
|
||||
|
||||
var globalCounter int = 0
|
||||
|
||||
//export_php: test(): void
|
||||
func test() {}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
analyzer := &SourceAnalyzer{}
|
||||
packageName, variables, internalFuncs, err := analyzer.analyze(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "main", packageName)
|
||||
|
||||
assert.Len(t, variables, 2, "Should extract exactly 2 variable declarations")
|
||||
|
||||
expectedVar1 := `var (
|
||||
mu sync.RWMutex
|
||||
cache = make(map[string]string)
|
||||
)`
|
||||
expectedVar2 := `var globalCounter int = 0`
|
||||
|
||||
assert.Equal(t, expectedVar1, variables[0], "First variable block should match")
|
||||
assert.Equal(t, expectedVar2, variables[1], "Second variable declaration should match")
|
||||
|
||||
assert.Empty(t, internalFuncs, "Should not have internal functions (only exported function)")
|
||||
}
|
||||
|
||||
func BenchmarkSourceAnalyzer_Analyze(b *testing.B) {
|
||||
content := `package main
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
// AUTOGENERATED FILE - DO NOT EDIT.
|
||||
//
|
||||
// This file has been automatically generated by FrankenPHP extension generator
|
||||
// and should not be edited as it will be overwritten when running the
|
||||
// extension generator again.
|
||||
//
|
||||
// You may edit the file and remove this comment if you plan to manually maintain
|
||||
// this file going forward.
|
||||
|
||||
#include <php.h>
|
||||
#include <Zend/zend_API.h>
|
||||
#include <Zend/zend_hash.h>
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
package {{.PackageName}}
|
||||
|
||||
// AUTOGENERATED FILE - DO NOT EDIT.
|
||||
//
|
||||
// This file has been automatically generated by FrankenPHP extension generator
|
||||
// and should not be edited as it will be overwritten when running the
|
||||
// extension generator again.
|
||||
//
|
||||
// You may edit the file and remove this comment if you plan to manually maintain
|
||||
// this file going forward.
|
||||
|
||||
// #include <stdlib.h>
|
||||
// #include "{{.BaseName}}.h"
|
||||
import "C"
|
||||
import (
|
||||
{{if not .Classes}}_ {{end}}"runtime/cgo"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
{{- range .Imports}}
|
||||
{{.}}
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.{{.BaseName}}_module_entry))
|
||||
frankenphp.RegisterExtension(unsafe.Pointer(&C.{{.SanitizedBaseName}}_module_entry))
|
||||
}
|
||||
|
||||
{{ range .Constants}}
|
||||
const {{.Name}} = {{.Value}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Variables}}
|
||||
|
||||
{{.}}
|
||||
{{- end}}
|
||||
{{- range .InternalFunctions}}
|
||||
{{.}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Functions}}
|
||||
//export {{.Name}}
|
||||
{{.GoFunction}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Classes}}
|
||||
type {{.GoStruct}} struct {
|
||||
{{- range .Properties}}
|
||||
{{.Name}} {{.GoType}}
|
||||
{{- end}}
|
||||
//export go_{{.Name}}
|
||||
func go_{{.Name}}({{extractGoFunctionSignatureParams .GoFunction}}) {{extractGoFunctionSignatureReturn .GoFunction}} {
|
||||
{{if not (isVoid .ReturnType)}}return {{end}}{{extractGoFunctionName .GoFunction}}({{extractGoFunctionCallParams .GoFunction}})
|
||||
}
|
||||
|
||||
{{- end}}
|
||||
@@ -68,12 +57,6 @@ func create_{{.GoStruct}}_object() C.uintptr_t {
|
||||
return registerGoObject(obj)
|
||||
}
|
||||
|
||||
{{- range .Methods}}
|
||||
{{- if .GoFunction}}
|
||||
{{.GoFunction}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
{{- range .Methods}}
|
||||
//export {{.Name}}_wrapper
|
||||
func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else if eq .PhpType "callable"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
// AUTOGENERATED FILE - DO NOT EDIT.
|
||||
//
|
||||
// This file has been automatically generated by FrankenPHP extension generator
|
||||
// and should not be edited as it will be overwritten when running the
|
||||
// extension generator again.
|
||||
//
|
||||
// You may edit the file and remove this comment if you plan to manually maintain
|
||||
// this file going forward.
|
||||
|
||||
#ifndef _{{.HeaderGuard}}
|
||||
#define _{{.HeaderGuard}}
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<?php
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
// AUTOGENERATED FILE - DO NOT EDIT.
|
||||
//
|
||||
// This file has been automatically generated by FrankenPHP extension generator
|
||||
// and should not be edited as it will be overwritten when running the
|
||||
// extension generator again.
|
||||
//
|
||||
// You may edit the file and remove this comment if you plan to manually maintain
|
||||
// this file going forward.
|
||||
{{if .Namespace}}
|
||||
namespace {{.Namespace}};
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user