package main import ( "fmt" "reflect" "strconv" "strings" "github.com/z7zmey/php-parser/freefloating" "github.com/z7zmey/php-parser/node" "github.com/z7zmey/php-parser/node/expr" "github.com/z7zmey/php-parser/node/expr/assign" "github.com/z7zmey/php-parser/node/expr/binary" "github.com/z7zmey/php-parser/node/name" "github.com/z7zmey/php-parser/node/scalar" "github.com/z7zmey/php-parser/node/stmt" ) func nodeTypeString(n node.Node) string { return reflect.TypeOf(n).String() } type parseErr struct { n node.Node childErr error } func (pe parseErr) Error() string { return fmt.Sprintf("Parsing %s on line %d: %s", nodeTypeString(pe.n), pe.n.GetPosition().StartLine, pe.childErr) } func (pe parseErr) Unwrap() error { return pe.childErr } // type conversionState struct { currentClassName string currentClassParentName string currentErrHandler string currentFunctionName string currentMethodName string currentNamespace string currentTraitName string // TODO importPackages map[string]struct{} } // func (this *conversionState) convert(n node.Node) (string, error) { // Get any whitespace/comments attached to this node // FIXME we are not calling convert() on some interior nodes of statements - some comments may be lost(!!) freePrefix := "" freeSuffix := "" if ff := n.GetFreeFloating(); ff != nil && !ff.IsEmpty() { for positionType, elements := range *ff { element: for _, element := range elements { if element.StringType == freefloating.TokenType { // Skip 0 { return "", parseErr{s, fmt.Errorf("please use `namespace Foo;` instead of blocks")} } namespace, err := this.resolveName(s.NamespaceName) if err != nil { return "", err } packageName = namespace this.currentNamespace = packageName default: this.currentErrHandler = "panic(err)\n" // top-level init/main behaviour sm, err := this.convert(s) if err != nil { return "", parseErr{s, err} } // Top-level function code - deter emission statements = append(statements, sm) } } // Emit ret := "package main\n\n" if len(this.importPackages) > 0 { ret += "import (\n" for packageName, _ := range this.importPackages { ret += "\t" + strconv.Quote(packageName) + "\n" } ret += ")\n" } if len(declarations) > 0 { ret += strings.Join(declarations, "\n") + "\n" } if len(statements) > 0 { topFunc := `init` if packageName == `main` { topFunc = `main` } ret += "func " + topFunc + "() {\n" ret += "\t" + strings.Join(statements, "\t") // Statements already added their own newline ret += "}\n" } return ret, nil case *node.Identifier: return n.Value, nil case Literal: // We expect literal statements to act like a *Stmt, i.e. be emitted with a trailing NL return n.Value + "\n", nil // // stmt // case *stmt.StmtList: // TODO keep track of variable types within this scope ret := "{\n" // new variable scope for _, s := range n.Stmts { line, err := this.convert(s) if err != nil { return "", parseErr{s, err} } ret += line // Statements already added a trailing newline } return ret + "}\n", nil case *stmt.Class: ret := "" prevClassName := this.currentClassName // almost certainly empty-string prevClassParentName := this.currentClassParentName className := n.ClassName.(*node.Identifier).Value this.currentClassName = className memberVars := []string{} memberConsts := []string{} memberFuncs := []string{} if n.Extends != nil { parentName, err := this.resolveName(n.Extends.ClassName) if err != nil { return "", parseErr{n, err} } memberVars = append(memberVars, parentName+" // parent") this.currentClassParentName = parentName } else { this.currentClassParentName = "" } // Walk all child nodes of the class for _, s_ := range n.Stmts { switch s := s_.(type) { case *stmt.PropertyList: // Class member variable // Doc comment // TODO scan for `@var {type}` strings // Name prop, ok := s.Properties[0].(*stmt.Property) if !ok { return "", parseErr{s, fmt.Errorf("unexpected propertylist structure")} } name := prop.Variable.(*expr.Variable).VarName.(*node.Identifier).Value // Type (unknown) memberType := unknownVarType // 'Modifiers' - protected public readonly ... // prop.Modifiers memberVars = append(memberVars, name+" "+memberType) case *stmt.ClassConstList: // Class constant // Go doesn't have class constants - convert it to just a package const, prefixed with the const name // TODO detect, intercept and re-reroute any future references to these consts! // That might need a separate compiler pass for _, c_ := range s.Consts { c, ok := c_.(*stmt.Constant) if !ok { return "", parseErr{c_, fmt.Errorf("expected stmt.Constant")} } name, err := applyVisibilityModifier(c.ConstantName.(*node.Identifier).Value, s.Modifiers) if err != nil { return "", parseErr{c_, err} } constExpr, err := this.convert(c.Expr) if err != nil { return "", parseErr{c.Expr, err} } memberConsts = append(memberConsts, className+name+" = "+constExpr) } case *stmt.ClassMethod: // Function name // If function is public/private/protected, set the first character to upper/lowercase funcName, err := applyVisibilityModifier(s.MethodName.(*node.Identifier).Value, s.Modifiers) if err != nil { return "", parseErr{s, err} } prevMethodName := this.currentMethodName this.currentMethodName = funcName // TODO implement abstract methods as method functions // Doc comment // TODO scan for `@param {type}` strings isConstructor := (strings.ToLower(funcName) == `__construct` || strings.ToLower(funcName) == strings.ToLower(className)) if isConstructor { // Constructor functions get transformed to NewFoo() (*Foo, error) // We need to force the return type returnType := name.NewName([]node.Node{name.NewNamePart(className)}) // We also need prefix + suffix statements allStmts := make([]node.Node, 0, 2+len(s.Stmt.(*stmt.StmtList).Stmts)) allStmts = append(allStmts, Literal{`this := &` + className + `{}`}) // TODO also insert variable type into the scope allStmts = append(allStmts, s.Stmt.(*stmt.StmtList).Stmts...) allStmts = append(allStmts, Literal{`return this, nil`}) // Method body funcStmt, err := this.convertFunctionCommon(s.Params, returnType, true /* always use ptr return */, allStmts) if err != nil { return "", parseErr{s, err} } memberFuncStmt := "func " + constructorName(className) + funcStmt + "\n" memberFuncs = append(memberFuncs, memberFuncStmt) } else { // Check if this is a static method hasStatic, err := hasModifier(s.Modifiers, `static`) if err != nil { return "", parseErr{s, err} } // Method body funcStmt, err := this.convertFunctionCommon(s.Params, s.ReturnType, s.ReturnsRef, s.Stmt.(*stmt.StmtList).Stmts) if err != nil { return "", parseErr{s, err} } if hasStatic { memberFuncs = append(memberFuncs, "func "+className+funcName+funcStmt+"\n") } else { memberFuncs = append(memberFuncs, "func (this *"+className+") "+funcName+funcStmt+"\n") } } // Reinstate stack this.currentMethodName = prevMethodName default: return "", parseErr{s, fmt.Errorf("Class '%s' contained unexpected AST node; expected PropertyList / ClassMethod", className)} } } // Write out any class constants if len(memberConsts) == 1 { ret += "const " + memberConsts[0] + "\n" } else if len(memberConsts) > 1 { ret += "const (\n" + strings.Join(memberConsts, "\n") + ")\n" } // Create struct typedef containing all explicit properties ret += "type " + className + " struct {\n" ret += "\t" + strings.Join(memberVars, "\n\t") + "\n" ret += "}\n" // Create all member functions ret += strings.Join(memberFuncs, "\n\n") if n.Implements != nil { for _, ifaceName_ := range n.Implements.InterfaceNames { ifaceName, err := this.resolveName(ifaceName_) if err != nil { return "", parseErr{ifaceName_, err} } // Add extra interface assertion statement ret += "var _ " + ifaceName + " = &" + className + "{} // interface assertion\n" } } // Done // Reinstate parent state before returning this.currentClassName = prevClassName this.currentClassParentName = prevClassParentName return ret, nil case *stmt.Interface: ifaceName := n.InterfaceName.(*node.Identifier).Value ret := "type " + ifaceName + " interface {\n" for _, ss := range n.Stmts { classMethod, ok := ss.(*stmt.ClassMethod) if !ok { return "", parseErr{ss, fmt.Errorf("expected stmt.ClassMethod")} } methodName, err := applyVisibilityModifier(classMethod.MethodName.(*node.Identifier).Value, classMethod.Modifiers) if err != nil { return "", parseErr{ss, err} } // TODO classMethod.PhpDocComment arglist, err := this.convertFunctionCommon(classMethod.Params, classMethod.ReturnType, classMethod.ReturnsRef, nil) if err != nil { return "", parseErr{ss, err} } ret += methodName + arglist + "\n" } ret += "}\n" return ret, nil case *stmt.Function: // Top-level function definition // TODO parse doc comment // FIXME is this the same as a closure? funcName := n.FunctionName.(*node.Identifier).Value if funcName == `super` { return "", parseErr{n, fmt.Errorf("Function name '%s' probably will not function correctly", funcName)} } // All top-level functions like this are public; ensure function name starts // with an uppercase letter funcName = toPublic(funcName) // Convert body prevFuncName := this.currentFunctionName this.currentFunctionName = funcName funcStmt, err := this.convertFunctionCommon(n.Params, n.ReturnType, n.ReturnsRef, n.Stmts) if err != nil { // FIXME an early-return will trash the `this` state around currentFunctionName, etc // That's OK for now since we don't practically have any error handling than an unmitigated panic return "", parseErr{n, err} } this.currentFunctionName = prevFuncName ret := "func " + funcName + funcStmt + "\n" return ret, nil case *stmt.Return: child, err := this.convert(n.Expr) if err != nil { return "", parseErr{n, err} } ret := "return " + child + ", nil\n" return ret, nil case *stmt.Throw: // throw (expr); // Treat as an err return // FIXME we don't know the default return type for the function we're in // PHP can only throw objects, not literals/scalars // If the expr is just new Exception, we can convert it to errors.New() //if str, ok := n.Expr.(*scalar.String); ok { // return "return nil, errors.New(" + str.Value + ")\n", nil //} child, err := this.convert(n.Expr) if err != nil { return "", parseErr{n, err} } return "return nil, " + child + "\n", nil case *stmt.For: var preinit, finit string var err error = nil if len(n.Init) == 0 { // No initialiser in loop } else if len(n.Init) == 1 { finit, err = this.convert(n.Init[0]) if err != nil { return "", parseErr{n, err} } } else { // We can handle the case of multiple init statements by hoisting them // above the loop. There is no negative impact on PHP scoping rules, but // it may cause an extra local variable after the loop that may result // in type mismatch (can be fixed by using an extra scope). for _, initStmt := range n.Init { singleInitStmt, err := this.convert(initStmt) if err != nil { return "", parseErr{initStmt, err} } preinit += singleInitStmt + "\n" } } if len(n.Cond) != 1 { return "", parseErr{n, fmt.Errorf("for loop can only have 1 cond clause, found %d", len(n.Cond))} } fcond, err := this.convert(n.Cond[0]) if err != nil { return "", parseErr{n, err} } if len(n.Loop) != 1 { return "", parseErr{n, fmt.Errorf("for loop can only have 1 loop clause, found %d", len(n.Loop))} } loopStmt := n.Loop[0] if preinc, ok := loopStmt.(*expr.PreInc); ok { // It's idiomatic to do for (,, ++i) but preincrement doesn't exist in Go // Luckily for the case of a for loop, we can just swap it to postincrement loopStmt = expr.NewPostInc(preinc.Variable) } else if predec, ok := loopStmt.(*expr.PreDec); ok { // Likewise loopStmt = expr.NewPostDec(predec.Variable) } floop, err := this.convert(loopStmt) if err != nil { return "", parseErr{n, err} } body, err := this.convert(convertToStmtList(n.Stmt)) if err != nil { return "", parseErr{n, err} } return preinit + "for " + finit + "; " + fcond + "; " + floop + " " + body + "\n", nil case *stmt.Foreach: iterand, err := this.convert(n.Expr) if err != nil { return "", parseErr{n, err} } valueReceiver, err := this.convert(n.Variable) if err != nil { return "", parseErr{n, err} } keyReceiver := `_` if n.Key != nil { keyReceiver, err = this.convert(n.Key) if err != nil { return "", parseErr{n, err} } } body, err := this.convert(convertToStmtList(n.Stmt)) if err != nil { return "", parseErr{n, err} } return "for " + keyReceiver + ", " + valueReceiver + " := range " + iterand + " " + body + "\n", nil case *stmt.While: cond, err := this.convert(n.Cond) if err != nil { return "", parseErr{n, err} } body, err := this.convert(convertToStmtList(n.Stmt)) if err != nil { return "", parseErr{n, err} } return "for " + cond + " " + body + "\n", nil case *stmt.Do: cond, err := this.convert(n.Cond) if err != nil { return "", parseErr{n, err} } bodyStmts := convertToStmtList(n.Stmt) bodyStmts.Stmts = append(bodyStmts.Stmts, Literal{"if " + cond + "{\nbreak\n}"}) body, err := this.convert(bodyStmts) if err != nil { return "", parseErr{n, err} } return "for " + cond + " " + body + "\n", nil case *stmt.Expression: // Special case if fncall, ok := n.Expr.(*expr.FunctionCall); ok { if fnname, err := this.resolveName(fncall.Function); err == nil && fnname == "super" { // Call to parent constructor if this.currentClassParentName == "" { return "", parseErr{n, fmt.Errorf("Call to parent constructor outside of class context")} } // We need to call the parent constructor function (NewX) with these arguments funcArgs, err := this.convertFuncCallArgsCommon(fncall.ArgumentList) if err != nil { return "", parseErr{n, err} } // Then we need to overwrite the embedded value type with the received pointer // That's not 100% safe in all cases if the *this pointer is leaked // within the constructor, but, our generated code will never do that // TODO replace our NewX constructors with split NewX + newXInPlace(??) // No need to use a child scope for the temporary name - // super() is a reserved name in PHP anyway ret := "super, err := " + constructorName(this.currentClassParentName) + funcArgs + "\n" ret += "if err != nil {\n" ret += this.currentErrHandler //"return err\n" ret += "}\n" ret += "this." + this.currentClassParentName + " = *super // copy by value\n" return ret, nil } if fnname, err := this.resolveName(fncall.Function); err == nil && fnname == "define" { // define() gets converted to const if len(fncall.ArgumentList.Arguments) != 2 { return "", parseErr{fncall, fmt.Errorf("expected define() to have 2 arguments, found %d", len(fncall.ArgumentList.Arguments))} } defineName := fncall.ArgumentList.Arguments[0].(*node.Argument).Expr // that much is always possible defineNameStr, ok := defineName.(*scalar.String) if !ok { return "", parseErr{fncall, fmt.Errorf("can't handle a complex expression in define() name")} } rawDefineName, err := phpUnquote(defineNameStr.Value) if err != nil { return "", parseErr{fncall, err} } // Convert to a final Const statement return this.convert(stmt.NewConstList([]node.Node{stmt.NewConstant(node.NewIdentifier(rawDefineName), fncall.ArgumentList.Arguments[1].(*node.Argument).Expr, "")})) } } // Assignment expressions can take on better error-handling behaviour // when we know the assignment is a top-level expression if a, ok := n.Expr.(*assign.Assign); ok { return this.convertAssignment(a, true) } // Non-assignment expression child, err := this.convert(n.Expr) if err != nil { return "", parseErr{n, err} } // If this is a simple expression (func call; indirect/method call; assignment of func/method call) then // we need to propagate errors switch n.Expr.(type) { case *expr.FunctionCall, *expr.StaticCall, *expr.New: ret := "_, err = " + child + "\n" ret += "if err != nil {\n" ret += this.currentErrHandler ret += "}\n" return ret, nil default: // Some other kind of general expression - no special error handling needed ret := child + "\n" // standalone expression statement return ret, nil } case *stmt.Echo: // Convert into fmt.Print args := make([]string, 0, len(n.Exprs)) for _, expr := range n.Exprs { exprGo, err := this.convert(expr) if err != nil { return "", parseErr{n, err} } args = append(args, removeParens(exprGo)) } this.importPackages["fmt"] = struct{}{} return "fmt.Print(" + strings.Join(args, ", ") + ")\n", nil // newline - standalone statement case *stmt.InlineHtml: // Convert into fmt.Print var quoted string if !strings.Contains(n.Value, "`") && strings.Count(n.Value, "\n") >= 3 { // TODO make the heuristic configurable // Use backtick-delimited multiline string quoted = "`" + n.Value + "`" } else { // Can't trivially represent it with backticks, or it's not multiline "enough" to bother - use full Go quoting quoted = strconv.Quote(n.Value) } this.importPackages["fmt"] = struct{}{} return "fmt.Print(" + quoted + ")\n", nil // newline - standalone statement case *stmt.ConstList: consts := make([]string, 0, len(n.Consts)) for _, c_ := range n.Consts { c, ok := c_.(*stmt.Constant) if !ok { return "", parseErr{c_, fmt.Errorf("expected stmt.Constant")} } // TODO c.PhpDocComment constName := c.ConstantName.(*node.Identifier).Value rvalue, err := this.convert(c.Expr) if err != nil { return "", parseErr{c, err} } consts = append(consts, constName+" = "+rvalue) } if len(n.Consts) == 1 { return "const " + consts[0] + "\n", nil } else { return "const (\n" + strings.Join(consts, "\n") + ")\n", nil } case *stmt.If: hasCondAssign, err := hasInteriorAssignment(n.Cond) if err != nil { return "", parseErr{n, err} } if hasCondAssign { return "", parseErr{n.Cond, fmt.Errorf("please remove assignment from if-expression")} } cond, err := this.convert(n.Cond) if err != nil { return "", parseErr{n, err} } body, err := this.convert(convertToStmtList(n.Stmt)) if err != nil { return "", parseErr{n, err} } // // `elseif` is a valid PHP keyword; but `else if` gets treated as `else { if ()` // If our discovered `else` clause has exactly one child If statement, // rewrite it as an else-if clause (and recurse) for n.Else != nil { els, ok := n.Else.(*stmt.Else) if !ok { return "", parseErr{n, fmt.Errorf("expected stmt.Else")} } elif, ok := els.Stmt.(*stmt.If) if !ok { break // exit loop } n.AddElseIf(stmt.NewElseIf(elif.Cond, elif.Stmt)) for _, extraElif := range elif.ElseIf { n.AddElseIf(extraElif) } n.Else = elif.Else // Wipe from future processing } // ret := "if " + cond + body for _, elif := range n.ElseIf { elif, ok := elif.(*stmt.ElseIf) if !ok { return "", parseErr{n, fmt.Errorf("expected stmt.ElseIf")} } cond, err := this.convert(elif.Cond) if err != nil { return "", parseErr{n, err} } body, err := this.convert(convertToStmtList(elif.Stmt)) if err != nil { return "", parseErr{n, err} } ret = strings.TrimRight(ret, "\n") ret += " else if " + cond + body } if n.Else != nil { els, ok := n.Else.(*stmt.Else) if !ok { return "", parseErr{n, fmt.Errorf("expected stmt.Else")} } body, err := this.convert(convertToStmtList(els.Stmt)) if err != nil { return "", parseErr{n, err} } ret = strings.TrimRight(ret, "\n") ret += " else " + body } return ret, nil case *stmt.Try: // Generate an if err != nil {} block that we can bring up whenever // an error is raised in a body function call // TODO put finally() code somewhere (before/after err checking??) handler := "" if len(n.Catches) == 0 { handler = "// suppress error" } else { catchVars := []string{} catchIfs := []string{} for _, catch_ := range n.Catches { catch, ok := catch_.(*stmt.Catch) if !ok { return "", parseErr{n, fmt.Errorf("expected stmt.Catch")} } catchVar := catch.Variable.(*expr.Variable).VarName.(*node.Identifier).Value catchBody, err := this.convert(stmt.NewStmtList(catch.Stmts)) if err != nil { return "", parseErr{catch_, err} } for _, typename_ := range catch.Types { typename, err := this.resolveName(typename_) if !ok { return "", parseErr{catch_, err} } if typename == `Exception` { // PHP base type - catches all exceptions catchStmt := catchBody catchIfs = append(catchIfs, catchStmt) } else { tempCatchVar := catchVar if len(catch.Types) > 1 { tempCatchVar += "" + typename } // It's common for PHP types to have 'Exception' in the suffix // We are probably free to elide that (stylistically) if strings.HasSuffix(tempCatchVar, `Exception`) { tempCatchVar = tempCatchVar[0 : len(tempCatchVar)-9] } catchVars = append(catchVars, tempCatchVar+"*"+typename) this.importPackages["errors"] = struct{}{} catchStmt := "if errors.As(err, &" + tempCatchVar + ") {\n" if len(catch.Types) > 1 { catchStmt += catchVar + " := " + tempCatchVar + " // rename\n" } catchStmt += catchBody // contains its own {} catchStmt += "}" // but without trailing NL catchIfs = append(catchIfs, catchStmt) } } } handler = "" if len(catchVars) == 1 { handler += "var " + catchVars[0] + "\n" } else { handler += "var (\n" + strings.Join(catchVars, "\n") + "\n)\n" } handler += strings.Join(catchIfs, " else ") + "\n" } // Store the handler to be used going forwards previousErrHandler := this.currentErrHandler this.currentErrHandler = handler body, err := this.convert(stmt.NewStmtList(n.Stmts)) if err != nil { return "", parseErr{n, err} } // Reinstate parent error handler this.currentErrHandler = previousErrHandler return body, nil // Try/catch behaviour is transparently encoded in err handlers case *stmt.Nop: return "", nil // // assign // case *assign.Assign: return this.convertAssignment(n, false /* no reason to believe we are a top-level statement */) // // expr // case *expr.FunctionCall: // All our generated functions return err, but this AST node may be in a single-rvalue context // TODO do something more intelligent here // We can't necessarily hoist the whole call, in case we are on the right-hand side of a && operator funcName, err := this.resolveName(n.Function) if err != nil { return "", parseErr{n, err} } callParams, err := this.convertFuncCallArgsCommon(n.ArgumentList) if err != nil { return "", parseErr{n, err} } // FIXME need to support `this.currentErrHandler` return funcName + callParams, nil // expr only, no semicolon/newline case *expr.StaticCall: className, err := this.resolveName(n.Class) if err != nil { return "", parseErr{n, err} } funcName, err := this.convert(n.Call) if err != nil { return "", parseErr{n, err} } callTarget := className + "." + funcName if className == "self" { if this.currentClassName == "" { return "", parseErr{n, fmt.Errorf("Made a self::Static method call while not in class context")} } // We're making a static call, and we renamed those to be top-level functions callTarget = this.currentClassName + funcName } callParams, err := this.convertFuncCallArgsCommon(n.ArgumentList) if err != nil { return "", parseErr{n, err} } // FIXME need to support `this.currentErrHandler` return callTarget + callParams, nil // expr only, no semicolon/newline case *expr.New: // new foo(xx) // Transparently convert to calling constructor function. nn, err := this.resolveName(n.Class) if err != nil { return "", parseErr{n, err} } // FIXME if there is a package specifier embedded in the result name, // the `New` will appear in the wrong place nn = `New` + nn // Convert resolved back to node.Name transparentNameNode := name.NewName([]node.Node{name.NewNamePart(nn)}) return this.convert(expr.NewFunctionCall(transparentNameNode, n.ArgumentList)) case *expr.ClassConstFetch: // We converted class constants to package-level constants className, err := this.resolveName(n.Class) if err != nil { return "", parseErr{n, err} } constName := n.ConstantName.(*node.Identifier).Value // TODO fix up visibility modifier return className + constName, nil case *expr.Exit: // die(0) - set process exit code and exit // die("message") - print to stdout and exit 0 // die - exit 0 this.importPackages["os"] = struct{}{} if n.Expr == nil { return "os.Exit(0)", nil } child, err := this.convert(n.Expr) if err != nil { return "", parseErr{n, err} } // Although it might have been a more complex string expression - we // don't currently know that // TODO type inference switch n.Expr.(type) { case *scalar.String: this.importPackages["fmt"] = struct{}{} return "fmt.Print(" + child + ")\nos.Exit(0)", nil // hopefully die() was standalone, and not "or die" default: return "os.Exit(" + child + ")", nil } case *expr.PreInc: // """In Go, i++ is a statement, not an expression. So you can't use its value in another expression such as a function call.""" v, err := this.convert(n.Variable) if err != nil { return "", parseErr{n, err} } return "++" + v, nil case *expr.PostInc: // """In Go, i++ is a statement, not an expression. So you can't use its value in another expression such as a function call.""" v, err := this.convert(n.Variable) if err != nil { return "", parseErr{n, err} } return v + "++", nil case *expr.MethodCall: // Foo->Bar(Baz) parent, err := this.convert(n.Variable) if err != nil { return "", parseErr{n, err} } child, err := this.convert(n.Method) if err != nil { return "", parseErr{n, err} } args, err := this.convertFuncCallArgsCommon(n.ArgumentList) if err != nil { return "", parseErr{n, err} } // FIXME need to support `this.currentErrHandler` return parent + "." + child + args, nil case *expr.PropertyFetch: // Foo->Bar parent, err := this.convert(n.Variable) if err != nil { return "", parseErr{n, err} } child, err := this.convert(n.Property) if err != nil { return "", parseErr{n, err} } return parent + "." + child, nil case *expr.Variable: return n.VarName.(*node.Identifier).Value, nil case *expr.ConstFetch: return this.resolveName(n.Constant) case *expr.Array: return this.convertArrayLiteralCommon(n.Items) case *expr.ShortArray: return this.convertArrayLiteralCommon(n.Items) case *expr.ArrayDimFetch: // Might be x[foo], might be x[] (i.e. append() call) // In order to make the append() transformation, we need to lookahead // for ArrayDimFetch in the `*assign.Assign` case vv, err := this.convert(n.Variable) if err != nil { return "", parseErr{n, err} } if n.Dim == nil { return "", parseErr{n, fmt.Errorf("found '%s[]' outside of lvalue assignment context", vv)} } idx, err := this.convert(n.Dim) if err != nil { return "", parseErr{n, err} } return vv + `[` + idx + `]`, nil // Same syntax as PHP // // binary // case *binary.BitwiseAnd: return this.convertBinaryCommon(n.Left, n.Right, `&`) case *binary.BitwiseOr: return this.convertBinaryCommon(n.Left, n.Right, `|`) case *binary.BitwiseXor: return this.convertBinaryCommon(n.Left, n.Right, `^`) // n.b. Go only supports this for integers; PHP also supports it for bools case *binary.BooleanAnd: return this.convertBinaryCommon(n.Left, n.Right, `&&`) case *binary.BooleanOr: return this.convertBinaryCommon(n.Left, n.Right, `||`) //case *binary.Coalesce: // TODO this can't be expressed in an rvalue context in Go (unless we create a typed closure..?) case *binary.Concat: return this.convertBinaryCommon(n.Left, n.Right, `+`) // PHP uses + for numbers, `.` for strings; Go uses `+` in both cases case *binary.Div: return this.convertBinaryCommon(n.Left, n.Right, `/`) // PHP will upgrade ints to floats, Go won't case *binary.Equal: return this.convertBinaryCommon(n.Left, n.Right, `==`) // Type-lax equality comparator case *binary.GreaterOrEqual: return this.convertBinaryCommon(n.Left, n.Right, `>=`) case *binary.Greater: return this.convertBinaryCommon(n.Left, n.Right, `>`) case *binary.Identical: return this.convertBinaryCommon(n.Left, n.Right, `==`) // PHP uses `===`, Go is already type-safe case *binary.LogicalAnd: // This is the lexer token when using `and` in PHP. It's equivalent to // `&&` but has different precedence // e.g. $a = $b && $c ==> $a = ($b && $c) // $a = $b and $c ==> ($a = $b) and $c // So far, we are relying on the PHP parser having already having handled // the precedence difference - transform to `&&` unconditionally return this.convertBinaryCommon(n.Left, n.Right, `&&`) case *binary.LogicalOr: // As above return this.convertBinaryCommon(n.Left, n.Right, `||`) case *binary.LogicalXor: // As above return this.convertBinaryCommon(n.Left, n.Right, `^`) // n.b. Go only supports this for integers; PHP also supports it for bools case *binary.Minus: return this.convertBinaryCommon(n.Left, n.Right, `-`) case *binary.Mod: // Go doesn't have a built-in operator for mod - convert to a call to math.Mod() rval, err := this.convert(n.Left) if err != nil { return "", parseErr{n, err} } modulo, err := this.convert(n.Right) if err != nil { return "", parseErr{n, err} } this.importPackages["math"] = struct{}{} return `math.Mod(` + rval + `, ` + modulo + `)`, nil case *binary.Mul: return this.convertBinaryCommon(n.Left, n.Right, `*`) case *binary.NotEqual: return this.convertBinaryCommon(n.Left, n.Right, `!=`) // Type-lax equality comparator case *binary.NotIdentical: return this.convertBinaryCommon(n.Left, n.Right, `!=`) // PHP uses `!==`, Go is already type-safe case *binary.Plus: return this.convertBinaryCommon(n.Left, n.Right, `+`) // PHP uses + for numbers, `.` for strings; Go uses `+` in both cases case *binary.Pow: // Go doesn't have a built-in operator for mod - convert to a call to math.Pow() base, err := this.convert(n.Left) if err != nil { return "", parseErr{n, err} } exponent, err := this.convert(n.Right) if err != nil { return "", parseErr{n, err} } this.importPackages["math"] = struct{}{} return `math.Pow(` + base + `, ` + exponent + `)`, nil case *binary.ShiftLeft: return this.convertBinaryCommon(n.Left, n.Right, `<<`) case *binary.ShiftRight: return this.convertBinaryCommon(n.Left, n.Right, `>>`) case *binary.SmallerOrEqual: return this.convertBinaryCommon(n.Left, n.Right, `<=`) case *binary.Smaller: return this.convertBinaryCommon(n.Left, n.Right, `<`) case *binary.Spaceship: // The spaceship operator returns -1 / 0 / 1 based on a gteq/leq comparison // Go doesn't have a built-in spaceship operator // The primary use case is in user-definded sort comparators, where Go // uses bools instead ints anyway. // Subtraction is a reasonable substitute return this.convertBinaryCommon(n.Left, n.Right, `-`) // // scalar // case *scalar.Lnumber: return n.Value, nil // number formats are compatible case *scalar.String: // We need to transform the string format - e.g. Go does not support // single-quoted strings rawValue, err := phpUnquote(n.Value) if err != nil { return "", parseErr{n, err} } return strconv.Quote(rawValue), nil // Go source code quoting format case *scalar.MagicConstant: // magic constants are case-insensitive switch strings.ToLower(n.Value) { case `__file__`, `__dir__`: // These are normally used in PHP for finding the absolute webroot directory (e.g. dirname(__FILE__) in index.php) // The letter of the law would be to replace them with the *.go file/dir // We could even emit a runtime call to get the Go file/line: @ref https://github.com/golang/go/issues/12876#issuecomment-146878684 // But in practice, the current working directory might be preferable return `""`, nil case `__line__`: return fmt.Sprintf("%d", n.Position.StartLine), nil case `__namespace__`: return this.currentNamespace, nil case `__function__`: if this.currentFunctionName == "" { return "", parseErr{n, fmt.Errorf("use of __FUNCTION__ outside of a function")} } return this.currentFunctionName, nil case `__class__`: if this.currentClassName == "" { return "", parseErr{n, fmt.Errorf("use of __CLASS__ outside of a class")} } return this.currentClassName, nil case `__method__`: if this.currentMethodName == "" { return "", parseErr{n, fmt.Errorf("use of __METHOD__ outside of a class method")} } return this.currentMethodName, nil case `__trait__`: if this.currentTraitName == "" { return "", parseErr{n, fmt.Errorf("use of __TRAIT__ outside of a trait")} } return this.currentTraitName, nil default: return "", parseErr{n, fmt.Errorf("unrecognized magic constant '%s'", n.Value)} } // // // default: return "", fmt.Errorf("unsupported node type %s", nodeTypeString(n)) } } func hasModifier(modifiers []node.Node, search string) (bool, error) { for _, mod := range modifiers { ident, ok := mod.(*node.Identifier) if !ok { return false, parseErr{mod, fmt.Errorf("expected node.Identifier")} } if strings.ToLower(ident.Value) == strings.ToLower(search) { return true, nil } } return false, nil } // applyVisibilityModifier renames a function to use an upper/lowercase first // letter based on PHP visibility modifiers. func applyVisibilityModifier(funcName string, modifiers []node.Node) (string, error) { hasPublic, err := hasModifier(modifiers, "public") if err != nil { return "", err } hasPrivate, err := hasModifier(modifiers, "private") if err != nil { return "", err } hasProtected, err := hasModifier(modifiers, "protected") if err != nil { return "", err } if (hasPublic && !hasPrivate && !hasProtected) /* explicitly public */ || (!hasPublic && !hasPrivate && !hasProtected) /* no modifiers defaults to public */ { return toPublic(funcName), nil } else if !hasPublic && (hasPrivate || hasProtected) { return toPrivate(funcName), nil } else { return "", fmt.Errorf("unexpected combination of modifiers") } } func toPublic(name string) string { nFirst := name[0:1] uFirst := strings.ToUpper(nFirst) if nFirst == uFirst { return name // avoid making more heap garbage } return uFirst + name[1:] } func toPrivate(name string) string { nFirst := name[0:1] lFirst := strings.ToLower(nFirst) if nFirst == lFirst { return name // avoid making more heap garbage } return lFirst + name[1:] } func constructorName(className string) string { return `New` + className } // removeParens removes surrounding parentheses from an expression. // This is only safe in cases where there is ambiguously a single rvalue wanted, // e.g. between ( and , in a function call argument func removeParens(expr string) string { for len(expr) > 2 && expr[0] == '(' && expr[len(expr)-1] == ')' { expr = expr[1 : len(expr)-1] } return expr } // resolveName turns a `*name.Name` node into a Go string. func (this *conversionState) resolveName(n node.Node) (string, error) { // TODO support namespace lookups ret := unknownVarType if n == nil || n == node.Node(nil) { return ret, nil } switch n := n.(type) { case *name.FullyQualified: if len(n.Parts) != 1 { return "", parseErr{n, fmt.Errorf("name has %d parts, expected 1", len(n.Parts))} } ret = n.Parts[0].(*name.NamePart).Value case *name.Name: if len(n.Parts) != 1 { return "", parseErr{n, fmt.Errorf("name has %d parts, expected 1", len(n.Parts))} } ret = n.Parts[0].(*name.NamePart).Value default: return "", fmt.Errorf("unexpected name type %#v", n) } // Handle class lookups if strings.ToLower(ret) == "parent" { if this.currentClassParentName == "" { return "", parseErr{n, fmt.Errorf("Lookup of 'parent' while not in an inherited child class context")} } return `this.` + this.currentClassParentName, nil } else if strings.ToLower(ret) == "self" { // Let it through as-is // return "", parseErr{n, fmt.Errorf("Lookup of 'self::' should have been resolved already")} /* if this.currentClassName == "" { return "", parseErr{n, fmt.Errorf("Lookup of 'self' while not in class context")} } return `this`, nil */ } else if strings.ToLower(ret) == "static" { return "", parseErr{n, fmt.Errorf("'static::' is not yet supported")} } else if strings.ToLower(ret) == "true" { return "true", nil } else if strings.ToLower(ret) == "false" { return "false", nil } else if strings.ToLower(ret) == "null" { return "nil", nil } return ret, nil } func (this *conversionState) convertAssignment(n *assign.Assign, isTopLevelStatement bool) (string, error) { rvalue, err := this.convert(n.Expression) if err != nil { return "", parseErr{n, err} } if dimf, ok := n.Variable.(*expr.ArrayDimFetch); ok && dimf.Dim == nil { // Special handling for the case of foo[] = bar // Transform into append() arrayVar, err := this.convert(dimf.Variable) if err != nil { return "", parseErr{dimf, err} } ret := arrayVar + ` = append(` + arrayVar + `, ` + rvalue + `)` if isTopLevelStatement { ret += "\n" } return ret, nil } else { // Normal assignment lvalue, err := this.convert(n.Variable) // might be a more complicated lvalue if err != nil { return "", parseErr{n, err} } // TODO this may need to use `:=` if isTopLevelStatement { // If this is a simple expression (func call; indirect/method call; assignment of func/method call) then // we need to propagate errors switch n.Expression.(type) { case *expr.FunctionCall, *expr.StaticCall, *expr.New: ret := lvalue + ", err = " + rvalue + "\n" ret += "if err != nil {\n" ret += this.currentErrHandler ret += "}\n" return ret, nil default: ret := lvalue + " = " + rvalue + "\n" // Just the basic assignment - with trailing NL return ret, nil } } else { return lvalue + " = " + rvalue, nil // Just the basic assignment - without trailing NL } } } // convertToStmtList asserts that the node is either a StmtList or wraps it in a // single-stmt StmtList if not. // Loop bodies may be a StmtList if it is wrapped in {}, or a single statement // if it is not; we want to enforce the use of {} for all loop bodies func convertToStmtList(n node.Node) *stmt.StmtList { if sl, ok := n.(*stmt.StmtList); ok { return sl // It's already a StmtList } return stmt.NewStmtList([]node.Node{n}) } func (this *conversionState) convertBinaryCommon(left, right node.Node, goBinaryOperator string) (string, error) { // PHP uses + for numbers, `.` for strings; Go uses `+` in both cases // Assume PHP/Go have the same associativity here lhs, err := this.convert(left) if err != nil { return "", parseErr{left, err} } rhs, err := this.convert(right) if err != nil { return "", parseErr{right, err} } // In case of an rvalue assignment expression, we need extra parens if _, ok := left.(*assign.Assign); ok { lhs = "(" + lhs + ")" } if _, ok := right.(*assign.Assign); ok { rhs = "(" + rhs + ")" } // We can elide even more parens in case of some commutative operators // We can only do this in the left-associative case // FIXME Plus and Concat aren't necessarily commutative together even though they both have + here(!!) if _, ok := left.(*binary.Plus); ok && goBinaryOperator == `+` { lhs = removeParens(lhs) } if _, ok := left.(*binary.Concat); ok && goBinaryOperator == `+` { lhs = removeParens(lhs) } // Done return "(" + lhs + " " + goBinaryOperator + " " + rhs + ")", nil } func (this *conversionState) convertFuncCallArgsCommon(args *node.ArgumentList) (string, error) { callParams := make([]string, 0, len(args.Arguments)) for _, arg_ := range args.Arguments { arg, ok := arg_.(*node.Argument) if !ok { return "", parseErr{arg_, fmt.Errorf("expected node.Argument")} } rvalue, err := this.convert(arg.Expr) if err != nil { return "", parseErr{arg, err} } if arg.IsReference { rvalue = "&" + rvalue } if arg.Variadic { rvalue = "..." + rvalue } callParams = append(callParams, removeParens(rvalue)) } return "(" + strings.Join(callParams, `, `) + ")", nil // expr only, no semicolon/newline } func (this *conversionState) convertArrayLiteralCommon(items []node.Node) (string, error) { // Array literal // We need to know the type. See if we can guess it from the first child element // At least, we may be able to determine if this is a map or an array entries := []string{} keyType := unknownVarType valType := unknownVarType isMapType := false for idx, itm_ := range items { itm, ok := itm_.(*expr.ArrayItem) if !ok { return "", parseErr{itm_, fmt.Errorf("expected ArrayItem")} } if idx == 0 { isMapType = (itm.Key != nil) } else { if isMapType != (itm.Key != nil) { return "", parseErr{itm, fmt.Errorf("Can't represent array and map in a single type")} } } vv, err := this.convert(itm.Val) if err != nil { return "", parseErr{itm, err} } if itm.Key != nil { kv, err := this.convert(itm.Key) if err != nil { return "", parseErr{itm, err} } entries = append(entries, kv+`: `+vv+`,`) } else { entries = append(entries, vv+`,`) } } if isMapType { return `map[` + keyType + `]` + valType + `{` + strings.Join(entries, " ") + `}`, nil } else { return `[]` + valType + `{` + strings.Join(entries, " ") + `}`, nil } } func (this *conversionState) convertFunctionCommon(params []node.Node, returnType node.Node, returnsRef bool, bodyStmts []node.Node) (string, error) { // TODO scan function and see if it contains any return statements at all // If not, then we only need an err return parameter, not anything else funcParams := []string{} for _, param := range params { param, ok := param.(*node.Parameter) // shadow if !ok { return "", parseErr{param, fmt.Errorf("expected node.Parameter")} } // VariableType: might be nil for untyped parameters paramType, err := this.resolveName(param.VariableType) if err != nil { return "", parseErr{param, err} } if param.ByRef { paramType = "*" + paramType } if param.Variadic { paramType = "..." + paramType } // Name paramName := param.Variable.(*expr.Variable).VarName.(*node.Identifier).Value funcParams = append(funcParams, paramName+" "+paramType) } // ReturnType funcReturn, err := this.resolveName(returnType) if err != nil { return "", parseErr{returnType, err} } if returnsRef { funcReturn = "*" + funcReturn } // Build function prototype ret := "(" + strings.Join(funcParams, ", ") + ") (" + funcReturn + ", error) " // Recurse through body statements if bodyStmts != nil { fullBody, err := this.convert(stmt.NewStmtList(bodyStmts)) if err != nil { return "", err } ret += fullBody + "\n" } // Done // No extra trailing newline in case this is part of a large expression return ret, nil } // hasInteriorAssignment recursively walks a node, to determine if it contains // any assignment expressions func hasInteriorAssignment(n node.Node) (hasAnyAssign bool, err error) { err = walk(n, func(n node.Node) error { if _, ok := n.(*assign.Assign); ok { hasAnyAssign = true } return nil }) return // named return }