def output_parent_function_graph(rule_classification_data_bundle): report_dict, reference_dict = rule_classification_data_bundle identifier_dict = { parent: f"p{index}" for index, parent in enumerate(report_dict.keys()) } dot = Digraph(**_GRAPH_SETTINGS) for parent, identifier in identifier_dict.items(): descriptions = "\l".join(report_dict[parent]) + "\l" with dot.subgraph( name=f"cluster_{identifier}", graph_attr={ "label": _get_function_display_name(parent), "fontsize": "16", }, ) as sub: sub.node(identifier, label=descriptions) edge_list = [] for parent, identifier in identifier_dict.items(): edge_list.extend([(identifier, identifier_dict[function]) for function in reference_dict[parent]]) dot.edges(edge_list) dot.render()
def _visit_blocks( self, graph: Digraph, block: Block, visited: Set[Block] = set(), calls: bool = True, format: str = None, interactive: bool = False, ) -> None: # Don't visit blocks twice. if block in visited: return visited.add(block) nodeshape, nodecolor, nodelabel = self.stylize_node(block) node_type = block.type() original_nodelabel = nodelabel nodelabel = "" if (self.isShort and not isinstance(node_type, ast.ClassDef) and not isinstance(node_type, ast.FunctionDef) and not isinstance(node_type, ast.If) and not isinstance(node_type, ast.While)): sub_pattern = r"""(\"|') # Group 1: " or ' (?=[^\"'\r\n]{20,}) # Enforce min length of 20 ([^\"'\r\n]{,20}) # Group 2: Words that stay ([^\"'\r\n]{,9999}) # Group 3: Shorten these (\"|')""" # Group 4: " or ' original_nodelabel = original_nodelabel.replace("\l", "\n") for line in original_nodelabel.splitlines(): tmp_line = re.sub(sub_pattern, r"\1\2...\4", line, flags=re.VERBOSE) nodelabel += tmp_line + "\l" else: nodelabel = original_nodelabel graph.node( str(block.id), label=nodelabel, _attributes={ "style": f"filled,{self.border_style(block, interactive)}", "shape": nodeshape, "fillcolor": self.fillcolor(block, interactive, nodecolor), }, ) if isinstance(block, TryBlock): for except_block in block.except_blocks.values(): self._visit_blocks(graph, except_block, visited, calls, format) if calls and block.func_calls: calls_node = str(block.id) + "_calls" # Remove any duplicates by splitting on newlines and creating a set calls_label = block.get_calls().strip() # Create a new subgraph for call statement calls_subgraph = gv.Digraph( name=f"cluster_{block.id}", format=format, graph_attr={ "rankdir": "TB", "ranksep": "0.02", "style": "filled", "color": self.fillcolor(block, interactive, "purple"), "compound": "true", "fontname": "DejaVu Sans Mono", "shape": self.node_styles[ast.Call][0], "label": "", }, node_attr={"fontname": "DejaVu Sans Mono"}, edge_attr={"fontname": "DejaVu Sans Mono"}, ) # Generate control flow edges for function arguments for func_block in block.func_blocks: graph.edge( str(block.id), str(func_block.id), label="calls", _attributes={"style": "dashed"}, ) self._visit_func( calls_subgraph, func_block, visited=set(), interactive=interactive, ) graph.subgraph(calls_subgraph) tmp = "" for line in calls_label.splitlines(): if "input" in line: input_node = str(block.id) + "_input" nodeshape, nodecolor = self.node_styles["input"] graph.node( input_node, label=line, _attributes={ "style": f"filled,{self.border_style(block, interactive)}", "shape": nodeshape, "fillcolor": self.fillcolor(block, interactive, nodecolor), }, ) graph.edge(input_node, str(block.id)) # yellow # _attributes={'style': 'dashed'}) else: line += "\l" tmp += line # Recursively visit all the blocks of the CFG. for exit in block.exits: assert block == exit.source self._visit_blocks( graph, exit.target, visited, calls=calls, format=format, interactive=interactive, ) edgeshape, edgecolor, edgelabel = self.stylize_edge(exit) graph.edge( str(block.id), str(exit.target.id), label=edgelabel, _attributes={"color": edgecolor}, )