2306 lines
65 KiB
Go
2306 lines
65 KiB
Go
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"
|
|
|
|
"php2go/parseutil"
|
|
)
|
|
|
|
func nodeTypeString(n node.Node) string {
|
|
return reflect.TypeOf(n).String()
|
|
}
|
|
|
|
type parseErr struct {
|
|
n node.Node
|
|
childErr error
|
|
}
|
|
|
|
func (pe parseErr) Error() string {
|
|
positionStr := ""
|
|
if posn := pe.n.GetPosition(); posn != nil {
|
|
positionStr = fmt.Sprintf(" on line %d", posn.StartLine)
|
|
}
|
|
|
|
return fmt.Sprintf("Parsing %s%s: %s", nodeTypeString(pe.n), positionStr, pe.childErr.Error())
|
|
}
|
|
|
|
func (pe parseErr) Unwrap() error {
|
|
return pe.childErr
|
|
}
|
|
|
|
//
|
|
|
|
type arityErr struct {
|
|
funcName string
|
|
got int
|
|
expected, expectedUpperRange int
|
|
}
|
|
|
|
func (ae arityErr) Error() string {
|
|
if ae.expected != ae.expectedUpperRange {
|
|
return fmt.Sprintf("Call to '%s' expected %d to %d argument(s), got %d", ae.funcName, ae.expected, ae.expectedUpperRange, ae.got)
|
|
} else {
|
|
return fmt.Sprintf("Call to '%s' expected %d argument(s), got %d", ae.funcName, ae.expected, ae.got)
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
type ParseContext int
|
|
|
|
const (
|
|
TopLevelContext ParseContext = iota // We are free to inject entire additional statements
|
|
RvalueFreeContext // We are free to make `x, ok := foo(); ok` transformations e.g. for array_key_exists()/isset()
|
|
RvalueRestrictedContext // We are not free to make semicolon transformations
|
|
)
|
|
|
|
//
|
|
|
|
type conversionState struct {
|
|
currentClassName string
|
|
currentClassParentName string
|
|
currentErrHandler string
|
|
currentFunctionName string
|
|
currentMethodName string
|
|
currentNamespace string
|
|
currentTraitName string // TODO
|
|
importPackages map[string]struct{}
|
|
}
|
|
|
|
//
|
|
|
|
func normaliseCommentStrFragment(s string) string {
|
|
if len(s) == 0 {
|
|
return s
|
|
}
|
|
|
|
lines := strings.Split(s, "\n")
|
|
for i, _ := range lines {
|
|
lines[i] = strings.TrimLeft(lines[i], "\t\r\n ")
|
|
if strings.HasPrefix(lines[i], `//`) {
|
|
lines[i] = lines[i][2:]
|
|
}
|
|
}
|
|
return `/*` + strings.TrimRight(strings.Join(lines, "\n"), "\n") + ` */`
|
|
}
|
|
|
|
func normaliseCommentStr(s string) string {
|
|
|
|
// A comment may be injected at any point in the source tree
|
|
// It's not necessarily valid to use line comments (`//`) mid-AST
|
|
// Avoid the problem by unconditionally transforming to range comments (`/**/`)
|
|
|
|
extents := strings.Split(s, `/*`)
|
|
ret := make([]string, 0, len(extents)*2)
|
|
for _, extent := range extents {
|
|
parts := strings.SplitN(extent, `*/`, 2) // any further */ after this has no effect
|
|
if len(parts) == 1 {
|
|
// No range comment, only line comment/whitespace
|
|
// Transform single range only
|
|
ret = append(ret, normaliseCommentStrFragment(parts[0]))
|
|
|
|
} else { // len(parts) == 2
|
|
// Range comment part is OK
|
|
// Only second middle part needs normalising
|
|
ret = append(ret, `/*`+parts[0]+`*/`, normaliseCommentStrFragment(parts[1]))
|
|
|
|
}
|
|
}
|
|
|
|
return strings.Join(ret, "")
|
|
}
|
|
|
|
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 <?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
|
|
}
|
|
|
|
//if len(freePrefix) > 0 && !(strings.HasSuffix(freePrefix, "\n") || strings.HasSuffix(freePrefix, `*/`)) {
|
|
// freePrefix += "\n"
|
|
//}
|
|
|
|
freePrefix = normaliseCommentStr(freePrefix)
|
|
freeSuffix = normaliseCommentStr(freeSuffix)
|
|
|
|
return freePrefix + ret + freeSuffix, nil
|
|
}
|
|
|
|
func (this *conversionState) convertNoFreeFloating(n_ node.Node) (string, error) {
|
|
|
|
switch n := n_.(type) {
|
|
|
|
//
|
|
// node
|
|
//
|
|
|
|
case *node.Root:
|
|
|
|
// Hoist all declarations first, and put any top-level code into a generated main() function
|
|
declarations := []string{}
|
|
statements := []string{}
|
|
packageName := `main`
|
|
|
|
for _, s := range n.Stmts {
|
|
|
|
switch s := s.(type) {
|
|
case *stmt.Class, *stmt.Function, *stmt.Interface:
|
|
this.currentErrHandler = "return nil, err\n"
|
|
|
|
sm, err := this.convert(s)
|
|
if err != nil {
|
|
return "", parseErr{s, err}
|
|
}
|
|
|
|
// Declaration - emit immediately (hoist)
|
|
declarations = append(declarations, sm)
|
|
|
|
case *stmt.Namespace:
|
|
if len(s.Stmts) > 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" + quoteGoString(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.AsGoString())
|
|
|
|
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) + "(" + strings.Join(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, arityErr{"define", len(fncall.ArgumentList.Arguments), 2, 2}}
|
|
}
|
|
|
|
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.convertAssignAssign(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
|
|
|
|
this.importPackages["fmt"] = struct{}{}
|
|
return "fmt.Print(" + quoteGoString(n.Value) + ")\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.Switch:
|
|
cond, err := this.convert(n.Cond)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
cases := make([]string, 0, len(n.CaseList.Cases))
|
|
for _, ncase := range n.CaseList.Cases {
|
|
switch ncase.(type) {
|
|
case *stmt.Default, *stmt.Case:
|
|
caseStr, err := this.convert(ncase)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
cases = append(cases, caseStr)
|
|
|
|
default:
|
|
return "", parseErr{ncase, fmt.Errorf("expected Case/Default")}
|
|
}
|
|
}
|
|
|
|
// TODO use dead code elimination after `break` to clean up redundant fallthrough statements
|
|
return "switch " + cond + " {\n" + strings.Join(cases, "\n") + "}\n", nil
|
|
|
|
case *stmt.Case:
|
|
// Add a `fallthrough` to the end
|
|
// TODO then use dead code elimination to clean them up
|
|
stmtBody, err := this.convert(stmt.NewStmtList(n.Stmts))
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
cond, err := this.convert(n.Cond)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
return "case " + cond + ":\n" + stmtBody + "\nfallthrough\n", nil
|
|
|
|
case *stmt.Default:
|
|
// Add a `fallthrough` to the end
|
|
// TODO then use dead code elimination to clean them up
|
|
stmtBody, err := this.convert(stmt.NewStmtList(n.Stmts))
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
return "default: " + stmtBody + "\nfallthrough\n", nil
|
|
|
|
case *stmt.Break:
|
|
if n.Expr != nil {
|
|
return "", parseErr{n, fmt.Errorf("break {expr} is not yet supported")} // TODO implement numbered break
|
|
}
|
|
|
|
return "break\n", nil
|
|
|
|
case *stmt.Continue:
|
|
if n.Expr != nil {
|
|
return "", parseErr{n, fmt.Errorf("continue {expr} is not yet supported")} // TODO implement numbered continue
|
|
}
|
|
|
|
return "continue\n", 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.convertAssignAssign(n, false /* no reason to believe we are a top-level statement */)
|
|
|
|
// Go has a limited set of assignment shorthands:
|
|
//
|
|
// ```
|
|
// assign_op = [ add_op | mul_op ] "=" .
|
|
//
|
|
// [...]
|
|
//
|
|
// add_op = "+" | "-" | "|" | "^" .
|
|
// mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .
|
|
// ```
|
|
|
|
case *assign.BitwiseAnd:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `&=`)
|
|
|
|
case *assign.BitwiseOr:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `|=`)
|
|
|
|
case *assign.BitwiseXor:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `^=`)
|
|
|
|
case *assign.Coalesce:
|
|
// Null coaslecing assignment expression - $x ??= $y;
|
|
// We don't have a natural equivalent for the coalescing operator
|
|
// We don't have an implementation of *binary.Coalesce yet either
|
|
// But when we do this will need an rvalue transformation to use it
|
|
|
|
return this.convert(assign.NewAssign(n.Variable, binary.NewCoalesce(n.Variable, n.Expression))) // FIXME also handle position/freefloating
|
|
|
|
case *assign.Concat:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `+=`)
|
|
|
|
case *assign.Div:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `/=`)
|
|
|
|
case *assign.Minus:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `-=`)
|
|
|
|
//case *assign.Mod:
|
|
// TODO
|
|
|
|
case *assign.Mul:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `*=`)
|
|
|
|
case *assign.Plus:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `+=`)
|
|
|
|
//case *assign.Pow:
|
|
// TODO
|
|
|
|
//case *assign.Reference:
|
|
|
|
case *assign.ShiftLeft:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `<<=`)
|
|
|
|
case *assign.ShiftRight:
|
|
return this.convertAssignment(n, n.Variable, n.Expression, false, `>>=`)
|
|
|
|
//
|
|
// name
|
|
//
|
|
|
|
case *name.Name:
|
|
return this.resolveName(n)
|
|
|
|
//
|
|
// 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
|
|
|
|
// We might be calling by name, or, we might be calling by an rvalue expression
|
|
// e.g. foo()()() or $x()
|
|
// Use full conversion logic for name resolution
|
|
funcName, err := this.convert(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`
|
|
|
|
// Transform some PHP standard library functions to their Go equivalents
|
|
// TODO check namespacing for collision prevention
|
|
if funcName == `strlen` || funcName == `count` || funcName == `sizeof` {
|
|
funcName = `len`
|
|
|
|
} else if funcName == `explode` {
|
|
if len(callParams) != 2 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 2, 2}}
|
|
}
|
|
this.importPackages["strings"] = struct{}{}
|
|
funcName = `strings.Split`
|
|
callParams[0], callParams[1] = callParams[1], callParams[0] // need to reverse function argument order
|
|
|
|
} else if funcName == `implode` {
|
|
if len(callParams) != 2 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 2, 2}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
funcName = `strings.Join`
|
|
callParams[0], callParams[1] = callParams[1], callParams[0] // need to reverse function argument order
|
|
|
|
} else if funcName == `sprintf` {
|
|
this.importPackages["fmt"] = struct{}{}
|
|
funcName = `fmt.Sprintf`
|
|
|
|
} else if funcName == `trim` {
|
|
if len(callParams) != 1 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 2, 2}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
funcName = `strings.TrimSpace`
|
|
|
|
} else if funcName == `rtrim` {
|
|
if len(callParams) != 1 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 1, 1}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
this.importPackages["unicode"] = struct{}{}
|
|
funcName = `strings.TrimRightFunc`
|
|
callParams = append(callParams, `unicode.IsSpace`)
|
|
|
|
} else if funcName == `ltrim` {
|
|
if len(callParams) != 1 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 1, 1}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
this.importPackages["unicode"] = struct{}{}
|
|
funcName = `strings.TrimLeftFunc`
|
|
callParams = append(callParams, `unicode.IsSpace`)
|
|
|
|
} else if funcName == `strtolower` {
|
|
if len(callParams) != 1 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 1, 1}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
funcName = `strings.ToLower`
|
|
|
|
} else if funcName == `strtoupper` {
|
|
if len(callParams) != 1 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 1, 1}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
funcName = `strings.ToUpper`
|
|
|
|
} else if funcName == `file_get_contents` {
|
|
if len(callParams) != 2 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 1, 1}}
|
|
}
|
|
|
|
this.importPackages["io/ioutil"] = struct{}{}
|
|
funcName = `ioutil.ReadFile` // (string filename)
|
|
|
|
} else if funcName == `file_put_contents` {
|
|
if len(callParams) != 2 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 2, 2}}
|
|
}
|
|
|
|
this.importPackages["io/ioutil"] = struct{}{}
|
|
funcName = `ioutil.WriteFile` // (string filename, []byte data, int perm)
|
|
|
|
callParams[1] = `[]byte(` + callParams[1] + `)`
|
|
callParams = append(callParams, `0644`) // default file permissions
|
|
|
|
} else if funcName == `str_replace` {
|
|
// str_replace( mixed $search , mixed $replace , mixed $subject [, int &$count ] )
|
|
// n.b. str_replace also supports arrays, that strings.Replace() doesn't do
|
|
if !(len(callParams) == 3 || len(callParams) == 4) {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 3, 4}}
|
|
}
|
|
|
|
this.importPackages["strings"] = struct{}{}
|
|
funcName = `strings.Replace` // (s, old, new string, n int) string
|
|
callParams[0], callParams[1], callParams[2] = callParams[2], callParams[0], callParams[1]
|
|
|
|
if len(callParams) == 3 {
|
|
callParams = append(callParams, `-1`) // replaceAll
|
|
// There is a strings.ReplaceAll() added in go1.12 (2018), which is still pretty recent i.e. not in Debian Stable (buster)
|
|
}
|
|
|
|
} else if funcName == `extract` {
|
|
return "", parseErr{n, fmt.Errorf("Unsupported dynamic function `\\extract()`")}
|
|
|
|
} else if funcName == `var_dump` {
|
|
// This is mostly called for debugging purposes. Printf should be good enough
|
|
this.importPackages["fmt"] = struct{}{}
|
|
callParams = append([]string{`"%#v\n"`}, callParams...) // Insert extra 1st parameter
|
|
funcName = `fmt.Printf`
|
|
|
|
} else if funcName == `max` {
|
|
this.importPackages["math"] = struct{}{}
|
|
funcName = `math.Max` // n.b. only works on 2 integers, not as wide as PHP's version
|
|
|
|
} else if funcName == `min` {
|
|
this.importPackages["math"] = struct{}{}
|
|
funcName = `math.Max` // n.b. only works on 2 integers, not as wide as PHP's version
|
|
|
|
} else if funcName == `floor` {
|
|
this.importPackages["math"] = struct{}{}
|
|
funcName = `math.Floor`
|
|
|
|
} else if funcName == `var_export` || funcName == `print_r` {
|
|
|
|
if len(callParams) == 1 {
|
|
this.importPackages["fmt"] = struct{}{}
|
|
callParams = append([]string{`"%#v\n"`}, callParams...) // Insert extra 1st parameter
|
|
funcName = `fmt.Printf`
|
|
|
|
} else if len(callParams) == 2 {
|
|
// Returning form
|
|
this.importPackages["fmt"] = struct{}{}
|
|
callParams = append([]string{`"%#v\n"`}, callParams...) // Insert extra 1st parameter
|
|
funcName = `fmt.Sprintf`
|
|
|
|
} else {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 3, 4}}
|
|
|
|
}
|
|
|
|
} else if funcName == `substr` {
|
|
// substr($string, $start [, $length]) => new string
|
|
// In Go all strings are immutable, and slicing an existing string produces a new immutable string so that works out
|
|
|
|
if len(callParams) == 2 {
|
|
return "(" + callParams[0] + ")[" + callParams[1] + ":]", nil
|
|
|
|
} else if len(callParams) == 3 {
|
|
// if lnum, ok := n.ArgumentList.Arguments[1].
|
|
|
|
if callParams[1] == `0` {
|
|
return "(" + callParams[0] + ")[:" + callParams[2] + "]", nil
|
|
}
|
|
|
|
// WARNING: We are doubling up on the first parameter
|
|
// This is a problem if it's not necessarily constant
|
|
return "(" + callParams[0] + ")[" + callParams[1] + ":" + callParams[1] + `+` + callParams[2] + "]", nil
|
|
|
|
} else {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 2, 3}}
|
|
}
|
|
|
|
} else if funcName == `rename` {
|
|
if len(callParams) != 2 {
|
|
return "", parseErr{n, arityErr{funcName, len(callParams), 2, 2}}
|
|
}
|
|
|
|
this.importPackages["os"] = struct{}{}
|
|
funcName = `os.Rename`
|
|
|
|
}
|
|
|
|
return funcName + "(" + strings.Join(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 + "(" + strings.Join(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
|
|
|
|
// Special case: `::class` is just the string name of the class
|
|
if constName == `class` {
|
|
// This can be known statically (e.g. MyClass::class --> "MyClass") but
|
|
// isn't generally known non-statically
|
|
if _, ok := n.Class.(*name.Name); ok {
|
|
// Static
|
|
return quoteGoString(className), nil
|
|
|
|
} else if className == `self` {
|
|
return quoteGoString(this.currentClassName), nil
|
|
|
|
} else {
|
|
// Dynamic
|
|
// Translate to reflect
|
|
// this.importPackages["reflect"] = struct{}{}
|
|
// return `reflect.TypeOf(` + className + `).String()`, nil
|
|
|
|
// Actually PHP doesn't support using ::class on variables
|
|
return "", parseErr{n, fmt.Errorf(`PHP Fatal error: Dynamic class names are not allowed in compile-time ::class fetch`)}
|
|
}
|
|
}
|
|
|
|
return className + constName, nil
|
|
|
|
case *expr.Closure:
|
|
// Need to decide what to do with `this` binding
|
|
// TODO figure out what it means to use `this` in all types of nested closures
|
|
|
|
if n.Static {
|
|
return "", parseErr{n, fmt.Errorf("use of `static` in closure implies `this`-insensitive, but we can't make that true")}
|
|
}
|
|
|
|
// TODO n.PhpDocComment
|
|
// TODO n.ClosureUse
|
|
|
|
body, err := this.convertFunctionCommon(n.Params, n.ReturnType, n.ReturnsRef, n.Stmts)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
return "(func" + strings.TrimRight(body, " \n") + ")", 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.Eval:
|
|
return "", parseErr{n, fmt.Errorf("Unsupported use of eval()")}
|
|
|
|
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.UnaryMinus:
|
|
v, err := this.convert(n.Expr)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
return "-(" + v + ")", nil
|
|
|
|
case *expr.UnaryPlus:
|
|
v, err := this.convert(n.Expr)
|
|
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 + "(" + strings.Join(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:
|
|
constRef, err := this.resolveName(n.Constant)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Handle PHP reserved constants
|
|
// @ref https://www.php.net/manual/en/reserved.constants.php
|
|
|
|
if constRef == `PHP_EOL` {
|
|
// Not 100% accurate, but realistically this works on Windows too nowadays
|
|
return `"\n"`, nil
|
|
|
|
} else if constRef == `PHP_INT_MAX` {
|
|
// Go has math.MaxInt64 in the Math package, etc
|
|
// But we don't know if this package will maybe be built on a 32-bit platform
|
|
// And we are generating plain `int` types, not necessarily int64 everywhere
|
|
|
|
// @ref https://stackoverflow.com/a/54421330
|
|
this.importPackages["math/bits"] = struct{}{}
|
|
return `((1 << bits.UintSize) / -2 /* INT_MAX */)`, nil
|
|
|
|
} else if constRef == `PHP_VERSION` {
|
|
return quoteGoString(fmt.Sprintf("%d.%d.%d%s", phpVersionMajor, phpVersionMinor, phpVersionPatch, phpVersionExtra)), nil
|
|
|
|
} else if constRef == `PHP_MAJOR_VERSION` {
|
|
return fmt.Sprintf("%d", phpVersionMajor), nil
|
|
|
|
} else if constRef == `PHP_MINOR_VERSION` {
|
|
return fmt.Sprintf("%d", phpVersionMinor), nil
|
|
|
|
} else if constRef == `PHP_RELEASE_VERSION` {
|
|
return fmt.Sprintf("%d", phpVersionPatch), nil
|
|
|
|
} else if constRef == `PHP_VERSION_ID` {
|
|
return fmt.Sprintf("%d%02d%02d", phpVersionMajor, phpVersionMinor, phpVersionPatch), nil
|
|
|
|
} else if constRef == `PHP_EXTRA_VERSION` {
|
|
return quoteGoString(phpVersionExtra), nil
|
|
|
|
} else if constRef == `PHP_ZTS` {
|
|
return "true /* PHP_ZTS */", nil
|
|
|
|
} else if constRef == `PHP_DEBUG` {
|
|
return "false /* PHP_DEBUG */", nil
|
|
|
|
} else if constRef == `PHP_MAXPATHLEN` {
|
|
// Not 100% accurate
|
|
// Go filesystem functions can transparently handle long paths
|
|
// PHP on Linux has a max path of 4096 by default
|
|
// Just use that
|
|
return "4096 /* MAXPATHLEN */", nil
|
|
|
|
} else if constRef == `PHP_OS` || constRef == `PHP_OS_FAMILY` {
|
|
// Not 100% accurate, but the right idea
|
|
this.importPackages["runtime"] = struct{}{}
|
|
return `runtime.GOOS`, nil
|
|
|
|
}
|
|
|
|
// No const transformation was required
|
|
return constRef, nil
|
|
|
|
case *expr.Array:
|
|
return this.convertArrayLiteralCommon(n.Items)
|
|
|
|
case *expr.ShortArray:
|
|
return this.convertArrayLiteralCommon(n.Items)
|
|
|
|
case *expr.BooleanNot:
|
|
rhs, err := this.convert(n.Expr)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
return "!(" + rhs + ")", nil
|
|
|
|
case *expr.Ternary:
|
|
cond, err := this.convert(n.Condition)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
iftrue, err := this.convert(n.IfTrue)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
iffalse, err := this.convert(n.IfFalse)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
// FIXME this is (A) not idiomatic, and (B) doesn't work in assignment expressions
|
|
return "(func() unknown {\nif (" + cond + ") {\nreturn (" + iftrue + ")\n} else {\nreturn (" + iffalse + ")\n } })()", nil
|
|
|
|
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:
|
|
// Integer (maybe with 0b / 0x / '0' prefix)
|
|
return n.Value, nil // number formats are compatible
|
|
|
|
case *scalar.Dnumber:
|
|
// Float (maybe with period / scientific notation)
|
|
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 quoteGoString(rawValue), nil // Go source code quoting format
|
|
|
|
case *scalar.EncapsedStringPart:
|
|
// The n.Value that we receive here does not contain quotes at all
|
|
// But it is still a fragment of a double-quoted string
|
|
rawValue, err := phpUnquote(`"` + strings.Replace(n.Value, `"`, `\"`, -1) + `"`)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
return quoteGoString(rawValue), nil // Go source code quoting format
|
|
|
|
case *scalar.Encapsed:
|
|
return this.convertEncapsedString(n.Parts)
|
|
|
|
case *scalar.Heredoc:
|
|
return this.convertEncapsedString(n.Parts)
|
|
|
|
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] == ')' && !strings.HasSuffix(expr, `()`) {
|
|
expr = expr[1 : len(expr)-1]
|
|
}
|
|
return expr
|
|
}
|
|
|
|
// asSingleExpression makes the expr into a single expression that is safe for
|
|
// embedding in a larger statement e.g. multiple string concatenation.
|
|
func asSingleExpression(expr string) string {
|
|
return `(` + removeParens(expr) + `)`
|
|
// TODO simplify this in cases where we can tell it is already a single expression e.g. quoted strings, number literals, variable literals, ...
|
|
}
|
|
|
|
func quoteGoString(s string) string {
|
|
|
|
if !strings.Contains(s, "`") && strings.Count(s, "\n") >= 3 { // TODO make the heuristic configurable
|
|
// Use backtick-delimited multiline string
|
|
return "`" + s + "`"
|
|
} else {
|
|
// Can't trivially represent it with backticks, or it's not multiline "enough" to bother - use full Go quoting
|
|
return strconv.Quote(s)
|
|
}
|
|
}
|
|
|
|
func (this *conversionState) convertEncapsedString(parts []node.Node) (string, error) {
|
|
// This is used for string interpolation: `echo "bar $ref";`
|
|
// And also for heredocs
|
|
// Child nodes are either EncapsedStringPart or expr.**
|
|
ret := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
pp, err := this.convert(part)
|
|
if err != nil {
|
|
return "", parseErr{part, err}
|
|
}
|
|
|
|
if _, ok := part.(*scalar.EncapsedStringPart); !ok {
|
|
// Subexpression - should probably parenthesise just in case
|
|
pp = asSingleExpression(pp)
|
|
}
|
|
|
|
ret = append(ret, pp)
|
|
}
|
|
|
|
return `(` + strings.Join(ret, ` + `) + `)`, nil
|
|
}
|
|
|
|
// 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.AsGoString()
|
|
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
|
|
|
|
case *node.Identifier:
|
|
ret = n.Value // e.g. `array`
|
|
|
|
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) convertAssignAssign(n *assign.Assign, isTopLevelStatement bool) (string, error) {
|
|
return this.convertAssignment(n, n.Variable, n.Expression, isTopLevelStatement, `=`)
|
|
}
|
|
|
|
func (this *conversionState) convertAssignment(n, nLValue, nRValue node.Node, isTopLevelStatement bool, operator string) (string, error) {
|
|
|
|
rvalue, err := this.convert(nRValue)
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
if dimf, ok := nLValue.(*expr.ArrayDimFetch); ok && dimf.Dim == nil {
|
|
|
|
if operator != `=` {
|
|
return "", parseErr{n, fmt.Errorf("can't yet handle complex shorthand append assignment operator statement")}
|
|
}
|
|
|
|
// 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 + `, ` + removeParens(rvalue) + `)`
|
|
if isTopLevelStatement {
|
|
ret += "\n"
|
|
}
|
|
return ret, nil
|
|
|
|
} else {
|
|
|
|
// Normal assignment
|
|
|
|
lvalue, err := this.convert(nLValue) // might be a more complicated lvalue
|
|
if err != nil {
|
|
return "", parseErr{n, err}
|
|
}
|
|
|
|
// TODO this may need to use `:=`
|
|
|
|
if isTopLevelStatement {
|
|
rvalue = removeParens(rvalue) // safe in top-level assignment context
|
|
|
|
// If this is a simple expression (func call; indirect/method call; assignment of func/method call) then
|
|
// we need to propagate errors
|
|
switch nRValue.(type) {
|
|
case *expr.FunctionCall, *expr.StaticCall, *expr.New:
|
|
|
|
if operator != `=` {
|
|
// We need to perform the variable assignment + err check, but we are in shorthand assignment context
|
|
// and can't just mess with the left hand of the expression
|
|
// Need to create a temporary accumulator variable
|
|
return "", parseErr{n, fmt.Errorf("can't yet handle err value extraction for function call in shorthand append assignment operator statement")}
|
|
}
|
|
|
|
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 + " " + operator + " " + 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 nil, parseErr{arg_, fmt.Errorf("expected node.Argument")}
|
|
}
|
|
|
|
rvalue, err := this.convert(arg.Expr)
|
|
if err != nil {
|
|
return nil, parseErr{arg, err}
|
|
}
|
|
if arg.IsReference {
|
|
rvalue = "&" + rvalue
|
|
}
|
|
if arg.Variadic {
|
|
rvalue = "..." + rvalue
|
|
}
|
|
|
|
callParams = append(callParams, removeParens(rvalue))
|
|
}
|
|
|
|
return callParams, nil
|
|
}
|
|
|
|
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
|
|
|
|
// TODO support unpack operator (...)
|
|
|
|
nChars := 0
|
|
|
|
isMapType := false
|
|
for idx, itm_ := range items {
|
|
itm, ok := itm_.(*expr.ArrayItem)
|
|
if !ok {
|
|
return "", parseErr{itm_, fmt.Errorf("expected ArrayItem")}
|
|
}
|
|
|
|
// If there is a trailing comma in the definition,
|
|
// the parser produces an empty ArrayItem with no detail information
|
|
if itm.Key == nil && itm.Val == nil && idx == len(items)-1 {
|
|
break
|
|
}
|
|
|
|
if idx == 0 {
|
|
isMapType = (itm.Key != nil)
|
|
} else {
|
|
thisElemIsMapType := (itm.Key != nil)
|
|
if isMapType != thisElemIsMapType {
|
|
return "", parseErr{itm, fmt.Errorf("Can't represent array and map in a single type (idx[0].isMap=%v != idx[%d].isMap=%v)", isMapType, idx, thisElemIsMapType)}
|
|
}
|
|
}
|
|
|
|
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+`,`)
|
|
|
|
nChars += len(kv) + len(vv) + 4 // assume an extra trailing space
|
|
} else {
|
|
entries = append(entries, vv+`,`)
|
|
nChars += len(vv) + 2
|
|
}
|
|
|
|
}
|
|
|
|
// Heuristic decision whether to break array literal onto multiple lines
|
|
// We have to do this because gofmt does not care about long lines otherwise
|
|
maybeNewline := " "
|
|
if nChars > 60 {
|
|
maybeNewline = "\n"
|
|
}
|
|
|
|
if isMapType {
|
|
return `map[` + keyType.AsGoString() + `]` + valType.AsGoString() + `{` + maybeNewline + strings.Join(entries, maybeNewline) + maybeNewline + `}`, nil
|
|
} else {
|
|
return `[]` + valType.AsGoString() + `{` + maybeNewline + strings.Join(entries, maybeNewline) + maybeNewline + `}`, 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 = parseutil.SimpleWalk(n, func(n node.Node) error {
|
|
if _, ok := n.(*assign.Assign); ok {
|
|
hasAnyAssign = true
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return // named return
|
|
}
|