Files
archived-frankenphp/internal/extgen/classparser_test.go
Kévin Dunglas 5514491a18 feat(extgen): support for mixed type (#1913)
* 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
2025-10-09 14:10:45 +02:00

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")
}
})
}
}