Coverage for src/renaissance/syntax_tree/ast_node.py: 95%
151 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
1"""AI: Abstract base class for AST node implementations, with the shared traversal and rewrite protocol."""
3from __future__ import annotations
5import re
6import sys
7from abc import ABC, abstractmethod
8from collections.abc import Callable, Sequence
9from enum import Enum
10from pathlib import Path
11from typing import Any, Self
13from renaissance.utils.ast_utils import format_node, next_sibling, preceding_sibling, process_node
14from renaissance.utils.text_utils import TextUtils
17# enum with ABORT, CONTINUE and SKIP
18class VisitorResult(Enum):
19 """AI: Signal how an AST traversal should continue after visiting a node."""
21 ABORT = 0
22 CONTINUE = 1
23 SKIP = 2
26class ASTReference[NodeT, TranslationUnitT]:
27 """AI: Represent a reference from one AST node to another, along with the kind of reference."""
29 def __init__(self, ast_node: ASTNode[NodeT, TranslationUnitT], ref_kind: str, properties: dict[str, Any]) -> None:
30 """AI: Represent a reference from one AST node to another, along with the kind of reference."""
31 self._node = ast_node
32 self._ref_kind = ref_kind
33 self._properties = properties
35 @property
36 def node(self) -> ASTNode[NodeT, TranslationUnitT]:
37 """AI: Return the AST node this reference points from."""
38 return self._node
40 @property
41 def ref_kind(self) -> str:
42 """AI: Return the kind of this reference."""
43 return self._ref_kind
45 @property
46 def properties(self) -> dict[str, Any]:
47 """AI: Return this reference's additional properties."""
48 return self._properties
51# To make usage of the concrete class methods easier, ASTNode MUST NOT have ABSTRACT public classes!!
52class ASTNode[NodeT, TranslationUnitT](ABC):
53 """The base class to represent an AST node.
55 It is an abstract class that should be inherited by concrete classes that represent specific AST nodes.
56 """
58 cache: dict[str, bytes] = {}
60 def __init__(self, root: Self) -> None:
61 """AI: Initialize a new AST node rooted at the given translation-unit-level node."""
62 super().__init__()
63 self._parent = None
64 self._children: list[Self] = []
65 self.show_props: bool = False
66 self.translation_unit: TranslationUnitT | None = None
67 self._kind: str = ""
68 self._length: int = 0
69 self._offset: int = 0
70 self._filename: str = ""
71 self.root: Self = root
72 self._properties = {}
73 self._name = ""
74 self.node: NodeT | None = None
75 self.indent = ""
77 def __repr__(self):
78 """AI: Return the formatted node representation."""
79 return format_node(self)
81 def is_part_of_translation_unit(self) -> bool:
82 """AI: Return whether this node's filename matches its root node's filename."""
83 return self.filename == self.root.filename
85 @property
86 def signature(self) -> str:
87 """AI: Return this node's source code text, or an empty string if it has no filename or span."""
88 start = self.offset
89 end = self.extended_end_offset
90 if start == end:
91 return ""
92 file = self.filename
93 if not file:
94 return ""
95 return self.content(start, end)
97 @property
98 def text(self) -> str:
99 """AI: Return this node's signature text shifted left to remove its leading indent."""
100 return TextUtils.shift_left(self.signature, len(self.indent), start_line=1)
102 def content(self, start: int, end: int) -> str:
103 """AI: Return the decoded source text between start and end offsets in the root's file content."""
104 content = self.root.binary_file_content()
105 return str(content[start:end], sys.getfilesystemencoding())
107 def binary_file_content(self, file_path: str | None = None) -> bytes:
108 """AI: Return the raw bytes of file_path (or the root's filename), reading and caching it on first access."""
109 if not file_path:
110 file_path = self.root.filename
111 try:
112 return ASTNode.cache[file_path]
113 except KeyError:
114 with Path(file_path).open("rb") as f:
115 content = f.read()
116 ASTNode.cache[file_path] = content
117 return content
119 @property
120 def preceding_sibling(self) -> Self | None:
121 """AI: Return the sibling node immediately preceding this one, or None."""
122 return preceding_sibling(self)
124 @property
125 def next_sibling(self) -> Self | None:
126 """AI: Return the sibling node immediately following this one, or None."""
127 return next_sibling(self)
129 @property
130 @abstractmethod
131 def references(self) -> list[ASTReference[NodeT, TranslationUnitT]]:
132 """AI: Return the references that this node points to."""
134 @property
135 @abstractmethod
136 def referenced_by(self) -> list[ASTReference[NodeT, TranslationUnitT]]:
137 """AI: Return the references that point to this node."""
139 def get_ancestor(self, kind: str | re.Pattern[str]) -> Self | None:
140 """AI: Return the nearest ancestor node whose kind matches the given kind pattern, or None."""
141 pattern = re.compile(kind, re.IGNORECASE) if isinstance(kind, str) else kind
142 parent = self.parent
143 if not parent:
144 return None
145 if pattern.match(parent.kind):
146 return parent
147 return parent.get_ancestor(pattern)
149 def is_descendant_of(self, node: Self) -> bool:
150 """AI: Return whether this node is a descendant of node."""
151 return node.is_ancestor_of(self)
153 def is_ancestor_of(self, descendant: Self) -> bool:
154 """AI: Return whether this node is an ancestor of descendant."""
155 parent: Self = descendant.parent
156 if parent == self:
157 return True
158 if not parent:
159 return False
160 return self.is_ancestor_of(parent)
162 @staticmethod
163 @abstractmethod
164 def load(file_path: Path, extra_args: Sequence[str], working_dir: Path) -> ASTNode[NodeT, TranslationUnitT]:
165 """AI: Parse the source file at file_path into an AST node."""
167 @staticmethod
168 @abstractmethod
169 def load_from_text(text: str, file_name: str, extra_args: Sequence[str], working_dir: Path) -> ASTNode[NodeT, TranslationUnitT]:
170 """AI: Parse source text (attributed to file_name) into an AST node."""
172 @property
173 def name(self) -> str:
174 """AI: Return this node's name."""
175 return self._name
177 @property
178 def filename(self) -> str:
179 """AI: Return this node's source file name."""
180 return self._filename
182 # TODO: Is this the best name: offset, start_offset, begin_offset, ...?
183 # TODO: Should offset return a slice object, https://docs.python.org/3/library/functions.html#slice, instead of an int?
184 # That would make it easier to get the text segment.
185 @property
186 def offset(self) -> int:
187 """AI: Return the character offset where this node begins in the source text."""
188 return self._offset
190 @property
191 def end_offset(self) -> int:
192 """AI: Return the character offset where this node ends in the source text."""
193 return self.offset + self.length
195 # TODO: Is this the really best solution to ensure that the modified code has the proper layout?
196 @property
197 @abstractmethod
198 def extended_end_offset(self) -> int:
199 """AI: Return the character offset where this node's extended span (including trailing layout) ends."""
201 @property
202 def length(self) -> int:
203 """AI: Return the length in characters of this node's source text."""
204 return self._length
206 @property
207 def kind(self) -> str:
208 """AI: Return this node's parser kind."""
209 return self._kind
211 @abstractmethod
212 def matches_kind(self, node: Self) -> bool:
213 """AI: Return whether node has the same kind as this node."""
215 # TODO: What is the best name: properties, attributes, syntax_attributes, ...?
216 @property
217 def properties(self) -> dict[str, int | str]: # TODO: Is int | str really sufficient? Shouldn't it be Any?
218 """AI: Return this node's syntax properties."""
219 return self._properties
221 @property
222 def parent(self) -> Self | None:
223 """AI: Return this node's parent node, or None if it has none."""
224 return self._parent
226 @property
227 @abstractmethod
228 def is_statement(self) -> bool:
229 """AI: Return whether this node represents a statement."""
231 @property
232 def children(self) -> list[Self]:
233 """AI: Return this node's child nodes."""
234 return self._children
236 def process(self, function: Callable[[Self], None]) -> None:
237 """AI: Apply function to this node and recursively to all of its descendants."""
238 process_node(self, function)
240 def accept(self, function: Callable[[Self], VisitorResult]) -> None:
241 """Accept a visitor function and apply it to the current node and its children.
243 Args:
244 function (Callable[[Self], VisitorResult]): A function that takes an ASTNode as an argument and returns a VisitorResult.
246 Returns:
247 None
249 """
250 if function(self) == VisitorResult.CONTINUE:
251 for child in self.children:
252 child.accept(function)