mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 09:02:11 +01:00
* feat(extgent): support for mixed type
* refactor: use unsafe.Pointer
* Revert "refactor: use unsafe.Pointer"
This reverts commit 8a0b9c1beb.
* fix docs
* fix docs
* cleanup template
* fix template
* fix tests
642 lines
15 KiB
Go
642 lines
15 KiB
Go
package extgen
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestClassParser(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected int
|
|
}{
|
|
{
|
|
name: "single class",
|
|
input: `package main
|
|
|
|
//export_php:class User
|
|
type UserStruct struct {
|
|
name string
|
|
Age int
|
|
}`,
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "multiple classes",
|
|
input: `package main
|
|
|
|
//export_php:class User
|
|
type UserStruct struct {
|
|
name string
|
|
Age int
|
|
}
|
|
|
|
//export_php:class Product
|
|
type ProductStruct struct {
|
|
Title string
|
|
Price float64
|
|
}`,
|
|
expected: 2,
|
|
},
|
|
{
|
|
name: "no php classes",
|
|
input: `package main
|
|
|
|
type RegularStruct struct {
|
|
Data string
|
|
}`,
|
|
expected: 0,
|
|
},
|
|
{
|
|
name: "class with nullable fields",
|
|
input: `package main
|
|
|
|
//export_php:class OptionalData
|
|
type OptionalStruct struct {
|
|
Required string
|
|
Optional *string
|
|
Count *int
|
|
}`,
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "class with methods",
|
|
input: `package main
|
|
|
|
//export_php:class User
|
|
type UserStruct struct {
|
|
name string
|
|
Age int
|
|
}
|
|
|
|
//export_php:method User::getName(): string
|
|
func GetUserName(u UserStruct) string {
|
|
return u.name
|
|
}
|
|
|
|
//export_php:method User::setAge(int $age): void
|
|
func SetUserAge(u *UserStruct, age int) {
|
|
u.Age = age
|
|
}`,
|
|
expected: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
fileName := filepath.Join(tmpDir, tt.name+".go")
|
|
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
|
|
|
|
parser := classParser{}
|
|
classes, err := parser.parse(fileName)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, classes, tt.expected, "parse() got wrong number of classes")
|
|
|
|
if tt.name == "single class" && len(classes) > 0 {
|
|
class := classes[0]
|
|
assert.Equal(t, "User", class.Name, "Expected class name 'User'")
|
|
assert.Equal(t, "UserStruct", class.GoStruct, "Expected Go struct 'UserStruct'")
|
|
assert.Len(t, class.Properties, 2, "Expected 2 properties")
|
|
}
|
|
|
|
if tt.name == "class with nullable fields" && len(classes) > 0 {
|
|
class := classes[0]
|
|
if len(class.Properties) >= 3 {
|
|
assert.False(t, class.Properties[0].IsNullable, "Required field should not be nullable")
|
|
assert.True(t, class.Properties[1].IsNullable, "Optional field should be nullable")
|
|
assert.True(t, class.Properties[2].IsNullable, "Count field should be nullable")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassMethods(t *testing.T) {
|
|
var input = []byte(`package main
|
|
|
|
//export_php:class User
|
|
type UserStruct struct {
|
|
name string
|
|
Age int
|
|
}
|
|
|
|
//export_php:method User::getName(): string
|
|
func GetUserName(u UserStruct) unsafe.Pointer {
|
|
return nil
|
|
}
|
|
|
|
//export_php:method User::setAge(int $age): void
|
|
func SetUserAge(u *UserStruct, age int64) {
|
|
u.Age = int(age)
|
|
}
|
|
|
|
//export_php:method User::getInfo(string $prefix = "User"): string
|
|
func GetUserInfo(u UserStruct, prefix *C.zend_string) unsafe.Pointer {
|
|
return nil
|
|
}`)
|
|
|
|
tmpDir := t.TempDir()
|
|
fileName := filepath.Join(tmpDir, "test.go")
|
|
require.NoError(t, os.WriteFile(fileName, input, 0644))
|
|
|
|
parser := classParser{}
|
|
classes, err := parser.parse(fileName)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, classes, 1, "Expected 1 class")
|
|
|
|
class := classes[0]
|
|
require.Len(t, class.Methods, 3, "Expected 3 methods")
|
|
|
|
getName := class.Methods[0]
|
|
assert.Equal(t, "getName", getName.Name, "Expected method name 'getName'")
|
|
assert.Equal(t, phpString, getName.ReturnType, "Expected return type 'string'")
|
|
assert.Empty(t, getName.Params, "Expected 0 params")
|
|
assert.Equal(t, "User", getName.ClassName, "Expected class name 'User'")
|
|
|
|
setAge := class.Methods[1]
|
|
assert.Equal(t, "setAge", setAge.Name, "Expected method name 'setAge'")
|
|
assert.Equal(t, phpVoid, setAge.ReturnType, "Expected return type 'void'")
|
|
require.Len(t, setAge.Params, 1, "Expected 1 param")
|
|
|
|
param := setAge.Params[0]
|
|
assert.Equal(t, "age", param.Name, "Expected param name 'age'")
|
|
assert.Equal(t, phpInt, param.PhpType, "Expected param type 'int'")
|
|
assert.False(t, param.IsNullable, "Expected param to not be nullable")
|
|
assert.False(t, param.HasDefault, "Expected param to not have default value")
|
|
|
|
getInfo := class.Methods[2]
|
|
assert.Equal(t, "getInfo", getInfo.Name, "Expected method name 'getInfo'")
|
|
assert.Equal(t, phpString, getInfo.ReturnType, "Expected return type 'string'")
|
|
require.Len(t, getInfo.Params, 1, "Expected 1 param")
|
|
|
|
param = getInfo.Params[0]
|
|
assert.Equal(t, "prefix", param.Name, "Expected param name 'prefix'")
|
|
assert.Equal(t, phpString, param.PhpType, "Expected param type 'string'")
|
|
assert.True(t, param.HasDefault, "Expected param to have default value")
|
|
assert.Equal(t, "User", param.DefaultValue, "Expected default value 'User'")
|
|
}
|
|
|
|
func TestMethodParameterParsing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
paramStr string
|
|
expectedParam phpParameter
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "simple int parameter",
|
|
paramStr: "int $age",
|
|
expectedParam: phpParameter{
|
|
Name: "age",
|
|
PhpType: phpInt,
|
|
IsNullable: false,
|
|
HasDefault: false,
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "nullable string parameter",
|
|
paramStr: "?string $name",
|
|
expectedParam: phpParameter{
|
|
Name: "name",
|
|
PhpType: phpString,
|
|
IsNullable: true,
|
|
HasDefault: false,
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "parameter with default value",
|
|
paramStr: `string $prefix = "default"`,
|
|
expectedParam: phpParameter{
|
|
Name: "prefix",
|
|
PhpType: phpString,
|
|
IsNullable: false,
|
|
HasDefault: true,
|
|
DefaultValue: "default",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "nullable parameter with default null",
|
|
paramStr: "?int $count = null",
|
|
expectedParam: phpParameter{
|
|
Name: "count",
|
|
PhpType: phpInt,
|
|
IsNullable: true,
|
|
HasDefault: true,
|
|
DefaultValue: "null",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid parameter format",
|
|
paramStr: "invalid",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
parser := classParser{}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
param, err := parser.parseMethodParameter(tt.paramStr)
|
|
|
|
if tt.expectError {
|
|
assert.Error(t, err, "Expected error for parameter '%s', but got none", tt.paramStr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err, "parseMethodParameter(%s) error", tt.paramStr)
|
|
|
|
assert.Equal(t, tt.expectedParam.Name, param.Name, "Expected name '%s'", tt.expectedParam.Name)
|
|
assert.Equal(t, tt.expectedParam.PhpType, param.PhpType, "Expected type '%s'", tt.expectedParam.PhpType)
|
|
assert.Equal(t, tt.expectedParam.IsNullable, param.IsNullable, "Expected isNullable %v", tt.expectedParam.IsNullable)
|
|
assert.Equal(t, tt.expectedParam.HasDefault, param.HasDefault, "Expected hasDefault %v", tt.expectedParam.HasDefault)
|
|
assert.Equal(t, tt.expectedParam.DefaultValue, param.DefaultValue, "Expected defaultValue '%s'", tt.expectedParam.DefaultValue)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGoTypeToPHPType(t *testing.T) {
|
|
tests := []struct {
|
|
goType string
|
|
expected phpType
|
|
}{
|
|
{"string", phpString},
|
|
{"*string", phpString},
|
|
{"int", phpInt},
|
|
{"int64", phpInt},
|
|
{"*int", phpInt},
|
|
{"float64", phpFloat},
|
|
{"*float32", phpFloat},
|
|
{"bool", phpBool},
|
|
{"*bool", phpBool},
|
|
{"[]string", phpArray},
|
|
{"map[string]int", phpArray},
|
|
{"*[]int", phpArray},
|
|
{"any", phpMixed},
|
|
{"CustomType", phpMixed},
|
|
}
|
|
|
|
parser := classParser{}
|
|
for _, tt := range tests {
|
|
t.Run(tt.goType, func(t *testing.T) {
|
|
result := parser.goTypeToPHPType(tt.goType)
|
|
assert.Equal(t, tt.expected, result, "goTypeToPHPType(%s) = %s, want %s", tt.goType, result, tt.expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTypeToString(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected []phpType
|
|
}{
|
|
{
|
|
name: "basic types",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestStruct struct {
|
|
StringField string
|
|
IntField int
|
|
FloatField float64
|
|
BoolField bool
|
|
}`,
|
|
expected: []phpType{phpString, phpInt, phpFloat, phpBool},
|
|
},
|
|
{
|
|
name: "pointer types",
|
|
input: `package main
|
|
|
|
//export_php:class NullableClass
|
|
type NullableStruct struct {
|
|
NullableString *string
|
|
NullableInt *int
|
|
NullableFloat *float64
|
|
NullableBool *bool
|
|
}`,
|
|
expected: []phpType{phpString, phpInt, phpFloat, phpBool},
|
|
},
|
|
{
|
|
name: "collection types",
|
|
input: `package main
|
|
|
|
//export_php:class CollectionClass
|
|
type CollectionStruct struct {
|
|
StringSlice []string
|
|
IntMap map[string]int
|
|
MixedSlice []any
|
|
}`,
|
|
expected: []phpType{phpArray, phpArray, phpArray},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
fileName := filepath.Join(tmpDir, tt.name+".go")
|
|
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0o644))
|
|
|
|
parser := classParser{}
|
|
classes, err := parser.parse(fileName)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, classes, 1, "Expected 1 class")
|
|
|
|
class := classes[0]
|
|
require.Len(t, class.Properties, len(tt.expected), "Expected %d properties", len(tt.expected))
|
|
|
|
for i, expectedType := range tt.expected {
|
|
assert.Equal(t, expectedType, class.Properties[i].PhpType, "Property %d: expected type %s", i, expectedType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassParserUnsupportedTypes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expectedClasses int
|
|
expectedMethods int
|
|
hasWarning bool
|
|
}{
|
|
{
|
|
name: "method with array parameter should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::arrayMethod(array $data): string
|
|
func (tc *TestClass) arrayMethod(data any) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "method with object parameter should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::objectMethod(object $obj): string
|
|
func (tc *TestClass) objectMethod(obj any) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "method with mixed parameter should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::mixedMethod(mixed $value): string
|
|
func (tc *TestClass) mixedMethod(value any) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "method with array return type should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::arrayReturn(string $name): array
|
|
func (tc *TestClass) arrayReturn(name *C.zend_string) any {
|
|
return []string{"result"}
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "method with object return type should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::objectReturn(string $name): object
|
|
func (tc *TestClass) objectReturn(name *C.zend_string) any {
|
|
return map[string]any{"key": "value"}
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "valid scalar types should pass",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::validMethod(string $name, int $count, float $rate, bool $active): string
|
|
func validMethod(tc *TestClass, name *C.zend_string, count int64, rate float64, active bool) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 1,
|
|
hasWarning: false,
|
|
},
|
|
{
|
|
name: "valid void return should pass",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::voidMethod(string $message): void
|
|
func voidMethod(tc *TestClass, message *C.zend_string) {
|
|
// Do something
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 1,
|
|
hasWarning: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
fileName := filepath.Join(tmpDir, tt.name+".go")
|
|
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
|
|
|
|
parser := &classParser{}
|
|
classes, err := parser.parse(fileName)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, classes, tt.expectedClasses, "parse() got wrong number of classes")
|
|
if len(classes) > 0 {
|
|
assert.Len(t, classes[0].Methods, tt.expectedMethods, "parse() got wrong number of methods")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassParserGoTypeMismatch(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expectedClasses int
|
|
expectedMethods int
|
|
hasWarning bool
|
|
}{
|
|
{
|
|
name: "method parameter count mismatch should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::countMismatch(string $name, int $count): string
|
|
func (tc *TestClass) countMismatch(name *C.zend_string) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "method parameter type mismatch should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::typeMismatch(string $name, int $count): string
|
|
func (tc *TestClass) typeMismatch(name *C.zend_string, count string) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "method return type mismatch should be rejected",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::returnMismatch(string $name): int
|
|
func (tc *TestClass) returnMismatch(name *C.zend_string) string {
|
|
return ""
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 0,
|
|
hasWarning: true,
|
|
},
|
|
{
|
|
name: "valid matching types should pass",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::validMatch(string $name, int $count): string
|
|
func validMatch(tc *TestClass, name *C.zend_string, count int64) unsafe.Pointer {
|
|
return nil
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 1,
|
|
hasWarning: false,
|
|
},
|
|
{
|
|
name: "valid bool types should pass",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::validBool(bool $flag): bool
|
|
func validBool(tc *TestClass, flag bool) bool {
|
|
return flag
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 1,
|
|
hasWarning: false,
|
|
},
|
|
{
|
|
name: "valid float types should pass",
|
|
input: `package main
|
|
|
|
//export_php:class TestClass
|
|
type TestClass struct {
|
|
Name string
|
|
}
|
|
|
|
//export_php:method TestClass::validFloat(float $value): float
|
|
func validFloat(tc *TestClass, value float64) float64 {
|
|
return value
|
|
}`,
|
|
expectedClasses: 1,
|
|
expectedMethods: 1,
|
|
hasWarning: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
fileName := filepath.Join(tmpDir, tt.name+".go")
|
|
require.NoError(t, os.WriteFile(fileName, []byte(tt.input), 0644))
|
|
|
|
parser := &classParser{}
|
|
classes, err := parser.parse(fileName)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, classes, tt.expectedClasses, "parse() got wrong number of classes")
|
|
if len(classes) > 0 {
|
|
assert.Len(t, classes[0].Methods, tt.expectedMethods, "parse() got wrong number of methods")
|
|
}
|
|
})
|
|
}
|
|
}
|