commit 872c8788573036f2531b0b546d82dce60261350f Author: mappu Date: Sun Apr 5 16:35:44 2020 +1200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b67e7f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Binaries +php2go + +# .. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ab9f63 --- /dev/null +++ b/README.md @@ -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 diff --git a/fixtures/0001.php b/fixtures/0001.php new file mode 100644 index 0000000..568f738 --- /dev/null +++ b/fixtures/0001.php @@ -0,0 +1,30 @@ +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(); diff --git a/fixtures/0001.php.parse.json b/fixtures/0001.php.parse.json new file mode 100644 index 0000000..938a94b --- /dev/null +++ b/fixtures/0001.php.parse.json @@ -0,0 +1,1168 @@ +{ + "type": "*node.Root", + "position": { + "startPos": 7, + "endPos": 400, + "startLine": 3, + "endLine": 30 + }, + "Stmts": [ + { + "type": "*stmt.Class", + "position": { + "startPos": 7, + "endPos": 250, + "startLine": 3, + "endLine": 19 + }, + "PhpDocComment": "", + "ClassName": { + "type": "*node.Identifier", + "position": { + "startPos": 13, + "endPos": 16, + "startLine": 3, + "endLine": 3 + }, + "Value": "Bar" + }, + "Stmts": [ + { + "type": "*stmt.PropertyList", + "position": { + "startPos": 20, + "endPos": 41, + "startLine": 4, + "endLine": 4 + }, + "Modifiers": [ + { + "type": "*node.Identifier", + "position": { + "startPos": 20, + "endPos": 29, + "startLine": 4, + "endLine": 4 + }, + "Value": "protected" + } + ], + "Properties": [ + { + "type": "*stmt.Property", + "position": { + "startPos": 30, + "endPos": 40, + "startLine": 4, + "endLine": 4 + }, + "PhpDocComment": "", + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 30, + "endPos": 33, + "startLine": 4, + "endLine": 4 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 30, + "endPos": 33, + "startLine": 4, + "endLine": 4 + }, + "Value": "mX" + } + }, + "Expr": { + "type": "*expr.ConstFetch", + "position": { + "startPos": 36, + "endPos": 40, + "startLine": 4, + "endLine": 4 + }, + "Constant": { + "type": "*name.Name", + "position": { + "startPos": 36, + "endPos": 40, + "startLine": 4, + "endLine": 4 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 36, + "endPos": 40, + "startLine": 4, + "endLine": 4 + }, + "Value": "null" + } + ] + } + } + } + ] + }, + { + "type": "*stmt.PropertyList", + "position": { + "startPos": 43, + "endPos": 61, + "startLine": 5, + "endLine": 5 + }, + "Modifiers": [ + { + "type": "*node.Identifier", + "position": { + "startPos": 43, + "endPos": 52, + "startLine": 5, + "endLine": 5 + }, + "Value": "protected" + } + ], + "Properties": [ + { + "type": "*stmt.Property", + "position": { + "startPos": 53, + "endPos": 60, + "startLine": 5, + "endLine": 5 + }, + "PhpDocComment": "", + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 53, + "endPos": 56, + "startLine": 5, + "endLine": 5 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 53, + "endPos": 56, + "startLine": 5, + "endLine": 5 + }, + "Value": "mY" + } + }, + "Expr": { + "type": "*scalar.Lnumber", + "position": { + "startPos": 59, + "endPos": 60, + "startLine": 5, + "endLine": 5 + }, + "Value": "3" + } + } + ] + }, + { + "type": "*stmt.ClassMethod", + "position": { + "startPos": 65, + "endPos": 119, + "startLine": 7, + "endLine": 9 + }, + "PhpDocComment": "", + "ReturnsRef": false, + "MethodName": { + "type": "*node.Identifier", + "position": { + "startPos": 74, + "endPos": 85, + "startLine": 7, + "endLine": 7 + }, + "Value": "__construct" + }, + "Params": [ + { + "type": "*node.Parameter", + "position": { + "startPos": 86, + "endPos": 95, + "startLine": 7, + "endLine": 7 + }, + "ByRef": false, + "Variadic": false, + "VariableType": { + "type": "*name.Name", + "position": { + "startPos": 86, + "endPos": 92, + "startLine": 7, + "endLine": 7 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 86, + "endPos": 92, + "startLine": 7, + "endLine": 7 + }, + "Value": "string" + } + ] + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 93, + "endPos": 95, + "startLine": 7, + "endLine": 7 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 93, + "endPos": 95, + "startLine": 7, + "endLine": 7 + }, + "Value": "x" + } + } + } + ], + "Stmt": { + "type": "*stmt.StmtList", + "position": { + "startPos": 97, + "endPos": 119, + "startLine": 7, + "endLine": 9 + }, + "Stmts": [ + { + "type": "*stmt.Expression", + "position": { + "startPos": 101, + "endPos": 116, + "startLine": 8, + "endLine": 8 + }, + "Expr": { + "type": "*assign.Assign", + "position": { + "startPos": 101, + "endPos": 115, + "startLine": 8, + "endLine": 8 + }, + "Variable": { + "type": "*expr.PropertyFetch", + "position": { + "startPos": 101, + "endPos": 110, + "startLine": 8, + "endLine": 8 + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 101, + "endPos": 106, + "startLine": 8, + "endLine": 8 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 101, + "endPos": 106, + "startLine": 8, + "endLine": 8 + }, + "Value": "this" + } + }, + "Property": { + "type": "*node.Identifier", + "position": { + "startPos": 108, + "endPos": 110, + "startLine": 8, + "endLine": 8 + }, + "Value": "mX" + } + }, + "Expression": { + "type": "*expr.Variable", + "position": { + "startPos": 113, + "endPos": 115, + "startLine": 8, + "endLine": 8 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 113, + "endPos": 115, + "startLine": 8, + "endLine": 8 + }, + "Value": "x" + } + } + } + } + ] + } + }, + { + "type": "*stmt.ClassMethod", + "position": { + "startPos": 123, + "endPos": 200, + "startLine": 11, + "endLine": 14 + }, + "PhpDocComment": "", + "ReturnsRef": false, + "MethodName": { + "type": "*node.Identifier", + "position": { + "startPos": 132, + "endPos": 137, + "startLine": 11, + "endLine": 11 + }, + "Value": "hello" + }, + "Stmt": { + "type": "*stmt.StmtList", + "position": { + "startPos": 140, + "endPos": 200, + "startLine": 11, + "endLine": 14 + }, + "Stmts": [ + { + "type": "*stmt.Echo", + "position": { + "startPos": 144, + "endPos": 166, + "startLine": 12, + "endLine": 12 + }, + "Exprs": [ + { + "type": "*binary.Concat", + "position": { + "startPos": 149, + "endPos": 165, + "startLine": 12, + "endLine": 12 + }, + "Left": { + "type": "*expr.PropertyFetch", + "position": { + "startPos": 149, + "endPos": 158, + "startLine": 12, + "endLine": 12 + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 149, + "endPos": 154, + "startLine": 12, + "endLine": 12 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 149, + "endPos": 154, + "startLine": 12, + "endLine": 12 + }, + "Value": "this" + } + }, + "Property": { + "type": "*node.Identifier", + "position": { + "startPos": 156, + "endPos": 158, + "startLine": 12, + "endLine": 12 + }, + "Value": "mX" + } + }, + "Right": { + "type": "*scalar.String", + "position": { + "startPos": 161, + "endPos": 165, + "startLine": 12, + "endLine": 12 + }, + "Value": "\"\\n\"" + } + } + ] + }, + { + "type": "*stmt.Throw", + "position": { + "startPos": 169, + "endPos": 197, + "startLine": 13, + "endLine": 13 + }, + "Expr": { + "type": "*expr.New", + "position": { + "startPos": 175, + "endPos": 196, + "startLine": 13, + "endLine": 13 + }, + "Class": { + "type": "*name.Name", + "position": { + "startPos": 179, + "endPos": 188, + "startLine": 13, + "endLine": 13 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 179, + "endPos": 188, + "startLine": 13, + "endLine": 13 + }, + "Value": "Exception" + } + ] + }, + "ArgumentList": { + "type": "*node.ArgumentList", + "position": { + "startPos": 188, + "endPos": 196, + "startLine": 13, + "endLine": 13 + }, + "Arguments": [ + { + "type": "*node.Argument", + "position": { + "startPos": 189, + "endPos": 195, + "startLine": 13, + "endLine": 13 + }, + "Variadic": false, + "IsReference": false, + "Expr": { + "type": "*scalar.String", + "position": { + "startPos": 189, + "endPos": 195, + "startLine": 13, + "endLine": 13 + }, + "Value": "\"asdf\"" + } + } + ] + } + } + } + ] + } + }, + { + "type": "*stmt.ClassMethod", + "position": { + "startPos": 204, + "endPos": 248, + "startLine": 16, + "endLine": 18 + }, + "ReturnsRef": false, + "PhpDocComment": "", + "MethodName": { + "type": "*node.Identifier", + "position": { + "startPos": 213, + "endPos": 226, + "startLine": 16, + "endLine": 16 + }, + "Value": "scalarThrower" + }, + "Stmt": { + "type": "*stmt.StmtList", + "position": { + "startPos": 229, + "endPos": 248, + "startLine": 16, + "endLine": 18 + }, + "Stmts": [ + { + "type": "*stmt.Throw", + "position": { + "startPos": 233, + "endPos": 245, + "startLine": 17, + "endLine": 17 + }, + "Expr": { + "type": "*scalar.String", + "position": { + "startPos": 239, + "endPos": 244, + "startLine": 17, + "endLine": 17 + }, + "Value": "\"str\"" + } + } + ] + } + } + ] + }, + { + "type": "*stmt.Function", + "position": { + "startPos": 252, + "endPos": 306, + "startLine": 21, + "endLine": 23 + }, + "PhpDocComment": "", + "ReturnsRef": false, + "FunctionName": { + "type": "*node.Identifier", + "position": { + "startPos": 261, + "endPos": 264, + "startLine": 21, + "endLine": 21 + }, + "Value": "foo" + }, + "Params": [ + { + "type": "*node.Parameter", + "position": { + "startPos": 265, + "endPos": 267, + "startLine": 21, + "endLine": 21 + }, + "Variadic": false, + "ByRef": false, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 265, + "endPos": 267, + "startLine": 21, + "endLine": 21 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 265, + "endPos": 267, + "startLine": 21, + "endLine": 21 + }, + "Value": "a" + } + } + }, + { + "type": "*node.Parameter", + "position": { + "startPos": 269, + "endPos": 275, + "startLine": 21, + "endLine": 21 + }, + "Variadic": false, + "ByRef": false, + "VariableType": { + "type": "*name.Name", + "position": { + "startPos": 269, + "endPos": 272, + "startLine": 21, + "endLine": 21 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 269, + "endPos": 272, + "startLine": 21, + "endLine": 21 + }, + "Value": "int" + } + ] + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 273, + "endPos": 275, + "startLine": 21, + "endLine": 21 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 273, + "endPos": 275, + "startLine": 21, + "endLine": 21 + }, + "Value": "b" + } + } + } + ], + "ReturnType": { + "type": "*name.Name", + "position": { + "startPos": 278, + "endPos": 281, + "startLine": 21, + "endLine": 21 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 278, + "endPos": 281, + "startLine": 21, + "endLine": 21 + }, + "Value": "int" + } + ] + }, + "Stmts": [ + { + "type": "*stmt.Return", + "position": { + "startPos": 285, + "endPos": 304, + "startLine": 22, + "endLine": 22 + }, + "Expr": { + "type": "*binary.Plus", + "position": { + "startPos": 292, + "endPos": 303, + "startLine": 22, + "endLine": 22 + }, + "Left": { + "type": "*binary.Plus", + "position": { + "startPos": 292, + "endPos": 298, + "startLine": 22, + "endLine": 22 + }, + "Left": { + "type": "*scalar.Lnumber", + "position": { + "startPos": 292, + "endPos": 293, + "startLine": 22, + "endLine": 22 + }, + "Value": "3" + }, + "Right": { + "type": "*expr.Variable", + "position": { + "startPos": 296, + "endPos": 298, + "startLine": 22, + "endLine": 22 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 296, + "endPos": 298, + "startLine": 22, + "endLine": 22 + }, + "Value": "a" + } + } + }, + "Right": { + "type": "*expr.Variable", + "position": { + "startPos": 301, + "endPos": 303, + "startLine": 22, + "endLine": 22 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 301, + "endPos": 303, + "startLine": 22, + "endLine": 22 + }, + "Value": "b" + } + } + } + } + ] + }, + { + "type": "*stmt.For", + "position": { + "startPos": 308, + "endPos": 361, + "startLine": 25, + "endLine": 27 + }, + "Init": [ + { + "type": "*assign.Assign", + "position": { + "startPos": 313, + "endPos": 319, + "startLine": 25, + "endLine": 25 + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 313, + "endPos": 315, + "startLine": 25, + "endLine": 25 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 313, + "endPos": 315, + "startLine": 25, + "endLine": 25 + }, + "Value": "i" + } + }, + "Expression": { + "type": "*scalar.Lnumber", + "position": { + "startPos": 318, + "endPos": 319, + "startLine": 25, + "endLine": 25 + }, + "Value": "0" + } + } + ], + "Cond": [ + { + "type": "*binary.Smaller", + "position": { + "startPos": 321, + "endPos": 327, + "startLine": 25, + "endLine": 25 + }, + "Left": { + "type": "*expr.Variable", + "position": { + "startPos": 321, + "endPos": 323, + "startLine": 25, + "endLine": 25 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 321, + "endPos": 323, + "startLine": 25, + "endLine": 25 + }, + "Value": "i" + } + }, + "Right": { + "type": "*scalar.Lnumber", + "position": { + "startPos": 326, + "endPos": 327, + "startLine": 25, + "endLine": 25 + }, + "Value": "3" + } + } + ], + "Loop": [ + { + "type": "*expr.PreInc", + "position": { + "startPos": 329, + "endPos": 333, + "startLine": 25, + "endLine": 25 + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 331, + "endPos": 333, + "startLine": 25, + "endLine": 25 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 331, + "endPos": 333, + "startLine": 25, + "endLine": 25 + }, + "Value": "i" + } + } + } + ], + "Stmt": { + "type": "*stmt.StmtList", + "position": { + "startPos": 335, + "endPos": 361, + "startLine": 25, + "endLine": 27 + }, + "Stmts": [ + { + "type": "*stmt.Echo", + "position": { + "startPos": 338, + "endPos": 359, + "startLine": 26, + "endLine": 26 + }, + "Exprs": [ + { + "type": "*binary.Concat", + "position": { + "startPos": 343, + "endPos": 358, + "startLine": 26, + "endLine": 26 + }, + "Left": { + "type": "*expr.FunctionCall", + "position": { + "startPos": 343, + "endPos": 353, + "startLine": 26, + "endLine": 26 + }, + "Function": { + "type": "*name.Name", + "position": { + "startPos": 343, + "endPos": 346, + "startLine": 26, + "endLine": 26 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 343, + "endPos": 346, + "startLine": 26, + "endLine": 26 + }, + "Value": "foo" + } + ] + }, + "ArgumentList": { + "type": "*node.ArgumentList", + "position": { + "startPos": 346, + "endPos": 353, + "startLine": 26, + "endLine": 26 + }, + "Arguments": [ + { + "type": "*node.Argument", + "position": { + "startPos": 347, + "endPos": 349, + "startLine": 26, + "endLine": 26 + }, + "Variadic": false, + "IsReference": false, + "Expr": { + "type": "*expr.Variable", + "position": { + "startPos": 347, + "endPos": 349, + "startLine": 26, + "endLine": 26 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 347, + "endPos": 349, + "startLine": 26, + "endLine": 26 + }, + "Value": "i" + } + } + }, + { + "type": "*node.Argument", + "position": { + "startPos": 351, + "endPos": 352, + "startLine": 26, + "endLine": 26 + }, + "Variadic": false, + "IsReference": false, + "Expr": { + "type": "*scalar.Lnumber", + "position": { + "startPos": 351, + "endPos": 352, + "startLine": 26, + "endLine": 26 + }, + "Value": "2" + } + } + ] + } + }, + "Right": { + "type": "*scalar.String", + "position": { + "startPos": 354, + "endPos": 358, + "startLine": 26, + "endLine": 26 + }, + "Value": "\"\\n\"" + } + } + ] + } + ] + } + }, + { + "type": "*stmt.Expression", + "position": { + "startPos": 363, + "endPos": 386, + "startLine": 29, + "endLine": 29 + }, + "Expr": { + "type": "*assign.Assign", + "position": { + "startPos": 363, + "endPos": 385, + "startLine": 29, + "endLine": 29 + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 363, + "endPos": 366, + "startLine": 29, + "endLine": 29 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 363, + "endPos": 366, + "startLine": 29, + "endLine": 29 + }, + "Value": "bb" + } + }, + "Expression": { + "type": "*expr.New", + "position": { + "startPos": 369, + "endPos": 385, + "startLine": 29, + "endLine": 29 + }, + "Class": { + "type": "*name.Name", + "position": { + "startPos": 373, + "endPos": 376, + "startLine": 29, + "endLine": 29 + }, + "Parts": [ + { + "type": "*name.NamePart", + "position": { + "startPos": 373, + "endPos": 376, + "startLine": 29, + "endLine": 29 + }, + "Value": "Bar" + } + ] + }, + "ArgumentList": { + "type": "*node.ArgumentList", + "position": { + "startPos": 376, + "endPos": 385, + "startLine": 29, + "endLine": 29 + }, + "Arguments": [ + { + "type": "*node.Argument", + "position": { + "startPos": 377, + "endPos": 384, + "startLine": 29, + "endLine": 29 + }, + "Variadic": false, + "IsReference": false, + "Expr": { + "type": "*scalar.String", + "position": { + "startPos": 377, + "endPos": 384, + "startLine": 29, + "endLine": 29 + }, + "Value": "\"hello\"" + } + } + ] + } + } + } + }, + { + "type": "*stmt.Expression", + "position": { + "startPos": 387, + "endPos": 400, + "startLine": 30, + "endLine": 30 + }, + "Expr": { + "type": "*expr.MethodCall", + "position": { + "startPos": 387, + "endPos": 399, + "startLine": 30, + "endLine": 30 + }, + "Variable": { + "type": "*expr.Variable", + "position": { + "startPos": 387, + "endPos": 390, + "startLine": 30, + "endLine": 30 + }, + "VarName": { + "type": "*node.Identifier", + "position": { + "startPos": 387, + "endPos": 390, + "startLine": 30, + "endLine": 30 + }, + "Value": "bb" + } + }, + "Method": { + "type": "*node.Identifier", + "position": { + "startPos": 392, + "endPos": 397, + "startLine": 30, + "endLine": 30 + }, + "Value": "hello" + }, + "ArgumentList": { + "type": "*node.ArgumentList", + "position": { + "startPos": 397, + "endPos": 399, + "startLine": 30, + "endLine": 30 + } + } + } + } + ] +} \ No newline at end of file diff --git a/fixtures/0002-loops.php b/fixtures/0002-loops.php new file mode 100644 index 0000000..31319f3 --- /dev/null +++ b/fixtures/0002-loops.php @@ -0,0 +1,21 @@ + $v) { + } + + foreach($foo2 as $v2) { + } + + while(true) { + } + + do { + } while (true); + +} + +// Loop with no separate body statement +while (true) echo "hello"; diff --git a/fixtures/0002-loops.php.parse.json b/fixtures/0002-loops.php.parse.json new file mode 100644 index 0000000..57748ed --- /dev/null +++ b/fixtures/0002-loops.php.parse.json @@ -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\"" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/fixtures_test.go b/fixtures_test.go new file mode 100644 index 0000000..082ef15 --- /dev/null +++ b/fixtures_test.go @@ -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) + } + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3fccbc3 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module php2go + +go 1.13 + +require github.com/z7zmey/php-parser v0.7.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f59bbb5 --- /dev/null +++ b/go.sum @@ -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= diff --git a/literal.go b/literal.go new file mode 100644 index 0000000..6a9b73e --- /dev/null +++ b/literal.go @@ -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{} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a8aec8e --- /dev/null +++ b/main.go @@ -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) +} diff --git a/node.go b/node.go new file mode 100644 index 0000000..f97a902 --- /dev/null +++ b/node.go @@ -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 +} diff --git a/scope.go b/scope.go new file mode 100644 index 0000000..4f94f67 --- /dev/null +++ b/scope.go @@ -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] +}