Files
archived-frankenphp/internal/extgen/funcparser.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

184 lines
4.4 KiB
Go

package extgen
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
var phpFuncRegex = regexp.MustCompile(`//\s*export_php:function\s+([^{}\n]+)(?:\s*{\s*})?`)
var signatureRegex = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)\s*:\s*(\??[\w|]+)`)
var typeNameRegex = regexp.MustCompile(`(\??[\w|]+)\s+\$?(\w+)`)
type FuncParser struct{}
func (fp *FuncParser) parse(filename string) (functions []phpFunction, err error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer func() {
e := file.Close()
if err == nil {
err = e
}
}()
scanner := bufio.NewScanner(file)
var currentPHPFunc *phpFunction
validator := Validator{}
lineNumber := 0
for scanner.Scan() {
lineNumber++
line := strings.TrimSpace(scanner.Text())
if matches := phpFuncRegex.FindStringSubmatch(line); matches != nil {
signature := strings.TrimSpace(matches[1])
phpFunc, err := fp.parseSignature(signature)
if err != nil {
fmt.Printf("Warning: Error parsing signature '%s': %v\n", signature, err)
continue
}
if err := validator.validateFunction(*phpFunc); err != nil {
fmt.Printf("Warning: Invalid function '%s': %v\n", phpFunc.Name, err)
continue
}
if err := validator.validateTypes(*phpFunc); err != nil {
fmt.Printf("Warning: Function '%s' uses unsupported types: %v\n", phpFunc.Name, err)
continue
}
phpFunc.lineNumber = lineNumber
currentPHPFunc = phpFunc
}
if currentPHPFunc != nil && strings.HasPrefix(line, "func ") {
goFunc, err := fp.extractGoFunction(scanner, line)
if err != nil {
return nil, fmt.Errorf("extracting Go function: %w", err)
}
currentPHPFunc.GoFunction = goFunc
if err := validator.validateGoFunctionSignatureWithOptions(*currentPHPFunc, false); err != nil {
fmt.Printf("Warning: Go function signature mismatch for %q: %v\n", currentPHPFunc.Name, err)
currentPHPFunc = nil
continue
}
functions = append(functions, *currentPHPFunc)
currentPHPFunc = nil
}
}
if currentPHPFunc != nil {
return nil, fmt.Errorf("//export_php function directive at line %d is not followed by a function declaration", currentPHPFunc.lineNumber)
}
return functions, scanner.Err()
}
func (fp *FuncParser) extractGoFunction(scanner *bufio.Scanner, firstLine string) (string, error) {
goFunc := firstLine + "\n"
braceCount := 1
for scanner.Scan() {
line := scanner.Text()
goFunc += line + "\n"
for _, char := range line {
switch char {
case '{':
braceCount++
case '}':
braceCount--
}
}
if braceCount == 0 {
break
}
}
return goFunc, nil
}
func (fp *FuncParser) parseSignature(signature string) (*phpFunction, error) {
matches := signatureRegex.FindStringSubmatch(signature)
if len(matches) != 4 {
return nil, fmt.Errorf("invalid signature format")
}
name := matches[1]
paramsStr := strings.TrimSpace(matches[2])
returnTypeStr := strings.TrimSpace(matches[3])
isReturnNullable := strings.HasPrefix(returnTypeStr, "?")
returnType := strings.TrimPrefix(returnTypeStr, "?")
var params []phpParameter
if paramsStr != "" {
paramParts := strings.SplitSeq(paramsStr, ",")
for part := range paramParts {
param, err := fp.parseParameter(strings.TrimSpace(part))
if err != nil {
return nil, fmt.Errorf("parsing parameter '%s': %w", part, err)
}
params = append(params, param)
}
}
return &phpFunction{
Name: name,
Signature: signature,
Params: params,
ReturnType: phpType(returnType),
IsReturnNullable: isReturnNullable,
}, nil
}
func (fp *FuncParser) parseParameter(paramStr string) (phpParameter, error) {
parts := strings.Split(paramStr, "=")
typePart := strings.TrimSpace(parts[0])
param := phpParameter{HasDefault: len(parts) > 1}
if param.HasDefault {
param.DefaultValue = fp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))
}
matches := typeNameRegex.FindStringSubmatch(typePart)
if len(matches) < 3 {
return phpParameter{}, fmt.Errorf("invalid parameter format: %s", paramStr)
}
typeStr := strings.TrimSpace(matches[1])
param.Name = strings.TrimSpace(matches[2])
param.IsNullable = strings.HasPrefix(typeStr, "?")
param.PhpType = phpType(strings.TrimPrefix(typeStr, "?"))
return param, nil
}
func (fp *FuncParser) sanitizeDefaultValue(value string) string {
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
return value
}
if strings.ToLower(value) == "null" {
return "null"
}
return strings.Trim(value, `'"`)
}