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

139 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-10-02 13:12 +0000

1"""AI: Factory for building C/C++ AST patterns from Clang-parsed source.""" 

2 

3import re 

4from collections.abc import Sequence 

5 

6from more_itertools import first 

7from more_itertools.more import last 

8 

9from renaissance.integrations.clang.cpp_utils import CPPUtils 

10from renaissance.integrations.clang.predicates import ( 

11 is_clang_compound_statement, 

12 is_clang_kind, 

13 is_clang_macro_definition, 

14) 

15from renaissance.syntax_tree.ast_factory import ASTFactory 

16from renaissance.syntax_tree.ast_finder import find_nodes 

17from renaissance.syntax_tree.ast_node import ASTNode 

18from renaissance.syntax_tree.ast_shower import ASTShower 

19from renaissance.syntax_tree.semantic_kind import SemanticKind 

20 

21SHOW_NODE = False 

22 

23 

24def _matches_kind(node, kind) -> bool: 

25 return kind is None or kind(node) 

26 

27 

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

29 """AI: Build the header text (includes, defines, declarations) needed to compile a standalone pattern snippet.""" 

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

31 header = "\n" 

32 if ref_node: 

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

34 offset = min( 

35 ( 

36 n.offset 

37 for n in ref_node.children 

38 if n.is_part_of_translation_unit() and n.parser_kind in {"InclusionDirective", "INCLUSION_DIRECTIVE"} 

39 ), 

40 default=0, 

41 ) 

42 

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

44 header += "\n".join( 

45 n.text + ";" 

46 for n in ref_node.children 

47 if n.is_part_of_translation_unit() 

48 and ( 

49 n.semantic_kind in {SemanticKind.FUNCTION, SemanticKind.DECLARATION, SemanticKind.DEFINITION} 

50 or is_clang_macro_definition(n) 

51 ) 

52 and len(find_nodes(n, is_clang_compound_statement)) == 0 

53 ) 

54 header += "\n" 

55 

56 return header, language 

57 

58 

59class CPatternFactory: 

60 """AI: Factory for building C-family AST patterns from text, with reserved placeholder names.""" 

61 

62 reserved_function_name = "__rejuvenation__reserved__function__name__" 

63 reserved_variable_name = "__rejuvenation__reserved__variable__name__" 

64 

65 def __init__( 

66 self, 

67 factory: ASTFactory, 

68 ref_node: ASTNode | None = None, 

69 language: str = "c", 

70 ): 

71 """AI: Prepare a pattern factory for creating C-family AST patterns from text.""" 

72 self.factory = factory 

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

74 

75 @staticmethod 

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

77 """AI: Strip the common leading indentation shared by every non-blank line of text.""" 

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

79 indent = split[0] if split else 0 

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

81 

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

83 """AI: Create an AST node for the given C/C++ expression text.""" 

84 if extra_declarations is None: 

85 extra_declarations = [] 

86 keywords = CPatternFactory._get_keywords_from_text(text) 

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

88 full_text = ( 

89 self.header 

90 + "\n".join(extra_declarations) 

91 + "\n" 

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

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

94 ) 

95 root = self._create(full_text) 

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

97 return last( 

98 n.children[0] 

99 for n in find_nodes(root.children[-1], lambda node: is_clang_kind(node, "ParenExpr", "PAREN_EXPR")) 

100 if n.is_part_of_translation_unit 

101 ) 

102 

103 def create_declarations( 

104 self, 

105 text: str, 

106 types=None, 

107 parameters=None, 

108 extra_declarations=None, 

109 declarations=None, 

110 ): 

111 """AI: Create the AST declaration nodes found in the given C/C++ text.""" 

112 if declarations is None: 

113 declarations = [] 

114 if extra_declarations is None: 

115 extra_declarations = [] 

116 if parameters is None: 

117 parameters = [] 

118 if types is None: 

119 types = [] 

120 keywords = CPatternFactory._get_keywords_from_text(text) 

121 keywords = [ 

122 k 

123 for k in keywords 

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

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

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

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

128 ] 

129 return self._create_body( 

130 text, 

131 types, 

132 [*parameters, *keywords], 

133 extra_declarations, 

134 lambda node: is_clang_kind(node, "DeclStmt", "DECL_STMT") or node.semantic_kind is SemanticKind.DECLARATION, 

135 ) 

136 

137 def create_declaration( 

138 self, 

139 text: str, 

140 types=None, 

141 parameters=None, 

142 extra_declarations=None, 

143 declarations=None, 

144 ) -> ASTNode: 

145 """AI: Create the single AST declaration node found in the given C/C++ text.""" 

146 if declarations is None: 

147 declarations = [] 

148 if extra_declarations is None: 

149 extra_declarations = [] 

150 if parameters is None: 

151 parameters = [] 

152 if types is None: 

153 types = [] 

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

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

156 return result[0] 

157 

158 def create_statements( 

159 self, 

160 text: str, 

161 types=None, 

162 extra_declarations=None, 

163 kind=None, 

164 ) -> Sequence[ASTNode]: 

165 """AI: Create the AST statement nodes found in the given C/C++ text.""" 

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

167 if extra_declarations is None: 

168 extra_declarations = [] 

169 if types is None: 

170 types = [] 

171 parameters = [ 

172 par 

173 for par in CPatternFactory._get_keywords_from_text(text) 

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

175 ] 

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

177 

178 def create(self, text: str, kind=None) -> ASTNode: 

179 """Create an object using the factory from the provided text. 

180 

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

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

183 

184 Args: 

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

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

187 

188 Returns: 

189 object: The object created by the factory. 

190 

191 """ 

192 # print(self.header + text) 

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

194 if kind: 

195 return first(find_nodes(root.children[-1], lambda node: _matches_kind(node, kind))) 

196 return root 

197 

198 def create_statement( 

199 self, 

200 text: str, 

201 types=None, 

202 extra_declarations=None, 

203 kind=None, 

204 ) -> ASTNode: 

205 """AI: Create the single AST statement node found in the given C/C++ text.""" 

206 if extra_declarations is None: 

207 extra_declarations = [] 

208 if types is None: 

209 types = [] 

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

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

212 return statements[0] 

213 

214 def _create_body( 

215 self, 

216 text: str, 

217 types: Sequence[str], 

218 parameters: Sequence[str], 

219 extra_declarations: Sequence[str], 

220 kind, 

221 ) -> list[ASTNode]: 

222 full_text = ( 

223 self.header 

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

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

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

227 + "\n" 

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

229 ) 

230 root = self._create(full_text) 

231 

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

233 # node of the specified kind 

234 

235 body = first(find_nodes(root.children[-1], is_clang_compound_statement)).children 

236 return list(n for n in body if n.is_part_of_translation_unit and first(find_nodes(n, lambda node: _matches_kind(node, kind)))) 

237 

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

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

240 if SHOW_NODE: 

241 ASTShower.show_node(atu) 

242 return atu 

243 

244 @staticmethod 

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

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

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

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

249 

250 @staticmethod 

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

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

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

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

255 

256 @staticmethod 

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

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

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

260 

261 @staticmethod 

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

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

264 

265 @staticmethod 

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

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

268 

269 

270class CPPPatternFactory(CPatternFactory): 

271 """AI: Factory for building C++-specific AST patterns, extending CPatternFactory with constructor-call support.""" 

272 

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

274 """AI: Prepare a pattern factory for creating C++-specific AST patterns from text.""" 

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

276 

277 def create_constructor_call(self, pattern: str): 

278 """AI: Create an AST node for a C++ constructor call matching the given pattern.""" 

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

280 if class_and_args: 

281 class_name = class_and_args.group(1) 

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

283 return self._create_constructor_call(class_name, args) 

284 return None 

285 

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

287 if args is None: 

288 args = [] 

289 arg_call_string = ",".join(args) 

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

291 code = f""" 

292 class {class_name}{{ 

293 public: 

294 {class_name}({arg_decl_string}) {{}} 

295 }}; 

296 class derived : public {class_name}{{ 

297 public: 

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

299 }}; 

300 """ 

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

302 target_class = root.children[-1] 

303 # this should yield something like: 

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

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

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

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

308 if SHOW_NODE: 

309 ASTShower.show_node(target_class) 

310 # search the call expr and the preceding type ref 

311 call_expr = last(find_nodes(target_class, lambda node: node.semantic_kind is SemanticKind.CALL)) 

312 # include the preceding type ref 

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

314 type_ref = call_expr.preceding_sibling 

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

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

317 

318 return call_expr