diff --git a/analyzer/analyzer.go b/analyzer/analyzer.go index f065d4b..1dc06c2 100644 --- a/analyzer/analyzer.go +++ b/analyzer/analyzer.go @@ -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 } @@ -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 @@ -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{} @@ -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. @@ -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 diff --git a/analyzer/analyzer_test.go b/analyzer/analyzer_test.go index bbb9185..ec1c421 100644 --- a/analyzer/analyzer_test.go +++ b/analyzer/analyzer_test.go @@ -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 { diff --git a/analyzer/graph.go b/analyzer/graph.go index ecb6a0f..0b00a47 100644 --- a/analyzer/graph.go +++ b/analyzer/graph.go @@ -8,10 +8,12 @@ package analyzer import ( "bufio" + "encoding/json" "fmt" "go/types" "io" "os" + "sort" "strconv" "strings" @@ -97,6 +99,295 @@ func graphOutput(pkgs []*packages.Package, queriedPackages map[*types.Package]st return w.Flush() } +type graphJSONOutputInfo struct { + Graphs []graphJSONGraph `json:"graphs"` +} + +type graphJSONGraph struct { + Root string `json:"root,omitempty"` + Roots []string `json:"roots,omitempty"` + Capabilities []string `json:"capabilities,omitempty"` + Nodes []graphJSONNode `json:"nodes"` + Edges []graphJSONEdge `json:"edges"` +} + +type graphJSONNode struct { + ID string `json:"id"` + Kind string `json:"kind"` + Package string `json:"package,omitempty"` +} + +type graphJSONEdge struct { + From string `json:"from"` + To string `json:"to"` + Kind string `json:"kind"` + Site *graphJSONSite `json:"site,omitempty"` +} + +type graphJSONSite struct { + Filename string `json:"filename"` + Line int64 `json:"line"` + Column int64 `json:"column"` +} + +func graphJSONOutput(pkgs []*packages.Package, queriedPackages map[*types.Package]struct{}, config *Config) error { + out, err := buildGraphJSON(pkgs, queriedPackages, config) + if err != nil { + return err + } + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(out) +} + +func buildGraphJSON(pkgs []*packages.Package, queriedPackages map[*types.Package]struct{}, config *Config) (*graphJSONOutputInfo, error) { + if config.GraphFunction != "" && config.GraphPerFunction { + return nil, fmt.Errorf("-graph_function and -graph_per_function cannot be used together") + } + safe, nodesByCapability, extraNodesByCapability := getPackageNodesWithCapability(pkgs, config) + nodesByCapability, allNodesWithExplicitCapability := mergeCapabilities(nodesByCapability, extraNodesByCapability) + extraNodesByCapability = nil + + nodesByCapability = filterNodesByCapability(nodesByCapability, config.CapabilitySet) + bfsFromCapabilities := searchBackwardsFromCapabilities(nodesByCapability, safe, allNodesWithExplicitCapability, config.Classifier) + + roots := reachableQueriedNodes(bfsFromCapabilities, queriedPackages) + if config.GraphFunction != "" { + root, ok := findGraphRoot(roots, config.GraphFunction) + if !ok { + return nil, fmt.Errorf("function %q was not found among queried functions that reach the selected capabilities", config.GraphFunction) + } + return &graphJSONOutputInfo{ + Graphs: []graphJSONGraph{ + collectGraphJSON([]*callgraph.Node{root}, nodesByCapability, allNodesWithExplicitCapability, bfsFromCapabilities, config.Classifier), + }, + }, nil + } + + if config.GraphPerFunction { + out := &graphJSONOutputInfo{} + for _, root := range roots { + out.Graphs = append(out.Graphs, + collectGraphJSON([]*callgraph.Node{root}, nodesByCapability, allNodesWithExplicitCapability, bfsFromCapabilities, config.Classifier)) + } + return out, nil + } + + return &graphJSONOutputInfo{ + Graphs: []graphJSONGraph{ + collectGraphJSON(roots, nodesByCapability, allNodesWithExplicitCapability, bfsFromCapabilities, config.Classifier), + }, + }, nil +} + +func filterNodesByCapability(nodesByCapability nodesetPerCapability, capabilitySet *CapabilitySet) nodesetPerCapability { + if capabilitySet == nil { + return nodesByCapability + } + filtered := make(nodesetPerCapability) + for capability, nodes := range nodesByCapability { + if capabilitySet.Has(capability) { + filtered[capability] = nodes + } + } + return filtered +} + +func reachableQueriedNodes(bfsFromCapabilities bfsStateMap, queriedPackages map[*types.Package]struct{}) []*callgraph.Node { + var roots []*callgraph.Node + for v := range bfsFromCapabilities { + if v.Func == nil || v.Func.Package() == nil { + continue + } + if _, ok := queriedPackages[v.Func.Package().Pkg]; ok { + roots = append(roots, v) + } + } + sort.Sort(byFunction(roots)) + return roots +} + +func findGraphRoot(roots []*callgraph.Node, name string) (*callgraph.Node, bool) { + for _, root := range roots { + if root.Func != nil && root.Func.String() == name { + return root, true + } + } + return nil, false +} + +func collectGraphJSON(roots []*callgraph.Node, nodesByCapability nodesetPerCapability, + allNodesWithExplicitCapability nodeset, bfsFromCapabilities bfsStateMap, classifier Classifier, +) graphJSONGraph { + collector := newGraphJSONCollector(roots) + startNodes := make(nodeset) + for _, root := range roots { + startNodes[root] = struct{}{} + } + searchForwardsFromQueriedFunctions( + startNodes, + nodesByCapability, + allNodesWithExplicitCapability, + bfsFromCapabilities, + classifier, + func(_ bfsStateMap, node *callgraph.Node, _ bfsStateMap) { + collector.addFunctionNode(node) + }, + func(edge *callgraph.Edge) { + collector.addFunctionNode(edge.Caller) + collector.addFunctionNode(edge.Callee) + collector.addCallEdge(edge) + }, + func(fn *callgraph.Node, capability string) { + collector.addFunctionNode(fn) + collector.addCapabilityNode(capability) + collector.addCapabilityEdge(fn, capability) + }) + return collector.graph() +} + +type graphJSONCollector struct { + roots []string + nodes map[string]graphJSONNode + edges map[string]graphJSONEdge + capabilities map[string]struct{} +} + +func newGraphJSONCollector(roots []*callgraph.Node) *graphJSONCollector { + var rootNames []string + for _, root := range roots { + rootNames = append(rootNames, graphNodeID(root)) + } + sort.Strings(rootNames) + return &graphJSONCollector{ + roots: rootNames, + nodes: make(map[string]graphJSONNode), + edges: make(map[string]graphJSONEdge), + capabilities: make(map[string]struct{}), + } +} + +func (c *graphJSONCollector) addFunctionNode(node *callgraph.Node) { + if node == nil { + return + } + id := graphNodeID(node) + n := graphJSONNode{ + ID: id, + Kind: "function", + } + if pkg := nodeToPackage(node); pkg != nil { + n.Package = pkg.Path() + } + c.nodes[id] = n +} + +func (c *graphJSONCollector) addCapabilityNode(capability string) { + id := graphCapabilityID(capability) + c.nodes[id] = graphJSONNode{ + ID: id, + Kind: "capability", + } + c.capabilities[capability] = struct{}{} +} + +func (c *graphJSONCollector) addCallEdge(edge *callgraph.Edge) { + e := graphJSONEdge{ + From: graphNodeID(edge.Caller), + To: graphNodeID(edge.Callee), + Kind: "call", + Site: graphJSONSiteFromEdge(edge), + } + c.edges[graphJSONEdgeKey(e)] = e +} + +func (c *graphJSONCollector) addCapabilityEdge(fn *callgraph.Node, capability string) { + e := graphJSONEdge{ + From: graphNodeID(fn), + To: graphCapabilityID(capability), + Kind: "capability", + } + c.edges[graphJSONEdgeKey(e)] = e +} + +func (c *graphJSONCollector) graph() graphJSONGraph { + g := graphJSONGraph{ + Capabilities: sortedStrings(c.capabilities), + Nodes: sortedGraphJSONNodes(c.nodes), + Edges: sortedGraphJSONEdges(c.edges), + } + if len(c.roots) == 1 { + g.Root = c.roots[0] + } else { + g.Roots = c.roots + } + return g +} + +func graphNodeID(node *callgraph.Node) string { + if node == nil { + return "" + } + if node.Func != nil { + return node.Func.String() + } + return strconv.Itoa(node.ID) +} + +func graphCapabilityID(capability string) string { + return "CAPABILITY_" + capability +} + +func graphJSONSiteFromEdge(edge *callgraph.Edge) *graphJSONSite { + if position := callsitePosition(edge); position.IsValid() { + return &graphJSONSite{ + Filename: position.Filename, + Line: int64(position.Line), + Column: int64(position.Column), + } + } + return nil +} + +func graphJSONEdgeKey(e graphJSONEdge) string { + return e.Kind + "\x00" + e.From + "\x00" + e.To +} + +func sortedGraphJSONNodes(nodes map[string]graphJSONNode) []graphJSONNode { + ids := make([]string, 0, len(nodes)) + for id := range nodes { + ids = append(ids, id) + } + sort.Strings(ids) + out := make([]graphJSONNode, 0, len(ids)) + for _, id := range ids { + out = append(out, nodes[id]) + } + return out +} + +func sortedGraphJSONEdges(edges map[string]graphJSONEdge) []graphJSONEdge { + keys := make([]string, 0, len(edges)) + for key := range edges { + keys = append(keys, key) + } + sort.Strings(keys) + out := make([]graphJSONEdge, 0, len(keys)) + for _, key := range keys { + out = append(out, edges[key]) + } + return out +} + +func sortedStrings(values map[string]struct{}) []string { + out := make([]string, 0, len(values)) + for value := range values { + out = append(out, value) + } + sort.Strings(out) + return out +} + type graphBuilder struct { io.Writer nodeNamer func(any) string diff --git a/analyzer/scan.go b/analyzer/scan.go index f40f9d8..3e31483 100644 --- a/analyzer/scan.go +++ b/analyzer/scan.go @@ -109,6 +109,8 @@ func RunCapslock(args []string, output string, pkgs []*packages.Package, queried return ctm.Execute(os.Stdout, cil) } else if output == "g" || output == "graph" { return graphOutput(pkgs, queriedPackages, config) + } else if output == "graph-json" || output == "graph_json" { + return graphJSONOutput(pkgs, queriedPackages, config) } cil := GetCapabilityCounts(pkgs, queriedPackages, config) ctm := template.Must(template.New("default.tmpl").Funcs(templateFuncMap).ParseFS(staticContent, "static/default.tmpl")) diff --git a/cmd/capslock/capslock.go b/cmd/capslock/capslock.go index 53a9384..d485e2c 100644 --- a/cmd/capslock/capslock.go +++ b/cmd/capslock/capslock.go @@ -33,7 +33,7 @@ import ( var ( packageList = flag.String("packages", "", "target patterns to be analysed; allows wildcarding") - output = flag.String("output", "", "output mode to use; non-default options are json, m, package, v, graph, and compare") + output = flag.String("output", "", "output mode to use; non-default options are json, m, package, v, graph, graph-json, and compare") verbose = flag.Int("v", 0, "verbosity level") noiseFlag = flag.Bool("noisy", false, "include output on unanalyzed function calls (can be noisy)") customMap = flag.String("capability_map", "", "use a custom capability map file") @@ -46,6 +46,8 @@ var ( memprofile = flag.String("memprofile", "", "write memory profile to specified file") granularity = flag.String("granularity", "", `the granularity to use for comparisons, either "package" or "function".`) + graphFunction = flag.String("graph_function", "", "for -output=graph-json, only emit the graph reachable from this exact function name") + graphPerFunction = flag.Bool("graph_per_function", false, "for -output=graph-json, emit one graph for each reachable queried function") forceLocalModule = flag.Bool("force_local_module", false, "if the requested packages cannot be loaded in the current workspace, return an error immediately, instead of trying to load them in a temporary module") omitPaths = flag.Bool("omit_paths", false, "omit example call paths from output") version = flag.Bool("version", false, "report Capslock version and exit") @@ -210,11 +212,13 @@ func run() error { return fmt.Errorf("Some packages had errors. Aborting analysis.") } err = analyzer.RunCapslock(flag.Args(), *output, pkgs, queriedPackages, &analyzer.Config{ - Classifier: classifier, - DisableBuiltin: *disableBuiltin, - Granularity: g, - CapabilitySet: cs, - OmitPaths: *omitPaths, + Classifier: classifier, + DisableBuiltin: *disableBuiltin, + Granularity: g, + CapabilitySet: cs, + GraphFunction: *graphFunction, + GraphPerFunction: *graphPerFunction, + OmitPaths: *omitPaths, }) if *memprofile != "" {