Coverage for src/renaissance/syntax_tree/ast_node.py: 95%
151 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-15 10:34 +0000
1# Copyright (C) 2024-2026 TNO
2#
3# Licensed under the terms specified in the LICENSE file in the root directory
4# of this repository, also available at:
5# https://github.com/TNO/Renaissance.Py/blob/main/LICENSE
7"""AI: Abstract base class for AST node implementations, with the shared traversal and rewrite protocol."""
9from __future__ import annotations
11import re
12import sys
13from abc import ABC, abstractmethod
14from collections.abc import Callable, Sequence
15from enum import Enum
16from pathlib import Path
17from typing import Any, Self
19from renaissance.utils.ast_utils import format_node, next_sibling, preceding_sibling, process_node
20from renaissance.utils.text_utils import TextUtils
23# enum with ABORT, CONTINUE and SKIP
24class VisitorResult(Enum):
25 """AI: Signal how an AST traversal should continue after visiting a node."""
27 ABORT = 0
28 CONTINUE = 1
29 SKIP = 2
32class ASTReference[NodeT, TranslationUnitT]:
33 """AI: Represent a reference from one AST node to another, along with the kind of reference."""
35 def __init__(self, ast_node: ASTNode[NodeT, TranslationUnitT], ref_kind: str, properties: dict[str, Any]) -> None:
36 """AI: Represent a reference from one AST node to another, along with the kind of reference."""
37 self._node = ast_node
38 self._ref_kind = ref_kind
39 self._properties = properties
41 @property
42 def node(self) -> ASTNode[NodeT, TranslationUnitT]:
43 """AI: Return the AST node this reference points from."""
44 return self._node
46 @property
47 def ref_kind(self) -> str:
48 """AI: Return the kind of this reference."""
49 return self._ref_kind
51 @property
52 def properties(self) -> dict[str, Any]:
53 """AI: Return this reference's additional properties."""
54 return self._properties
57# To make usage of the concrete class methods easier, ASTNode MUST NOT have ABSTRACT public classes!!
58class ASTNode[NodeT, TranslationUnitT](ABC):
59 """The base class to represent an AST node.
61 It is an abstract class that should be inherited by concrete classes that represent specific AST nodes.
62 """
64 cache: dict[str, bytes] = {}
66 def __init__(self, root: Self) -> None:
67 """AI: Initialize a new AST node rooted at the given translation-unit-level node."""
68 super().__init__()
69 self._parent = None
70 self._children: list[Self] = []
71 self.show_props: bool = False
72 self.translation_unit: TranslationUnitT | None = None
73 self._kind: str = ""
74 self._length: int = 0
75 self._offset: int = 0
76 self._filename: str = ""
77 self.root: Self = root
78 self._properties = {}
79 self._name = ""
80 self.node: NodeT | None = None
81 self.indent = ""
83 def __repr__(self) -> str:
84 """AI: Return the formatted node representation."""
85 return format_node(self)
87 def is_part_of_translation_unit(self) -> bool:
88 """AI: Return whether this node's filename matches its root node's filename."""
89 return self.filename == self.root.filename
91 @property
92 def signature(self) -> str:
93 """AI: Return this node's source code text, or an empty string if it has no filename or span."""
94 start = self.offset
95 end = self.extended_end_offset
96 if start == end:
97 return ""
98 file = self.filename
99 if not file:
100 return ""
101 return self.content(start, end)
103 @property
104 def text(self) -> str:
105 """AI: Return this node's signature text shifted left to remove its leading indent."""
106 return TextUtils.shift_left(self.signature, len(self.indent), start_line=1)
108 def content(self, start: int, end: int) -> str:
109 """AI: Return the decoded source text between start and end offsets in the root's file content."""
110 content = self.root.binary_file_content()
111 # TODO: see docs/developer/architecture/position-consistency.md - decodes with the host encoding, not the adapter's.
112 return str(content[start:end], sys.getfilesystemencoding())
114 def binary_file_content(self, file_path: str | None = None) -> bytes:
115 """AI: Return the raw bytes of file_path (or the root's filename), reading and caching it on first access."""
116 if not file_path:
117 file_path = self.root.filename
118 try:
119 return ASTNode.cache[file_path]
120 except KeyError:
121 with Path(file_path).open("rb") as f:
122 content = f.read()
123 ASTNode.cache[file_path] = content
124 return content
126 @property
127 def preceding_sibling(self) -> Self | None:
128 """AI: Return the sibling node immediately preceding this one, or None."""
129 return preceding_sibling(self)
131 @property
132 def next_sibling(self) -> Self | None:
133 """AI: Return the sibling node immediately following this one, or None."""
134 return next_sibling(self)
136 @property
137 @abstractmethod
138 def references(self) -> list[ASTReference[NodeT, TranslationUnitT]]:
139 """AI: Return the references that this node points to."""
141 @property
142 @abstractmethod
143 def referenced_by(self) -> list[ASTReference[NodeT, TranslationUnitT]]:
144 """AI: Return the references that point to this node."""
146 def get_ancestor(self, kind: str | re.Pattern[str]) -> Self | None:
147 """AI: Return the nearest ancestor node whose kind matches the given kind pattern, or None."""
148 pattern = re.compile(kind, re.IGNORECASE) if isinstance(kind, str) else kind
149 parent = self.parent
150 if not parent:
151 return None
152 if pattern.match(parent.kind):
153 return parent
154 return parent.get_ancestor(pattern)
156 def is_descendant_of(self, node: Self) -> bool:
157 """AI: Return whether this node is a descendant of node."""
158 return node.is_ancestor_of(self)
160 def is_ancestor_of(self, descendant: Self) -> bool:
161 """AI: Return whether this node is an ancestor of descendant."""
162 parent: Self | None = descendant.parent
163 if parent == self:
164 return True
165 if not parent:
166 return False
167 return self.is_ancestor_of(parent)
169 @staticmethod
170 @abstractmethod
171 def load(file_path: Path, extra_args: Sequence[str], working_dir: Path) -> ASTNode[NodeT, TranslationUnitT]:
172 """AI: Parse the source file at file_path into an AST node."""
174 @staticmethod
175 @abstractmethod
176 def load_from_text(text: str, file_name: str, extra_args: Sequence[str], working_dir: Path) -> ASTNode[NodeT, TranslationUnitT]:
177 """AI: Parse source text (attributed to file_name) into an AST node."""
179 @property
180 def name(self) -> str:
181 """AI: Return this node's name."""
182 return self._name
184 @property
185 def filename(self) -> str:
186 """AI: Return this node's source file name."""
187 return self._filename
189 # TODO: Is this the best name: offset, start_offset, begin_offset, ...?
190 # TODO: Should offset return a slice object, https://docs.python.org/3/library/functions.html#slice, instead of an int?
191 # That would make it easier to get the text segment.
192 @property
193 def offset(self) -> int:
194 """AI: Return the character offset where this node begins in the source text."""
195 return self._offset
197 @property
198 def end_offset(self) -> int:
199 """AI: Return the character offset where this node ends in the source text."""
200 return self.offset + self.length
202 # TODO: Is this the really best solution to ensure that the modified code has the proper layout?
203 @property
204 @abstractmethod
205 def extended_end_offset(self) -> int:
206 """AI: Return the character offset where this node's extended span (including trailing layout) ends."""
208 @property
209 def length(self) -> int:
210 """AI: Return the length in characters of this node's source text."""
211 return self._length
213 @property
214 def kind(self) -> str:
215 """AI: Return this node's parser kind."""
216 return self._kind
218 @abstractmethod
219 def matches_kind(self, node: Self) -> bool:
220 """AI: Return whether node has the same kind as this node."""
222 # TODO: What is the best name: properties, attributes, syntax_attributes, ...?
223 @property
224 def properties(self) -> dict[str, int | str]: # TODO: Is int | str really sufficient? Shouldn't it be Any?
225 """AI: Return this node's syntax properties."""
226 return self._properties
228 @property
229 def parent(self) -> Self | None:
230 """AI: Return this node's parent node, or None if it has none."""
231 return self._parent
233 @property
234 @abstractmethod
235 def is_statement(self) -> bool:
236 """AI: Return whether this node represents a statement."""
238 @property
239 def children(self) -> list[Self]:
240 """AI: Return this node's child nodes."""
241 return self._children
243 def process(self, function: Callable[[Self], None]) -> None:
244 """AI: Apply function to this node and recursively to all of its descendants."""
245 process_node(self, function)
247 def accept(self, function: Callable[[Self], VisitorResult]) -> None:
248 """Accept a visitor function and apply it to the current node and its children.
250 Args:
251 function (Callable[[Self], VisitorResult]): A function that takes an ASTNode as an argument and returns a VisitorResult.
253 """
254 if function(self) == VisitorResult.CONTINUE:
255 for child in self.children:
256 child.accept(function)