Coverage for src / renaissance / syntax_tree / ast_node.py: 91%
159 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-09-09 14:04 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-09-09 14:04 +0000
1from __future__ import annotations
3import re
4import sys
5from abc import ABC, abstractmethod
6from collections.abc import Callable, Sequence
7from enum import Enum
8from pathlib import Path
9from typing import Any, Self
11from renaissance.utils.ast_utils import format_node, next_sibling, preceding_sibling, process_node
12from renaissance.utils.text_utils import TextUtils
15# enum with ABORT, CONTINUE and SKIP
16class VisitorResult(Enum):
17 ABORT = 0
18 CONTINUE = 1
19 SKIP = 2
22class ASTReference:
23 def __init__(self, ast_node: ASTNode, ref_kind: str, properties: dict[str, Any]) -> None:
24 self._node = ast_node
25 self._ref_kind = ref_kind
26 self._properties = properties
28 @property
29 def node(self) -> ASTNode:
30 return self._node
32 @property
33 def ref_kind(self) -> str:
34 return self._ref_kind
36 @property
37 def properties(self) -> dict[str, Any]:
38 return self._properties
41# To make usage of the concrete class methods easier, ASTNode MUST NOT have ABSTRACT public classes!!
42class ASTNode(ABC):
43 cache: dict[str, bytes] = {}
44 """
45 The base class to represent an AST node.
46 It is an abstract class that should be inherited by concrete classes that represent specific AST nodes.
47 """
49 def __init__(self, root: Self) -> None:
50 super().__init__()
51 self._parent = None
52 self._children = None
53 self.show_props = None
54 self.translation_unit = None
55 self._kind = None
56 self._length = None
57 self._offset = None
58 self._filename = None
59 self.root: Self = root
60 self._properties = {}
61 self._name = ""
62 self.node = None
63 self.indent = ""
65 def __repr__(self):
66 return format_node(self)
68 def is_part_of_translation_unit(self) -> bool:
69 return self.filename == self.root.filename
71 @property
72 def signature(self) -> str:
73 start = self.offset
74 end = self.extended_end_offset
75 if start == end:
76 return ""
77 file = self.filename
78 if not file:
79 return ""
80 return self.content(start, end)
82 @property
83 def text(self) -> str:
84 return TextUtils.shift_left(self.signature, len(self.indent), start_line=1)
86 def content(self, start: int, end: int) -> str:
87 content = self.root.binary_file_content()
88 return str(content[start:end], sys.getfilesystemencoding())
90 def binary_file_content(self, file_path: str | None = None) -> bytes:
91 if not file_path:
92 file_path = self.root.filename
93 try:
94 return ASTNode.cache[file_path]
95 except KeyError:
96 with Path(file_path).open("rb") as f:
97 content = f.read()
98 ASTNode.cache[file_path] = content
99 return content
101 @property
102 def preceding_sibling(self) -> Self | None:
103 return preceding_sibling(self)
105 @property
106 def next_sibling(self) -> Self | None:
107 return next_sibling(self)
109 @property
110 @abstractmethod
111 def references(self) -> list[ASTReference]:
112 pass
114 @property
115 @abstractmethod
116 def referenced_by(self) -> list[ASTReference]:
117 pass
119 def get_ancestor(self, kind: str | re.Pattern[str]) -> Self | None:
120 pattern = re.compile(kind, re.IGNORECASE) if isinstance(kind, str) else kind
121 parent = self.parent
122 if not parent:
123 return None
124 if pattern.match(parent.kind):
125 return parent
126 return parent.get_ancestor(pattern)
128 def is_descendant_of(self, node: Self) -> bool:
129 return node.is_ancestor_of(self)
131 def is_ancestor_of(self, descendant: Self) -> bool:
132 parent: Self = descendant.parent
133 if parent == self:
134 return True
135 if not parent:
136 return False
137 return self.is_ancestor_of(parent)
139 @staticmethod
140 @abstractmethod
141 def load(file_path: Path, extra_args: Sequence[str], working_dir: Path) -> ASTNode:
142 pass
144 @staticmethod
145 @abstractmethod
146 def load_from_text(text: str, file_name: str, extra_args: Sequence[str], working_dir: Path) -> ASTNode:
147 pass
149 @property
150 def name(self) -> str:
151 return self._name
153 @property
154 def filename(self) -> str:
155 return self._filename
157 # TODO: Is this the best name: offset, start_offset, begin_offset, ...?
158 # TODO: Should offset return a slice object, https://docs.python.org/3/library/functions.html#slice, instead of an int?
159 # That would make it easier to get the text segment.
160 @property
161 def offset(self) -> int:
162 return self._offset
164 @property
165 def end_offset(self) -> int:
166 return self.offset + self.length
168 # TODO: Is this the really best solution to ensure that the modified code has the proper layout?
169 @property
170 @abstractmethod
171 def extended_end_offset(self) -> int:
172 pass
174 @property
175 def length(self) -> int:
176 return self._length
178 @property
179 def kind(self) -> str:
180 return self._kind
182 @abstractmethod
183 def matches_kind(self, node: Self) -> bool:
184 pass
186 # TODO: What is the best name: properties, attributes, syntax_attributes, ...?
187 @property
188 def properties(self) -> dict[str, int | str]: # TODO: Is int | str really sufficient? Shouldn't it be Any?
189 return self._properties
191 @property
192 def parent(self) -> Self | None:
193 return self._parent
195 @property
196 @abstractmethod
197 def is_statement(self) -> bool:
198 pass
200 @property
201 def children(self) -> list[Self]:
202 return self._children
204 def process(self, function: Callable[[Self], None]) -> None:
205 process_node(self, function)
207 def accept(self, function: Callable[[Self], VisitorResult]) -> None:
208 """Accepts a visitor function and applies it to the current node and its children.
210 Args:
211 function (Callable[[Self], VisitorResult]): A function that takes an ASTNode as an argument and returns a VisitorResult.
213 Returns:
214 None
216 """
217 if function(self) == VisitorResult.CONTINUE:
218 for child in self.children:
219 child.accept(function)