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

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 

6 

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

8 

9from __future__ import annotations 

10 

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 

18 

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

20from renaissance.utils.text_utils import TextUtils 

21 

22 

23# enum with ABORT, CONTINUE and SKIP 

24class VisitorResult(Enum): 

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

26 

27 ABORT = 0 

28 CONTINUE = 1 

29 SKIP = 2 

30 

31 

32class ASTReference[NodeT, TranslationUnitT]: 

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

34 

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 

40 

41 @property 

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

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

44 return self._node 

45 

46 @property 

47 def ref_kind(self) -> str: 

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

49 return self._ref_kind 

50 

51 @property 

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

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

54 return self._properties 

55 

56 

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. 

60 

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

62 """ 

63 

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

65 

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

82 

83 def __repr__(self) -> str: 

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

85 return format_node(self) 

86 

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 

90 

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) 

102 

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) 

107 

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

113 

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 

125 

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) 

130 

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) 

135 

136 @property 

137 @abstractmethod 

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

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

140 

141 @property 

142 @abstractmethod 

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

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

145 

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) 

155 

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) 

159 

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) 

168 

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

173 

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

178 

179 @property 

180 def name(self) -> str: 

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

182 return self._name 

183 

184 @property 

185 def filename(self) -> str: 

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

187 return self._filename 

188 

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 

196 

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 

201 

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

207 

208 @property 

209 def length(self) -> int: 

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

211 return self._length 

212 

213 @property 

214 def kind(self) -> str: 

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

216 return self._kind 

217 

218 @abstractmethod 

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

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

221 

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 

227 

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 

232 

233 @property 

234 @abstractmethod 

235 def is_statement(self) -> bool: 

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

237 

238 @property 

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

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

241 return self._children 

242 

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) 

246 

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

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

249 

250 Args: 

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

252 

253 """ 

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

255 for child in self.children: 

256 child.accept(function)