|
| 1 | +############################################################################# |
| 2 | +# Copyright (C) 2020-2024 MEmilio |
| 3 | +# |
| 4 | +# Authors: Maximilian Betz, Daniel Richter |
| 5 | +# |
| 6 | +# Contact: Martin J. Kuehn <Martin.Kuehn@DLR.de> |
| 7 | +# |
| 8 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 9 | +# you may not use this file except in compliance with the License. |
| 10 | +# You may obtain a copy of the License at |
| 11 | +# |
| 12 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 13 | +# |
| 14 | +# Unless required by applicable law or agreed to in writing, software |
| 15 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 16 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 17 | +# See the License for the specific language governing permissions and |
| 18 | +# limitations under the License. |
| 19 | +############################################################################# |
| 20 | + |
| 21 | +import os |
| 22 | +import logging |
| 23 | +from typing import Callable |
| 24 | +from graphviz import Digraph |
| 25 | +from clang.cindex import Cursor |
| 26 | +from memilio.generation.ast import AST |
| 27 | + |
| 28 | + |
| 29 | +class Visualization: |
| 30 | + """! Class for plotting the abstract syntax tree in different formats. |
| 31 | + """ |
| 32 | + @staticmethod |
| 33 | + def output_ast_terminal(ast: AST, cursor: Cursor) -> None: |
| 34 | + """! Output the abstract syntax tree to terminal. |
| 35 | +
|
| 36 | + @param ast: ast object from AST class. |
| 37 | + @param cursor: The current node of the AST as a cursor object from libclang. |
| 38 | + """ |
| 39 | + |
| 40 | + def terminal_writer(level: int, cursor_label: str) -> None: |
| 41 | + print(indent(level) + cursor_label) |
| 42 | + |
| 43 | + _output_cursor_and_children(cursor, ast, terminal_writer) |
| 44 | + |
| 45 | + logging.info("AST-Terminal written.") |
| 46 | + |
| 47 | + @staticmethod |
| 48 | + def output_ast_png(cursor: Cursor, max_depth: int, output_file_name: str = 'ast_graph') -> None: |
| 49 | + """! Output the abstract syntax tree to a .png. Set the starting node and the max depth. |
| 50 | +
|
| 51 | + To save the abstract syntax tree as an png with a starting node and a depth u cann use the following command |
| 52 | +
|
| 53 | + Example command: aviz.output_ast_png(ast.get_node_by_index(1), 2) |
| 54 | +
|
| 55 | + aviz -> instance of the Visualization class. |
| 56 | +
|
| 57 | + ast -> instance of the AST class. |
| 58 | +
|
| 59 | + .get_node_by_index -> get a specific node by id (use .output_ast_formatted to see node ids) |
| 60 | +
|
| 61 | + The number 2 is a example for the depth the graph will show |
| 62 | +
|
| 63 | + @param cursor: The current node of the AST as a cursor object from libclang. |
| 64 | + @param max_depth: Maximal depth the graph displays. |
| 65 | + """ |
| 66 | + |
| 67 | + graph = Digraph(format='png') |
| 68 | + |
| 69 | + _output_cursor_and_children_graphviz_digraph( |
| 70 | + cursor, graph, max_depth, 0) |
| 71 | + |
| 72 | + graph.render(filename=output_file_name, view=False) |
| 73 | + |
| 74 | + output_path = os.path.abspath(f"{output_file_name}.png") |
| 75 | + logging.info(f"AST-png written to {output_path}") |
| 76 | + |
| 77 | + @staticmethod |
| 78 | + def output_ast_formatted(ast: AST, cursor: Cursor, output_file_name: str = 'ast_formated.txt') -> None: |
| 79 | + """!Output the abstract syntax tree to a file. |
| 80 | +
|
| 81 | + @param ast: ast object from AST class. |
| 82 | + @param cursor: The current node of the AST as a cursor object from libclang. |
| 83 | + """ |
| 84 | + |
| 85 | + with open(output_file_name, 'w') as f: |
| 86 | + def file_writer(level: int, cursor_label: str) -> None: |
| 87 | + f.write(indent(level) + cursor_label + newline()) |
| 88 | + _output_cursor_and_children(cursor, ast, file_writer) |
| 89 | + |
| 90 | + output_path = os.path.abspath(f"{output_file_name}") |
| 91 | + logging.info(f"AST-formated written to {output_path}") |
| 92 | + |
| 93 | + |
| 94 | +def indent(level: int) -> str: |
| 95 | + """! Create an indentation based on the level. |
| 96 | + """ |
| 97 | + return '│ ' * level + '├── ' |
| 98 | + |
| 99 | + |
| 100 | +def newline() -> str: |
| 101 | + """! Create a new line. |
| 102 | + """ |
| 103 | + return '\n' |
| 104 | + |
| 105 | + |
| 106 | +def _output_cursor_and_children(cursor: Cursor, ast: AST, writer: Callable[[int, str], None], level: int = 0) -> None: |
| 107 | + """!Generic function to output the cursor and its children with a specified writer. |
| 108 | +
|
| 109 | + @param cursor: The current node of the AST as a libclang cursor object. |
| 110 | + @param ast: AST object from the AST class. |
| 111 | + @param writer: Function that takes `level` and `cursor_label` and handles output. |
| 112 | + @param level: The current depth in the AST for indentation purposes. |
| 113 | + """ |
| 114 | + |
| 115 | + cursor_id = ast.get_node_id(cursor) |
| 116 | + |
| 117 | + cursor_kind = f"<CursorKind.{cursor.kind.name}>" |
| 118 | + file_path = cursor.location.file.name if cursor.location.file else "" |
| 119 | + |
| 120 | + if cursor.spelling: |
| 121 | + cursor_label = (f'ID:{cursor_id} {cursor.spelling} ' |
| 122 | + f'{cursor_kind} ' |
| 123 | + f'{file_path}') |
| 124 | + else: |
| 125 | + cursor_label = f'ID:{cursor_id} {cursor_kind} {file_path}' |
| 126 | + |
| 127 | + writer(level, cursor_label) |
| 128 | + |
| 129 | + for child in cursor.get_children(): |
| 130 | + _output_cursor_and_children( |
| 131 | + child, ast, writer, level + 1) |
| 132 | + |
| 133 | + |
| 134 | +def _output_cursor_and_children_graphviz_digraph(cursor: Cursor, graph: Digraph, max_d: int, current_d: int, parent_node: str = None) -> None: |
| 135 | + """! Output the cursor and its children as a graph using Graphviz. |
| 136 | +
|
| 137 | + @param cursor: The current node of the AST as a Cursor object from libclang. |
| 138 | + @param graph: Graphviz Digraph object where the nodes and edges will be added. |
| 139 | + @param max_d: Maximal depth. |
| 140 | + @param current_d: Current depth. |
| 141 | + @param parent_node: Name of the parent node in the graph (None for the root node). |
| 142 | + """ |
| 143 | + |
| 144 | + if current_d > max_d: |
| 145 | + return |
| 146 | + |
| 147 | + node_label = f"{cursor.kind.name}{newline()}({cursor.spelling})" if cursor.spelling else cursor.kind.name |
| 148 | + |
| 149 | + current_node = f"{cursor.kind.name}_{cursor.hash}" |
| 150 | + |
| 151 | + graph.node(current_node, label=node_label) |
| 152 | + |
| 153 | + if parent_node: |
| 154 | + graph.edge(parent_node, current_node) |
| 155 | + |
| 156 | + if cursor.kind.is_reference(): |
| 157 | + referenced_label = f"ref_to_{cursor.referenced.kind.name}{newline()}({cursor.referenced.spelling})" |
| 158 | + referenced_node = f"ref_{cursor.referenced.hash}" |
| 159 | + graph.node(referenced_node, label=referenced_label) |
| 160 | + graph.edge(current_node, referenced_node) |
| 161 | + |
| 162 | + for child in cursor.get_children(): |
| 163 | + _output_cursor_and_children_graphviz_digraph( |
| 164 | + child, graph, max_d, current_d + 1, current_node) |
0 commit comments