php2go/node.go

1237 lines
32 KiB
Go
Raw Normal View History

2020-04-05 04:35:44 +00:00
package main
import (
"fmt"
"reflect"
2020-04-05 05:11:11 +00:00
"strconv"
2020-04-05 04:35:44 +00:00
"strings"
2020-04-07 06:27:20 +00:00
"github.com/z7zmey/php-parser/freefloating"
2020-04-05 04:35:44 +00:00
"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 {
2020-04-05 07:11:05 +00:00
currentClassName string
currentClassParentName string
}
2020-04-05 07:11:05 +00:00
//
2020-04-07 06:27:20 +00:00
func (this *conversionState) convert(n node.Node) (string, error) {
// Get any whitespace/comments attached to this node
2020-04-07 09:53:07 +00:00
// FIXME we are not calling convert() on some interior nodes of statements - some comments may be lost(!!)
2020-04-07 06:27:20 +00:00
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 <?php
continue element
}
if element.StringType == freefloating.WhiteSpaceType {
// We can't insert arbitrary whitespace
// TODO the number of newlines would be fine ONLY IF this is a *stmt
continue element
}
switch positionType {
default:
fallthrough
case freefloating.Start:
freePrefix += element.Value
case freefloating.End, freefloating.AltEnd:
freeSuffix += element.Value
}
}
}
}
// Convert the node itself
ret, err := this.convertNoFreeFloating(n)
if err != nil {
return "", err
}
return freePrefix + ret + freeSuffix, nil
}
func (this *conversionState) convertNoFreeFloating(n_ node.Node) (string, error) {
2020-04-05 04:35:44 +00:00
switch n := n_.(type) {
//
// node
//
case *node.Root:
ret := "package main\n\n"
// Hoist all declarations first, and put any top-level code into a generated main() function
statements := []string{}
for _, s := range n.Stmts {
sm, err := this.convert(s)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{s, err}
}
switch s.(type) {
2020-04-07 09:53:00 +00:00
case *stmt.Class, *stmt.Function, *stmt.Interface:
2020-04-05 04:35:44 +00:00
// Declaration - emit immediately (hoist)
ret += sm + "\n"
default:
// Top-level function code - deter emission
statements = append(statements, sm)
}
}
// Emit deferred statements
if len(statements) > 0 {
ret += "func init() {\n"
2020-04-05 06:23:28 +00:00
ret += "\t" + strings.Join(statements, "\t") // Statements already added their own newline
2020-04-05 04:35:44 +00:00
ret += "}\n"
}
return ret, nil
2020-04-05 05:05:22 +00:00
case *node.Identifier:
return n.Value, nil
case Literal:
2020-04-05 06:23:28 +00:00
// We expect literal statements to act like a *Stmt, i.e. be emitted with a trailing NL
return n.Value + "\n", nil
2020-04-05 05:05:22 +00:00
2020-04-05 04:35:44 +00:00
//
// 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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{s, err}
}
2020-04-05 06:23:28 +00:00
ret += line // Statements already added a trailing newline
2020-04-05 04:35:44 +00:00
}
return ret + "}\n", nil
case *stmt.Class:
ret := ""
2020-04-05 07:11:05 +00:00
prevClassName := this.currentClassName // almost certainly empty-string
prevClassParentName := this.currentClassParentName
2020-04-05 04:35:44 +00:00
className := n.ClassName.(*node.Identifier).Value
2020-04-05 07:11:05 +00:00
this.currentClassName = className
2020-04-05 04:35:44 +00:00
memberVars := []string{}
memberFuncs := []string{}
if n.Extends != nil {
2020-04-05 07:11:05 +00:00
parentName, err := this.resolveName(n.Extends.ClassName)
if err != nil {
return "", parseErr{n, err}
}
memberVars = append(memberVars, parentName+" // parent")
2020-04-05 07:11:05 +00:00
this.currentClassParentName = parentName
} else {
this.currentClassParentName = ""
}
2020-04-05 04:35:44 +00:00
// 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.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}
}
2020-04-07 09:53:07 +00:00
// TODO implement abstract methods as method functions
2020-04-05 04:35:44 +00:00
// 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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{s, err}
}
2020-04-05 07:11:05 +00:00
memberFuncStmt := "func " + constructorName(className) + funcStmt + "\n"
2020-04-05 04:35:44 +00:00
memberFuncs = append(memberFuncs, memberFuncStmt)
} else {
2020-04-05 06:05:32 +00:00
// Check if this is a static method
hasStatic, err := hasModifier(s.Modifiers, `static`)
if err != nil {
return "", parseErr{s, err}
}
2020-04-05 04:35:44 +00:00
// Method body
funcStmt, err := this.convertFunctionCommon(s.Params, s.ReturnType, s.ReturnsRef, s.Stmt.(*stmt.StmtList).Stmts)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{s, err}
}
2020-04-05 06:05:32 +00:00
if hasStatic {
memberFuncs = append(memberFuncs, "func "+className+funcName+funcStmt+"\n")
} else {
memberFuncs = append(memberFuncs, "func (this *"+className+") "+funcName+funcStmt+"\n")
}
2020-04-05 04:35:44 +00:00
}
default:
return "", parseErr{s, fmt.Errorf("Class '%s' contained unexpected AST node; expected PropertyList / ClassMethod", className)}
}
}
// 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")
2020-04-07 09:53:00 +00:00
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"
}
}
2020-04-05 04:35:44 +00:00
// Done
2020-04-05 07:11:05 +00:00
// Reinstate parent state before returning
this.currentClassName = prevClassName
this.currentClassParentName = prevClassParentName
2020-04-05 04:35:44 +00:00
return ret, nil
2020-04-07 09:53:00 +00:00
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
2020-04-05 04:35:44 +00:00
case *stmt.Function:
2020-04-05 07:11:05 +00:00
// Top-level function definition
2020-04-05 04:35:44 +00:00
// TODO parse doc comment
// FIXME is this the same as a closure?
funcName := n.FunctionName.(*node.Identifier).Value
2020-04-05 07:11:05 +00:00
if funcName == `super` {
return "", parseErr{n, fmt.Errorf("Function name '%s' probably will not function correctly", funcName)}
}
2020-04-05 04:35:44 +00:00
// All top-level functions like this are public; ensure function name starts
// with an uppercase letter
funcName = toPublic(funcName)
// Convert body
funcStmt, err := this.convertFunctionCommon(n.Params, n.ReturnType, n.ReturnsRef, n.Stmts)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
ret := "func " + funcName + funcStmt + "\n"
return ret, nil
case *stmt.Return:
child, err := this.convert(n.Expr)
2020-04-05 04:35:44 +00:00
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
// If the expr is a string literal, we can convert it to errors.New()
// Although we probably can't do this in general for stringly-typed expressions
if str, ok := n.Expr.(*scalar.String); ok {
return "return nil, errors.New(" + str.Value + ")\n", nil
}
child, err := this.convert(n.Expr)
2020-04-05 04:35:44 +00:00
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 {
2020-04-05 04:35:44 +00:00
// 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"
}
2020-04-05 04:35:44 +00:00
}
2020-04-05 04:41:36 +00:00
if len(n.Cond) != 1 {
2020-04-05 04:35:44 +00:00
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])
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
2020-04-05 04:41:36 +00:00
if len(n.Loop) != 1 {
2020-04-05 04:35:44 +00:00
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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
body, err := this.convert(convertToStmtList(n.Stmt))
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return preinit + "for " + finit + "; " + fcond + "; " + floop + " " + body + "\n", nil
2020-04-05 04:35:44 +00:00
case *stmt.Foreach:
iterand, err := this.convert(n.Expr)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
valueReceiver, err := this.convert(n.Variable)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
keyReceiver := `_`
if n.Key != nil {
keyReceiver, err = this.convert(n.Key)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
}
body, err := this.convert(convertToStmtList(n.Stmt))
2020-04-05 04:35:44 +00:00
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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
body, err := this.convert(convertToStmtList(n.Stmt))
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return "for " + cond + " " + body + "\n", nil
case *stmt.Do:
cond, err := this.convert(n.Cond)
2020-04-05 04:35:44 +00:00
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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return "for " + cond + " " + body + "\n", nil
case *stmt.Expression:
2020-04-05 07:11:05 +00:00
// 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"
2020-04-05 07:11:05 +00:00
ret += "if err != nil {\n"
ret += "return err\n"
ret += "}\n"
ret += "this." + this.currentClassParentName + " = *super // copy by value\n"
2020-04-05 07:11:05 +00:00
return ret, nil
}
}
// Regular case
child, err := this.convert(n.Expr)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
args = append(args, exprGo)
}
return "fmt.Print(" + strings.Join(args, ", ") + ")\n", nil // newline - standalone statement
2020-04-05 05:11:11 +00:00
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)
}
return "fmt.Print(" + quoted + ")\n", nil // newline - standalone statement
2020-04-05 05:11:11 +00:00
2020-04-05 07:41:02 +00:00
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")}
}
2020-04-05 07:41:02 +00:00
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}
}
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 += " 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 += " else " + body
}
return ret, nil
2020-04-05 05:11:11 +00:00
case *stmt.Nop:
return "", nil
2020-04-05 04:35:44 +00:00
//
// assign
//
case *assign.Assign:
rvalue, err := this.convert(n.Expression)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
2020-04-05 05:39:22 +00:00
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)
2020-04-05 05:39:22 +00:00
if err != nil {
return "", parseErr{dimf, err}
}
return arrayVar + ` = append(` + arrayVar + `, ` + rvalue + `)`, nil
} else {
// Normal assignment
lvalue, err := this.convert(n.Variable) // might be a more complicated lvalue
2020-04-05 05:39:22 +00:00
if err != nil {
return "", parseErr{n, err}
}
// TODO this may need to use `:=`
return lvalue + " = " + rvalue, nil
}
2020-04-05 04:35:44 +00:00
//
// 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
2020-04-05 07:11:05 +00:00
funcName, err := this.resolveName(n.Function)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
2020-04-05 07:11:05 +00:00
if funcName == "super" {
return "", parseErr{n, fmt.Errorf("Unexpected use of parent constructor in rvalue context")}
}
callParams, err := this.convertFuncCallArgsCommon(n.ArgumentList)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return funcName + callParams, nil // expr only, no semicolon/newline
2020-04-05 06:23:52 +00:00
case *expr.StaticCall:
2020-04-05 07:11:05 +00:00
className, err := this.resolveName(n.Class)
2020-04-05 06:23:52 +00:00
if err != nil {
return "", parseErr{n, err}
}
funcName, err := this.convert(n.Call)
2020-04-05 06:23:52 +00:00
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)
2020-04-05 06:23:52 +00:00
if err != nil {
return "", parseErr{n, err}
}
return callTarget + callParams, nil // expr only, no semicolon/newline
2020-04-05 06:23:52 +00:00
2020-04-05 04:35:44 +00:00
case *expr.New:
// new foo(xx)
// Transparently convert to calling constructor function.
2020-04-05 07:11:05 +00:00
nn, err := this.resolveName(n.Class)
2020-04-05 04:35:44 +00:00
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))
2020-04-05 04:35:44 +00:00
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)
2020-04-05 04:35:44 +00:00
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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return v + "++", nil
case *expr.MethodCall:
// Foo->Bar(Baz)
parent, err := this.convert(n.Variable)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
child, err := this.convert(n.Method)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
args, err := this.convertFuncCallArgsCommon(n.ArgumentList)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return parent + "." + child + args, nil
case *expr.PropertyFetch:
// Foo->Bar
parent, err := this.convert(n.Variable)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
child, err := this.convert(n.Property)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{n, err}
}
return parent + "." + child, nil
case *expr.Variable:
return n.VarName.(*node.Identifier).Value, nil
case *expr.ConstFetch:
2020-04-05 07:11:05 +00:00
return this.resolveName(n.Constant)
2020-04-05 04:35:44 +00:00
2020-04-05 05:39:22 +00:00
case *expr.Array:
return this.convertArrayLiteralCommon(n.Items)
2020-04-05 05:39:22 +00:00
case *expr.ShortArray:
return this.convertArrayLiteralCommon(n.Items)
2020-04-05 05:39:22 +00:00
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)
2020-04-05 05:39:22 +00:00
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)
2020-04-05 05:39:22 +00:00
if err != nil {
return "", parseErr{n, err}
}
return vv + `[` + idx + `]`, nil // Same syntax as PHP
2020-04-05 04:35:44 +00:00
//
// binary
//
case *binary.BitwiseAnd:
return this.convertBinaryCommon(n.Left, n.Right, `&`)
2020-04-05 04:35:44 +00:00
case *binary.BitwiseOr:
return this.convertBinaryCommon(n.Left, n.Right, `|`)
2020-04-05 04:35:44 +00:00
case *binary.BitwiseXor:
return this.convertBinaryCommon(n.Left, n.Right, `^`) // n.b. Go only supports this for integers; PHP also supports it for bools
2020-04-05 04:35:44 +00:00
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
2020-04-05 04:35:44 +00:00
case *binary.GreaterOrEqual:
return this.convertBinaryCommon(n.Left, n.Right, `>=`)
2020-04-05 04:35:44 +00:00
case *binary.Greater:
return this.convertBinaryCommon(n.Left, n.Right, `>`)
2020-04-05 04:35:44 +00:00
case *binary.Identical:
return this.convertBinaryCommon(n.Left, n.Right, `==`) // PHP uses `===`, Go is already type-safe
2020-04-05 04:35:44 +00:00
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}
}
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}
}
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, `-`)
2020-04-05 04:35:44 +00:00
//
// 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
2020-04-05 04:35:44 +00:00
//
//
//
default:
return "", fmt.Errorf("unsupported node type %s", nodeTypeString(n))
}
}
2020-04-05 06:05:32 +00:00
func hasModifier(modifiers []node.Node, search string) (bool, error) {
2020-04-05 04:35:44 +00:00
for _, mod := range modifiers {
ident, ok := mod.(*node.Identifier)
if !ok {
2020-04-05 06:05:32 +00:00
return false, parseErr{mod, fmt.Errorf("expected node.Identifier")}
2020-04-05 04:35:44 +00:00
}
2020-04-05 06:05:32 +00:00
if strings.ToLower(ident.Value) == strings.ToLower(search) {
return true, nil
2020-04-05 04:35:44 +00:00
}
2020-04-05 06:05:32 +00:00
}
return false, nil
}
2020-04-05 04:35:44 +00:00
2020-04-05 06:05:32 +00:00
// 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
2020-04-05 04:35:44 +00:00
}
2020-04-05 06:05:32 +00:00
hasProtected, err := hasModifier(modifiers, "protected")
if err != nil {
return "", err
}
if (hasPublic && !hasPrivate && !hasProtected) /* explicitly public */ ||
(!hasPublic && !hasPrivate && !hasProtected) /* no modifiers defaults to public */ {
2020-04-05 04:35:44 +00:00
return toPublic(funcName), nil
2020-04-05 06:05:32 +00:00
} else if !hasPublic && (hasPrivate || hasProtected) {
2020-04-05 04:35:44 +00:00
return toPrivate(funcName), nil
2020-04-05 06:05:32 +00:00
} else {
return "", fmt.Errorf("unexpected combination of modifiers")
2020-04-05 04:35:44 +00:00
}
}
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:]
}
2020-04-05 07:11:05 +00:00
func constructorName(className string) string {
return `New` + className
}
2020-04-05 04:35:44 +00:00
// resolveName turns a `*name.Name` node into a Go string.
2020-04-05 07:11:05 +00:00
func (this *conversionState) resolveName(n node.Node) (string, error) {
2020-04-05 04:35:44 +00:00
// TODO support namespace lookups
paramType := unknownVarType
if vt, ok := n.(*name.Name); ok {
if len(vt.Parts) != 1 {
return "", parseErr{n, fmt.Errorf("name has %d parts, expected 1", len(vt.Parts))}
}
paramType = vt.Parts[0].(*name.NamePart).Value
}
2020-04-05 07:11:05 +00:00
// Handle class lookups
if strings.ToLower(paramType) == "parent" {
if this.currentClassParentName == "" {
2020-04-05 07:11:05 +00:00
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(paramType) == "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(paramType) == "static" {
return "", parseErr{n, fmt.Errorf("'static::' is not yet supported")}
2020-04-05 07:11:05 +00:00
}
2020-04-05 04:35:44 +00:00
return paramType, nil
}
// 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) {
2020-04-05 04:35:44 +00:00
// PHP uses + for numbers, `.` for strings; Go uses `+` in both cases
// Assume PHP/Go have the same associativity here
lhs, err := this.convert(left)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{left, err}
}
rhs, err := this.convert(right)
2020-04-05 04:35:44 +00:00
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 + ")"
}
2020-04-05 04:35:44 +00:00
return "(" + lhs + " " + goBinaryOperator + " " + rhs + ")", nil
}
func (this *conversionState) convertFuncCallArgsCommon(args *node.ArgumentList) (string, error) {
2020-04-05 04:35:44 +00:00
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)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{arg, err}
}
if arg.IsReference {
rvalue = "&" + rvalue
}
if arg.Variadic {
rvalue = "..." + rvalue
}
callParams = append(callParams, rvalue)
}
return "(" + strings.Join(callParams, `, `) + ")", nil // expr only, no semicolon/newline
}
func (this *conversionState) convertArrayLiteralCommon(items []node.Node) (string, error) {
2020-04-05 05:39:22 +00:00
// 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)
2020-04-05 05:39:22 +00:00
if err != nil {
return "", parseErr{itm, err}
}
if itm.Key != nil {
kv, err := this.convert(itm.Key)
2020-04-05 05:39:22 +00:00
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) {
2020-04-05 04:35:44 +00:00
// 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
2020-04-05 07:11:05 +00:00
paramType, err := this.resolveName(param.VariableType)
2020-04-05 04:35:44 +00:00
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
2020-04-05 07:11:05 +00:00
funcReturn, err := this.resolveName(returnType)
2020-04-05 04:35:44 +00:00
if err != nil {
return "", parseErr{returnType, err}
}
if returnsRef {
funcReturn = "*" + funcReturn
}
// Build function prototype
2020-04-05 06:23:28 +00:00
ret := "(" + strings.Join(funcParams, ", ") + ") (" + funcReturn + ", error) "
2020-04-05 04:35:44 +00:00
// Recurse through body statements
2020-04-07 09:53:00 +00:00
if bodyStmts != nil {
fullBody, err := this.convert(stmt.NewStmtList(bodyStmts))
if err != nil {
return "", err
}
2020-04-05 04:35:44 +00:00
2020-04-07 09:53:00 +00:00
ret += fullBody + "\n"
}
2020-04-05 06:23:28 +00:00
2020-04-05 04:35:44 +00:00
// 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
}