Skip to content

Commit

Permalink
locate the real delete function for the typed Delete method
Browse files Browse the repository at this point in the history
  • Loading branch information
magodo committed Oct 25, 2021
1 parent 2eb77c0 commit 5ad5a0b
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 31 deletions.
33 changes: 33 additions & 0 deletions .tools/generate-provider-resource-mapping/ast_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/packages"
)

func functionDeclOfMethod(pkg *packages.Package, nt *types.Named, methodName string) (*ast.FuncDecl, error) {
fileMap := map[*token.File]*ast.File{}
for _, f := range pkg.Syntax {
fileMap[pkg.Fset.File(f.Pos())] = f
}

for i := 0; i < nt.NumMethods(); i++ {
method := nt.Method(i)
if method.Name() != methodName {
continue
}

f := fileMap[pkg.Fset.File(method.Pos())]
// Lookup the function declaration from the method identifier position.
// The returned enclosing interval starts from the identifier node, then the function declaration node.
nodes, _ := astutil.PathEnclosingInterval(f, method.Pos(), method.Pos())
fdecl := nodes[1].(*ast.FuncDecl)
return fdecl, nil
}

return nil, fmt.Errorf("failed to find the method %q in type %q", methodName, nt.Obj().Name())
}
12 changes: 11 additions & 1 deletion .tools/generate-provider-resource-mapping/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package main

import (
"errors"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/callgraph/cha"

"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/ssa"
Expand All @@ -11,6 +13,7 @@ import (
type Package struct {
GoPackage *packages.Package
SSAPackage *ssa.Package
CallGraph *callgraph.Graph
}

func loadPackage(dir string, args []string) ([]*Package, error) {
Expand All @@ -25,18 +28,25 @@ func loadPackage(dir string, args []string) ([]*Package, error) {
return nil, errors.New("go packages contain errors during loading")
}

_, ssapkgs := ssautil.Packages(gopkgs, 0)
prog, ssapkgs := ssautil.Packages(gopkgs, 0)
for _, p := range ssapkgs {
if p != nil {
p.Build()
}
}
// CHA is a good fit here since we are not building the SSA bodies for dependencies (ssautil.Packages).
// CHA is sound to run on partial program (i.e. no main package is required). In our case, we are using the CHA
// callgraph to find any callee of a provider resource method (typically the "Delete" method) which belong to
// the corresponding function from the Go SDK, but not necessarily need to follow the callee outside the package
// under processed.
graph := cha.CallGraph(prog)

for idx := range ssapkgs {
pkgs = append(pkgs,
&Package{
GoPackage: gopkgs[idx],
SSAPackage: ssapkgs[idx],
CallGraph: graph,
},
)
}
Expand Down
119 changes: 89 additions & 30 deletions .tools/generate-provider-resource-mapping/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@ import (
"errors"
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/ssa"
"log"
"strconv"

"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/packages"
)

func main() {
pkgs, err := loadPackage("/home/magodo/projects/terraform-provider-azurerm", []string{"./internal/sdk", "./internal/services/web"})
pkgs, err := loadPackage("/home/magodo/github/terraform-provider-azurerm", []string{"./internal/sdk", "./internal/services/web"})
if err != nil {
log.Fatal(err)
}
Expand Down Expand Up @@ -95,17 +92,17 @@ func handleTypedRegistration(pkg *Package, obj types.Object) (map[string]string,
if !ok {
return nil, fmt.Errorf("the returned resource %s is not a composite literal", pkg.GoPackage.Fset.Position(resExpr.Pos()))
}
resTypeObj, ok := pkg.GoPackage.TypesInfo.Defs[resComplit.Type.(*ast.Ident)]

resTypeObj, ok := pkg.GoPackage.TypesInfo.Uses[resComplit.Type.(*ast.Ident)]
if !ok {
return nil, fmt.Errorf("failed to find the type definition for %s", pkg.GoPackage.Fset.Position(resExpr.Pos()))
return nil, fmt.Errorf("failed to find the type info for %s", pkg.GoPackage.Fset.Position(resExpr.Pos()))
}

tfName, apiPath, err := handleTypedResource(pkg, resTypeObj)
if err != nil {
return nil, err
}
resourceMapping[tfName] = apiPath

}

return resourceMapping, nil
Expand Down Expand Up @@ -133,35 +130,97 @@ func handleTypedResource(pkg *Package, obj types.Object) (string, string, error)

// Identify the Azure API path.
// The API path comes from its Delete() method
deleteMethod := pkg.SSAPackage.Prog.LookupMethod(obj.Type(), obj.Pkg(), "Delete")
if deleteMethod == nil {
return "", "", fmt.Errorf(`can't find the "Delete" method for object %s in the SSA program`, pkg.GoPackage.Fset.Position(obj.Pos()))
}

return tfResourceType, "", errors.New("TODO")
}
// In current implementation, there are two patterns used to define the "Delete()" method:
// 1. Directly return a composite literal of the sdk.ResourceFunc: e.g. https://github.com/hashicorp/terraform-provider-azurerm/blob/cf19ce361eabe87192e3d5265d67d55ceb10e327/internal/services/web/app_service_environment_v3_resource.go#L393
// 2. Return a function invocation, which returns a sdk.ResourceFunc: e.g. https://github.com/hashicorp/terraform-provider-azurerm/blob/9c8a6259bee42db8c4611549b30155bc03586ba2/internal/services/policy/assignment_resource.go#L43
// Therefore, we need to do data flow analysis backwards from the return instruction.
deleteFunc, err := getRealDeleteFunc(pkg, deleteMethod)
if err != nil {
return "", "", err
}

func handleUntypedRegistration(pkg *Package, obj types.Object) (map[string]string, error) {
// TF resource type -> Azure api path
resourceMapping := map[string]string{}
return resourceMapping, nil
deleteFuncNode, ok := pkg.CallGraph.Nodes[deleteFunc]
if !ok {
return "", "", fmt.Errorf(`can't find the real "Delete" function for object %s in the callgraph`, pkg.GoPackage.Fset.Position(obj.Pos()))
}
callees := AllCalleesOf(deleteFuncNode)
_ = callees

return tfResourceType, "", errors.New("TODO")
}

func functionDeclOfMethod(pkg *packages.Package, nt *types.Named, methodName string) (*ast.FuncDecl, error) {
fileMap := map[*token.File]*ast.File{}
for _, f := range pkg.Syntax {
fileMap[pkg.Fset.File(f.Pos())] = f
}
func getRealDeleteFunc(pkg *Package, deleteMethod *ssa.Function) (*ssa.Function, error) {
bbs := deleteMethod.DomPreorder()
lastBB := bbs[len(bbs)-1]
returnInstr := lastBB.Instrs[len(lastBB.Instrs)-1].(*ssa.Return)

for i := 0; i < nt.NumMethods(); i++ {
method := nt.Method(i)
if method.Name() != methodName {
continue
switch ret := returnInstr.Results[0].(type) {
case *ssa.Call:
callcom := ret.Common()
if callcom.Method != nil {
return nil, fmt.Errorf("expected CallCommon to be call-mode, but got invoke-mode for the return value in %s", pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}
f, ok := callcom.Value.(*ssa.Function)
if !ok {
return nil, fmt.Errorf("expected value of CallCommon to be Function for the return value in %s", pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}
return getRealDeleteFunc(pkg, f)
case *ssa.UnOp:
referrers := ret.X.Referrers()
if referrers == nil {
return nil, fmt.Errorf(`unexpected nil referrers of the returned value of the "Delete" method %s in the SSA program`, pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}
var funcFieldAddr *ssa.FieldAddr
for _, referer := range *referrers {
fieldAddr, ok := referer.(*ssa.FieldAddr)
if !ok {
continue
}
if fieldAddr.Field != 0 {
continue
}
funcFieldAddr = fieldAddr
}
if funcFieldAddr == nil {
return nil, fmt.Errorf(`can't find FieldAddr for the "Func" field in the "Delete" method %s in the SSA program`, pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}

f := fileMap[pkg.Fset.File(method.Pos())]
// Lookup the function declaration from the method identifier position.
// The returned enclosing interval starts from the identifier node, then the function declaration node.
nodes, _ := astutil.PathEnclosingInterval(f, method.Pos(), method.Pos())
fdecl := nodes[1].(*ast.FuncDecl)
return fdecl, nil
referrers = funcFieldAddr.Referrers()
if referrers == nil {
return nil, fmt.Errorf(`unexpected nil referrers of the "Func" FieldAddr value of the "Delete" method %s in the SSA program`, pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}
var targetFunc *ssa.Function
for _, referrer := range *referrers {
store, ok := referrer.(*ssa.Store)
if !ok {
continue
}
changeType, ok := store.Val.(*ssa.ChangeType)
if !ok {
continue
}
f, ok := changeType.X.(*ssa.Function)
if !ok {
continue
}
targetFunc = f
}
if targetFunc == nil {
return nil, fmt.Errorf(`can't find Store instruction for the "Func" field in the "Delete" method %s in the SSA program`, pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}
return targetFunc, nil
default:
return nil, fmt.Errorf("unexpected type of the return value in %s", pkg.GoPackage.Fset.Position(deleteMethod.Pos()))
}
}

return nil, fmt.Errorf("failed to find the method %q in type %q", methodName, nt.Obj().Name())
func handleUntypedRegistration(pkg *Package, obj types.Object) (map[string]string, error) {
// TF resource type -> Azure api path
resourceMapping := map[string]string{}
return resourceMapping, nil
}
29 changes: 29 additions & 0 deletions .tools/generate-provider-resource-mapping/ssa_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import "golang.org/x/tools/go/callgraph"

func AllCalleesOf(root *callgraph.Node) map[*callgraph.Node]bool {
s := map[*callgraph.Node]bool{}
wl := map[*callgraph.Node]bool{root: true}
for len(wl) != 0 {
callees := map[*callgraph.Node]bool{}
for node := range wl {
directCallees := callgraph.CalleesOf(node)
for k := range directCallees {
callees[k] = true
}
}
for k := range wl {
s[k] = true
}

wl = map[*callgraph.Node]bool{}
for k := range callees {
if !s[k] {
wl[k] = true
}
}
}
delete(s, root)
return s
}

0 comments on commit 5ad5a0b

Please sign in to comment.