initial commit

This commit is contained in:
mappu 2020-04-05 16:35:44 +12:00
commit 872c878857
13 changed files with 2629 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Binaries
php2go
# ..

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# php2go
Convert PHP source code to Go by AST walking.
The goal is to produce idiomatic, maintainable Go code as part of a one-off conversion. This is not generally possible for highly dynamic PHP code, that may require manual fixups.
## Progress
[X] Convert some small programs
[X] All functions return `(type, error)`
- [X] Convert `throw` to err return
- [ ] Elide error return for functions that cannot throw
- ?? Could be a standalone Go refactoring tool
- [ ] Non-leaf function calls need to check + bubble errors
[ ] Comprehensive coverage of all AST node types
- [ ] Expr
- [ ] Binary
- [ ] Scalar
- [ ] ...
- [ ] Convert instanceof for Exception types to Go1.13 `errors.Is`
[ ] Array handling
- [ ] Infer whether to use slice/map for PHP array
- [ ] Infer whether map use should be order-preserving
[ ] Multi-file programs
- [ ] Include/Require
[ ] Namespaces
[ ] Generators
[X] Convert top-level calls to `init()`/`main()`
- *Currently always `init`*
[ ] Class/object transformations
- [X] Convert `new X` constructor calls to `NewX`
- [ ] Convert class inheritance to interfaces with interface assertion
- [ ] Traits / `use`
- [ ] Abstract methods
[ ] Type inference
- [ ] Parse extra types from phpdoc blocks
[ ] Infer whether to declare variable (`var` / `:=`) or reuse (`=`)
- [ ] Track current visibility scope
[ ] Standard library transformations
- [ ] Track golang package imports
- [ ] Handle conflicts between golang stdlib packages / local variable names
[ ] PHP Superglobal transformations
- [ ] `$_SERVER['argv']` to os.Args
- [ ] `$_GET['name']` to r.FormValue('name')
[ ] Closures
- [ ] Handle value/reference captures
[X] Variadic function parameters
[ ] Convert wordpress / mediawiki / symfony

30
fixtures/0001.php Normal file
View File

@ -0,0 +1,30 @@
<?php
class Bar {
protected $mX = null;
protected $mY = 3;
function __construct(string $x) {
$this->mX = $x;
}
function hello() {
echo $this->mX . "\n";
throw new Exception("asdf");
}
function scalarThrower() {
throw "str";
}
}
function foo($a, int $b): int {
return 3 + $a + $b;
}
for ($i = 0; $i < 3; ++$i) {
echo foo($i, 2)."\n";
}
$bb = new Bar("hello");
$bb->hello();

1168
fixtures/0001.php.parse.json Normal file

File diff suppressed because it is too large Load Diff

21
fixtures/0002-loops.php Normal file
View File

@ -0,0 +1,21 @@
<?php
// Different types of loop statement
for($i = 0; $i < 3; ++$i) {
foreach($foo as $k => $v) {
}
foreach($foo2 as $v2) {
}
while(true) {
}
do {
} while (true);
}
// Loop with no separate body statement
while (true) echo "hello";

View File

@ -0,0 +1,437 @@
{
"type": "*node.Root",
"position": {
"startPos": 44,
"endPos": 254,
"startLine": 4,
"endLine": 21
},
"Stmts": [
{
"type": "*stmt.For",
"position": {
"startPos": 44,
"endPos": 186,
"startLine": 4,
"endLine": 18
},
"Init": [
{
"type": "*assign.Assign",
"position": {
"startPos": 48,
"endPos": 54,
"startLine": 4,
"endLine": 4
},
"Variable": {
"type": "*expr.Variable",
"position": {
"startPos": 48,
"endPos": 50,
"startLine": 4,
"endLine": 4
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 48,
"endPos": 50,
"startLine": 4,
"endLine": 4
},
"Value": "i"
}
},
"Expression": {
"type": "*scalar.Lnumber",
"position": {
"startPos": 53,
"endPos": 54,
"startLine": 4,
"endLine": 4
},
"Value": "0"
}
}
],
"Cond": [
{
"type": "*binary.Smaller",
"position": {
"startPos": 56,
"endPos": 62,
"startLine": 4,
"endLine": 4
},
"Left": {
"type": "*expr.Variable",
"position": {
"startPos": 56,
"endPos": 58,
"startLine": 4,
"endLine": 4
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 56,
"endPos": 58,
"startLine": 4,
"endLine": 4
},
"Value": "i"
}
},
"Right": {
"type": "*scalar.Lnumber",
"position": {
"startPos": 61,
"endPos": 62,
"startLine": 4,
"endLine": 4
},
"Value": "3"
}
}
],
"Loop": [
{
"type": "*expr.PreInc",
"position": {
"startPos": 64,
"endPos": 68,
"startLine": 4,
"endLine": 4
},
"Variable": {
"type": "*expr.Variable",
"position": {
"startPos": 66,
"endPos": 68,
"startLine": 4,
"endLine": 4
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 66,
"endPos": 68,
"startLine": 4,
"endLine": 4
},
"Value": "i"
}
}
}
],
"Stmt": {
"type": "*stmt.StmtList",
"position": {
"startPos": 70,
"endPos": 186,
"startLine": 4,
"endLine": 18
},
"Stmts": [
{
"type": "*stmt.Foreach",
"position": {
"startPos": 75,
"endPos": 105,
"startLine": 6,
"endLine": 7
},
"Expr": {
"type": "*expr.Variable",
"position": {
"startPos": 83,
"endPos": 87,
"startLine": 6,
"endLine": 6
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 83,
"endPos": 87,
"startLine": 6,
"endLine": 6
},
"Value": "foo"
}
},
"Key": {
"type": "*expr.Variable",
"position": {
"startPos": 91,
"endPos": 93,
"startLine": 6,
"endLine": 6
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 91,
"endPos": 93,
"startLine": 6,
"endLine": 6
},
"Value": "k"
}
},
"Variable": {
"type": "*expr.Variable",
"position": {
"startPos": 97,
"endPos": 99,
"startLine": 6,
"endLine": 6
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 97,
"endPos": 99,
"startLine": 6,
"endLine": 6
},
"Value": "v"
}
},
"Stmt": {
"type": "*stmt.StmtList",
"position": {
"startPos": 101,
"endPos": 105,
"startLine": 6,
"endLine": 7
},
"Stmts": [
]
}
} {
"type": "*stmt.Foreach",
"position": {
"startPos": 109,
"endPos": 135,
"startLine": 9,
"endLine": 10
},
"Expr": {
"type": "*expr.Variable",
"position": {
"startPos": 117,
"endPos": 122,
"startLine": 9,
"endLine": 9
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 117,
"endPos": 122,
"startLine": 9,
"endLine": 9
},
"Value": "foo2"
}
},
"Variable": {
"type": "*expr.Variable",
"position": {
"startPos": 126,
"endPos": 129,
"startLine": 9,
"endLine": 9
},
"VarName": {
"type": "*node.Identifier",
"position": {
"startPos": 126,
"endPos": 129,
"startLine": 9,
"endLine": 9
},
"Value": "v2"
}
},
"Stmt": {
"type": "*stmt.StmtList",
"position": {
"startPos": 131,
"endPos": 135,
"startLine": 9,
"endLine": 10
},
"Stmts": [
]
}
} {
"type": "*stmt.While",
"position": {
"startPos": 139,
"endPos": 155,
"startLine": 12,
"endLine": 13
},
"Cond": {
"type": "*expr.ConstFetch",
"position": {
"startPos": 145,
"endPos": 149,
"startLine": 12,
"endLine": 12
},
"Constant": {
"type": "*name.Name",
"position": {
"startPos": 145,
"endPos": 149,
"startLine": 12,
"endLine": 12
},
"Parts": [
{
"type": "*name.NamePart",
"position": {
"startPos": 145,
"endPos": 149,
"startLine": 12,
"endLine": 12
},
"Value": "true"
}
]
}
},
"Stmt": {
"type": "*stmt.StmtList",
"position": {
"startPos": 151,
"endPos": 155,
"startLine": 12,
"endLine": 13
},
"Stmts": [
]
}
} {
"type": "*stmt.Do",
"position": {
"startPos": 159,
"endPos": 182,
"startLine": 15,
"endLine": 16
},
"Stmt": {
"type": "*stmt.StmtList",
"position": {
"startPos": 162,
"endPos": 168,
"startLine": 15,
"endLine": 16
},
"Stmts": [
]
},
"Cond": {
"type": "*expr.ConstFetch",
"position": {
"startPos": 176,
"endPos": 180,
"startLine": 16,
"endLine": 16
},
"Constant": {
"type": "*name.Name",
"position": {
"startPos": 176,
"endPos": 180,
"startLine": 16,
"endLine": 16
},
"Parts": [
{
"type": "*name.NamePart",
"position": {
"startPos": 176,
"endPos": 180,
"startLine": 16,
"endLine": 16
},
"Value": "true"
}
]
}
}
}
]
}
},
{
"type": "*stmt.While",
"position": {
"startPos": 228,
"endPos": 254,
"startLine": 21,
"endLine": 21
},
"Cond": {
"type": "*expr.ConstFetch",
"position": {
"startPos": 235,
"endPos": 239,
"startLine": 21,
"endLine": 21
},
"Constant": {
"type": "*name.Name",
"position": {
"startPos": 235,
"endPos": 239,
"startLine": 21,
"endLine": 21
},
"Parts": [
{
"type": "*name.NamePart",
"position": {
"startPos": 235,
"endPos": 239,
"startLine": 21,
"endLine": 21
},
"Value": "true"
}
]
}
},
"Stmt": {
"type": "*stmt.Echo",
"position": {
"startPos": 241,
"endPos": 254,
"startLine": 21,
"endLine": 21
},
"Exprs": [
{
"type": "*scalar.String",
"position": {
"startPos": 246,
"endPos": 253,
"startLine": 21,
"endLine": 21
},
"Value": "\"hello\""
}
]
}
}
]
}

39
fixtures_test.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
const fixtureDirectory = `fixtures`
func TestConvertFixtures(t *testing.T) {
fh, err := os.Open(fixtureDirectory)
if err != nil {
t.Fatal(err)
}
defer fh.Close()
fixtures, err := fh.Readdirnames(-1)
if err != nil {
t.Fatal(err)
}
for _, fixture := range fixtures {
if !strings.HasSuffix(fixture, `.php`) {
continue
}
ret, err := ConvertFile(filepath.Join(fixtureDirectory, fixture))
if err != nil {
t.Errorf("In test fixture %s:\n %s", fixture, err.Error())
continue
}
// Success
t.Logf("Successful test for fixture %s:\n%s", fixture, ret)
}
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module php2go
go 1.13
require github.com/z7zmey/php-parser v0.7.0

25
go.sum Normal file
View File

@ -0,0 +1,25 @@
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.4.0 h1:uCmaf4vVbWAOZz36k1hrQD7ijGRzLwaME8Am/7a4jZI=
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
github.com/yookoala/realpath v1.0.0 h1:7OA9pj4FZd+oZDsyvXWQvjn5oBdcHRTV44PpdMSuImQ=
github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE=
github.com/z7zmey/php-parser v0.7.0 h1:3pVIdijGn0OtIExr1IxjHxUq+/Es8Nulb89J+VNoy08=
github.com/z7zmey/php-parser v0.7.0/go.mod h1:r03mwVJvNhQKrTqKFzK0MIepU1uO62Z0p9ES3A7KTu4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

21
literal.go Normal file
View File

@ -0,0 +1,21 @@
package main
import (
"github.com/z7zmey/php-parser/freefloating"
"github.com/z7zmey/php-parser/node"
"github.com/z7zmey/php-parser/position"
"github.com/z7zmey/php-parser/walker"
)
type Literal struct {
Value string
}
func (l Literal) Walk(v walker.Visitor) {}
func (l Literal) Attributes() map[string]interface{} { return nil }
func (l Literal) SetPosition(p *position.Position) {}
func (l Literal) GetPosition() *position.Position { return nil }
func (l Literal) GetFreeFloating() *freefloating.Collection { return nil }
// interface assertion
var _ node.Node = Literal{}

71
main.go Normal file
View File

@ -0,0 +1,71 @@
package main
import (
"errors"
"flag"
"fmt"
"go/format"
"io/ioutil"
"os"
"github.com/z7zmey/php-parser/parser"
"github.com/z7zmey/php-parser/visitor"
)
func ConvertFile(filename string) (string, error) {
inputFile, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
namespaces := visitor.NewNamespaceResolver()
// scope := NewScope()
p, err := parser.NewParser([]byte(inputFile), "7.4")
if err != nil {
panic(err)
}
p.Parse()
for _, err := range p.GetErrors() {
return "", errors.New(err.String())
}
// Walk and print JSON...
if fh, err := os.OpenFile(filename+`.parse.json`, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err == nil {
v := visitor.NewPrettyJsonDumper(fh, namespaces)
p.GetRootNode().Walk(v)
fh.Close()
}
// Walk and print (converted)
ret, err := convert(p.GetRootNode())
if err != nil {
return "", err
}
// Gofmt output
// TODO pass flags to get -s/-r equivalent for more agressive simplification
formatted, err := format.Source([]byte(ret))
if err != nil {
ret += "// Gofmt failed: " + err.Error()
return ret, nil
}
return string(formatted), nil
}
func main() {
filename := flag.String("InFile", "", "Select file to convert")
flag.Parse()
ret, err := ConvertFile(*filename)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(ret)
}

700
node.go Normal file
View File

@ -0,0 +1,700 @@
package main
import (
"fmt"
"reflect"
//"strconv"
"strings"
"github.com/z7zmey/php-parser/node"
"github.com/z7zmey/php-parser/node/expr"
"github.com/z7zmey/php-parser/node/expr/assign"
"github.com/z7zmey/php-parser/node/expr/binary"
"github.com/z7zmey/php-parser/node/name"
"github.com/z7zmey/php-parser/node/scalar"
"github.com/z7zmey/php-parser/node/stmt"
)
func nodeTypeString(n node.Node) string {
return reflect.TypeOf(n).String()
}
type parseErr struct {
n node.Node
childErr error
}
func (pe parseErr) Error() string {
return fmt.Sprintf("Parsing %s on line %d: %s", nodeTypeString(pe.n), pe.n.GetPosition().StartLine, pe.childErr)
}
func (pe parseErr) Unwrap() error {
return pe.childErr
}
//
func convert(n_ node.Node) (string, error) {
switch n := n_.(type) {
//
// node
//
case *node.Root:
ret := "package main\n\n"
// Hoist all declarations first, and put any top-level code into a generated main() function
statements := []string{}
for _, s := range n.Stmts {
sm, err := convert(s)
if err != nil {
return "", parseErr{s, err}
}
switch s.(type) {
case *stmt.Class, *stmt.Function:
// Declaration - emit immediately (hoist)
ret += sm + "\n"
default:
// Top-level function code - deter emission
statements = append(statements, sm)
}
}
// Emit deferred statements
if len(statements) > 0 {
ret += "func init() {\n"
ret += "\t" + strings.Join(statements, "\n\t") + "\n"
ret += "}\n"
}
return ret, 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 := convert(s)
if err != nil {
return "", parseErr{s, err}
}
ret += line + "\n"
}
return ret + "}\n", nil
case *stmt.Class:
ret := ""
className := n.ClassName.(*node.Identifier).Value
memberVars := []string{}
memberFuncs := []string{}
// Walk all child nodes of the class
for _, s_ := range n.Stmts {
switch s := s_.(type) {
case *stmt.PropertyList:
// Class member variable
// Doc comment
// TODO scan for `@var {type}` strings
// Name
prop, ok := s.Properties[0].(*stmt.Property)
if !ok {
return "", parseErr{s, fmt.Errorf("unexpected propertylist structure")}
}
name := prop.Variable.(*expr.Variable).VarName.(*node.Identifier).Value
// Type (unknown)
memberType := unknownVarType
// 'Modifiers' - protected public readonly ...
// prop.Modifiers
memberVars = append(memberVars, name+" "+memberType)
case *stmt.ClassMethod:
// Function name
// If function is public/private/protected, set the first character to upper/lowercase
funcName, err := applyVisibilityModifier(s.MethodName.(*node.Identifier).Value, s.Modifiers)
if err != nil {
return "", parseErr{s, err}
}
// 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 := convertFunctionCommon(s.Params, returnType, true /* always use ptr return */, allStmts)
if err != nil {
return "", parseErr{s, err}
}
memberFuncStmt := "func New" + className + funcStmt + "\n"
memberFuncs = append(memberFuncs, memberFuncStmt)
} else {
// Method body
funcStmt, err := convertFunctionCommon(s.Params, s.ReturnType, s.ReturnsRef, s.Stmt.(*stmt.StmtList).Stmts)
if err != nil {
return "", parseErr{s, err}
}
memberFuncStmt := "func (this *" + className + ") " + funcName + funcStmt + "\n"
memberFuncs = append(memberFuncs, memberFuncStmt)
}
default:
return "", parseErr{s, fmt.Errorf("Class '%s' contained unexpected AST node; expected PropertyList / ClassMethod", className)}
}
}
// Create struct typedef containing all explicit properties
ret += "type " + className + " struct {\n"
ret += "\t" + strings.Join(memberVars, "\n\t") + "\n"
ret += "}\n"
// Create all member functions
ret += strings.Join(memberFuncs, "\n\n")
// Done
return ret, nil
case *stmt.Function:
// Top-level function
// TODO parse doc comment
// FIXME is this the same as a closure?
funcName := n.FunctionName.(*node.Identifier).Value
// All top-level functions like this are public; ensure function name starts
// with an uppercase letter
funcName = toPublic(funcName)
// Convert body
funcStmt, err := convertFunctionCommon(n.Params, n.ReturnType, n.ReturnsRef, n.Stmts)
if err != nil {
return "", parseErr{n, err}
}
ret := "func " + funcName + funcStmt + "\n"
return ret, nil
case *stmt.Return:
child, err := 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
// If the expr is a string literal, we can convert it to errors.New()
// Although we probably can't do this in general for stringly-typed expressions
if str, ok := n.Expr.(*scalar.String); ok {
return "return nil, errors.New(" + str.Value + ")\n", nil
}
child, err := convert(n.Expr)
if err != nil {
return "", parseErr{n, err}
}
return "return nil, " + child + "\n", nil
case *stmt.For:
if len(n.Init) != 1 {
// 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).
return "", parseErr{n, fmt.Errorf("for loop can only have 1 init clause, found %d", len(n.Init))}
}
finit, err := convert(n.Init[0])
if err != nil {
return "", parseErr{n, err}
}
if len(n.Init) != 1 {
return "", parseErr{n, fmt.Errorf("for loop can only have 1 cond clause, found %d", len(n.Cond))}
}
fcond, err := convert(n.Cond[0])
if err != nil {
return "", parseErr{n, err}
}
if len(n.Init) != 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 := convert(loopStmt)
if err != nil {
return "", parseErr{n, err}
}
body, err := convert(convertToStmtList(n.Stmt))
if err != nil {
return "", parseErr{n, err}
}
return "for " + finit + "; " + fcond + "; " + floop + " " + body + "\n", nil
case *stmt.Foreach:
iterand, err := convert(n.Expr)
if err != nil {
return "", parseErr{n, err}
}
valueReceiver, err := convert(n.Variable)
if err != nil {
return "", parseErr{n, err}
}
keyReceiver := `_`
if n.Key != nil {
keyReceiver, err = convert(n.Key)
if err != nil {
return "", parseErr{n, err}
}
}
body, err := 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 := convert(n.Cond)
if err != nil {
return "", parseErr{n, err}
}
body, err := convert(convertToStmtList(n.Stmt))
if err != nil {
return "", parseErr{n, err}
}
return "for " + cond + " " + body + "\n", nil
case *stmt.Do:
cond, err := 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 := convert(bodyStmts)
if err != nil {
return "", parseErr{n, err}
}
return "for " + cond + " " + body + "\n", nil
case *stmt.Expression:
child, err := convert(n.Expr)
if err != nil {
return "", parseErr{n, err}
}
ret := child + "\n" // standalone expression statement
return ret, nil
case *stmt.Echo:
// Convert into fmt.Print
args := make([]string, 0, len(n.Exprs))
for _, expr := range n.Exprs {
exprGo, err := convert(expr)
if err != nil {
return "", parseErr{n, err}
}
args = append(args, exprGo)
}
return "fmt.Print(" + strings.Join(args, ", ") + ")\n", nil // newline - standalone statement
//
// assign
//
case *assign.Assign:
lvalue, err := convert(n.Variable) // might be a more complicated lvalue
if err != nil {
return "", parseErr{n, err}
}
rvalue, err := convert(n.Expression)
if err != nil {
return "", parseErr{n, err}
}
// TODO this may need to use `:=`
return lvalue + " = " + rvalue, nil
//
// special literals
//
case Literal:
return n.Value, nil
case *node.Identifier:
return n.Value, nil
//
// expr
//
case *expr.FunctionCall:
// All our generated functions return err, but this AST node may be in a single-rvalue context
// TODO do something more intelligent here
// We can't necessarily hoist the whole call, in case we are on the right-hand side of a && operator
funcName, err := resolveName(n.Function)
if err != nil {
return "", parseErr{n, err}
}
callParams, err := convertFuncCallArgsCommon(n.ArgumentList)
if err != nil {
return "", parseErr{n, err}
}
return funcName + callParams, nil // expr only, no semicolon/newline
case *expr.New:
// new foo(xx)
// Transparently convert to calling constructor function.
nn, err := 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 convert(expr.NewFunctionCall(transparentNameNode, n.ArgumentList))
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 := 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 := convert(n.Variable)
if err != nil {
return "", parseErr{n, err}
}
return v + "++", nil
case *expr.MethodCall:
// Foo->Bar(Baz)
parent, err := convert(n.Variable)
if err != nil {
return "", parseErr{n, err}
}
child, err := convert(n.Method)
if err != nil {
return "", parseErr{n, err}
}
args, err := convertFuncCallArgsCommon(n.ArgumentList)
if err != nil {
return "", parseErr{n, err}
}
return parent + "." + child + args, nil
case *expr.PropertyFetch:
// Foo->Bar
parent, err := convert(n.Variable)
if err != nil {
return "", parseErr{n, err}
}
child, err := convert(n.Property)
if err != nil {
return "", parseErr{n, err}
}
return parent + "." + child, nil
case *expr.Variable:
return n.VarName.(*node.Identifier).Value, nil
case *expr.ConstFetch:
return resolveName(n.Constant)
//
// binary
//
case *binary.Plus:
// PHP uses + for numbers, `.` for strings; Go uses `+` in both cases
return convertBinaryCommon(n.Left, n.Right, `+`)
case *binary.Smaller:
return convertBinaryCommon(n.Left, n.Right, `<`)
case *binary.SmallerOrEqual:
return convertBinaryCommon(n.Left, n.Right, `<=`)
case *binary.Greater:
return convertBinaryCommon(n.Left, n.Right, `>`)
case *binary.GreaterOrEqual:
return convertBinaryCommon(n.Left, n.Right, `>=`)
case *binary.Equal:
return convertBinaryCommon(n.Left, n.Right, `==`)
case *binary.Identical: // PHP triple-equals
return convertBinaryCommon(n.Left, n.Right, `===`)
case *binary.Concat:
// PHP uses + for numbers, `.` for strings; Go uses `+` in both cases
return convertBinaryCommon(n.Left, n.Right, `+`)
//
// scalar
//
case *scalar.Lnumber:
return n.Value, nil // number formats are compatible
case *scalar.String:
return n.Value, nil // It's already quoted in PHP format
// return strconv.Quote(n.Value), nil // Go source code quoting format
//
//
//
default:
return "", fmt.Errorf("unsupported node type %s", nodeTypeString(n))
}
}
// 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) {
isPublic := true
for _, mod := range modifiers {
ident, ok := mod.(*node.Identifier)
if !ok {
return "", parseErr{mod, fmt.Errorf("expected node.Identifier")}
}
switch ident.Value {
case "public":
isPublic = true
case "private", "protected":
isPublic = false
}
}
if isPublic {
return toPublic(funcName), nil
} else {
return toPrivate(funcName), nil
}
}
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:]
}
// resolveName turns a `*name.Name` node into a Go string.
func resolveName(n node.Node) (string, error) {
// TODO support namespace lookups
paramType := unknownVarType
if vt, ok := n.(*name.Name); ok {
if len(vt.Parts) != 1 {
return "", parseErr{n, fmt.Errorf("name has %d parts, expected 1", len(vt.Parts))}
}
paramType = vt.Parts[0].(*name.NamePart).Value
}
return paramType, nil
}
// convertToStmtList asserts that the node is either a StmtList or wraps it in a
// single-stmt StmtList if not.
// Loop bodies may be a StmtList if it is wrapped in {}, or a single statement
// if it is not; we want to enforce the use of {} for all loop bodies
func convertToStmtList(n node.Node) *stmt.StmtList {
if sl, ok := n.(*stmt.StmtList); ok {
return sl // It's already a StmtList
}
return stmt.NewStmtList([]node.Node{n})
}
func 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 := convert(left)
if err != nil {
return "", parseErr{left, err}
}
rhs, err := convert(right)
if err != nil {
return "", parseErr{right, err}
}
return "(" + lhs + " " + goBinaryOperator + " " + rhs + ")", nil
}
func convertFuncCallArgsCommon(args *node.ArgumentList) (string, error) {
callParams := make([]string, 0, len(args.Arguments))
for _, arg_ := range args.Arguments {
arg, ok := arg_.(*node.Argument)
if !ok {
return "", parseErr{arg_, fmt.Errorf("expected node.Argument")}
}
rvalue, err := convert(arg.Expr)
if err != nil {
return "", parseErr{arg, err}
}
if arg.IsReference {
rvalue = "&" + rvalue
}
if arg.Variadic {
rvalue = "..." + rvalue
}
callParams = append(callParams, rvalue)
}
return "(" + strings.Join(callParams, `, `) + ")", nil // expr only, no semicolon/newline
}
func 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 := 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 := resolveName(returnType)
if err != nil {
return "", parseErr{returnType, err}
}
if returnsRef {
funcReturn = "*" + funcReturn
}
// Build function prototype
ret := "(" + strings.Join(funcParams, ", ") + ") (" + funcReturn + ", error) {\n"
// Recurse through body statements
for _, s := range bodyStmts {
bodyStmt, err := convert(s)
if err != nil {
return "", parseErr{s, err}
}
ret += bodyStmt + "\n"
}
// Done
// No extra trailing newline in case this is part of a large expression
ret += "}"
return ret, nil
}

60
scope.go Normal file
View File

@ -0,0 +1,60 @@
package main
const (
unknownVarType string = `unknown` // placeholder
mixedVarType string = `mixed` // when setting an incompatible type
)
type LocalVar struct {
Name string
Type string
}
type Scope struct {
parent *Scope
locals []LocalVar
}
func NewScope() *Scope {
return &Scope{}
}
func (this *Scope) NewScope() *Scope {
return &Scope{
parent: this,
}
}
func (this *Scope) Has(varName string) *LocalVar {
for idx := range this.locals {
if this.locals[idx].Name == varName {
return &this.locals[idx] // Mutable
}
}
if this.parent != nil {
return this.parent.Has(varName)
}
return nil // not found
}
func (this *Scope) Set(Name, Type string) *LocalVar {
if lv := this.Has(Name); lv != nil {
// Update known type for existing variable
if lv.Type == unknownVarType {
lv.Type = Type
} else if lv.Type == Type {
// no-op, more evidence for the same type
} else if lv.Type != Type {
// conflicting type information
lv.Type = mixedVarType
}
return lv
}
// Insert new
this.locals = append(this.locals, LocalVar{Name: Name, Type: Type})
return &this.locals[len(this.locals)-1]
}