Coverage for src / renaissance / integrations / clang / c_pattern_factory.py: 72%

136 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-09-09 14:04 +0000

1import re 

2from collections.abc import Sequence 

3 

4from more_itertools import first 

5from more_itertools.more import last 

6 

7from renaissance.integrations.clang.cpp_utils import CPPUtils 

8from renaissance.integrations.types import ( 

9 Call, 

10 CompoundStatement, 

11 Declaration, 

12 FunctionDef, 

13 InclusionDirective, 

14 MacroDef, 

15 ParenthesizedExpression, 

16 Type, 

17 TypedefDef, 

18 VariableDef, 

19) 

20from renaissance.syntax_tree.ast_factory import ASTFactory 

21from renaissance.syntax_tree.ast_finder import find_ast_type 

22from renaissance.syntax_tree.ast_node import ASTNode 

23from renaissance.syntax_tree.ast_shower import ASTShower 

24 

25SHOW_NODE = False 

26 

27 

28def derive_header_text(language: str, ref_node: ASTNode | None): 

29 # collect includes #defines and var decl from the refNode 

30 header = "\n" 

31 if ref_node: 

32 language = ref_node.filename.split(".")[-1] 

33 offset = min( 

34 (n.offset for n in ref_node.children if n.is_part_of_translation_unit() and n.ast_type == InclusionDirective), 

35 default=0, 

36 ) 

37 

38 header = CPatternFactory.remove_indent(ref_node.content(0, offset)) 

39 header += "\n".join( 

40 n.text + ";" 

41 for n in ref_node.children 

42 if n.is_part_of_translation_unit() 

43 and isinstance(n.ast_type(), (FunctionDef, VariableDef | TypedefDef, MacroDef)) 

44 and len(find_ast_type(n, CompoundStatement)) == 0 

45 ) 

46 # and isinstance(n.ast_type, (Declaration, MacroDefinition)) 

47 # and len(find_ast_type(n, CompoundStatement)) == 0 

48 header += "\n" 

49 

50 return header, language 

51 

52 

53class CPatternFactory: 

54 reserved_function_name = "__rejuvenation__reserved__function__name__" 

55 reserved_variable_name = "__rejuvenation__reserved__variable__name__" 

56 

57 def __init__( 

58 self, 

59 factory: ASTFactory, 

60 ref_node: ASTNode | None = None, 

61 language: str = "c", 

62 ): 

63 self.factory = factory 

64 self.header, self.language = derive_header_text(language, ref_node) 

65 

66 @staticmethod 

67 def remove_indent(text: str) -> str: 

68 split = [len(line) - len(line.lstrip()) for line in text.splitlines() if line.strip()] 

69 indent = split[0] if split else 0 

70 return "\n".join([line[indent:] for line in text.splitlines()]) 

71 

72 def create_expression(self, text: str, extra_declarations=None) -> ASTNode: 

73 if extra_declarations is None: 

74 extra_declarations = [] 

75 keywords = CPatternFactory._get_keywords_from_text(text) 

76 keywords = [k for k in keywords if not any(k in ed for ed in extra_declarations)] 

77 full_text = ( 

78 self.header 

79 + "\n".join(extra_declarations) 

80 + "\n" 

81 + "\n".join(CPatternFactory._to_declaration(keywords)) 

82 + f"\nvoid {CPatternFactory.reserved_function_name}() {{ int {CPatternFactory.reserved_variable_name} = ({text}); }}" 

83 ) 

84 root = self._create(full_text) 

85 # return the first expression found in the tree as a ASTNode 

86 return last(n.children[0] for n in find_ast_type(root.children[-1], ParenthesizedExpression) if n.is_part_of_translation_unit) 

87 

88 def create_declarations( 

89 self, 

90 text: str, 

91 types=None, 

92 parameters=None, 

93 extra_declarations=None, 

94 declarations=None, 

95 ): 

96 if declarations is None: 

97 declarations = [] 

98 if extra_declarations is None: 

99 extra_declarations = [] 

100 if parameters is None: 

101 parameters = [] 

102 if types is None: 

103 types = [] 

104 keywords = CPatternFactory._get_keywords_from_text(text) 

105 keywords = [ 

106 k 

107 for k in keywords 

108 if not any(k in ed for ed in extra_declarations) 

109 and not any(k in ed for ed in parameters) 

110 and not any(k in ed for ed in types) 

111 and not any(k in ed for ed in declarations) 

112 ] 

113 return self._create_body(text, types, [*parameters, *keywords], extra_declarations, Declaration) 

114 

115 def create_declaration( 

116 self, 

117 text: str, 

118 types=None, 

119 parameters=None, 

120 extra_declarations=None, 

121 declarations=None, 

122 ) -> ASTNode: 

123 if declarations is None: 

124 declarations = [] 

125 if extra_declarations is None: 

126 extra_declarations = [] 

127 if parameters is None: 

128 parameters = [] 

129 if types is None: 

130 types = [] 

131 result = self.create_declarations(text, types, parameters, extra_declarations, declarations) 

132 assert len(result) > 0, "At least one declaration is expected" 

133 return result[0] 

134 

135 def create_statements( 

136 self, 

137 text: str, 

138 types=None, 

139 extra_declarations=None, 

140 kind: type[Type] = Type, 

141 ) -> Sequence[ASTNode]: 

142 # create a reference for all used variables excluding the specified types 

143 if extra_declarations is None: 

144 extra_declarations = [] 

145 if types is None: 

146 types = [] 

147 parameters = [ 

148 par 

149 for par in CPatternFactory._get_keywords_from_text(text) 

150 if par not in types and not any(par in ed for ed in extra_declarations) 

151 ] 

152 return self._create_body(text, types, parameters, extra_declarations, kind) 

153 

154 def create(self, text: str, kind: type[Type] = None) -> ASTNode: 

155 """Creates an object using the factory from the provided text. 

156 The object is created by the factory using the provided text and the header of the provided reference node. 

157 It is up to the user to pick the right node for pattern matching. 

158 

159 Args: 

160 text (str): The input text used to create the object. 

161 kind (str, optional): The kind of the node to be returned. Defaults to None. 

162 

163 Returns: 

164 object: The object created by the factory. 

165 

166 """ 

167 # print(self.header + text) 

168 root = self.factory.create_from_text(self.header + text, "test." + self.language) 

169 if kind: 

170 return first(find_ast_type(root.children[-1], kind)) 

171 return root 

172 

173 def create_statement( 

174 self, 

175 text: str, 

176 types=None, 

177 extra_declarations=None, 

178 kind: str = Type, 

179 ) -> ASTNode: 

180 if extra_declarations is None: 

181 extra_declarations = [] 

182 if types is None: 

183 types = [] 

184 statements = list(self.create_statements(text, types, extra_declarations, kind)) 

185 assert len(statements) == 1, "Only one statement is expected" 

186 return statements[0] 

187 

188 def _create_body( 

189 self, 

190 text: str, 

191 types: Sequence[str], 

192 parameters: Sequence[str], 

193 extra_declarations: Sequence[str], 

194 kind: type[Type], 

195 ) -> list[ASTNode]: 

196 full_text = ( 

197 self.header 

198 + "\n".join(CPatternFactory._to_typedef(types)) 

199 + "\n\n".join(CPatternFactory._to_declaration(parameters)) 

200 + "\n\n".join(extra_declarations) 

201 + "\n" 

202 "\nvoid " + CPatternFactory.reserved_function_name + "(){\n" + text + "\n}" 

203 ) 

204 root = self._create(full_text) 

205 

206 # from the children of the compound statement that contains the text, get for each child the first 

207 # node of the specified kind 

208 

209 body = first(find_ast_type(root.children[-1], CompoundStatement)).children 

210 return list(n for n in body if n.is_part_of_translation_unit and first(find_ast_type(n, kind))) 

211 

212 def _create(self, text: str) -> ASTNode: 

213 atu = self.factory.create_from_text(text, "test." + self.language) 

214 if SHOW_NODE: 

215 ASTShower.show_node(atu) 

216 return atu 

217 

218 @staticmethod 

219 def _get_keywords_from_text(text: str) -> Sequence[str]: 

220 # regex to get keywords that start with one of two dollars followed by a \\w+ 

221 pattern = re.compile(r"\${0,2}[a-zA-Z]\w*") 

222 return list(k for k in set(re.findall(pattern, text)) if k not in CPPUtils.RESERVED_KEYWORDS) 

223 

224 @staticmethod 

225 def _get_dollar_keywords_from_text(text: str) -> Sequence[str]: 

226 # regex to get keywords that start with one of two dollars followed by a \\w+ 

227 pattern = re.compile(r"\${1,2}[a-zA-Z]\w*") 

228 return list(set(re.findall(pattern, text))) 

229 

230 @staticmethod 

231 def _get_non_dollar_keywords_from_text(text: str) -> Sequence[str]: 

232 pattern = re.compile(r"[^$][a-zA-Z]\w*") 

233 return list(set(re.findall(pattern, text))) 

234 

235 @staticmethod 

236 def _to_declaration(keywords: Sequence[str], prefix: str = "int ", postfix: str = ";") -> Sequence[str]: 

237 return [prefix + keyword + postfix for keyword in keywords] 

238 

239 @staticmethod 

240 def _to_typedef(keywords: Sequence[str], prefix: str = "typedef int ", postfix: str = ";") -> Sequence[str]: 

241 return [prefix + keyword + postfix for keyword in keywords] 

242 

243 

244class CPPPatternFactory(CPatternFactory): 

245 def __init__(self, factory: ASTFactory, ref_node: ASTNode | None = None): 

246 super().__init__(factory, ref_node, "cpp") 

247 

248 def create_constructor_call(self, pattern: str): 

249 class_and_args = re.match(R"([$\w]+)\(([^)]+)\)", pattern.replace(" ", "")) 

250 if class_and_args: 

251 class_name = class_and_args.group(1) 

252 args = class_and_args.group(2).split(",") 

253 return self._create_constructor_call(class_name, args) 

254 return None 

255 

256 def _create_constructor_call(self, class_name: str, args=None): 

257 if args is None: 

258 args = [] 

259 arg_call_string = ",".join(args) 

260 arg_decl_string = ",".join("int " + arg for arg in args) 

261 code = f""" 

262 class {class_name}{{ 

263 public: 

264 {class_name}({arg_decl_string}) {{}} 

265 }}; 

266 class derived : public {class_name}{{ 

267 public: 

268 derived({arg_decl_string}) : {class_name}({arg_call_string}) {{ }} 

269 }}; 

270 """ 

271 root: ASTNode = self.factory.create_from_text(code, "test." + self.language) 

272 target_class = root.children[-1] 

273 # this should yield something like: 

274 # (TYPE_REF, $var, test.cpp[237:241]): |$var| 

275 # (CALL_EXPR, , test.cpp[237:266]): |$var($container,$headerCount)| 

276 # (DECL_REF_EXPR, $container, test.cpp[242:252]): |$container| 

277 # (DECL_REF_EXPR, $headerCount, test.cpp[253:265]): |$headerCount| 

278 if SHOW_NODE: 

279 ASTShower.show_node(target_class) 

280 # search the call expr and the preceding type ref 

281 call_expr = last(find_ast_type(target_class, Call)) 

282 # include the preceding type ref 

283 assert isinstance(call_expr, ASTNode), "No call expression found" 

284 type_ref = call_expr.preceding_sibling 

285 assert isinstance(type_ref, ASTNode), "No type ref found" 

286 # return the constrained pattern where the first node must be of type TypeRef 

287 

288 return call_expr