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

1from __future__ import annotations 

2 

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 

10 

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

12from renaissance.utils.text_utils import TextUtils 

13 

14 

15# enum with ABORT, CONTINUE and SKIP 

16class VisitorResult(Enum): 

17 ABORT = 0 

18 CONTINUE = 1 

19 SKIP = 2 

20 

21 

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 

27 

28 @property 

29 def node(self) -> ASTNode: 

30 return self._node 

31 

32 @property 

33 def ref_kind(self) -> str: 

34 return self._ref_kind 

35 

36 @property 

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

38 return self._properties 

39 

40 

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

48 

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

64 

65 def __repr__(self): 

66 return format_node(self) 

67 

68 def is_part_of_translation_unit(self) -> bool: 

69 return self.filename == self.root.filename 

70 

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) 

81 

82 @property 

83 def text(self) -> str: 

84 return TextUtils.shift_left(self.signature, len(self.indent), start_line=1) 

85 

86 def content(self, start: int, end: int) -> str: 

87 content = self.root.binary_file_content() 

88 return str(content[start:end], sys.getfilesystemencoding()) 

89 

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 

100 

101 @property 

102 def preceding_sibling(self) -> Self | None: 

103 return preceding_sibling(self) 

104 

105 @property 

106 def next_sibling(self) -> Self | None: 

107 return next_sibling(self) 

108 

109 @property 

110 @abstractmethod 

111 def references(self) -> list[ASTReference]: 

112 pass 

113 

114 @property 

115 @abstractmethod 

116 def referenced_by(self) -> list[ASTReference]: 

117 pass 

118 

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) 

127 

128 def is_descendant_of(self, node: Self) -> bool: 

129 return node.is_ancestor_of(self) 

130 

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) 

138 

139 @staticmethod 

140 @abstractmethod 

141 def load(file_path: Path, extra_args: Sequence[str], working_dir: Path) -> ASTNode: 

142 pass 

143 

144 @staticmethod 

145 @abstractmethod 

146 def load_from_text(text: str, file_name: str, extra_args: Sequence[str], working_dir: Path) -> ASTNode: 

147 pass 

148 

149 @property 

150 def name(self) -> str: 

151 return self._name 

152 

153 @property 

154 def filename(self) -> str: 

155 return self._filename 

156 

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 

163 

164 @property 

165 def end_offset(self) -> int: 

166 return self.offset + self.length 

167 

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 

173 

174 @property 

175 def length(self) -> int: 

176 return self._length 

177 

178 @property 

179 def kind(self) -> str: 

180 return self._kind 

181 

182 @abstractmethod 

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

184 pass 

185 

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 

190 

191 @property 

192 def parent(self) -> Self | None: 

193 return self._parent 

194 

195 @property 

196 @abstractmethod 

197 def is_statement(self) -> bool: 

198 pass 

199 

200 @property 

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

202 return self._children 

203 

204 def process(self, function: Callable[[Self], None]) -> None: 

205 process_node(self, function) 

206 

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

208 """Accepts a visitor function and applies it to the current node and its children. 

209 

210 Args: 

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

212 

213 Returns: 

214 None 

215 

216 """ 

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

218 for child in self.children: 

219 child.accept(function)