mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-09-26 19:41:13 +08:00
391 lines
9.8 KiB
Go
391 lines
9.8 KiB
Go
package extgen
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
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
|
|
className string
|
|
}
|
|
|
|
type classParser struct{}
|
|
|
|
func (cp *classParser) Parse(filename string) ([]phpClass, error) {
|
|
return cp.parse(filename)
|
|
}
|
|
|
|
func (cp *classParser) parse(filename string) (classes []phpClass, err error) {
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing file: %w", err)
|
|
}
|
|
|
|
validator := Validator{}
|
|
|
|
exportDirectives := cp.collectExportDirectives(node, fset)
|
|
methods, err := cp.parseMethods(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing methods: %w", err)
|
|
}
|
|
|
|
// match structs to directives
|
|
matchedDirectives := make(map[int]bool)
|
|
|
|
var genDecl *ast.GenDecl
|
|
var ok bool
|
|
for _, decl := range node.Decls {
|
|
if genDecl, ok = decl.(*ast.GenDecl); !ok || genDecl.Tok != token.TYPE {
|
|
continue
|
|
}
|
|
|
|
for _, spec := range genDecl.Specs {
|
|
var typeSpec *ast.TypeSpec
|
|
if typeSpec, ok = spec.(*ast.TypeSpec); !ok {
|
|
continue
|
|
}
|
|
|
|
var structType *ast.StructType
|
|
if structType, ok = typeSpec.Type.(*ast.StructType); !ok {
|
|
continue
|
|
}
|
|
|
|
var phpCl string
|
|
var directiveLine int
|
|
if phpCl, directiveLine = cp.extractPHPClassCommentWithLine(genDecl.Doc, fset); phpCl == "" {
|
|
continue
|
|
}
|
|
|
|
matchedDirectives[directiveLine] = true
|
|
|
|
class := phpClass{
|
|
Name: phpCl,
|
|
GoStruct: typeSpec.Name.Name,
|
|
}
|
|
|
|
class.Properties = cp.parseStructFields(structType.Fields.List)
|
|
|
|
// associate methods with this class
|
|
for _, method := range methods {
|
|
if method.ClassName == phpCl {
|
|
class.Methods = append(class.Methods, method)
|
|
}
|
|
}
|
|
|
|
if err := validator.validateClass(class); err != nil {
|
|
fmt.Printf("Warning: Invalid class '%s': %v\n", class.Name, err)
|
|
continue
|
|
}
|
|
|
|
classes = append(classes, class)
|
|
}
|
|
}
|
|
|
|
for _, directive := range exportDirectives {
|
|
if !matchedDirectives[directive.line] {
|
|
return nil, fmt.Errorf("//export_php class directive at line %d is not followed by a struct declaration", directive.line)
|
|
}
|
|
}
|
|
|
|
return classes, nil
|
|
}
|
|
|
|
func (cp *classParser) collectExportDirectives(node *ast.File, fset *token.FileSet) []exportDirective {
|
|
var directives []exportDirective
|
|
|
|
for _, commentGroup := range node.Comments {
|
|
for _, comment := range commentGroup.List {
|
|
if matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {
|
|
pos := fset.Position(comment.Pos())
|
|
directives = append(directives, exportDirective{
|
|
line: pos.Line,
|
|
className: matches[1],
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return directives
|
|
}
|
|
|
|
func (cp *classParser) extractPHPClassCommentWithLine(commentGroup *ast.CommentGroup, fset *token.FileSet) (string, int) {
|
|
if commentGroup == nil {
|
|
return "", 0
|
|
}
|
|
|
|
for _, comment := range commentGroup.List {
|
|
if matches := phpClassRegex.FindStringSubmatch(comment.Text); matches != nil {
|
|
pos := fset.Position(comment.Pos())
|
|
return matches[1], pos.Line
|
|
}
|
|
}
|
|
|
|
return "", 0
|
|
}
|
|
|
|
func (cp *classParser) parseStructFields(fields []*ast.Field) []phpClassProperty {
|
|
var properties []phpClassProperty
|
|
|
|
for _, field := range fields {
|
|
for _, name := range field.Names {
|
|
prop := cp.parseStructField(name.Name, field)
|
|
properties = append(properties, prop)
|
|
}
|
|
}
|
|
|
|
return properties
|
|
}
|
|
|
|
func (cp *classParser) parseStructField(fieldName string, field *ast.Field) phpClassProperty {
|
|
prop := phpClassProperty{Name: fieldName}
|
|
|
|
// check if field is a pointer (nullable)
|
|
if starExpr, isPointer := field.Type.(*ast.StarExpr); isPointer {
|
|
prop.IsNullable = true
|
|
prop.GoType = cp.typeToString(starExpr.X)
|
|
} else {
|
|
prop.IsNullable = false
|
|
prop.GoType = cp.typeToString(field.Type)
|
|
}
|
|
|
|
prop.PhpType = cp.goTypeToPHPType(prop.GoType)
|
|
|
|
return prop
|
|
}
|
|
|
|
func (cp *classParser) typeToString(expr ast.Expr) string {
|
|
switch t := expr.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.StarExpr:
|
|
return "*" + cp.typeToString(t.X)
|
|
case *ast.ArrayType:
|
|
return "[]" + cp.typeToString(t.Elt)
|
|
case *ast.MapType:
|
|
return "map[" + cp.typeToString(t.Key) + "]" + cp.typeToString(t.Value)
|
|
default:
|
|
return "interface{}"
|
|
}
|
|
}
|
|
|
|
func (cp *classParser) goTypeToPHPType(goType string) phpType {
|
|
goType = strings.TrimPrefix(goType, "*")
|
|
|
|
typeMap := map[string]phpType{
|
|
"string": phpString,
|
|
"int": phpInt, "int64": phpInt, "int32": phpInt, "int16": phpInt, "int8": phpInt,
|
|
"uint": phpInt, "uint64": phpInt, "uint32": phpInt, "uint16": phpInt, "uint8": phpInt,
|
|
"float64": phpFloat, "float32": phpFloat,
|
|
"bool": phpBool,
|
|
}
|
|
|
|
if phpType, exists := typeMap[goType]; exists {
|
|
return phpType
|
|
}
|
|
|
|
if strings.HasPrefix(goType, "[]") || strings.HasPrefix(goType, "map[") {
|
|
return phpArray
|
|
}
|
|
|
|
return phpMixed
|
|
}
|
|
|
|
func (cp *classParser) parseMethods(filename string) (methods []phpClassMethod, 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 currentMethod *phpClassMethod
|
|
|
|
lineNumber := 0
|
|
for scanner.Scan() {
|
|
lineNumber++
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
if matches := phpMethodRegex.FindStringSubmatch(line); matches != nil {
|
|
className := strings.TrimSpace(matches[1])
|
|
signature := strings.TrimSpace(matches[2])
|
|
|
|
method, err := cp.parseMethodSignature(className, signature)
|
|
if err != nil {
|
|
fmt.Printf("Warning: Error parsing method signature %q: %v\n", signature, err)
|
|
|
|
continue
|
|
}
|
|
|
|
validator := Validator{}
|
|
phpFunc := phpFunction{
|
|
Name: method.Name,
|
|
Signature: method.Signature,
|
|
Params: method.Params,
|
|
ReturnType: method.ReturnType,
|
|
IsReturnNullable: method.isReturnNullable,
|
|
}
|
|
|
|
if err := validator.validateScalarTypes(phpFunc); err != nil {
|
|
fmt.Printf("Warning: Method \"%s::%s\" uses unsupported types: %v\n", className, method.Name, err)
|
|
|
|
continue
|
|
}
|
|
|
|
method.lineNumber = lineNumber
|
|
currentMethod = method
|
|
}
|
|
|
|
if currentMethod != nil && strings.HasPrefix(line, "func ") {
|
|
goFunc, err := cp.extractGoMethodFunction(scanner, line)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("extracting Go method function: %w", err)
|
|
}
|
|
|
|
currentMethod.GoFunction = goFunc
|
|
|
|
validator := Validator{}
|
|
phpFunc := phpFunction{
|
|
Name: currentMethod.Name,
|
|
Signature: currentMethod.Signature,
|
|
GoFunction: currentMethod.GoFunction,
|
|
Params: currentMethod.Params,
|
|
ReturnType: currentMethod.ReturnType,
|
|
IsReturnNullable: currentMethod.isReturnNullable,
|
|
}
|
|
|
|
if err := validator.validateGoFunctionSignatureWithOptions(phpFunc, true); err != nil {
|
|
fmt.Printf("Warning: Go method signature mismatch for '%s::%s': %v\n", currentMethod.ClassName, currentMethod.Name, err)
|
|
currentMethod = nil
|
|
continue
|
|
}
|
|
|
|
methods = append(methods, *currentMethod)
|
|
currentMethod = nil
|
|
}
|
|
}
|
|
|
|
if currentMethod != nil {
|
|
return nil, fmt.Errorf("//export_php:method directive at line %d is not followed by a function declaration", currentMethod.lineNumber)
|
|
}
|
|
|
|
return methods, scanner.Err()
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return &phpClassMethod{
|
|
Name: methodName,
|
|
PhpName: methodName,
|
|
ClassName: className,
|
|
Signature: signature,
|
|
Params: params,
|
|
ReturnType: phpType(returnType),
|
|
isReturnNullable: isReturnNullable,
|
|
}, 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
|
|
|
|
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
|
|
}
|