Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions internal/extgen/classparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (

var phpClassRegex = regexp.MustCompile(`//\s*export_php:class\s+(\w+)`)
var phpMethodRegex = regexp.MustCompile(`//\s*export_php:method\s+(\w+)::([^{}\n]+)(?:\s*{\s*})?`)
var methodSignatureRegex = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)\s*:\s*(\??[\w|]+)`)
var methodParamTypeNameRegex = regexp.MustCompile(`(\??[\w|]+)\s+\$?(\w+)`)

type exportDirective struct {
line int
Expand Down Expand Up @@ -292,79 +290,22 @@ func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod,
}

func (cp *classParser) parseMethodSignature(className, signature string) (*phpClassMethod, error) {
matches := methodSignatureRegex.FindStringSubmatch(signature)

if len(matches) != 4 {
return nil, fmt.Errorf("invalid method signature format")
}

methodName := 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 := cp.parseMethodParameter(strings.TrimSpace(part))
if err != nil {
return nil, fmt.Errorf("parsing parameter '%s': %w", part, err)
}

params = append(params, param)
}
name, params, returnType, nullable, err := parseSignatureParams(signature)
if err != nil {
return nil, err
}

return &phpClassMethod{
Name: methodName,
PhpName: methodName,
Name: name,
PhpName: name,
ClassName: className,
Signature: signature,
Params: params,
ReturnType: phpType(returnType),
isReturnNullable: isReturnNullable,
isReturnNullable: nullable,
}, nil
}

func (cp *classParser) parseMethodParameter(paramStr string) (phpParameter, error) {
parts := strings.Split(paramStr, "=")
typePart := strings.TrimSpace(parts[0])

param := phpParameter{HasDefault: len(parts) > 1}

if param.HasDefault {
param.DefaultValue = cp.sanitizeDefaultValue(strings.TrimSpace(parts[1]))
}

matches := methodParamTypeNameRegex.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 (cp *classParser) sanitizeDefaultValue(value string) string {
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
return value
}

if strings.ToLower(value) == "null" {
return "null"
}

return strings.Trim(value, `'"`)
}

func (cp *classParser) extractGoMethodFunction(scanner *bufio.Scanner, firstLine string) (string, error) {
goFunc := firstLine + "\n"
braceCount := 1
Expand Down
3 changes: 1 addition & 2 deletions internal/extgen/classparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,9 @@ func TestMethodParameterParsing(t *testing.T) {
},
}

parser := classParser{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
param, err := parser.parseMethodParameter(tt.paramStr)
param, err := parseParameter(tt.paramStr)

if tt.expectError {
assert.Error(t, err, "Expected error for parameter '%s', but got none", tt.paramStr)
Expand Down
65 changes: 4 additions & 61 deletions internal/extgen/funcparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import (
)

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{}

Expand Down Expand Up @@ -113,71 +111,16 @@ func (fp *FuncParser) extractGoFunction(scanner *bufio.Scanner, firstLine string
}

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)
}
name, params, returnType, nullable, err := parseSignatureParams(signature)
if err != nil {
return nil, err
}

return &phpFunction{
Name: name,
Signature: signature,
Params: params,
ReturnType: phpType(returnType),
IsReturnNullable: isReturnNullable,
IsReturnNullable: nullable,
}, 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, `'"`)
}
3 changes: 1 addition & 2 deletions internal/extgen/funcparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,9 @@ func TestParameterParsing(t *testing.T) {
},
}

parser := &FuncParser{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
param, err := parser.parseParameter(tt.paramStr)
param, err := parseParameter(tt.paramStr)

if tt.expectError {
assert.Error(t, err, "parseParameter() expected error but got none")
Expand Down
76 changes: 76 additions & 0 deletions internal/extgen/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package extgen

import (
"fmt"
"regexp"
"strings"
)

// Shared patterns for both function and method signatures.
var (
signaturePattern = regexp.MustCompile(`(\w+)\s*\(([^)]*)\)\s*:\s*(\??[\w|]+)`)
paramPattern = regexp.MustCompile(`(\??[\w|]+)\s+\$?(\w+)`)
)

// parseSignatureParams splits a "name(params): returnType" signature into its parts.
// Returns name, slice of parameters, return type (without leading "?") and whether it was nullable.
func parseSignatureParams(signature string) (name string, params []phpParameter, returnType string, nullable bool, err error) {
matches := signaturePattern.FindStringSubmatch(signature)
if len(matches) != 4 {
return "", nil, "", false, fmt.Errorf("invalid signature format")
}

name = matches[1]
paramsStr := strings.TrimSpace(matches[2])
returnTypeStr := strings.TrimSpace(matches[3])

nullable = strings.HasPrefix(returnTypeStr, "?")
returnType = strings.TrimPrefix(returnTypeStr, "?")

if paramsStr != "" {
for part := range strings.SplitSeq(paramsStr, ",") {
param, perr := parseParameter(strings.TrimSpace(part))
if perr != nil {
return "", nil, "", false, fmt.Errorf("parsing parameter '%s': %w", part, perr)
}
params = append(params, param)
}
}

return name, params, returnType, nullable, nil
}

// parseParameter parses a single PHP parameter declaration like "?int $name = 42".
func parseParameter(paramStr string) (phpParameter, error) {
parts := strings.SplitN(paramStr, "=", 2)
typePart := strings.TrimSpace(parts[0])

param := phpParameter{HasDefault: len(parts) > 1}
if param.HasDefault {
param.DefaultValue = sanitizeDefaultValue(strings.TrimSpace(parts[1]))
}

matches := paramPattern.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
}

// sanitizeDefaultValue normalizes a PHP default value literal: keeps array literals,
// preserves "null", and strips surrounding quotes for scalar strings.
func sanitizeDefaultValue(value string) string {
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
return value
}
if strings.EqualFold(value, "null") {
return "null"
}
return strings.Trim(value, `'"`)
}
Loading