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

139 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: Factory for building C/C++ AST patterns from Clang-parsed source.""" 

8 

9import re 

10from collections.abc import Sequence 

11 

12from more_itertools import first 

13from more_itertools.more import last 

14 

15from renaissance.integrations.clang.cpp_utils import CPPUtils 

16from renaissance.integrations.clang.predicates import ( 

17 is_clang_compound_statement, 

18 is_clang_kind, 

19 is_clang_macro_definition, 

20) 

21from renaissance.syntax_tree.ast_factory import ASTFactory 

22from renaissance.syntax_tree.ast_finder import find_nodes 

23from renaissance.syntax_tree.ast_node import ASTNode 

24from renaissance.syntax_tree.ast_shower import ASTShower 

25from renaissance.syntax_tree.semantic_kind import SemanticKind 

26 

27SHOW_NODE = False 

28 

29 

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

31 return kind is None or kind(node) 

32 

33 

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

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

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

37 header = "\n" 

38 if ref_node: 

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

40 offset = min( 

41 ( 

42 n.offset 

43 for n in ref_node.children 

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

45 ), 

46 default=0, 

47 ) 

48 

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

50 header += "\n".join( 

51 n.text + ";" 

52 for n in ref_node.children 

53 if n.is_part_of_translation_unit() 

54 and ( 

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

56 or is_clang_macro_definition(n) 

57 ) 

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

59 ) 

60 header += "\n" 

61 

62 return header, language 

63 

64 

65class CPatternFactory: 

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

67 

68 reserved_function_name = "__rejuvenation__reserved__function__name__" 

69 reserved_variable_name = "__rejuvenation__reserved__variable__name__" 

70 

71 def __init__( 

72 self, 

73 factory: ASTFactory, 

74 ref_node: ASTNode | None = None, 

75 language: str = "c", 

76 ) -> None: 

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

78 self.factory = factory 

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

80 

81 @staticmethod 

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

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

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

85 indent = split[0] if split else 0 

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

87 

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

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

90 if extra_declarations is None: 

91 extra_declarations = [] 

92 keywords = CPatternFactory._get_keywords_from_text(text) 

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

94 full_text = ( 

95 self.header 

96 + "\n".join(extra_declarations) 

97 + "\n" 

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

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

100 ) 

101 root = self._create(full_text) 

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

103 return last( 

104 n.children[0] 

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

106 if n.is_part_of_translation_unit 

107 ) 

108 

109 def create_declarations( 

110 self, 

111 text: str, 

112 types=None, 

113 parameters=None, 

114 extra_declarations=None, 

115 declarations=None, 

116 ): 

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

118 if declarations is None: 

119 declarations = [] 

120 if extra_declarations is None: 

121 extra_declarations = [] 

122 if parameters is None: 

123 parameters = [] 

124 if types is None: 

125 types = [] 

126 keywords = CPatternFactory._get_keywords_from_text(text) 

127 keywords = [ 

128 k 

129 for k in keywords 

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

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

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

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

134 ] 

135 return self._create_body( 

136 text, 

137 types, 

138 [*parameters, *keywords], 

139 extra_declarations, 

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

141 ) 

142 

143 def create_declaration( 

144 self, 

145 text: str, 

146 types=None, 

147 parameters=None, 

148 extra_declarations=None, 

149 declarations=None, 

150 ) -> ASTNode: 

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

152 if declarations is None: 

153 declarations = [] 

154 if extra_declarations is None: 

155 extra_declarations = [] 

156 if parameters is None: 

157 parameters = [] 

158 if types is None: 

159 types = [] 

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

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

162 return result[0] 

163 

164 def create_statements( 

165 self, 

166 text: str, 

167 types=None, 

168 extra_declarations=None, 

169 kind=None, 

170 ) -> Sequence[ASTNode]: 

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

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

173 if extra_declarations is None: 

174 extra_declarations = [] 

175 if types is None: 

176 types = [] 

177 parameters = [ 

178 par 

179 for par in CPatternFactory._get_keywords_from_text(text) 

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

181 ] 

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

183 

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

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

186 

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

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

189 

190 Args: 

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

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

193 

194 Returns: 

195 object: The object created by the factory. 

196 

197 """ 

198 # print(self.header + text) 

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

200 if kind: 

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

202 return root 

203 

204 def create_statement( 

205 self, 

206 text: str, 

207 types=None, 

208 extra_declarations=None, 

209 kind=None, 

210 ) -> ASTNode: 

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

212 if extra_declarations is None: 

213 extra_declarations = [] 

214 if types is None: 

215 types = [] 

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

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

218 return statements[0] 

219 

220 def _create_body( 

221 self, 

222 text: str, 

223 types: Sequence[str], 

224 parameters: Sequence[str], 

225 extra_declarations: Sequence[str], 

226 kind, 

227 ) -> list[ASTNode]: 

228 full_text = ( 

229 self.header 

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

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

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

233 + "\n" 

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

235 ) 

236 root = self._create(full_text) 

237 

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

239 # node of the specified kind 

240 

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

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

243 

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

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

246 if SHOW_NODE: 

247 ASTShower.show_node(atu) 

248 return atu 

249 

250 @staticmethod 

251 def _get_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"\${0,2}[a-zA-Z]\w*") 

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

255 

256 @staticmethod 

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

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

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

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

261 

262 @staticmethod 

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

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

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

266 

267 @staticmethod 

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

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

270 

271 @staticmethod 

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

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

274 

275 

276class CPPPatternFactory(CPatternFactory): 

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

278 

279 def __init__(self, factory: ASTFactory, ref_node: ASTNode | None = None) -> None: 

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

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

282 

283 def create_constructor_call(self, pattern: str): 

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

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

286 if class_and_args: 

287 class_name = class_and_args.group(1) 

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

289 return self._create_constructor_call(class_name, args) 

290 return None 

291 

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

293 if args is None: 

294 args = [] 

295 arg_call_string = ",".join(args) 

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

297 code = f""" 

298 class {class_name}{{ 

299 public: 

300 {class_name}({arg_decl_string}) {{}} 

301 }}; 

302 class derived : public {class_name}{{ 

303 public: 

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

305 }}; 

306 """ 

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

308 target_class = root.children[-1] 

309 # this should yield something like: 

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

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

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

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

314 if SHOW_NODE: 

315 ASTShower.show_node(target_class) 

316 # search the call expr and the preceding type ref 

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

318 # include the preceding type ref 

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

320 type_ref = call_expr.preceding_sibling 

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

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

323 

324 return call_expr