Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 121 additions & 6 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ type Config struct {
// CapabilitySet is the set of capabilities to use for graph output mode.
// If CapabilitySet is nil, all capabilities are used.
CapabilitySet *CapabilitySet
// GraphFunction limits graph-json output to paths starting at the named
// function. The name must match the call graph function name exactly.
GraphFunction string
// GraphPerFunction emits a separate graph-json graph for each reachable
// function in the queried packages.
GraphPerFunction bool
// OmitPaths disables output of example call paths.
OmitPaths bool
}
Expand Down Expand Up @@ -72,11 +78,11 @@ func GetClassifier(excludeUnanalyzed bool) *interesting.Classifier {
// those packages that have a path in the callgraph to a function with a
// capability.
//
// GetCapabilityInfo does not return every possible path (see the function
// CapabilityGraph for a way to get all paths). Which entries are returned
// depends on the value of Config.Granularity:
// Which entries are returned depends on the value of Config.Granularity:
// - For "function" granularity (the default), one CapabilityInfo is returned
// for each combination of capability and function in pkgs.
// for each call path from a function in pkgs to a capability. If OmitPaths
// is set, paths cannot be distinguished in the output, so one entry is
// returned for each combination of capability and function in pkgs.
// - For "package" granularity, one CapabilityInfo is returned for each
// combination of capability and package in pkgs.
// - For "intermediate" granularity, one CapabilityInfo is returned for each
Expand All @@ -94,7 +100,11 @@ func GetCapabilityInfo(pkgs []*packages.Package, queriedPackages map[*types.Pack
*ssa.Function // used for sorting
}
var caps []output
forEachPath(pkgs, queriedPackages,
pathFn := forEachPath
if config.Granularity == GranularityFunction && !config.OmitPaths {
pathFn = forEachSimplePath
}
pathFn(pkgs, queriedPackages,
func(cap string, nodes bfsStateMap, v *callgraph.Node) {
i := 0
c := cpb.CapabilityInfo{}
Expand Down Expand Up @@ -137,7 +147,10 @@ func GetCapabilityInfo(pkgs []*packages.Package, queriedPackages map[*types.Pack
if x, y := caps[i].CapabilityInfo.GetCapability(), caps[j].CapabilityInfo.GetCapability(); x != y {
return x < y
}
return funcCompare(caps[i].Function, caps[j].Function) < 0
if c := funcCompare(caps[i].Function, caps[j].Function); c != 0 {
return c < 0
}
return caps[i].CapabilityInfo.GetDepPath() < caps[j].CapabilityInfo.GetDepPath()
})
if config.Granularity == GranularityPackage {
// Keep only the first entry in the sorted list for each (capability, package) pair.
Expand Down Expand Up @@ -790,6 +803,108 @@ func forEachPath(pkgs []*packages.Package, queriedPackages map[*types.Package]st
}
}

// forEachSimplePath calls fn once for each simple call path from a function in
// queriedPackages to a function with the current capability.
//
// It first runs the same backward search as forEachPath to find nodes that can
// reach the capability, then walks forward through only those nodes. The
// per-callback bfsStateMap contains the edges for exactly one path.
func forEachSimplePath(pkgs []*packages.Package, queriedPackages map[*types.Package]struct{},
fn func(cap string, nodes bfsStateMap, v *callgraph.Node), config *Config,
) {
safe, nodesByCapability, extraNodesByCapability := getPackageNodesWithCapability(pkgs, config)
nodesByCapability, allNodesWithExplicitCapability := mergeCapabilities(nodesByCapability, extraNodesByCapability)
extraNodesByCapability = nil // we don't use extraNodesByCapability again.
var caps []string
for cap := range nodesByCapability {
caps = append(caps, cap)
}
sort.Strings(caps)
for _, cap := range caps {
nodes := nodesByCapability[cap]
visited := searchBackwardsFromCapabilities(
nodesetPerCapability{cap: nodes},
safe,
allNodesWithExplicitCapability,
config.Classifier)

var queriedNodes []*callgraph.Node
for w := range visited {
if w.Func == nil || w.Func.Package() == nil {
continue
}
if _, ok := queriedPackages[w.Func.Package().Pkg]; !ok {
continue
}
queriedNodes = append(queriedNodes, w)
}
sort.Sort(byFunction(queriedNodes))
for _, w := range queriedNodes {
forEachSimplePathFrom(cap, w, nodes, visited, config.Classifier, fn)
}
}
}

func forEachSimplePathFrom(cap string, start *callgraph.Node, targetNodes nodeset,
canReachTarget bfsStateMap, classifier Classifier,
fn func(cap string, nodes bfsStateMap, v *callgraph.Node),
) {
var path []*callgraph.Edge
onPath := make(nodeset)
var walk func(*callgraph.Node)
walk = func(v *callgraph.Node) {
if _, ok := targetNodes[v]; ok {
pathState := make(bfsStateMap, len(path)+1)
for _, edge := range path {
pathState[edge.Caller] = bfsState{edge: edge}
}
pathState[v] = bfsState{}
fn(cap, pathState, start)
return
}
onPath[v] = struct{}{}
defer delete(onPath, v)

for _, edge := range reachableOutgoingEdges(v, canReachTarget, classifier) {
if _, ok := onPath[edge.Callee]; ok {
continue
}
path = append(path, edge)
walk(edge.Callee)
path = path[:len(path)-1]
}
}
walk(start)
}

func reachableOutgoingEdges(v *callgraph.Node, canReachTarget bfsStateMap, classifier Classifier) []*callgraph.Edge {
var outEdges []*callgraph.Edge
for _, edge := range v.Out {
if !classifier.IncludeCall(edge) {
continue
}
if edge.Callee == nil || edge.Callee.Func == nil {
continue
}
if _, ok := canReachTarget[edge.Callee]; !ok {
continue
}
outEdges = append(outEdges, edge)
}
sort.Sort(byCallee(outEdges))
// The JSON path uses function names, so multiple callsites to the same
// callee would produce duplicate-looking paths. Keep the first callsite in
// source order, matching CapabilityGraph's node-edge behavior.
deduped := outEdges[:0]
for _, edge := range outEdges {
if len(deduped) != 0 && edge.Callee == deduped[len(deduped)-1].Callee {
continue
}
deduped = append(deduped, edge)
}
return deduped
}

// intermediatePackages returns a CapabilityInfo for each unique (P, C) pair
// where there is a call path from a function in one of the queried packages
// to a function with capability C, and the call path includes a function in
Expand Down
205 changes: 205 additions & 0 deletions analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,211 @@ func TestAnalysisWithClassifier(t *testing.T) {
}
}

func TestAnalysisReportsPathsThatDivergeAfterIntermediateCall(t *testing.T) {
filemap := map[string]string{
"example.com/testlib/testlib.go": `package testlib

import "example.com/dep"

func A() { B() }
func B() {
dep.C()
dep.D()
}
`,
"example.com/dep/dep.go": `package dep

func C() {}
func D() {}
`,
}
classifier := testClassifier{
functions: map[[2]string]string{
{"example.com/dep", "example.com/dep.C"}: "FILES",
{"example.com/dep", "example.com/dep.D"}: "FILES",
},
ignoredEdges: nil,
}
pkgs, queriedPackages, cleanup, err := setup(filemap, "example.com/testlib")
if cleanup != nil {
defer cleanup()
}
if err != nil {
t.Fatalf("setup: %v", err)
}
cil := GetCapabilityInfo(pkgs, queriedPackages, &Config{
Classifier: &classifier,
DisableBuiltin: true,
})
expected := &cpb.CapabilityInfoList{
CapabilityInfo: []*cpb.CapabilityInfo{{
PackageName: proto.String("testlib"),
Capability: cpb.Capability_CAPABILITY_FILES.Enum(),
CapabilityName: proto.String("FILES"),
DepPath: proto.String("example.com/testlib.A example.com/testlib.B example.com/dep.C"),
Path: []*cpb.Function{
&cpb.Function{Name: proto.String("example.com/testlib.A"), Package: proto.String("example.com/testlib")},
&cpb.Function{Name: proto.String("example.com/testlib.B"), Package: proto.String("example.com/testlib")},
&cpb.Function{Name: proto.String("example.com/dep.C"), Package: proto.String("example.com/dep")},
},
PackageDir: proto.String("example.com/testlib"),
CapabilityType: cpb.CapabilityType_CAPABILITY_TYPE_TRANSITIVE.Enum(),
}, {
PackageName: proto.String("testlib"),
Capability: cpb.Capability_CAPABILITY_FILES.Enum(),
CapabilityName: proto.String("FILES"),
DepPath: proto.String("example.com/testlib.A example.com/testlib.B example.com/dep.D"),
Path: []*cpb.Function{
&cpb.Function{Name: proto.String("example.com/testlib.A"), Package: proto.String("example.com/testlib")},
&cpb.Function{Name: proto.String("example.com/testlib.B"), Package: proto.String("example.com/testlib")},
&cpb.Function{Name: proto.String("example.com/dep.D"), Package: proto.String("example.com/dep")},
},
PackageDir: proto.String("example.com/testlib"),
CapabilityType: cpb.CapabilityType_CAPABILITY_TYPE_TRANSITIVE.Enum(),
}, {
PackageName: proto.String("testlib"),
Capability: cpb.Capability_CAPABILITY_FILES.Enum(),
CapabilityName: proto.String("FILES"),
DepPath: proto.String("example.com/testlib.B example.com/dep.C"),
Path: []*cpb.Function{
&cpb.Function{Name: proto.String("example.com/testlib.B"), Package: proto.String("example.com/testlib")},
&cpb.Function{Name: proto.String("example.com/dep.C"), Package: proto.String("example.com/dep")},
},
PackageDir: proto.String("example.com/testlib"),
CapabilityType: cpb.CapabilityType_CAPABILITY_TYPE_TRANSITIVE.Enum(),
}, {
PackageName: proto.String("testlib"),
Capability: cpb.Capability_CAPABILITY_FILES.Enum(),
CapabilityName: proto.String("FILES"),
DepPath: proto.String("example.com/testlib.B example.com/dep.D"),
Path: []*cpb.Function{
&cpb.Function{Name: proto.String("example.com/testlib.B"), Package: proto.String("example.com/testlib")},
&cpb.Function{Name: proto.String("example.com/dep.D"), Package: proto.String("example.com/dep")},
},
PackageDir: proto.String("example.com/testlib"),
CapabilityType: cpb.CapabilityType_CAPABILITY_TYPE_TRANSITIVE.Enum(),
}},
}
opts := []cmp.Option{
protocmp.Transform(),
protocmp.SortRepeated(func(a, b *cpb.CapabilityInfo) bool {
return a.GetDepPath() < b.GetDepPath()
}),
protocmp.IgnoreFields(&cpb.CapabilityInfoList{}, "package_info"),
protocmp.IgnoreFields(&cpb.Function{}, "site"),
}
if diff := cmp.Diff(expected, cil, opts...); diff != "" {
t.Errorf("GetCapabilityInfo: got %v, want %v; diff %s", cil, expected, diff)
}

counts := GetCapabilityCounts(pkgs, queriedPackages, &Config{
Classifier: &classifier,
DisableBuiltin: true,
})
if got := counts.CapabilityCounts["FILES"]; got != 2 {
t.Errorf("GetCapabilityCounts: FILES count = %d, want 2", got)
}
}

func TestGraphJSONForSingleFunction(t *testing.T) {
filemap := map[string]string{
"example.com/testlib/testlib.go": `package testlib

import "example.com/dep"

func A() { B() }
func B() {
dep.C()
dep.D()
}
`,
"example.com/dep/dep.go": `package dep

func C() {}
func D() {}
`,
}
classifier := testClassifier{
functions: map[[2]string]string{
{"example.com/dep", "example.com/dep.C"}: "FILES",
{"example.com/dep", "example.com/dep.D"}: "FILES",
},
ignoredEdges: nil,
}
pkgs, queriedPackages, cleanup, err := setup(filemap, "example.com/testlib")
if cleanup != nil {
defer cleanup()
}
if err != nil {
t.Fatalf("setup: %v", err)
}

out, err := buildGraphJSON(pkgs, queriedPackages, &Config{
Classifier: &classifier,
DisableBuiltin: true,
GraphFunction: "example.com/testlib.A",
})
if err != nil {
t.Fatalf("buildGraphJSON: %v", err)
}
if got := len(out.Graphs); got != 1 {
t.Fatalf("buildGraphJSON returned %d graphs, want 1", got)
}
graph := out.Graphs[0]
if graph.Root != "example.com/testlib.A" {
t.Errorf("graph.Root = %q, want example.com/testlib.A", graph.Root)
}
if !reflect.DeepEqual(graph.Capabilities, []string{"FILES"}) {
t.Errorf("graph.Capabilities = %v, want [FILES]", graph.Capabilities)
}

nodes := make(map[string]string)
for _, node := range graph.Nodes {
nodes[node.ID] = node.Kind
}
expectedNodes := map[string]string{
"CAPABILITY_FILES": "capability",
"example.com/dep.C": "function",
"example.com/dep.D": "function",
"example.com/testlib.A": "function",
"example.com/testlib.B": "function",
}
if !reflect.DeepEqual(nodes, expectedNodes) {
t.Errorf("graph nodes = %v, want %v", nodes, expectedNodes)
}

edges := make(map[[3]string]struct{})
for _, edge := range graph.Edges {
edges[[3]string{edge.From, edge.To, edge.Kind}] = struct{}{}
}
expectedEdges := map[[3]string]struct{}{
{"example.com/testlib.A", "example.com/testlib.B", "call"}: {},
{"example.com/testlib.B", "example.com/dep.C", "call"}: {},
{"example.com/testlib.B", "example.com/dep.D", "call"}: {},
{"example.com/dep.C", "CAPABILITY_FILES", "capability"}: {},
{"example.com/dep.D", "CAPABILITY_FILES", "capability"}: {},
}
if !reflect.DeepEqual(edges, expectedEdges) {
t.Errorf("graph edges = %v, want %v", edges, expectedEdges)
}

out, err = buildGraphJSON(pkgs, queriedPackages, &Config{
Classifier: &classifier,
DisableBuiltin: true,
GraphPerFunction: true,
})
if err != nil {
t.Fatalf("buildGraphJSON with GraphPerFunction: %v", err)
}
var roots []string
for _, graph := range out.Graphs {
roots = append(roots, graph.Root)
}
if !reflect.DeepEqual(roots, []string{"example.com/testlib.A", "example.com/testlib.B"}) {
t.Errorf("per-function graph roots = %v, want [example.com/testlib.A example.com/testlib.B]", roots)
}
}

func TestGraphWithClassifier(t *testing.T) {
pkgs, queriedPackages, cleanup, err := setup(filemap, "testlib")
if cleanup != nil {
Expand Down
Loading