From 7fbde3e9f23343e09e65e0d69433058c0eea340b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 24 Apr 2026 10:20:16 +0200 Subject: [PATCH] refactor(extgen): share signature and parameter parsing helpers --- internal/extgen/classparser.go | 71 +++------------------------ internal/extgen/classparser_test.go | 3 +- internal/extgen/funcparser.go | 65 ++---------------------- internal/extgen/funcparser_test.go | 3 +- internal/extgen/signature.go | 76 +++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 130 deletions(-) create mode 100644 internal/extgen/signature.go diff --git a/internal/extgen/classparser.go b/internal/extgen/classparser.go index caef0ea239..2f87cebc51 100644 --- a/internal/extgen/classparser.go +++ b/internal/extgen/classparser.go @@ -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 @@ -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 diff --git a/internal/extgen/classparser_test.go b/internal/extgen/classparser_test.go index 11454c48e2..6445e76664 100644 --- a/internal/extgen/classparser_test.go +++ b/internal/extgen/classparser_test.go @@ -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) diff --git a/internal/extgen/funcparser.go b/internal/extgen/funcparser.go index 5e64de1b4c..b964bb77f2 100644 --- a/internal/extgen/funcparser.go +++ b/internal/extgen/funcparser.go @@ -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{} @@ -113,29 +111,9 @@ 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{ @@ -143,41 +121,6 @@ func (fp *FuncParser) parseSignature(signature string) (*phpFunction, error) { 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, `'"`) -} diff --git a/internal/extgen/funcparser_test.go b/internal/extgen/funcparser_test.go index 282d23b4f4..c448a8d168 100644 --- a/internal/extgen/funcparser_test.go +++ b/internal/extgen/funcparser_test.go @@ -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") diff --git a/internal/extgen/signature.go b/internal/extgen/signature.go new file mode 100644 index 0000000000..a1f5090b14 --- /dev/null +++ b/internal/extgen/signature.go @@ -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, `'"`) +}