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

1"""AI: Abstract base class for AST node implementations, with the shared traversal and rewrite protocol.""" 

2 

3from __future__ import annotations 

4 

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 

12 

13from renaissance.utils.ast_utils import format_node, next_sibling, preceding_sibling, process_node 

14from renaissance.utils.text_utils import TextUtils 

15 

16 

17# enum with ABORT, CONTINUE and SKIP 

18class VisitorResult(Enum): 

19 """AI: Signal how an AST traversal should continue after visiting a node.""" 

20 

21 ABORT = 0 

22 CONTINUE = 1 

23 SKIP = 2 

24 

25 

26class ASTReference[NodeT, TranslationUnitT]: 

27 """AI: Represent a reference from one AST node to another, along with the kind of reference.""" 

28 

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 

34 

35 @property 

36 def node(self) -> ASTNode[NodeT, TranslationUnitT]: 

37 """AI: Return the AST node this reference points from.""" 

38 return self._node 

39 

40 @property 

41 def ref_kind(self) -> str: 

42 """AI: Return the kind of this reference.""" 

43 return self._ref_kind 

44 

45 @property 

46 def properties(self) -> dict[str, Any]: 

47 """AI: Return this reference's additional properties.""" 

48 return self._properties 

49 

50 

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. 

54 

55 It is an abstract class that should be inherited by concrete classes that represent specific AST nodes. 

56 """ 

57 

58 cache: dict[str, bytes] = {} 

59 

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 = "" 

76 

77 def __repr__(self): 

78 """AI: Return the formatted node representation.""" 

79 return format_node(self) 

80 

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 

84 

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) 

96 

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) 

101 

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()) 

106 

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 

118 

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) 

123 

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) 

128 

129 @property 

130 @abstractmethod 

131 def references(self) -> list[ASTReference[NodeT, TranslationUnitT]]: 

132 """AI: Return the references that this node points to.""" 

133 

134 @property 

135 @abstractmethod 

136 def referenced_by(self) -> list[ASTReference[NodeT, TranslationUnitT]]: 

137 """AI: Return the references that point to this node.""" 

138 

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) 

148 

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) 

152 

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) 

161 

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.""" 

166 

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.""" 

171 

172 @property 

173 def name(self) -> str: 

174 """AI: Return this node's name.""" 

175 return self._name 

176 

177 @property 

178 def filename(self) -> str: 

179 """AI: Return this node's source file name.""" 

180 return self._filename 

181 

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 

189 

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 

194 

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.""" 

200 

201 @property 

202 def length(self) -> int: 

203 """AI: Return the length in characters of this node's source text.""" 

204 return self._length 

205 

206 @property 

207 def kind(self) -> str: 

208 """AI: Return this node's parser kind.""" 

209 return self._kind 

210 

211 @abstractmethod 

212 def matches_kind(self, node: Self) -> bool: 

213 """AI: Return whether node has the same kind as this node.""" 

214 

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 

220 

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 

225 

226 @property 

227 @abstractmethod 

228 def is_statement(self) -> bool: 

229 """AI: Return whether this node represents a statement.""" 

230 

231 @property 

232 def children(self) -> list[Self]: 

233 """AI: Return this node's child nodes.""" 

234 return self._children 

235 

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) 

239 

240 def accept(self, function: Callable[[Self], VisitorResult]) -> None: 

241 """Accept a visitor function and apply it to the current node and its children. 

242 

243 Args: 

244 function (Callable[[Self], VisitorResult]): A function that takes an ASTNode as an argument and returns a VisitorResult. 

245 

246 Returns: 

247 None 

248 

249 """ 

250 if function(self) == VisitorResult.CONTINUE: 

251 for child in self.children: 

252 child.accept(function)