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 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 }