681 lines
23 KiB
Python
681 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Copyright 2020 MongoDB Inc.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included
|
|
# in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
|
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
#
|
|
"""
|
|
Libdeps Graph Analysis Tool.
|
|
|
|
This will perform various metric's gathering and linting on the
|
|
graph generated from SCons generate-libdeps-graph target. The graph
|
|
represents the dependency information between all binaries from the build.
|
|
"""
|
|
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import networkx
|
|
|
|
from libdeps_graph_enums import CountTypes, DependsReportTypes, LinterTypes, EdgeProps, NodeProps
|
|
|
|
sys.path.append(str(Path(__file__).parent.parent))
|
|
import scons # pylint: disable=wrong-import-position
|
|
|
|
sys.path.append(str(Path(scons.MONGODB_ROOT).joinpath('site_scons')))
|
|
from libdeps_next import deptype # pylint: disable=wrong-import-position
|
|
|
|
|
|
# newer pylints contain the fix: https://github.com/PyCQA/pylint/pull/2926/commits/35e1c61026eab90af504806ef9da6241b096e659
|
|
#signature-mutators=buildscripts.libdeps.graph_analyzer.schema_check
|
|
# pylint: disable=no-value-for-parameter
|
|
def parametrized(dec):
|
|
"""Allow parameters passed to the decorator."""
|
|
|
|
def layer(*args, **kwargs):
|
|
def repl(func):
|
|
return dec(func, *args, **kwargs)
|
|
|
|
return repl
|
|
|
|
return layer
|
|
|
|
|
|
@parametrized
|
|
def schema_check(func, schema_version):
|
|
"""Check the version for a function against the graph."""
|
|
|
|
def check(*args, **kwargs):
|
|
if schema_version <= args[0].graph.graph.get('graph_schema_version'):
|
|
return func(*args, **kwargs)
|
|
else:
|
|
sys.stderr.write(
|
|
f"WARNING: analysis for '{func.__name__}' requires graph schema version '{schema_version}'\n"
|
|
+
|
|
f"but detected graph schema version '{args[0].graph.graph.get('graph_schema_version')}'\n"
|
|
+ f"Not running analysis for {func.__name__}\n\n")
|
|
return "GRAPH_SCHEMA_VERSION_ERR"
|
|
|
|
return check
|
|
|
|
|
|
class Analyzer:
|
|
"""Base class for different types of analyzers."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store the graph and extract the build_dir from the graph."""
|
|
|
|
self.graph = graph
|
|
self.build_dir = Path(graph.graph['build_dir'])
|
|
|
|
def _strip_build_dir(self, node):
|
|
"""Small util function for making args match the graph paths."""
|
|
|
|
node = Path(node)
|
|
if str(node).startswith(str(self.build_dir)):
|
|
return str(node.relative_to(self.build_dir))
|
|
else:
|
|
raise Exception(
|
|
f"build path not in node path: node: {node} build_dir: {self.build_dir}")
|
|
|
|
def _strip_build_dirs(self, nodes):
|
|
"""Small util function for making a list of nodes match graph paths."""
|
|
|
|
return [self._strip_build_dir(node) for node in nodes]
|
|
|
|
|
|
class Counter(Analyzer):
|
|
"""Base Counter Analyzer class for various counters."""
|
|
|
|
def number_of_edge_types(self, edge_type, value):
|
|
"""Count the graphs edges based on type."""
|
|
|
|
return len(
|
|
[edge for edge in self.graph.edges(data=True) if edge[2].get(edge_type) == value])
|
|
|
|
def node_type_count(self, node_type, value):
|
|
"""Count the graphs nodes based on type."""
|
|
|
|
return len(
|
|
[node for node in self.graph.nodes(data=True) if node[1].get(node_type) == value])
|
|
|
|
def report(self, report):
|
|
"""Report the results for the current type."""
|
|
|
|
report[self.count_type] = self.run()
|
|
|
|
|
|
class NodeCounter(Counter):
|
|
"""Counts and reports number of nodes in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.node.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs nodes."""
|
|
|
|
return self.graph.number_of_nodes()
|
|
|
|
|
|
class EdgeCounter(Counter):
|
|
"""Counts and reports number of edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs edges."""
|
|
|
|
return self.graph.number_of_edges()
|
|
|
|
|
|
class DirectEdgeCounter(Counter):
|
|
"""Counts and reports number of direct edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.dir_edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs direct edges."""
|
|
|
|
return self.number_of_edge_types(EdgeProps.direct.name, True)
|
|
|
|
|
|
class TransEdgeCounter(Counter):
|
|
"""Counts and reports number of transitive edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.trans_edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs transitive edges."""
|
|
|
|
return self.number_of_edge_types(EdgeProps.direct.name, False)
|
|
|
|
|
|
class DirectPubEdgeCounter(Counter):
|
|
"""Counts and reports number of direct public edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.dir_pub_edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs direct public edges."""
|
|
|
|
return len([
|
|
edge for edge in self.graph.edges(data=True) if edge[2].get(EdgeProps.direct.name)
|
|
and edge[2].get(EdgeProps.visibility.name) == int(deptype.Public)
|
|
])
|
|
|
|
|
|
class PublicEdgeCounter(Counter):
|
|
"""Counts and reports number of public edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.pub_edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs public edges."""
|
|
|
|
return self.number_of_edge_types(EdgeProps.visibility.name, int(deptype.Public))
|
|
|
|
|
|
class PrivateEdgeCounter(Counter):
|
|
"""Counts and reports number of private edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.priv_edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs private edges."""
|
|
|
|
return self.number_of_edge_types(EdgeProps.visibility.name, int(deptype.Private))
|
|
|
|
|
|
class InterfaceEdgeCounter(Counter):
|
|
"""Counts and reports number of interface edges in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.if_edge.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs interface edges."""
|
|
|
|
return self.number_of_edge_types(EdgeProps.visibility.name, int(deptype.Interface))
|
|
|
|
|
|
class ShimCounter(Counter):
|
|
"""Counts and reports number of shim nodes in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.shim.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs shim nodes."""
|
|
|
|
return self.node_type_count(NodeProps.shim.name, True)
|
|
|
|
|
|
class LibCounter(Counter):
|
|
"""Counts and reports number of library nodes in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.lib.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs lib nodes."""
|
|
|
|
return self.node_type_count(NodeProps.bin_type.name, 'SharedLibrary')
|
|
|
|
|
|
class ProgCounter(Counter):
|
|
"""Counts and reports number of program nodes in the graph."""
|
|
|
|
def __init__(self, graph):
|
|
"""Store graph and set type."""
|
|
|
|
super().__init__(graph)
|
|
self.count_type = CountTypes.prog.name
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Count the graphs program nodes."""
|
|
|
|
return self.node_type_count(NodeProps.bin_type.name, 'Program')
|
|
|
|
|
|
def counter_factory(graph, counters):
|
|
"""Construct counters from a list of strings."""
|
|
|
|
counter_map = {
|
|
CountTypes.node.name: NodeCounter,
|
|
CountTypes.edge.name: EdgeCounter,
|
|
CountTypes.dir_edge.name: DirectEdgeCounter,
|
|
CountTypes.trans_edge.name: TransEdgeCounter,
|
|
CountTypes.dir_pub_edge.name: DirectPubEdgeCounter,
|
|
CountTypes.pub_edge.name: PublicEdgeCounter,
|
|
CountTypes.priv_edge.name: PrivateEdgeCounter,
|
|
CountTypes.if_edge.name: InterfaceEdgeCounter,
|
|
CountTypes.shim.name: ShimCounter,
|
|
CountTypes.lib.name: LibCounter,
|
|
CountTypes.prog.name: ProgCounter,
|
|
}
|
|
|
|
if not isinstance(counters, list):
|
|
counters = [counters]
|
|
|
|
counter_objs = []
|
|
for counter in counters:
|
|
if counter in counter_map:
|
|
counter_objs.append(counter_map[counter](graph))
|
|
else:
|
|
print(f"Skipping unknown counter: {counter}")
|
|
|
|
return counter_objs
|
|
|
|
|
|
class CommonDependencies(Analyzer):
|
|
"""Finds common dependencies for a set of given nodes."""
|
|
|
|
def __init__(self, graph, nodes):
|
|
"""Store graph and strip the nodes."""
|
|
|
|
super().__init__(graph)
|
|
self.nodes = self._strip_build_dirs(nodes)
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""For a given set of nodes, report what nodes depend on all nodes from that set."""
|
|
|
|
neighbor_sets = [set(self.graph[node]) for node in self.nodes]
|
|
return list(set.intersection(*neighbor_sets))
|
|
|
|
def report(self, report):
|
|
"""Add the common depends list for this tuple of nodes."""
|
|
|
|
if DependsReportTypes.common_depends.name not in report:
|
|
report[DependsReportTypes.common_depends.name] = {}
|
|
report[DependsReportTypes.common_depends.name][tuple(self.nodes)] = self.run()
|
|
|
|
|
|
class DirectDependencies(Analyzer):
|
|
"""Finds direct dependencies for a given node."""
|
|
|
|
def __init__(self, graph, node):
|
|
"""Store graph and strip the node."""
|
|
|
|
super().__init__(graph)
|
|
self.node = self._strip_build_dir(node)
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""For given nodes, report what nodes depend directly on that node."""
|
|
|
|
return [
|
|
depender for depender in self.graph[self.node]
|
|
if self.graph[self.node][depender].get(EdgeProps.direct.name)
|
|
]
|
|
|
|
def report(self, report):
|
|
"""Add the direct depends list for this node."""
|
|
|
|
if DependsReportTypes.direct_depends.name not in report:
|
|
report[DependsReportTypes.direct_depends.name] = {}
|
|
report[DependsReportTypes.direct_depends.name][self.node] = self.run()
|
|
|
|
|
|
class ExcludeDependencies(Analyzer):
|
|
"""Finds finds dependencies which include one node, but exclude others."""
|
|
|
|
def __init__(self, graph, nodes):
|
|
"""Store graph and strip the nodes."""
|
|
|
|
super().__init__(graph)
|
|
self.nodes = self._strip_build_dirs(nodes)
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Find depends with exclusions.
|
|
|
|
Given a node, and a set of other nodes, find what nodes depend on the given
|
|
node, but do not depend on the set of nodes.
|
|
"""
|
|
|
|
valid_depender_nodes = []
|
|
for depender_node in set(self.graph[self.nodes[0]]):
|
|
if all([
|
|
bool(excludes_node not in set(self.graph.rgraph[depender_node]))
|
|
for excludes_node in self.nodes[1:]
|
|
]):
|
|
valid_depender_nodes.append(depender_node)
|
|
return valid_depender_nodes
|
|
|
|
def report(self, report):
|
|
"""Add the exclude depends list for this tuple of nodes."""
|
|
|
|
if DependsReportTypes.exclude_depends.name not in report:
|
|
report[DependsReportTypes.exclude_depends.name] = {}
|
|
report[DependsReportTypes.exclude_depends.name][tuple(self.nodes)] = self.run()
|
|
|
|
|
|
class UnusedPublicLinter(Analyzer):
|
|
"""Lints the graph for any public libdeps that are unused in all resulting transitive edges."""
|
|
|
|
def _check_edge_no_symbols(self, edge, original_nodes, checked_edges):
|
|
"""Check the edge's transitive tree and made sure no edges have symbols."""
|
|
|
|
if edge not in checked_edges:
|
|
checked_edges.add(edge)
|
|
original_node = edge[0]
|
|
depender = edge[1]
|
|
try:
|
|
edge_attribs = self.graph[original_node][depender]
|
|
|
|
if (edge_attribs.get(EdgeProps.visibility.name) == int(deptype.Public)
|
|
or edge_attribs.get(EdgeProps.visibility.name) == int(deptype.Interface)):
|
|
if not edge_attribs.get(EdgeProps.symbols.name):
|
|
if not self._tree_uses_no_symbols(depender, original_nodes, checked_edges):
|
|
return False
|
|
else:
|
|
return False
|
|
except KeyError:
|
|
pass
|
|
|
|
return True
|
|
|
|
def _tree_uses_no_symbols(self, node, original_nodes, checked_edges):
|
|
"""Recursive walk for a public node.
|
|
|
|
Walk the dependency tree for a given Public node, and check if all edges
|
|
in that tree do not have symbol dependencies.
|
|
"""
|
|
|
|
for depender in self.graph[node]:
|
|
for original_node in original_nodes:
|
|
edge = (original_node, depender)
|
|
if not self._check_edge_no_symbols(edge, original_nodes, checked_edges):
|
|
return False
|
|
return True
|
|
|
|
def _check_trans_nodes_no_symbols(self, edge, trans_pub_nodes):
|
|
"""Check the edge against the transitive nodes for symbols."""
|
|
|
|
for trans_node in self.graph.rgraph[edge[0]]:
|
|
if (self.graph.rgraph[edge[0]][trans_node].get(EdgeProps.visibility.name) == int(
|
|
deptype.Public) or self.graph.rgraph[edge[0]][trans_node].get(
|
|
EdgeProps.visibility.name) == int(deptype.Interface)):
|
|
trans_pub_nodes.add(trans_node)
|
|
try:
|
|
if self.graph[trans_node][edge[1]].get(EdgeProps.symbols.name):
|
|
return True
|
|
except KeyError:
|
|
pass
|
|
return False
|
|
|
|
@schema_check(schema_version=1)
|
|
def run(self):
|
|
"""Run the unused public linter.
|
|
|
|
Run the linter to check for and PUBLIC libdeps which are
|
|
unnecessary and can be converted to PRIVATE.
|
|
"""
|
|
|
|
unused_public_libdeps = []
|
|
checked_edges = set()
|
|
|
|
for edge in self.graph.edges:
|
|
edge_attribs = self.graph[edge[0]][edge[1]]
|
|
|
|
if (edge_attribs.get(EdgeProps.direct.name)
|
|
and edge_attribs.get(EdgeProps.visibility.name) == int(deptype.Public)
|
|
and self.graph[edge[0]].get(NodeProps.shim.name)
|
|
and self.graph[edge[0]].get(NodeProps.bin_type.name) == 'SharedLibrary'):
|
|
|
|
# First we will get all the transitive libdeps the dependnet node
|
|
# induces, while we are getting those we also check if the depender
|
|
# node has any symbol dependencies to that transitive libdep.
|
|
trans_pub_nodes = set([edge[0]])
|
|
found_symbols = self._check_trans_nodes_no_symbols(edge, trans_pub_nodes)
|
|
|
|
# If the depender node has no symbol dependencies on the induced libdeps,
|
|
# then we will walk up the tree for the depender node, checking if any of the
|
|
# induced dependencies have symbols. If there are no simples between all transitive
|
|
# edges from this direct public libdep, its safe to change it to public.
|
|
if not found_symbols and self._tree_uses_no_symbols(edge[1], list(trans_pub_nodes),
|
|
checked_edges):
|
|
unused_public_libdeps.append((edge[0], edge[1]))
|
|
|
|
return unused_public_libdeps
|
|
|
|
def report(self, report):
|
|
"""Report the lint issies."""
|
|
|
|
report[LinterTypes.public_unused.name] = self.run()
|
|
|
|
|
|
def linter_factory(graph, linters):
|
|
"""Construct linters from a list of strings."""
|
|
|
|
linter_map = {
|
|
LinterTypes.public_unused.name: UnusedPublicLinter,
|
|
}
|
|
|
|
if not isinstance(linters, list):
|
|
linters = [linters]
|
|
|
|
linters_objs = []
|
|
for linter in linters:
|
|
if linter in linter_map:
|
|
linters_objs.append(linter_map[linter](graph))
|
|
else:
|
|
print(f"Skipping unknown counter: {linter}")
|
|
|
|
return linters_objs
|
|
|
|
|
|
class BuildDataReport(Analyzer):
|
|
"""Adds the build and graph meta data to the report."""
|
|
|
|
@schema_check(schema_version=1)
|
|
def report(self, report):
|
|
"""Add the build data from the graph to the report."""
|
|
|
|
report['invocation'] = self.graph.graph.get('invocation')
|
|
report['git_hash'] = self.graph.graph.get('git_hash')
|
|
report['graph_schema_version'] = self.graph.graph.get('graph_schema_version')
|
|
|
|
|
|
class LibdepsGraph(networkx.DiGraph):
|
|
"""Class for analyzing the graph."""
|
|
|
|
def __init__(self, graph=networkx.DiGraph()):
|
|
"""Load the graph data."""
|
|
|
|
super().__init__(incoming_graph_data=graph)
|
|
|
|
# Load in the graph and store a reversed version as well for quick look ups
|
|
# the in directions.
|
|
self.rgraph = graph.reverse()
|
|
|
|
|
|
class LibdepsGraphAnalysis:
|
|
"""Runs the given analysis on the input graph."""
|
|
|
|
def __init__(self, libdeps_graph, analysis):
|
|
"""Perform analysis based off input args."""
|
|
|
|
self.libdeps_graph = libdeps_graph
|
|
|
|
self.results = {}
|
|
for analyzer in analysis:
|
|
analyzer.report(self.results)
|
|
|
|
def get_results(self):
|
|
"""Return the results fo the analysis."""
|
|
|
|
return self.results
|
|
|
|
|
|
class GaPrinter:
|
|
"""Base class for printers of the graph analysis."""
|
|
|
|
def __init__(self, libdeps_graph_analysis):
|
|
"""Store the graph analysis for use when printing."""
|
|
|
|
self.libdeps_graph_analysis = libdeps_graph_analysis
|
|
|
|
|
|
class GaJsonPrinter(GaPrinter):
|
|
"""Printer for json output."""
|
|
|
|
def serialize(self, dictionary):
|
|
"""Serialize the k,v pairs in the dictionary."""
|
|
|
|
new = {}
|
|
for key, value in dictionary.items():
|
|
if isinstance(value, dict):
|
|
value = self.serialize(value)
|
|
new[str(key)] = value
|
|
return new
|
|
|
|
def print(self):
|
|
"""Print the result data."""
|
|
|
|
import json
|
|
results = self.libdeps_graph_analysis.get_results()
|
|
print(json.dumps(self.serialize(results)))
|
|
|
|
|
|
class GaPrettyPrinter(GaPrinter):
|
|
"""Printer for pretty console output."""
|
|
|
|
count_descs = {
|
|
CountTypes.node.name: "Nodes in Graph: {}",
|
|
CountTypes.edge.name: "Edges in Graph: {}",
|
|
CountTypes.dir_edge.name: "Direct Edges in Graph: {}",
|
|
CountTypes.trans_edge.name: "Transitive Edges in Graph: {}",
|
|
CountTypes.dir_pub_edge.name: "Direct Public Edges in Graph: {}",
|
|
CountTypes.pub_edge.name: "Public Edges in Graph: {}",
|
|
CountTypes.priv_edge.name: "Private Edges in Graph: {}",
|
|
CountTypes.if_edge.name: "Interface Edges in Graph: {}",
|
|
CountTypes.shim.name: "Shim Nodes in Graph: {}",
|
|
CountTypes.lib.name: "Library Nodes in Graph: {}",
|
|
CountTypes.prog.name: "Program Nodes in Graph: {}",
|
|
}
|
|
|
|
@staticmethod
|
|
def _print_results_node_list(heading, nodes):
|
|
"""Util function for printing a list of nodes for depend reports."""
|
|
|
|
print(heading)
|
|
for i, depender in enumerate(nodes, start=1):
|
|
print(f" {i}: {depender}")
|
|
print("")
|
|
|
|
def print(self):
|
|
"""Print the result data."""
|
|
results = self.libdeps_graph_analysis.get_results()
|
|
|
|
if 'invocation' in results:
|
|
print(
|
|
textwrap.dedent(f"""\
|
|
Graph built from git hash:
|
|
{results['git_hash']}
|
|
|
|
Graph Schema version:
|
|
{results['graph_schema_version']}
|
|
|
|
Build invocation:
|
|
{results['invocation']}
|
|
"""))
|
|
|
|
for count_type in CountTypes.__members__.items():
|
|
if count_type[0] in self.count_descs and count_type[0] in results:
|
|
print(self.count_descs[count_type[0]].format(results[count_type[0]]))
|
|
|
|
if DependsReportTypes.direct_depends.name in results:
|
|
print("\nNodes that directly depend on:")
|
|
for node in results[DependsReportTypes.direct_depends.name]:
|
|
self._print_results_node_list(f'=>depends on {node}:',
|
|
results[DependsReportTypes.direct_depends.name][node])
|
|
|
|
if DependsReportTypes.common_depends.name in results:
|
|
print("\nNodes that commonly depend on:")
|
|
for nodes in results[DependsReportTypes.common_depends.name]:
|
|
self._print_results_node_list(
|
|
f'=>depends on {nodes}:',
|
|
results[DependsReportTypes.common_depends.name][nodes])
|
|
|
|
if DependsReportTypes.exclude_depends.name in results:
|
|
print("\nNodes that depend on a node, but exclude others:")
|
|
for nodes in results[DependsReportTypes.exclude_depends.name]:
|
|
self._print_results_node_list(
|
|
f"=>depends: {nodes[0]}, exclude: {nodes[1:]}:",
|
|
results[DependsReportTypes.exclude_depends.name][nodes])
|
|
|
|
if LinterTypes.public_unused.name in results:
|
|
print(
|
|
f"\nLibdepsLinter: PUBLIC libdeps that could be PRIVATE: {len(results[LinterTypes.public_unused.name])}"
|
|
)
|
|
for issue in sorted(results[LinterTypes.public_unused.name],
|
|
key=lambda item: item[1] + item[0]):
|
|
print(f" {issue[1]}: PUBLIC -> {issue[0]} -> PRIVATE")
|