sollniss
sollniss

Reputation: 2005

Is it possible to find out dependencies of a function parameter using static analysis in Go?

I'm trying to build a linter that is able to check whether a specific function A is called from a set of functions B and report any function B' that does not call A (directly or indirectly).

I'm using the analysis framework so every package is analysed separately. Using Facts to export type information and importing that type information whenever I find a function B', I am able to build a call graph of the current package and related dependencies (that result in a call to A) and finally check whether there exists a connection from B to A.

Everything works fine for my test cases, but as soon as I test it on a real project, the analysis takes forever to run (literally times out).

I've figured out that I add too many dependencies when finding a function in B. What I do currently is pre-analyze every package and look for A and any function that calls A. Once I find a transitive call to A, I mark the package.

In the next step, after I find a B', I check every import and build the imported package only if it was marked (non-marked packages are added to the ssa.Program as dummies).

Now, this does not work with functions like this:

func Caller(callee func()) { callee() }

When a function is passed as a parameter to a function in a different package, I lose all information about it (because everything is modular) and I can't know what packages imports the package with Caller.

For this reason, I mark every package that exports a function with a function type parameter as interesting (I know you can have functions in structs, I still need to figure out how to solve that).

So now, for my actual question: Is it possible to know for a function call a(b) what dependencies each of the parameters attracts (what packages they are used by)?

With this information I could build a program that only contains relevant packages from B' to A, I think.

I would also be fine with only the dependencies of the function call itself.

I use the following code to generate the program:

preScanRes, ok := pass.ResultOf[PreScanAnalyzer].(*preScanResult)
if !ok {
    panic("no pre scan result")
}

if preScanRes.canAccessCallee || preScanRes.hasFuncWithFuncParam {
    // Export type info of the current package.
    fact := typesFact{pass.TypesInfo}
    pass.ExportPackageFact(&fact)
}

if !preScanRes.hasCaller {
    return nil, nil
}

// Build program.

prog := ssa.NewProgram(pass.Fset, ssa.InstantiateGenerics)

// Add imported packages to the program.
// This is similar to what buildssa.Analyzer does, but we also build imported packages.
var addImports func(pp []*types.Package, depth int)
addImports = func(pp []*types.Package, depth int) {
    // For debugging only.
    //if depth > 2 {
    //  return
    //}
    for _, p := range pp {
        tfact := &typesFact{}
        // To build the imported package, we need it's type info.
        if ok := pass.ImportPackageFact(p, tfact); !ok {
            // No data. Create as dummy.
            pkg := prog.CreatePackage(p, nil, nil, true)
            pkg.Build()
            continue
        }

        // Get package files from type info.
        files := make([]*ast.File, len(tfact.typesInfo.FileVersions))
        i := 0
        for f := range tfact.typesInfo.FileVersions {
            files[i] = f
            i++
        }
        // Add package to program.
        pkg := prog.CreatePackage(p, files, tfact.typesInfo, true)

        // Add imports of the imported package.
        addImports(p.Imports(), depth+1)

        pkg.Build()
    }
}
addImports(pass.Pkg.Imports(), 0)

// Add the primary package.
prog.CreatePackage(pass.Pkg, pass.Files, pass.TypesInfo, false)
prog.Build()

// Build call graph, find path from B to A.
// ...

Upvotes: -3

Views: 79

Answers (0)

Related Questions