Coverage for src/renaissance/integrations/clang/clang_json_ast_node.py: 89%

420 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: ASTNode implementation backed by Clang's JSON AST dump output.""" 

8 

9# create a class that inherits syntax tree ASTNode 

10 

11import json 

12import os 

13import re 

14import shutil 

15import subprocess 

16import sys 

17import tempfile 

18import warnings 

19from collections.abc import Sequence 

20from functools import cache 

21from importlib.metadata import version 

22from pathlib import Path 

23from typing import Any, Self, override 

24 

25from renaissance.integrations.clang.cpp_utils import CPPUtils, matches_node_kind 

26from renaissance.integrations.clang.kinds import CLANG_KIND_MAP 

27from renaissance.integrations.clang.predicates import ( 

28 is_clang_declaration_reference, 

29 is_clang_kind, 

30) 

31from renaissance.syntax_tree import ASTNode, ASTReference 

32from renaissance.syntax_tree.pattern_kind import PatternKind 

33from renaissance.syntax_tree.semantic_kind import SemanticKind 

34from renaissance.utils.ast_utils import match_children, match_props 

35 

36EMPTY_DICT = {} 

37EMPTY_STR = "" 

38EMPTY_LIST: list[ClangJsonASTReference] = [] 

39ON_NODE_ID_TAGS = ["previousDecl", "parentDeclContextId"] 

40ID_TAGS = [ 

41 "id", 

42 "typeAliasDeclId", 

43 "templateDeclId", 

44 "templateSpecializationDeclId", 

45 "referencedDeclId", 

46 *ON_NODE_ID_TAGS, 

47] 

48 

49STMT_PARENT_KINDS = {"CompoundStmt", "COMPOUND_STMT", "TranslationUnitDecl", "TRANSLATION_UNIT", "translation_unit"} 

50IRRELEVANT_PROPS = {"macro_expansion", "start_point", "end_point", "source_code", "location", "type"} 

51IRRELEVANT_NODE_KINDS = {"comment", "Comment", "MacroDefinition", "MACRO_DEFINITION", "FullComment"} 

52VERBOSE = False 

53COMPILER_ENV_VAR = "RENAISSANCE_CLANG" 

54# The libclang bindings used by ClangASTNode are pinned by this distribution, so its version is the reference. 

55LIBCLANG_DISTRIBUTION = "libclang-ng" 

56COMPILER_HINT = ( 

57 f"Install LLVM and put clang (clang++ for C++) on PATH, set the {COMPILER_ENV_VAR} environment variable, " 

58 f"or call ClangJsonASTNode.set_compiler_path()." 

59) 

60 

61 

62@cache 

63def verify_compiler(compiler: str) -> None: 

64 """AI: Check the clang driver runs and warn when its major version differs from the pinned libclang version.""" 

65 arguments = [compiler, "--version"] 

66 try: 

67 result = subprocess.run(arguments, capture_output=True, text=True, check=False) # noqa: S603 (the compiler comes from the configuration, not from parsed input) 

68 except OSError as error: 

69 message = f"The clang driver {compiler} cannot be executed. {COMPILER_HINT}" 

70 raise FileNotFoundError(message) from error 

71 found = re.search(r"clang version (\d+)", result.stdout) 

72 pinned = version(LIBCLANG_DISTRIBUTION).split(".")[0] 

73 if found and found.group(1) != pinned: 

74 message = f"{compiler} is LLVM {found.group(1)} while libclang is pinned to LLVM {pinned}; the ASTs may differ." 

75 warnings.warn(message, stacklevel=2) 

76 

77 

78def resolve_compiler(file_path: Path) -> str: 

79 """AI: Return the clang driver to invoke: the pinned path, the environment override, or the one found on PATH.""" 

80 name = "clang++" if file_path.suffix == ".cpp" else "clang" 

81 compiler = ClangJsonASTNode.compiler_path or os.environ.get(COMPILER_ENV_VAR) or shutil.which(name) 

82 if compiler is None: 

83 message = f"No clang driver found. {COMPILER_HINT}" 

84 raise FileNotFoundError(message) 

85 verify_compiler(compiler) 

86 return compiler 

87 

88 

89class ClangJsonASTReference: 

90 """AI: Represent a reference from one clang-json AST node to another by id.""" 

91 

92 def __init__(self, node_id: str, ref_kind: str, properties: dict[str, Any]) -> None: 

93 """AI: Represent a reference from one clang-json AST node to another by id.""" 

94 self.node_id = node_id 

95 self.ref_kind = ref_kind 

96 self.properties = properties 

97 

98 

99class ClangJsonTranslationUnit: 

100 """AI: Wrap a parsed clang JSON AST dump with lazily-built reference caches.""" 

101 

102 def __init__(self, json_root: dict[str, Any], file_name: str) -> None: 

103 """AI: Wrap a parsed clang JSON AST dump with lazily-built reference caches.""" 

104 self.json_root = json_root 

105 self.filename = file_name 

106 self.references_initialized = False 

107 # references are used as a cache to store the references of a node 

108 # they are stored as id for lazy creation 

109 self._references: dict[str, list[ClangJsonASTReference]] = {} 

110 self._referenced_by: dict[str, list[ClangJsonASTReference]] = {} 

111 self._nodes: dict[str, ClangJsonASTNode] = {} 

112 

113 def lazy_create_references(self, node: ClangJsonASTNode) -> None: 

114 """AI: Build the translation unit's reference and record-reference caches on first use.""" 

115 # TODO: Do I correctly assume that the usage of this function must be synchronized? 

116 if self.references_initialized: 

117 return 

118 node.root.process(ReferenceHelper.create_references) 

119 node.root.process(ReferenceHelper.add_record_references) 

120 self.references_initialized = True 

121 

122 

123class ClangJsonASTNode(ASTNode[dict[str, Any], ClangJsonTranslationUnit]): 

124 """AI: ASTNode implementation backed by clang's JSON AST dump format.""" 

125 

126 parse_args = [ 

127 "-fparse-all-comments", 

128 "-ferror-limit=0", 

129 "-Xclang", 

130 "-ast-dump=json", 

131 "-fsyntax-only", 

132 ] 

133 compiler_path: str | None = None 

134 

135 @staticmethod 

136 def set_compiler_path(path: str | Path | None) -> None: 

137 """AI: Pin the clang driver used for the JSON AST dump; pass None to fall back to the environment.""" 

138 ClangJsonASTNode.compiler_path = str(path) if path else None 

139 

140 def __init__( 

141 self, 

142 node: dict[str, Any], 

143 translation_unit: ClangJsonTranslationUnit, 

144 parent: Self | None = None, 

145 start_offset: int | None = None, 

146 length: int | None = None, 

147 insert_kind: str | None = None, 

148 insert_name: str | None = None, 

149 ) -> None: 

150 """AI: Wrap a clang JSON AST dict node as an AST node within the given translation unit.""" 

151 super().__init__(self if parent is None else parent.root) 

152 self.node = node 

153 self._children: Sequence[ClangJsonASTNode] | None = None 

154 self._parent = parent 

155 self.translation_unit = translation_unit 

156 self._filename = translation_unit.filename 

157 self.inserted = insert_kind is not None 

158 self.show_props = False 

159 # if the node has not been added to the translation unit, add it 

160 # a node might already be added if it is split into multiple nodes 

161 # an example is for base types like int, char, etc. which are split into multiple nodes 

162 if "id" in node and self.translation_unit._nodes.get(node["id"]) is None: 

163 self.translation_unit._nodes[node["id"]] = self 

164 self._offset = start_offset if start_offset is not None else self.__derive_start_offset() 

165 self._end_offset = self._offset + length if length is not None else self.__derive_end_offset() 

166 self._length = self._end_offset - self._offset 

167 self._kind = insert_kind if insert_kind is not None else self.__derive_kind() 

168 self.parser_kind = self._kind 

169 self.semantic_kind = CLANG_KIND_MAP.get(self.parser_kind, SemanticKind.NODE) 

170 self.pattern_kind = { 

171 "MatchOne": PatternKind.MATCH_ONE, 

172 "MatchAll": PatternKind.MATCH_ALL, 

173 }.get(self.parser_kind) 

174 self._name = insert_name if insert_name is not None else self._derive_name() 

175 # a fake child is introduced to handle the case where the type of declaration is not found 

176 # for example in the case of a base type. 

177 # without the fake child pattern matching on types will be difficult 

178 self.__inserted_children: list[ClangJsonASTNode] = [] 

179 node_type = self.node.get("type") 

180 if insert_kind is None and node_type and not self.node.get("implicit") and re.fullmatch("(Var|Function|CxxMethod)Decl", self._kind): 

181 declared_type = node_type["qualType"].replace("(", "").replace(")", "").strip() 

182 if self.node.get("loc"): 

183 loc = self.node["loc"] 

184 offset = loc["offset"] if loc.get("offset") else self._get(["loc", "expansionLoc", "offset"], 0) 

185 tok_len = loc["tokLen"] if loc.get("tokLen") else self._get(["loc", "expansionLoc", "tokLen"], 0) 

186 if tok_len != 0: 

187 insert_child = ClangJsonASTNode( 

188 self.node, 

189 self.translation_unit, 

190 self, 

191 offset, 

192 tok_len, 

193 "DeclLoc", 

194 ) 

195 insert_child._children = [] 

196 self.__inserted_children.append(insert_child) 

197 if "TypeRef" not in [inner["kind"] for inner in self.node.get("inner", [])]: 

198 # deep clone the type node and remove the parentheses 

199 base_type = node_type.get("desugaredQualType", declared_type).replace("(", "").replace(")", "").strip() 

200 if base_type in CPPUtils.RESERVED_KEYWORDS: 

201 length_ref = len(declared_type.encode(sys.getdefaultencoding())) 

202 insert_child = ClangJsonASTNode( 

203 self.node, 

204 self.translation_unit, 

205 self, 

206 self._offset, 

207 length_ref, 

208 "TypeRef", 

209 declared_type, 

210 ) 

211 insert_child._children = [] 

212 self.__inserted_children.append(insert_child) 

213 # add the declaration as node 

214 # deep clone the type node and remove the parentheses 

215 elif is_clang_declaration_reference(self): 

216 if self.name.startswith("$$"): 

217 self._kind = "MatchAll" 

218 self.pattern_kind = PatternKind.MATCH_ALL 

219 elif self.name.startswith("$"): 

220 self._kind = "MatchOne" 

221 self.pattern_kind = PatternKind.MATCH_ONE 

222 self._children = self.__inserted_children + [ 

223 ClangJsonASTNode( 

224 ClangJsonASTNode._remove_wrapper(n), 

225 translation_unit=self.translation_unit, 

226 parent=self, 

227 ) 

228 for n in self.node.get("inner", []) 

229 if not n.get("isImplicit", False) 

230 ] 

231 self._children = [n for n in self._children if n.parser_kind not in IRRELEVANT_NODE_KINDS] 

232 

233 def __eq__(self, other) -> bool: 

234 """AI: Return whether this node is structurally equal to `other`, ignoring irrelevant properties/children.""" 

235 return ( 

236 isinstance(other, type(self)) 

237 and self.kind == other.kind 

238 and match_props(self.properties, other.properties, IRRELEVANT_PROPS) 

239 and match_children(self.children, other.children, IRRELEVANT_NODE_KINDS) 

240 ) 

241 

242 @override 

243 @staticmethod 

244 def load( 

245 file_path: Path, 

246 extra_args: Sequence[str], 

247 working_dir: Path, 

248 code: str | None = None, 

249 ) -> Self: 

250 # in a shell process compile the file_path with clang compiler 

251 try: 

252 # remove the compiler name if it is the first argument 

253 if len(extra_args) > 0 and re.match(r".*(g\+\+|gcc|cl\.exe).*", extra_args[0]): 

254 extra_args = extra_args[1:] 

255 # add clang compiler if it is not in the arguments 

256 if len(extra_args) == 0 or "clang" not in extra_args[0]: 

257 extra_args = [resolve_compiler(file_path), *extra_args] 

258 

259 command = [*extra_args, *ClangJsonASTNode.parse_args] 

260 if code: 

261 if str(file_path) in command: 

262 command.remove(str(file_path)) 

263 compile_flag = "-xc++" if file_path.suffix == ".cpp" else "-xc" 

264 if compile_flag not in command: 

265 command.append(compile_flag) 

266 if "-" not in command: 

267 command.append("-") 

268 # command.append('-main-file-name=' + str(file_path)) 

269 result = subprocess.run( 

270 command, 

271 input=code, 

272 capture_output=True, 

273 text=True, 

274 cwd=working_dir, 

275 ) 

276 length = len(code) 

277 else: 

278 if str(file_path) not in command: 

279 command.append(str(file_path)) 

280 result = subprocess.run( 

281 command, 

282 capture_output=True, 

283 text=True, 

284 cwd=working_dir, 

285 ) 

286 length = Path(working_dir / file_path).stat().st_size 

287 json_dump = result.stdout.replace("<stdin>", str(file_path)) 

288 error = result.stderr 

289 

290 if VERBOSE: 

291 temp_dir = tempfile.gettempdir() 

292 temp_file_name = Path(temp_dir) / (file_path.name + ".ast.json") 

293 with temp_file_name.open("w") as std_out_file: 

294 print("result stored in " + str(temp_file_name)) 

295 std_out_file.write(json_dump) 

296 print(error, file=sys.stderr) 

297 json_atu = json.loads(json_dump) 

298 atu = ClangJsonASTNode( 

299 json_atu, 

300 translation_unit=ClangJsonTranslationUnit(json_atu, file_name=str(file_path)), 

301 length=length, 

302 ) 

303 if code: 

304 # TODO: see docs/developer/architecture/position-consistency.md - clang decides the encoding, not the host. 

305 atu.cache[str(file_path)] = code.encode(sys.getfilesystemencoding()) 

306 else: 

307 with Path(working_dir / file_path).open("rb") as f: 

308 atu.cache[str(file_path)] = f.read() 

309 # cache the result of the temp file before deleting it 

310 atu.content(0, 0) 

311 return atu 

312 

313 except Exception as e: 

314 print("Call to clang failed. Did you install clang?, is it on the env path?") 

315 raise e 

316 

317 @override 

318 @staticmethod 

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

320 return ClangJsonASTNode.load(Path(file_name), extra_args, working_dir, code=text) 

321 

322 @cache 

323 def _get_containing_filename(self) -> str: 

324 if self.node.get("isImplicit", False): 

325 return "" 

326 if self.node.get("implicit", False): 

327 return "" 

328 if not self.parent: 

329 return self.translation_unit.file_name 

330 # return the file name of the node if it exists else return the file name of the parent node 

331 containing_file = self._get(["loc", "file"], EMPTY_STR) 

332 if containing_file: 

333 return containing_file 

334 included_file = self._get(["loc", "includedFrom", "file"], "") 

335 if included_file: # included but no file location is provided in the node so we don't know the file name 

336 return "" 

337 included_file = self._get(["loc", "spellingLoc", "includedFrom", "file"], "") 

338 if included_file: # included but no file location is provided in the node so we don't know the file name 

339 return "" 

340 # not included and no file location so it is the same as the parent 

341 if self.parent: 

342 return self.parent.filename 

343 return EMPTY_STR 

344 

345 @override 

346 @property 

347 def extended_end_offset(self) -> int: 

348 try: 

349 # TODO: Do I correctly assume this is for Expression Statements like 

350 end_offset = self._end_offset 

351 # "f(x,y);" and "a = f(3);" that are according to clang NOT statements, 

352 # but expressions (without the semicolon) 

353 if (not self._is_statement_or_declaration()) and (self.parent and self.parent.parser_kind in STMT_PARENT_KINDS): 

354 content = self.root.binary_file_content() 

355 while ( 

356 end_offset < len(content) and content[end_offset - 1] not in b";" 

357 ): # Why use 'in' when list has one element, i.e. ';'? 

358 end_offset += 1 

359 return end_offset 

360 except Exception: 

361 return 0 

362 

363 def _is_statement_or_declaration(self): 

364 return re.match("(?i).*(Stmt|Decl)", self.kind) 

365 return self.semantic_kind in {SemanticKind.STATEMENT, SemanticKind.DECLARATION, SemanticKind.DEFINITION} 

366 

367 # TODO: can @property work here given matches_kind takes a required node argument? 

368 @override 

369 @property 

370 def matches_kind(self, node: ASTNode[dict[str, Any], ClangJsonTranslationUnit]) -> bool: 

371 return matches_node_kind(self, node) 

372 

373 @override 

374 @property 

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

376 # get all the attributes of self.node except the inner nodes, id, location, range, kind and name and 

377 # all reference nodes (that is children with 'id) 

378 properties = { 

379 k: ClangJsonASTNode._remove_ids(v) 

380 for k, v in self.node.items() 

381 if ClangJsonASTNode.__is_property(k) and ClangJsonASTNode._is_reference(v) is not None 

382 } 

383 if self._get(["range", "end", "expansionLoc", "offset"], -1) != -1: # dealing with a macro expansion 

384 properties["macro_expansion"] = self.text 

385 # matching name through props 

386 if is_clang_declaration_reference(self): 

387 properties["name"] = self.name 

388 

389 return properties 

390 

391 @override 

392 @property 

393 def referenced_by(self) -> Sequence[ASTReference[dict[str, Any], ClangJsonTranslationUnit]]: 

394 if self.inserted: 

395 return [] 

396 self.translation_unit.lazy_create_references(self) 

397 ref_by = self.translation_unit._referenced_by.get(self.node["id"], EMPTY_LIST) 

398 definition_node_id = self._get_function_definition() 

399 if definition_node_id: 

400 # try to find the definition which might have references 

401 ref_by += self.translation_unit._referenced_by.get(definition_node_id, EMPTY_LIST) 

402 return [ 

403 ASTReference(self.translation_unit._nodes[ref.node_id], ref.ref_kind, ref.properties) 

404 for ref in ref_by 

405 if ref.node_id != self.node["id"] 

406 ] 

407 

408 def _get_function_definition(self): 

409 refs = self.translation_unit._referenced_by.get(self.node["id"], EMPTY_LIST) 

410 for ref in refs: 

411 if ref.ref_kind == "previousDecl": 

412 return ref.node_id 

413 return None 

414 

415 @override 

416 @property 

417 def references(self) -> list[ASTReference[dict[str, Any], ClangJsonTranslationUnit]]: 

418 if self.inserted: 

419 return [] 

420 self.translation_unit.lazy_create_references(self) 

421 

422 refs = self.translation_unit._references.get(self.node["id"], EMPTY_LIST) 

423 definition_node_id = self._get_function_definition() 

424 # TODO: also class definitions, type definitions, ... 

425 if definition_node_id: 

426 # try to find the definition which might have references 

427 refs += self.translation_unit._references.get(definition_node_id, EMPTY_LIST) 

428 # remove duplicates 

429 refs = list({ref.node_id: ref for ref in refs}.values()) 

430 

431 return [ 

432 ASTReference(self.translation_unit._nodes[ref.node_id], ref.ref_kind, ref.properties) 

433 for ref in refs 

434 if ref.node_id != self.node["id"] 

435 ] 

436 

437 @override 

438 @property 

439 def is_statement(self) -> bool: 

440 return ( 

441 self.parent is not None and self.parent.parser_kind in STMT_PARENT_KINDS 

442 ) # TODO: Why look at the kind of your parent and not at your own kind? 

443 

444 def _derive_name(self) -> str: 

445 name = self.node.get("name") 

446 if name: 

447 return name 

448 kind = self.node.get("kind") 

449 decl_ref_name_path = ["referencedDecl", "name"] 

450 if kind == "CallExpr": 

451 # equalize with libclang 

452 decl_ref_child = [inner["kind"] for inner in self.node.get("inner", []) if inner.get("kind") == "DeclRefExpr"] 

453 if decl_ref_child: 

454 return self._get_property(decl_ref_child[0], decl_ref_name_path, default=EMPTY_STR) 

455 if kind == "DeclRefExpr": 

456 return self._get(decl_ref_name_path, default=EMPTY_STR) 

457 if kind == "StringLiteral": 

458 return self._get(["value"], default=EMPTY_STR) 

459 return self.node.get("name", EMPTY_STR) 

460 

461 def __derive_start_offset(self) -> int: 

462 offset = self._get(["range", "begin", "offset"], default=-1) 

463 if offset == -1: 

464 # we might be dealing with a macro in that case use the expansion location 

465 offset = self._get(["range", "begin", "expansionLoc", "offset"], default=0) 

466 return offset 

467 

468 def __derive_end_offset(self) -> int: 

469 if self.__derive_kind() == "TranslationUnitDecl": 

470 return len(self.binary_file_content(self.filename)) 

471 offset = self._get(["range", "end", "offset"], default=-1) 

472 tok_len = self._get(["range", "end", "tokLen"], default=-1) 

473 if offset == -1: 

474 # we might be dealing with a macro in that case use the expansion location 

475 offset = self._get(["range", "end", "expansionLoc", "offset"], default=0) 

476 tok_len = self._get(["range", "end", "expansionLoc", "tokLen"], default=0) 

477 

478 return offset + tok_len 

479 

480 def __derive_kind(self) -> str: 

481 return self.node.get("kind", EMPTY_STR) 

482 

483 @staticmethod 

484 def _remove_wrapper(node) -> dict[str, Any]: 

485 try: 

486 if ClangJsonASTNode._is_wrapped(node): 

487 return ClangJsonASTNode._remove_wrapper(list(node["inner"])[0]) 

488 except Exception: 

489 pass 

490 return node 

491 

492 @staticmethod 

493 def _remove_ids(json_node) -> Any: # noqa: ANN401 (passes non-dict JSON values through unchanged, so the type is the caller's) 

494 if not isinstance(json_node, dict): 

495 return json_node 

496 return {k: v for k, v in json_node.items() if k not in ID_TAGS} 

497 

498 @staticmethod 

499 def _is_reference(json_node) -> bool: 

500 return len(ReferenceHelper._get_reference_ids(json_node)) > 0 

501 

502 @staticmethod 

503 @cache 

504 def __is_property(key) -> bool: 

505 return key not in [ 

506 "id", 

507 "inner", 

508 "loc", 

509 "range", 

510 "kind", 

511 "name", 

512 "isUsed", 

513 "isReferenced", 

514 "referencedDecl", 

515 "mangledName", 

516 *ON_NODE_ID_TAGS, 

517 ] 

518 

519 @staticmethod 

520 def _is_wrapped(node) -> bool: 

521 """Check if a node is wrapped. 

522 

523 A node is considered wrapped if it meets the following conditions: 

524 1. The node does not have an 'id' or its 'kind' starts with "Implicit". 

525 2. The node has exactly one inner node. 

526 """ 

527 return (not node.get("id") or node["kind"].startswith("Implicit")) and len(list(node["inner"])) == 1 

528 

529 def _get[T](self, path: Sequence[str], default: T) -> T: 

530 return self._get_property(self.node, path, default) 

531 

532 @staticmethod 

533 def _get_property[T](target: dict[str, Any], path: Sequence[str], default: T) -> T: 

534 assert default is not None, "default value must be provided" 

535 try: 

536 for p in path: 

537 target = target[p] 

538 # TODO: Is this code really correct when path contains multiple strings? 

539 # Doesn't target become an Any, and hence might not support __get_item__ any more? 

540 return target if isinstance(target, type(default)) else default 

541 except Exception: 

542 return default 

543 

544 @property 

545 def is_implicit(self) -> None: 

546 """AI: Return whether this node is implicitly generated (part of the translation unit).""" 

547 self.is_part_of_translation_unit() 

548 

549 

550class ReferenceHelper: 

551 """AI: Static helpers that populate reference caches between clang-json AST nodes.""" 

552 

553 @staticmethod 

554 def create_references(ast_node: ClangJsonASTNode) -> None: 

555 """AI: Populate the reference and referenced-by caches for ast_node's translation unit.""" 

556 assert isinstance(ast_node, ClangJsonASTNode), ( 

557 f"Expected ClangJsonASTNode but got {type(ast_node)}" 

558 ) # TODO: still needed when using type hints? 

559 if ast_node.inserted: 

560 return 

561 references = [] 

562 node_id = ast_node.node["id"] 

563 ast_node.translation_unit._references[node_id] = references 

564 refs = {k: v for k, v in ast_node.node.items() if not ReferenceHelper._is_child_node(k) and ClangJsonASTNode._is_reference(v)} 

565 for k in [k for k in ast_node.node if k in ON_NODE_ID_TAGS]: 

566 refs[k] = ast_node.node 

567 # add the node if it contains a reference for example in case of previousDecl 

568 

569 # to make clang json compatible with clang python, we add the reference of the DeclRefExpr child to the CallExpr 

570 if ast_node.semantic_kind is SemanticKind.CALL: 

571 for n in ast_node.children: 

572 if is_clang_declaration_reference(n): 

573 ref_child = { 

574 k: v for k, v in n.node.items() if not ReferenceHelper._is_child_node(k) and ClangJsonASTNode._is_reference(v) 

575 } 

576 refs.update(ref_child) 

577 

578 for kind, ref in refs.items(): 

579 for ref_id in ReferenceHelper._get_reference_ids(ref): 

580 if ref_id == node_id: 

581 continue 

582 properties = {k: p for k, p in ref.items() if k != ref_id} if ref != ast_node.node else EMPTY_DICT 

583 reference = ClangJsonASTReference(ref_id, kind, properties) 

584 referenced_by = ClangJsonASTReference(node_id, kind, properties) 

585 try: 

586 ast_node.translation_unit._referenced_by[ref_id].append(referenced_by) 

587 except Exception: 

588 ast_node.translation_unit._referenced_by[ref_id] = [referenced_by] 

589 references.append(reference) 

590 

591 @staticmethod 

592 def add_record_references(ast_node: ClangJsonASTNode) -> None: 

593 """JSON does not contain direct references between classes and their base classes. 

594 

595 Hence these references are created in this method. 

596 

597 This method checks if the given AST node is of kind 'CXXRecordDecl' and has a tag 'class'. 

598 If so, it processes the base classes of the node and creates references for them. 

599 

600 Args: 

601 ast_node (ClangJsonASTNode): The AST node to process. 

602 

603 Raises: 

604 AssertionError: If the provided ast_node is not an instance of ClangJsonASTNode. 

605 

606 """ 

607 assert isinstance(ast_node, ClangJsonASTNode), ( 

608 f"Expected ClangJsonASTNode but got {type(ast_node)}" 

609 ) # TODO: still needed when using type hints? 

610 if ast_node.inserted: 

611 return 

612 

613 bases = ast_node._get(["bases"], []) 

614 if not bases: 

615 bases = [ast_node.node] if ast_node.node.get("type") else None 

616 if not bases: 

617 return 

618 node_id = ast_node.node["id"] 

619 for base in bases: 

620 ref_ids = ReferenceHelper._get_record_decl(ast_node, base) 

621 for kind, ref_id in ref_ids: 

622 properties = {k: p for k, p in base.items() if k != "type"} 

623 reference = ClangJsonASTReference(ref_id, kind, properties) 

624 referenced_by = ClangJsonASTReference(node_id, kind, properties) 

625 try: 

626 ast_node.translation_unit._referenced_by[ref_id].append(referenced_by) 

627 except Exception: 

628 ast_node.translation_unit._referenced_by[ref_id] = [referenced_by] 

629 try: 

630 ast_node.translation_unit._references[node_id].append(reference) 

631 except Exception: 

632 ast_node.translation_unit._references[node_id] = [reference] 

633 

634 @staticmethod 

635 def _get_record_decl(ast_node, base) -> Sequence[str]: 

636 try: 

637 tp = base["type"] 

638 if "desugaredQualType" in tp and "::" in tp["desugaredQualType"]: 

639 # split desugaredQualType to derive the parent namespaces 

640 namespaces = tp["desugaredQualType"].split("::")[:-1][::-1] 

641 else: 

642 namespaces = [] 

643 qual_type = tp["qualType"] 

644 node_ids = [] 

645 ctor_type = EMPTY_STR 

646 if is_clang_kind(ast_node, "CXXConstructExpr", "CXX_CONSTRUCT_EXPR"): 

647 ctor_type = ast_node._get(["ctorType", "qualType"], EMPTY_STR) 

648 

649 for node_id, node in ast_node.translation_unit._nodes.items(): 

650 if node.semantic_kind is SemanticKind.CLASS and node.name == qual_type: 

651 parent = node.parent 

652 matches = True 

653 for ns in namespaces: 

654 if ns != parent.name or not is_clang_kind(parent, "NamespaceDecl", "NAMESPACE_DECL"): 

655 matches = False 

656 parent = parent.parent 

657 if matches: 

658 node_ids.append((node.parser_kind, node_id)) 

659 if ctor_type != EMPTY_STR and is_clang_kind(node, "CXXConstructorDecl", "CXX_CONSTRUCTOR"): 

660 # link all matching 

661 matches = node._get(["type", "qualType"], EMPTY_STR) == ctor_type 

662 if matches: 

663 node_ids.append((node.parser_kind, node_id)) 

664 return node_ids 

665 except Exception: 

666 pass 

667 return [] 

668 

669 @staticmethod 

670 def _get_reference_ids(json_node) -> list[Any]: 

671 result = [] 

672 if not isinstance(json_node, dict): 

673 return result 

674 for key in ID_TAGS: 

675 value = json_node.get(key) 

676 if value is not None: 

677 result.append(value) 

678 return result 

679 

680 @staticmethod 

681 @cache 

682 def _is_child_node(key) -> bool: 

683 return key == "inner"