1#!/usr/bin/env python3 2""" 3# Created: Mon Jun 20 19:42:53 2022 (-0400) 4# @author: Jacob Faibussowitsch 5""" 6from __future__ import annotations 7 8import functools 9 10from .._typing import * 11 12from ..util._color import Color 13 14def verbose_print(*args, **kwargs) -> bool: 15 """filter predicate for show_ast: show all""" 16 return True 17 18def no_system_includes(cursor: CursorLike, level: IndentLevel, **kwargs) -> bool: 19 """ 20 filter predicate for show_ast: filter out verbose stuff from system include files 21 """ 22 return level != 1 or ( 23 cursor.location.file is not None and not cursor.location.file.name.startswith('/usr/include') 24 ) 25 26def only_files(cursor: CursorLike, level: IndentLevel, **kwargs) -> bool: 27 """ 28 filter predicate to only show ast defined in file 29 """ 30 filename = kwargs['filename'] 31 return level != 1 or (cursor.location.file is not None and cursor.location.file.name == filename) 32 33# A function show(level, *args) would have been simpler but less fun 34# and you'd need a separate parameter for the AST walkers if you want it to be exchangeable. 35class IndentLevel(int): 36 """ 37 represent currently visited level of a tree 38 """ 39 def view(self, *args) -> str: 40 """ 41 pretty print an indented line 42 """ 43 return ' '*self+' '.join(map(str, args)) 44 45 def __add__(self, inc: int) -> IndentLevel: 46 """ 47 increase number of tabs and newlines 48 """ 49 return IndentLevel(super().__add__(inc)) 50 51def check_valid_type(t: clx.Type) -> bool: 52 import clang.cindex as clx # type: ignore[import] 53 54 return not t.kind == clx.TypeKind.INVALID 55 56def fully_qualify(t: clx.Type) -> list[str]: 57 q = [] 58 if t.is_const_qualified(): q.append('const') 59 if t.is_volatile_qualified(): q.append('volatile') 60 if t.is_restrict_qualified(): q.append('restrict') 61 return q 62 63def view_type(t: clx.Type, level: IndentLevel, title: str) -> list[str]: 64 """ 65 pretty print type AST 66 """ 67 ret_list = [level.view(title, str(t.kind), ' '.join(fully_qualify(t)))] 68 if check_valid_type(t.get_pointee()): 69 ret_list.extend(view_type(t.get_pointee(), level + 1, 'points to:')) 70 return ret_list 71 72def view_ast_from_cursor(cursor: CursorLike, pred: Callable[..., bool] = verbose_print, level: IndentLevel = IndentLevel(), max_depth: int = -1, **kwargs) -> list[str]: 73 """ 74 pretty print cursor AST 75 """ 76 ret_list: list[str] = [] 77 if max_depth >= 0: 78 if int(level) > max_depth: 79 return ret_list 80 if pred(cursor, level, **kwargs): 81 ret_list.append(level.view(cursor.kind, cursor.spelling, cursor.displayname, cursor.location)) 82 if check_valid_type(cursor.type): 83 ret_list.extend(view_type(cursor.type, level + 1, 'type:')) 84 ret_list.extend(view_type(cursor.type.get_canonical(), level + 1, 'canonical type:')) 85 for c in cursor.get_children(): 86 ret_list.extend( 87 view_ast_from_cursor(c, pred=pred, level=level + 1, max_depth=max_depth, **kwargs) 88 ) 89 return ret_list 90 91# surprise, surprise, we end up reading the same files over and over again when 92# constructing the error messages and diagnostics and hence we make about a 8x performance 93# improvement by caching the files read 94@functools.lru_cache 95def read_file_lines_cached(*args, **kwargs) -> list[str]: 96 with open(*args, **kwargs) as fd: 97 ret: list[str] = fd.readlines() 98 return ret 99 100def get_raw_source_from_source_range(source_range: SourceRangeLike, num_before_context: int = 0, num_after_context: int = 0, num_context: int = 0, trim: bool = False, tight: bool = False) -> str: 101 num_before_context = num_before_context if num_before_context else num_context 102 num_after_context = num_after_context if num_after_context else num_context 103 rstart, rend = source_range.start, source_range.end 104 line_begin, line_end = rstart.line, rend.line 105 lobound = max(1, line_begin - num_before_context) 106 hibound = line_end + num_after_context 107 108 line_list = read_file_lines_cached(rstart.file.name, 'r')[lobound - 1:hibound] 109 110 if tight: 111 assert line_begin == line_end 112 # index into line_list where our actual line starts if we have context 113 loidx, hiidx = line_begin - lobound, hibound - line_begin 114 cbegin, cend = rstart.column - 1, rend.column - 1 115 if loidx == hiidx: 116 # same line, then we need to do it in 1 step to keep the indexing correct 117 line_list[loidx] = line_list[loidx][cbegin:cend] 118 else: 119 line_list[loidx] = line_list[loidx][cbegin:] 120 line_list[hiidx] = line_list[hiidx][:cend] 121 # Find number of spaces to remove from beginning of line based on lowest. 122 # This keeps indentation between lines, but doesn't start the string halfway 123 # across the screeen 124 if trim: 125 min_spaces = min(len(s) - len(s.lstrip()) for s in line_list if s.replace('\n', '')) 126 return '\n'.join([s[min_spaces:].rstrip() for s in line_list]) 127 return ''.join(line_list) 128 129def get_raw_source_from_cursor(cursor: CursorLike, **kwargs) -> str: 130 return get_raw_source_from_source_range(cursor.extent, **kwargs) 131 132def get_formatted_source_from_source_range(source_range: SourceRangeLike, num_before_context: int = 0, num_after_context: int = 0, num_context: int = 0, view: bool = False, highlight: bool = True, trim: bool = True) -> str: 133 num_before_context = num_before_context if num_before_context else num_context 134 num_after_context = num_after_context if num_after_context else num_context 135 begin, end = source_range.start, source_range.end 136 line_begin, line_end = begin.line, end.line 137 138 lo_bound = max(1, line_begin - num_before_context) 139 hi_bound = line_end + num_after_context 140 max_width = len(str(hi_bound)) 141 142 if highlight: 143 symbol_begin = begin.column - 1 144 symbol_end = end.column - 1 145 begin_offset = max(symbol_begin, 0) 146 len_underline = max(abs(max(symbol_end, 1) - begin_offset), 1) 147 underline = begin_offset * ' ' + Color.bright_yellow() + len_underline * '^' + Color.reset() 148 149 line_list = [] 150 raw_lines = read_file_lines_cached(begin.file.name, 'r')[lo_bound - 1:hi_bound] 151 for line_file, line in enumerate(raw_lines, start=lo_bound): 152 indicator = '>' if (line_begin <= line_file <= line_end) else ' ' 153 prefix = f'{indicator} {line_file: <{max_width}}: ' 154 if highlight and (line_file == line_begin): 155 line = f'{line[:symbol_begin]}{Color.bright_yellow()}{line[symbol_begin:symbol_end]}{Color.reset()}{line[symbol_end:]}' 156 line_list.extend([ 157 (prefix, line), 158 (' ' * len(prefix), underline) 159 ]) 160 else: 161 line_list.append((prefix, line)) 162 # Find number of spaces to remove from beginning of line based on lowest. 163 # This keeps indentation between lines, but doesn't start the string halfway 164 # across the screen 165 if trim: 166 try: 167 min_spaces = min(len(s) - len(s.lstrip(' ')) for _, s in line_list if s.replace('\n', '')) 168 except: 169 min_spaces = 0 170 else: 171 min_spaces = 0 172 src_str = '\n'.join(p + s[min_spaces:].rstrip() for p, s in line_list) 173 if view: 174 print(src_str) 175 return src_str 176 177def get_formatted_source_from_cursor(cursor: CursorLike, **kwargs) -> str: 178 return get_formatted_source_from_source_range(cursor.extent, **kwargs) 179 180def view_cursor_full(cursor: CursorLike, **kwargs) -> list[str]: 181 ret = [ 182 f'Spelling: {cursor.spelling}', 183 f'Type: {cursor.type.spelling}', 184 f'Kind: {cursor.kind}', 185 f'Storage Class: {cursor.storage_class}', 186 f'Linkage: {cursor.linkage}', 187 ] 188 try: 189 ret.append('Arguments: '+' '.join([a.displayname for a in cursor.get_arguments()])) 190 except AttributeError: 191 pass 192 try: 193 ret.append(f'Semantic Parent: {cursor.semantic_parent.displayname}') 194 except AttributeError: 195 pass 196 try: 197 ret.append(f'Lexical Parent: {cursor.lexical_parent.displayname}') 198 except AttributeError: 199 pass 200 ret.append('Children: '+' '.join([c.spelling for c in cursor.get_children()])) 201 ret.append(get_formatted_source_from_cursor(cursor, num_context=2)) 202 ret.append('------------------ AST View: ------------------') 203 ret.extend(view_ast_from_cursor(cursor, **kwargs)) 204 return ret 205