xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/classes/_util.py (revision 4c7cc9c8e01791debde927bc0816c9a347055c8f)
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