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"
2020-04-15 07:06:09 +00:00
"php2go/parseutil"
2020-04-05 04:35:44 +00:00
)
func nodeTypeString ( n node . Node ) string {
return reflect . TypeOf ( n ) . String ( )
}
type parseErr struct {
n node . Node
childErr error
}
func ( pe parseErr ) Error ( ) string {
2020-04-10 04:20:38 +00:00
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 ( ) )
2020-04-05 04:35:44 +00:00
}
func ( pe parseErr ) Unwrap ( ) error {
return pe . childErr
}
//
2020-04-15 07:08:18 +00:00
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
)
//
2020-04-05 06:45:14 +00:00
type conversionState struct {
2020-04-05 07:11:05 +00:00
currentClassName string
currentClassParentName string
2020-04-07 10:52:42 +00:00
currentErrHandler string
2020-04-09 07:39:23 +00:00
currentFunctionName string
currentMethodName string
currentNamespace string
currentTraitName string // TODO
2020-04-08 07:57:59 +00:00
importPackages map [ string ] struct { }
2020-04-05 06:45:14 +00:00
}
2020-04-05 07:11:05 +00:00
//
2020-04-12 02:40:46 +00:00
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 : ]
}
}
2020-04-16 06:37:27 +00:00
return ` /* ` + strings . TrimRight ( strings . Join ( lines , "\n" ) , "\n" ) + ` */ `
2020-04-12 02:40:46 +00:00
}
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
2020-04-16 06:37:27 +00:00
ret = append ( ret , ` /* ` + parts [ 0 ] + ` */ ` , normaliseCommentStrFragment ( parts [ 1 ] ) )
2020-04-12 02:40:46 +00:00
}
}
return strings . Join ( ret , "" )
}
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
}
2020-04-12 02:40:46 +00:00
//if len(freePrefix) > 0 && !(strings.HasSuffix(freePrefix, "\n") || strings.HasSuffix(freePrefix, `*/`)) {
// freePrefix += "\n"
//}
freePrefix = normaliseCommentStr ( freePrefix )
freeSuffix = normaliseCommentStr ( freeSuffix )
2020-04-10 08:07:45 +00:00
2020-04-07 06:27:20 +00:00
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 :
// Hoist all declarations first, and put any top-level code into a generated main() function
2020-04-08 07:57:59 +00:00
declarations := [ ] string { }
2020-04-05 04:35:44 +00:00
statements := [ ] string { }
2020-04-08 07:57:59 +00:00
packageName := ` main `
2020-04-05 04:35:44 +00:00
for _ , s := range n . Stmts {
2020-04-07 10:52:42 +00:00
2020-04-08 07:57:59 +00:00
switch s := s . ( type ) {
2020-04-07 10:52:42 +00:00
case * stmt . Class , * stmt . Function , * stmt . Interface :
this . currentErrHandler = "return nil, err\n"
2020-04-08 07:57:59 +00:00
sm , err := this . convert ( s )
if err != nil {
return "" , parseErr { s , err }
}
2020-04-05 04:35:44 +00:00
// Declaration - emit immediately (hoist)
2020-04-08 07:57:59 +00:00
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
2020-04-09 07:39:23 +00:00
this . currentNamespace = packageName
2020-04-05 04:35:44 +00:00
default :
2020-04-08 07:57:59 +00:00
this . currentErrHandler = "panic(err)\n" // top-level init/main behaviour
sm , err := this . convert ( s )
if err != nil {
return "" , parseErr { s , err }
}
2020-04-05 04:35:44 +00:00
// Top-level function code - deter emission
statements = append ( statements , sm )
}
2020-04-08 07:57:59 +00:00
}
// Emit
ret := "package main\n\n"
if len ( this . importPackages ) > 0 {
ret += "import (\n"
for packageName , _ := range this . importPackages {
2020-04-12 02:39:38 +00:00
ret += "\t" + quoteGoString ( packageName ) + "\n"
2020-04-08 07:57:59 +00:00
}
ret += ")\n"
}
if len ( declarations ) > 0 {
ret += strings . Join ( declarations , "\n" ) + "\n"
2020-04-05 04:35:44 +00:00
}
if len ( statements ) > 0 {
2020-04-08 07:57:59 +00:00
topFunc := ` init `
if packageName == ` main ` {
topFunc = ` main `
}
ret += "func " + topFunc + "() {\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 {
2020-04-05 06:45:14 +00:00
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 { }
2020-04-08 08:24:36 +00:00
memberConsts := [ ] string { }
2020-04-05 04:35:44 +00:00
memberFuncs := [ ] string { }
2020-04-05 06:24:14 +00:00
if n . Extends != nil {
2020-04-05 07:11:05 +00:00
parentName , err := this . resolveName ( n . Extends . ClassName )
2020-04-05 06:24:14 +00:00
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 06:24:14 +00:00
}
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
2020-04-11 00:49:08 +00:00
memberVars = append ( memberVars , name + " " + memberType . AsGoString ( ) )
2020-04-05 04:35:44 +00:00
2020-04-08 08:24:36 +00:00
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 )
}
2020-04-05 04:35:44 +00:00
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-09 07:39:23 +00:00
prevMethodName := this . currentMethodName
this . currentMethodName = funcName
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
2020-04-05 06:45:14 +00:00
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
2020-04-05 06:45:14 +00:00
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
}
2020-04-09 07:39:23 +00:00
// Reinstate stack
this . currentMethodName = prevMethodName
2020-04-05 04:35:44 +00:00
default :
return "" , parseErr { s , fmt . Errorf ( "Class '%s' contained unexpected AST node; expected PropertyList / ClassMethod" , className ) }
}
}
2020-04-08 08:24:36 +00:00
// 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"
}
2020-04-05 04:35:44 +00:00
// 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
2020-04-09 07:39:23 +00:00
prevFuncName := this . currentFunctionName
this . currentFunctionName = funcName
2020-04-05 06:45:14 +00:00
funcStmt , err := this . convertFunctionCommon ( n . Params , n . ReturnType , n . ReturnsRef , n . Stmts )
2020-04-05 04:35:44 +00:00
if err != nil {
2020-04-09 07:39:23 +00:00
// 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
2020-04-05 04:35:44 +00:00
return "" , parseErr { n , err }
}
2020-04-09 07:39:23 +00:00
this . currentFunctionName = prevFuncName
2020-04-05 04:35:44 +00:00
ret := "func " + funcName + funcStmt + "\n"
return ret , nil
case * stmt . Return :
2020-04-05 06:45:14 +00:00
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
2020-04-07 10:52:12 +00:00
// PHP can only throw objects, not literals/scalars
// If the expr is just new Exception, we can convert it to errors.New()
2020-04-05 04:35:44 +00:00
2020-04-07 10:52:12 +00:00
//if str, ok := n.Expr.(*scalar.String); ok {
// return "return nil, errors.New(" + str.Value + ")\n", nil
//}
2020-04-05 04:35:44 +00:00
2020-04-05 06:45:14 +00:00
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 :
2020-04-05 04:43:27 +00:00
var preinit , finit string
var err error = nil
if len ( n . Init ) == 0 {
// No initialiser in loop
} else if len ( n . Init ) == 1 {
2020-04-05 06:45:14 +00:00
finit , err = this . convert ( n . Init [ 0 ] )
2020-04-05 04:43:27 +00:00
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).
2020-04-05 04:43:27 +00:00
for _ , initStmt := range n . Init {
2020-04-05 06:45:14 +00:00
singleInitStmt , err := this . convert ( initStmt )
2020-04-05 04:43:27 +00:00
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 ) ) }
}
2020-04-05 06:45:14 +00:00
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 )
}
2020-04-05 06:45:14 +00:00
floop , err := this . convert ( loopStmt )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
body , err := this . convert ( convertToStmtList ( n . Stmt ) )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 04:43:27 +00:00
return preinit + "for " + finit + "; " + fcond + "; " + floop + " " + body + "\n" , nil
2020-04-05 04:35:44 +00:00
case * stmt . Foreach :
2020-04-05 06:45:14 +00:00
iterand , err := this . convert ( n . Expr )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
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 {
2020-04-05 06:45:14 +00:00
keyReceiver , err = this . convert ( n . Key )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
}
2020-04-05 06:45:14 +00:00
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 :
2020-04-05 06:45:14 +00:00
cond , err := this . convert ( n . Cond )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
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 :
2020-04-05 06:45:14 +00:00
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}" } )
2020-04-05 06:45:14 +00:00
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(??)
2020-04-05 07:12:42 +00:00
// No need to use a child scope for the temporary name -
// super() is a reserved name in PHP anyway
2020-04-10 08:08:40 +00:00
ret := "super, err := " + constructorName ( this . currentClassParentName ) + "(" + strings . Join ( funcArgs , ", " ) + ")\n"
2020-04-05 07:11:05 +00:00
ret += "if err != nil {\n"
2020-04-07 10:52:42 +00:00
ret += this . currentErrHandler //"return err\n"
2020-04-05 07:11:05 +00:00
ret += "}\n"
2020-04-05 07:12:42 +00:00
ret += "this." + this . currentClassParentName + " = *super // copy by value\n"
2020-04-05 07:11:05 +00:00
return ret , nil
}
2020-04-08 08:24:44 +00:00
if fnname , err := this . resolveName ( fncall . Function ) ; err == nil && fnname == "define" {
// define() gets converted to const
if len ( fncall . ArgumentList . Arguments ) != 2 {
return "" , parseErr { fncall , fmt . Errorf ( "expected define() to have 2 arguments, found %d" , len ( fncall . ArgumentList . Arguments ) ) }
}
defineName := fncall . ArgumentList . Arguments [ 0 ] . ( * node . Argument ) . Expr // that much is always possible
defineNameStr , ok := defineName . ( * scalar . String )
if ! ok {
return "" , parseErr { fncall , fmt . Errorf ( "can't handle a complex expression in define() name" ) }
}
rawDefineName , err := phpUnquote ( defineNameStr . Value )
if err != nil {
return "" , parseErr { fncall , err }
}
// Convert to a final Const statement
return this . convert ( stmt . NewConstList ( [ ] node . Node { stmt . NewConstant ( node . NewIdentifier ( rawDefineName ) , fncall . ArgumentList . Arguments [ 1 ] . ( * node . Argument ) . Expr , "" ) } ) )
}
2020-04-05 07:11:05 +00:00
}
2020-04-07 11:25:43 +00:00
// 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 {
2020-04-11 00:51:09 +00:00
return this . convertAssignAssign ( a , true )
2020-04-07 11:25:43 +00:00
}
2020-04-05 07:11:05 +00:00
2020-04-07 11:25:43 +00:00
// Non-assignment expression
2020-04-05 06:45:14 +00:00
child , err := this . convert ( n . Expr )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-07 11:25:43 +00:00
// 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
}
2020-04-05 04:35:44 +00:00
case * stmt . Echo :
// Convert into fmt.Print
args := make ( [ ] string , 0 , len ( n . Exprs ) )
for _ , expr := range n . Exprs {
2020-04-05 06:45:14 +00:00
exprGo , err := this . convert ( expr )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-08 07:58:35 +00:00
args = append ( args , removeParens ( exprGo ) )
2020-04-05 04:35:44 +00:00
}
2020-04-08 07:57:59 +00:00
this . importPackages [ "fmt" ] = struct { } { }
2020-04-05 04:35:44 +00:00
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
2020-04-05 05:46:25 +00:00
2020-04-08 07:57:59 +00:00
this . importPackages [ "fmt" ] = struct { } { }
2020-04-12 02:39:38 +00:00
return "fmt.Print(" + quoteGoString ( n . Value ) + ")\n" , nil // newline - standalone statement
2020-04-05 05:11:11 +00:00
2020-04-08 08:02:30 +00:00
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
}
2020-04-05 07:41:02 +00:00
case * stmt . If :
2020-04-07 06:52:30 +00:00
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 }
}
2020-04-09 23:23:37 +00:00
//
// `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
}
//
2020-04-05 07:41:02 +00:00
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 }
}
2020-04-09 23:23:08 +00:00
ret = strings . TrimRight ( ret , "\n" )
2020-04-05 07:41:02 +00:00
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 }
}
2020-04-09 23:23:08 +00:00
ret = strings . TrimRight ( ret , "\n" )
2020-04-05 07:41:02 +00:00
ret += " else " + body
}
return ret , nil
2020-04-12 02:41:23 +00:00
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
2020-04-07 10:52:42 +00:00
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 {
2020-04-07 11:26:44 +00:00
catchVars := [ ] string { }
2020-04-07 10:52:42 +00:00
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 }
}
2020-04-07 11:26:44 +00:00
if typename == ` Exception ` {
// PHP base type - catches all exceptions
2020-04-07 10:52:42 +00:00
2020-04-07 11:26:44 +00:00
catchStmt := catchBody
catchIfs = append ( catchIfs , catchStmt )
2020-04-07 10:52:42 +00:00
2020-04-07 11:26:44 +00:00
} 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 ]
}
2020-04-07 10:52:42 +00:00
2020-04-07 11:26:44 +00:00
catchVars = append ( catchVars , tempCatchVar + "*" + typename )
2020-04-08 07:57:59 +00:00
this . importPackages [ "errors" ] = struct { } { }
2020-04-07 11:26:44 +00:00
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 )
}
2020-04-07 10:52:42 +00:00
}
}
2020-04-07 11:26:44 +00:00
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"
2020-04-07 10:52:42 +00:00
}
// 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
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 :
2020-04-11 00:51:09 +00:00
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 )
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-11 00:51:09 +00:00
// 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 )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
callParams , err := this . convertFuncCallArgsCommon ( n . ArgumentList )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-07 10:52:42 +00:00
// FIXME need to support `this.currentErrHandler`
2020-04-10 08:08:40 +00:00
// 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 {
2020-04-11 01:11:39 +00:00
return "" , parseErr { n , fmt . Errorf ( "Call to '%s' expected 2 argument(s), got %d" , funcName , len ( callParams ) ) }
2020-04-10 08:08:40 +00:00
}
this . importPackages [ "strings" ] = struct { } { }
funcName = ` strings.Split `
2020-04-11 01:11:07 +00:00
callParams [ 0 ] , callParams [ 1 ] = callParams [ 1 ] , callParams [ 0 ] // need to reverse function argument order
2020-04-10 08:08:40 +00:00
} else if funcName == ` implode ` {
if len ( callParams ) != 2 {
2020-04-11 01:11:39 +00:00
return "" , parseErr { n , fmt . Errorf ( "Call to '%s' expected 2 argument(s), got %d" , funcName , len ( callParams ) ) }
2020-04-10 08:08:40 +00:00
}
this . importPackages [ "strings" ] = struct { } { }
funcName = ` strings.Join `
2020-04-11 01:11:07 +00:00
callParams [ 0 ] , callParams [ 1 ] = callParams [ 1 ] , callParams [ 0 ] // need to reverse function argument order
2020-04-10 08:08:40 +00:00
2020-04-11 01:11:39 +00:00
} else if funcName == ` sprintf ` {
this . importPackages [ "fmt" ] = struct { } { }
funcName = ` fmt.Sprintf `
} else if funcName == ` trim ` {
if len ( callParams ) != 1 {
return "" , parseErr { n , fmt . Errorf ( "Call to '%s' expected 2 argument(s), got %d" , funcName , len ( callParams ) ) }
}
this . importPackages [ "strings" ] = struct { } { }
funcName = ` strings.TrimSpace `
} else if funcName == ` rtrim ` {
if len ( callParams ) != 1 {
return "" , parseErr { n , fmt . Errorf ( "Call to '%s' expected 1 argument(s), got %d" , funcName , len ( callParams ) ) }
}
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 , fmt . Errorf ( "Call to '%s' expected 1 argument(s), got %d" , funcName , len ( callParams ) ) }
}
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 , fmt . Errorf ( "Call to '%s' expected 2 argument(s), got %d" , funcName , len ( callParams ) ) }
}
this . importPackages [ "strings" ] = struct { } { }
funcName = ` strings.ToLower `
} else if funcName == ` strtoupper ` {
if len ( callParams ) != 1 {
return "" , parseErr { n , fmt . Errorf ( "Call to '%s' expected 2 argument(s), got %d" , funcName , len ( callParams ) ) }
}
this . importPackages [ "strings" ] = struct { } { }
funcName = ` strings.ToUpper `
} else if funcName == ` file_put_contents ` {
if len ( callParams ) != 2 {
return "" , parseErr { n , fmt . Errorf ( "Call to '%s' expected 2 argument(s), got %d" , funcName , len ( callParams ) ) }
}
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 , fmt . Errorf ( "Call to '%s' expected 3 or 4 argument(s), got %d" , funcName , len ( callParams ) ) }
}
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)
}
2020-04-15 07:06:09 +00:00
} else if funcName == ` extract ` {
return "" , parseErr { n , fmt . Errorf ( "Unsupported dynamic function `\\extract()`" ) }
2020-04-10 08:08:40 +00:00
}
return funcName + "(" + strings . Join ( callParams , ", " ) + ")" , nil // expr only, no semicolon/newline
2020-04-05 04:35:44 +00:00
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 }
}
2020-04-05 06:45:14 +00:00
funcName , err := this . convert ( n . Call )
2020-04-05 06:23:52 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 07:26:36 +00:00
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
}
2020-04-05 06:45:14 +00:00
callParams , err := this . convertFuncCallArgsCommon ( n . ArgumentList )
2020-04-05 06:23:52 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-07 10:52:42 +00:00
// FIXME need to support `this.currentErrHandler`
2020-04-10 08:08:40 +00:00
return callTarget + "(" + strings . Join ( 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 ) } )
2020-04-05 06:45:14 +00:00
return this . convert ( expr . NewFunctionCall ( transparentNameNode , n . ArgumentList ) )
2020-04-05 04:35:44 +00:00
2020-04-08 08:24:36 +00:00
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
2020-04-11 00:44:37 +00:00
// 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
2020-04-12 02:39:38 +00:00
return quoteGoString ( className ) , nil
2020-04-11 00:44:37 +00:00
} else if className == ` self ` {
2020-04-12 02:39:38 +00:00
return quoteGoString ( this . currentClassName ) , nil
2020-04-11 00:44:37 +00:00
} 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 ` ) }
}
}
2020-04-08 08:24:36 +00:00
return className + constName , nil
2020-04-11 00:46:28 +00:00
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
2020-04-08 07:58:09 +00:00
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
}
2020-04-15 07:06:09 +00:00
case * expr . Eval :
return "" , parseErr { n , fmt . Errorf ( "Unsupported use of eval()" ) }
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."""
2020-04-05 06:45:14 +00:00
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."""
2020-04-05 06:45:14 +00:00
v , err := this . convert ( n . Variable )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
return v + "++" , nil
2020-04-12 02:40:57 +00:00
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
2020-04-05 04:35:44 +00:00
case * expr . MethodCall :
// Foo->Bar(Baz)
2020-04-05 06:45:14 +00:00
parent , err := this . convert ( n . Variable )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
child , err := this . convert ( n . Method )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
args , err := this . convertFuncCallArgsCommon ( n . ArgumentList )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-07 10:52:42 +00:00
// FIXME need to support `this.currentErrHandler`
2020-04-10 08:08:40 +00:00
return parent + "." + child + "(" + strings . Join ( args , ", " ) + ")" , nil
2020-04-05 04:35:44 +00:00
case * expr . PropertyFetch :
// Foo->Bar
2020-04-05 06:45:14 +00:00
parent , err := this . convert ( n . Variable )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
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 :
2020-04-05 06:45:14 +00:00
return this . convertArrayLiteralCommon ( n . Items )
2020-04-05 05:39:22 +00:00
case * expr . ShortArray :
2020-04-05 06:45:14 +00:00
return this . convertArrayLiteralCommon ( n . Items )
2020-04-05 05:39:22 +00:00
2020-04-10 08:07:57 +00:00
case * expr . BooleanNot :
rhs , err := this . convert ( n . Expr )
if err != nil {
return "" , parseErr { n , err }
}
return "!(" + rhs + ")" , nil
2020-04-10 08:08:15 +00:00
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
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
2020-04-05 06:45:14 +00:00
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 ) }
}
2020-04-05 06:45:14 +00:00
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
//
2020-04-05 05:05:37 +00:00
case * binary . BitwiseAnd :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` & ` )
2020-04-05 04:35:44 +00:00
2020-04-05 05:05:37 +00:00
case * binary . BitwiseOr :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` | ` )
2020-04-05 04:35:44 +00:00
2020-04-05 05:05:37 +00:00
case * binary . BitwiseXor :
2020-04-05 06:45:14 +00:00
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
2020-04-05 05:05:37 +00:00
case * binary . BooleanAnd :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` && ` )
2020-04-05 05:05:37 +00:00
case * binary . BooleanOr :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` || ` )
2020-04-05 05:05:37 +00:00
//case *binary.Coalesce:
// TODO this can't be expressed in an rvalue context in Go (unless we create a typed closure..?)
case * binary . Concat :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` + ` ) // PHP uses + for numbers, `.` for strings; Go uses `+` in both cases
2020-04-05 05:05:37 +00:00
case * binary . Div :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` / ` ) // PHP will upgrade ints to floats, Go won't
2020-04-05 05:05:37 +00:00
case * binary . Equal :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` == ` ) // Type-lax equality comparator
2020-04-05 04:35:44 +00:00
case * binary . GreaterOrEqual :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` >= ` )
2020-04-05 04:35:44 +00:00
2020-04-05 05:05:37 +00:00
case * binary . Greater :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` > ` )
2020-04-05 04:35:44 +00:00
2020-04-05 05:05:37 +00:00
case * binary . Identical :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` == ` ) // PHP uses `===`, Go is already type-safe
2020-04-05 04:35:44 +00:00
2020-04-05 05:05:37 +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
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` && ` )
2020-04-05 05:05:37 +00:00
case * binary . LogicalOr :
// As above
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` || ` )
2020-04-05 05:05:37 +00:00
case * binary . LogicalXor :
// As above
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` ^ ` ) // n.b. Go only supports this for integers; PHP also supports it for bools
2020-04-05 05:05:37 +00:00
case * binary . Minus :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` - ` )
2020-04-05 05:05:37 +00:00
case * binary . Mod :
// Go doesn't have a built-in operator for mod - convert to a call to math.Mod()
2020-04-05 06:45:14 +00:00
rval , err := this . convert ( n . Left )
2020-04-05 05:05:37 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
modulo , err := this . convert ( n . Right )
2020-04-05 05:05:37 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-08 07:57:59 +00:00
this . importPackages [ "math" ] = struct { } { }
2020-04-05 05:05:37 +00:00
return ` math.Mod( ` + rval + ` , ` + modulo + ` ) ` , nil
case * binary . Mul :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` * ` )
2020-04-05 05:05:37 +00:00
case * binary . NotEqual :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` != ` ) // Type-lax equality comparator
2020-04-05 05:05:37 +00:00
case * binary . NotIdentical :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` != ` ) // PHP uses `!==`, Go is already type-safe
2020-04-05 05:05:37 +00:00
case * binary . Plus :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` + ` ) // PHP uses + for numbers, `.` for strings; Go uses `+` in both cases
2020-04-05 05:05:37 +00:00
case * binary . Pow :
// Go doesn't have a built-in operator for mod - convert to a call to math.Pow()
2020-04-05 06:45:14 +00:00
base , err := this . convert ( n . Left )
2020-04-05 05:05:37 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-05 06:45:14 +00:00
exponent , err := this . convert ( n . Right )
2020-04-05 05:05:37 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-08 07:57:59 +00:00
this . importPackages [ "math" ] = struct { } { }
2020-04-05 05:05:37 +00:00
return ` math.Pow( ` + base + ` , ` + exponent + ` ) ` , nil
case * binary . ShiftLeft :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` << ` )
2020-04-05 05:05:37 +00:00
case * binary . ShiftRight :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` >> ` )
2020-04-05 05:05:37 +00:00
case * binary . SmallerOrEqual :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` <= ` )
2020-04-05 05:05:37 +00:00
case * binary . Smaller :
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` < ` )
2020-04-05 05:05:37 +00:00
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
2020-04-05 06:45:14 +00:00
return this . convertBinaryCommon ( n . Left , n . Right , ` - ` )
2020-04-05 04:35:44 +00:00
//
// scalar
//
case * scalar . Lnumber :
2020-04-15 07:06:28 +00:00
// Integer (maybe with 0b / 0x / '0' prefix)
return n . Value , nil // number formats are compatible
case * scalar . Dnumber :
// Float (maybe with period / scientific notation)
2020-04-05 04:35:44 +00:00
return n . Value , nil // number formats are compatible
case * scalar . String :
2020-04-07 10:51:31 +00:00
// 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 }
}
2020-04-12 02:39:38 +00:00
return quoteGoString ( rawValue ) , nil // Go source code quoting format
2020-04-05 04:35:44 +00:00
2020-04-12 02:40:19 +00:00
case * scalar . EncapsedStringPart :
// The n.Value that we receive here does not contain quotes at all
2020-04-16 06:38:05 +00:00
// 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
2020-04-12 02:40:19 +00:00
case * scalar . Encapsed :
return this . convertEncapsedString ( n . Parts )
case * scalar . Heredoc :
return this . convertEncapsedString ( n . Parts )
2020-04-09 07:39:23 +00:00
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 ) }
}
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-08 07:58:35 +00:00
// 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 {
2020-04-10 08:08:40 +00:00
for len ( expr ) > 2 && expr [ 0 ] == '(' && expr [ len ( expr ) - 1 ] == ')' && ! strings . HasSuffix ( expr , ` () ` ) {
2020-04-08 07:58:35 +00:00
expr = expr [ 1 : len ( expr ) - 1 ]
}
return expr
}
2020-04-12 02:40:19 +00:00
// 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, ...
}
2020-04-12 02:39:38 +00:00
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 ) {
2020-04-12 02:40:19 +00:00
// 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
}
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
2020-04-11 00:49:08 +00:00
ret := unknownVarType . AsGoString ( )
2020-04-07 10:52:42 +00:00
if n == nil || n == node . Node ( nil ) {
2020-04-08 08:33:37 +00:00
return ret , nil
2020-04-07 10:52:42 +00:00
}
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 ) ) }
}
2020-04-08 08:33:37 +00:00
ret = n . Parts [ 0 ] . ( * name . NamePart ) . Value
2020-04-07 10:52:42 +00:00
case * name . Name :
if len ( n . Parts ) != 1 {
return "" , parseErr { n , fmt . Errorf ( "name has %d parts, expected 1" , len ( n . Parts ) ) }
2020-04-05 04:35:44 +00:00
}
2020-04-08 08:33:37 +00:00
ret = n . Parts [ 0 ] . ( * name . NamePart ) . Value
2020-04-07 10:52:42 +00:00
2020-04-12 02:41:08 +00:00
case * node . Identifier :
ret = n . Value // e.g. `array`
2020-04-07 10:52:42 +00:00
default :
return "" , fmt . Errorf ( "unexpected name type %#v" , n )
2020-04-05 04:35:44 +00:00
}
2020-04-05 07:11:05 +00:00
// Handle class lookups
2020-04-08 08:33:37 +00:00
if strings . ToLower ( ret ) == "parent" {
2020-04-05 07:26:36 +00:00
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" ) }
}
2020-04-05 07:26:36 +00:00
return ` this. ` + this . currentClassParentName , nil
2020-04-08 08:33:37 +00:00
} else if strings . ToLower ( ret ) == "self" {
2020-04-05 07:26:36 +00:00
// 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
* /
2020-04-08 08:33:37 +00:00
} else if strings . ToLower ( ret ) == "static" {
2020-04-05 07:26:36 +00:00
return "" , parseErr { n , fmt . Errorf ( "'static::' is not yet supported" ) }
2020-04-08 08:33:53 +00:00
} 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
2020-04-05 07:11:05 +00:00
}
2020-04-08 08:33:37 +00:00
return ret , nil
2020-04-05 04:35:44 +00:00
}
2020-04-11 00:51:09 +00:00
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 ) {
2020-04-07 11:25:43 +00:00
2020-04-11 00:51:09 +00:00
rvalue , err := this . convert ( nRValue )
2020-04-07 11:25:43 +00:00
if err != nil {
return "" , parseErr { n , err }
}
2020-04-11 00:51:09 +00:00
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" ) }
}
2020-04-07 11:25:43 +00:00
// Special handling for the case of foo[] = bar
// Transform into append()
arrayVar , err := this . convert ( dimf . Variable )
if err != nil {
return "" , parseErr { dimf , err }
}
2020-04-11 00:47:37 +00:00
ret := arrayVar + ` = append( ` + arrayVar + ` , ` + removeParens ( rvalue ) + ` ) `
2020-04-07 11:25:43 +00:00
if isTopLevelStatement {
ret += "\n"
}
return ret , nil
} else {
// Normal assignment
2020-04-11 00:51:09 +00:00
lvalue , err := this . convert ( nLValue ) // might be a more complicated lvalue
2020-04-07 11:25:43 +00:00
if err != nil {
return "" , parseErr { n , err }
}
// TODO this may need to use `:=`
if isTopLevelStatement {
2020-04-11 00:47:37 +00:00
rvalue = removeParens ( rvalue ) // safe in top-level assignment context
2020-04-07 11:25:43 +00:00
// If this is a simple expression (func call; indirect/method call; assignment of func/method call) then
// we need to propagate errors
2020-04-11 00:51:09 +00:00
switch nRValue . ( type ) {
2020-04-07 11:25:43 +00:00
case * expr . FunctionCall , * expr . StaticCall , * expr . New :
2020-04-11 00:51:09 +00:00
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" ) }
}
2020-04-08 07:58:56 +00:00
ret := lvalue + ", err = " + rvalue + "\n"
2020-04-07 11:25:43 +00:00
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 {
2020-04-11 00:51:09 +00:00
return lvalue + " " + operator + " " + rvalue , nil // Just the basic assignment - without trailing NL
2020-04-07 11:25:43 +00:00
}
}
}
2020-04-05 04:35:44 +00:00
// 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 } )
}
2020-04-05 06:45:14 +00:00
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
2020-04-05 06:45:14 +00:00
lhs , err := this . convert ( left )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { left , err }
}
2020-04-05 07:41:21 +00:00
2020-04-05 06:45:14 +00:00
rhs , err := this . convert ( right )
2020-04-05 04:35:44 +00:00
if err != nil {
return "" , parseErr { right , err }
}
2020-04-05 07:41:21 +00:00
// 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-09 07:40:18 +00:00
// 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
2020-04-05 04:35:44 +00:00
return "(" + lhs + " " + goBinaryOperator + " " + rhs + ")" , nil
}
2020-04-10 08:08:40 +00:00
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 {
2020-04-10 08:08:40 +00:00
return nil , parseErr { arg_ , fmt . Errorf ( "expected node.Argument" ) }
2020-04-05 04:35:44 +00:00
}
2020-04-05 06:45:14 +00:00
rvalue , err := this . convert ( arg . Expr )
2020-04-05 04:35:44 +00:00
if err != nil {
2020-04-10 08:08:40 +00:00
return nil , parseErr { arg , err }
2020-04-05 04:35:44 +00:00
}
if arg . IsReference {
rvalue = "&" + rvalue
}
if arg . Variadic {
rvalue = "..." + rvalue
}
2020-04-08 07:58:35 +00:00
callParams = append ( callParams , removeParens ( rvalue ) )
2020-04-05 04:35:44 +00:00
}
2020-04-10 08:08:40 +00:00
return callParams , nil
2020-04-05 04:35:44 +00:00
}
2020-04-05 06:45:14 +00:00
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
2020-04-11 00:45:26 +00:00
// TODO support unpack operator (...)
2020-04-05 05:39:22 +00:00
isMapType := false
for idx , itm_ := range items {
itm , ok := itm_ . ( * expr . ArrayItem )
if ! ok {
return "" , parseErr { itm_ , fmt . Errorf ( "expected ArrayItem" ) }
}
2020-04-11 00:45:26 +00:00
// 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
}
2020-04-05 05:39:22 +00:00
if idx == 0 {
isMapType = ( itm . Key != nil )
} else {
2020-04-11 00:45:05 +00:00
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 ) }
2020-04-05 05:39:22 +00:00
}
}
2020-04-05 06:45:14 +00:00
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 {
2020-04-05 06:45:14 +00:00
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 {
2020-04-11 00:49:08 +00:00
return ` map[ ` + keyType . AsGoString ( ) + ` ] ` + valType . AsGoString ( ) + ` { ` + strings . Join ( entries , " " ) + ` } ` , nil
2020-04-05 05:39:22 +00:00
} else {
2020-04-11 00:49:08 +00:00
return ` [] ` + valType . AsGoString ( ) + ` { ` + strings . Join ( entries , " " ) + ` } ` , nil
2020-04-05 05:39:22 +00:00
}
}
2020-04-05 06:45:14 +00:00
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
}
2020-04-07 06:52:30 +00:00
// hasInteriorAssignment recursively walks a node, to determine if it contains
// any assignment expressions
func hasInteriorAssignment ( n node . Node ) ( hasAnyAssign bool , err error ) {
2020-04-15 07:06:09 +00:00
err = parseutil . SimpleWalk ( n , func ( n node . Node ) error {
2020-04-07 06:52:30 +00:00
if _ , ok := n . ( * assign . Assign ) ; ok {
hasAnyAssign = true
}
return nil
} )
return // named return
}