diff --git a/README.md b/README.md index 2d35450..341e6a7 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ The goal is to produce idiomatic, maintainable Go code as part of a one-off conv - [X] Static methods - [X] Inheritance *partial* - [ ] Interfaces - - [ ] super - - [ ] Need to track current conversion state through into function generator, to select the concrete parent - - [ ] parent:: + - [X] super + - [X] Need to track current conversion state through into function generator, to select the concrete parent + - [X] parent:: - [ ] static, self - [ ] Traits / `use` - [ ] Abstract methods diff --git a/fixtures/0005-inheritance.php b/fixtures/0005-inheritance.php index 48d59d9..cc4e9ed 100644 --- a/fixtures/0005-inheritance.php +++ b/fixtures/0005-inheritance.php @@ -7,11 +7,13 @@ class Base { $this->mX = $x; } - public function Val() { - return 3; + public function Val(): int { + return self::TheStatic(); } - static function TheStatic() {} + static function TheStatic(): int { + return 3; + } } class Child extends Base { diff --git a/node.go b/node.go index 6c4ab85..3128709 100644 --- a/node.go +++ b/node.go @@ -37,8 +37,12 @@ func (pe parseErr) Unwrap() error { // type conversionState struct { + currentClassName string + currentClassParentName string } +// + func (this *conversionState) convert(n_ node.Node) (string, error) { switch n := n_.(type) { @@ -106,17 +110,24 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { 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{} memberFuncs := []string{} if n.Extends != nil { - parentName, err := resolveName(n.Extends.ClassName) + 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 @@ -174,7 +185,7 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { if err != nil { return "", parseErr{s, err} } - memberFuncStmt := "func New" + className + funcStmt + "\n" + memberFuncStmt := "func " + constructorName(className) + funcStmt + "\n" memberFuncs = append(memberFuncs, memberFuncStmt) } else { @@ -213,14 +224,21 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { ret += strings.Join(memberFuncs, "\n\n") // Done + // Reinstate parent state before returning + this.currentClassName = prevClassName + this.currentClassParentName = prevClassParentName return ret, nil case *stmt.Function: - // Top-level 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) @@ -379,6 +397,42 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { 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(??) + + // Use a child scope for the temporary name + ret := "{\n" + ret += "super, err := " + constructorName(this.currentClassParentName) + funcArgs + "\n" + ret += "if err != nil {\n" + ret += "return err\n" + ret += "}\n" + ret += "this." + this.currentClassParentName + " = *super\n" + ret += "}\n" + ret += "\n" + return ret, nil + } + } + + // Regular case + child, err := this.convert(n.Expr) if err != nil { return "", parseErr{n, err} @@ -459,11 +513,15 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { // 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) + funcName, err := this.resolveName(n.Function) if err != nil { return "", parseErr{n, err} } + if funcName == "super" { + return "", parseErr{n, fmt.Errorf("Unexpected use of parent constructor in rvalue context")} + } + callParams, err := this.convertFuncCallArgsCommon(n.ArgumentList) if err != nil { return "", parseErr{n, err} @@ -472,7 +530,7 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { return funcName + callParams, nil // expr only, no semicolon/newline case *expr.StaticCall: - className, err := resolveName(n.Class) + className, err := this.resolveName(n.Class) if err != nil { return "", parseErr{n, err} } @@ -492,7 +550,7 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { case *expr.New: // new foo(xx) // Transparently convert to calling constructor function. - nn, err := resolveName(n.Class) + nn, err := this.resolveName(n.Class) if err != nil { return "", parseErr{n, err} } @@ -560,7 +618,7 @@ func (this *conversionState) convert(n_ node.Node) (string, error) { return n.VarName.(*node.Identifier).Value, nil case *expr.ConstFetch: - return resolveName(n.Constant) + return this.resolveName(n.Constant) case *expr.Array: return this.convertArrayLiteralCommon(n.Items) @@ -796,8 +854,12 @@ func toPrivate(name string) string { return lFirst + name[1:] } +func constructorName(className string) string { + return `New` + className +} + // resolveName turns a `*name.Name` node into a Go string. -func resolveName(n node.Node) (string, error) { +func (this *conversionState) resolveName(n node.Node) (string, error) { // TODO support namespace lookups paramType := unknownVarType @@ -808,6 +870,15 @@ func resolveName(n node.Node) (string, error) { paramType = vt.Parts[0].(*name.NamePart).Value } + // Handle class lookups + if strings.ToLower(paramType) == "parent" { + if this.currentClassParentName != "" { + return `this.` + this.currentClassParentName, nil + } else { + return "", parseErr{n, fmt.Errorf("Lookup of 'parent' while not in an inherited child class context")} + } + } + return paramType, nil } @@ -926,7 +997,7 @@ func (this *conversionState) convertFunctionCommon(params []node.Node, returnTyp } // VariableType: might be nil for untyped parameters - paramType, err := resolveName(param.VariableType) + paramType, err := this.resolveName(param.VariableType) if err != nil { return "", parseErr{param, err} } @@ -944,7 +1015,7 @@ func (this *conversionState) convertFunctionCommon(params []node.Node, returnTyp } // ReturnType - funcReturn, err := resolveName(returnType) + funcReturn, err := this.resolveName(returnType) if err != nil { return "", parseErr{returnType, err} }