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

389 statements  

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

1"""AI: ASTNode implementation backed by Clang's JSON AST dump output.""" 

2 

3# create a class that inherits syntax tree ASTNode 

4 

5import json 

6import re 

7import subprocess 

8import sys 

9import tempfile 

10from collections.abc import Sequence 

11from functools import cache 

12from pathlib import Path 

13from typing import Any, Self, override 

14 

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

16from renaissance.integrations.clang.kinds import CLANG_KIND_MAP 

17from renaissance.integrations.clang.predicates import ( 

18 is_clang_declaration_reference, 

19 is_clang_kind, 

20) 

21from renaissance.syntax_tree import ASTNode, ASTReference 

22from renaissance.syntax_tree.pattern_kind import PatternKind 

23from renaissance.syntax_tree.semantic_kind import SemanticKind 

24from renaissance.utils.ast_utils import match_children, match_props 

25 

26EMPTY_DICT = {} 

27EMPTY_STR = "" 

28EMPTY_LIST: list[ClangJsonASTReference] = [] 

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

30ID_TAGS = [ 

31 "id", 

32 "typeAliasDeclId", 

33 "templateDeclId", 

34 "templateSpecializationDeclId", 

35 "referencedDeclId", 

36 *ON_NODE_ID_TAGS, 

37] 

38 

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

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

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

42VERBOSE = False 

43 

44 

45class ClangJsonASTReference: 

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

47 

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

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

50 self.node_id = node_id 

51 self.ref_kind = ref_kind 

52 self.properties = properties 

53 

54 

55class ClangJsonTranslationUnit: 

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

57 

58 def __init__(self, json_root: dict[str, Any], file_name: str): 

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

60 self.json_root = json_root 

61 self.filename = file_name 

62 self.references_initialized = False 

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

64 # they are stored as id for lazy creation 

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

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

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

68 

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

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

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

72 if self.references_initialized: 

73 return 

74 node.root.process(ReferenceHelper.create_references) 

75 node.root.process(ReferenceHelper.add_record_references) 

76 self.references_initialized = True 

77 

78 

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

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

81 

82 parse_args = [ 

83 "-fparse-all-comments", 

84 "-ferror-limit=0", 

85 "-Xclang", 

86 "-ast-dump=json", 

87 "-fsyntax-only", 

88 ] 

89 

90 def __init__( 

91 self, 

92 node: dict[str, Any], 

93 translation_unit: ClangJsonTranslationUnit, 

94 parent: Self | None = None, 

95 start_offset: int | None = None, 

96 length: int | None = None, 

97 insert_kind: str | None = None, 

98 insert_name: str | None = None, 

99 ) -> None: 

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

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

102 self.node = node 

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

104 self._parent = parent 

105 self.translation_unit = translation_unit 

106 self._filename = translation_unit.filename 

107 self.inserted = insert_kind is not None 

108 self.show_props = False 

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

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

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

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

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

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

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

116 self._length = self._end_offset - self._offset 

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

118 self.parser_kind = self._kind 

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

120 self.pattern_kind = { 

121 "MatchOne": PatternKind.MATCH_ONE, 

122 "MatchAll": PatternKind.MATCH_ALL, 

123 }.get(self.parser_kind) 

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

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

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

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

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

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

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

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

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

133 loc = self.node["loc"] 

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

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

136 if tok_len != 0: 

137 insert_child = ClangJsonASTNode( 

138 self.node, 

139 self.translation_unit, 

140 self, 

141 offset, 

142 tok_len, 

143 "DeclLoc", 

144 ) 

145 insert_child._children = [] 

146 self.__inserted_children.append(insert_child) 

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

148 # deep clone the type node and remove the parentheses 

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

150 if base_type in CPPUtils.RESERVED_KEYWORDS: 

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

152 insert_child = ClangJsonASTNode( 

153 self.node, 

154 self.translation_unit, 

155 self, 

156 self._offset, 

157 length_ref, 

158 "TypeRef", 

159 declared_type, 

160 ) 

161 insert_child._children = [] 

162 self.__inserted_children.append(insert_child) 

163 # add the declaration as node 

164 # deep clone the type node and remove the parentheses 

165 elif is_clang_declaration_reference(self): 

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

167 self._kind = "MatchAll" 

168 self.pattern_kind = PatternKind.MATCH_ALL 

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

170 self._kind = "MatchOne" 

171 self.pattern_kind = PatternKind.MATCH_ONE 

172 self._children = self.__inserted_children + [ 

173 ClangJsonASTNode( 

174 ClangJsonASTNode._remove_wrapper(n), 

175 translation_unit=self.translation_unit, 

176 parent=self, 

177 ) 

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

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

180 ] 

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

182 

183 def __eq__(self, other): 

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

185 return ( 

186 isinstance(other, type(self)) 

187 and self.kind == other.kind 

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

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

190 ) 

191 

192 @override 

193 @staticmethod 

194 def load( 

195 file_path: Path, 

196 extra_args: Sequence[str], 

197 working_dir: Path, 

198 code: str | None = None, 

199 ) -> Self: 

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

201 try: 

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

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

204 extra_args = extra_args[1:] 

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

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

207 clang = "clang++" if file_path.suffix == ".cpp" else "clang" 

208 extra_args = [clang, *extra_args] 

209 

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

211 if code: 

212 if str(file_path) in command: 

213 command.remove(str(file_path)) 

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

215 if compile_flag not in command: 

216 command.append(compile_flag) 

217 if "-" not in command: 

218 command.append("-") 

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

220 result = subprocess.run( 

221 command, 

222 input=code, 

223 capture_output=True, 

224 text=True, 

225 cwd=working_dir, 

226 ) 

227 length = len(code) 

228 else: 

229 if str(file_path) not in command: 

230 command.append(str(file_path)) 

231 result = subprocess.run( 

232 command, 

233 capture_output=True, 

234 text=True, 

235 cwd=working_dir, 

236 ) 

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

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

239 error = result.stderr 

240 

241 if VERBOSE: 

242 temp_dir = tempfile.gettempdir() 

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

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

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

246 std_out_file.write(json_dump) 

247 print(error, file=sys.stderr) 

248 json_atu = json.loads(json_dump) 

249 atu = ClangJsonASTNode( 

250 json_atu, 

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

252 length=length, 

253 ) 

254 if code: 

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

256 else: 

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

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

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

260 atu.content(0, 0) 

261 return atu 

262 

263 except Exception as e: 

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

265 raise e 

266 

267 @override 

268 @staticmethod 

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

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

271 

272 @cache 

273 def _get_containing_filename(self) -> str: 

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

275 return "" 

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

277 return "" 

278 if not self.parent: 

279 return self.translation_unit.file_name 

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

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

282 if containing_file: 

283 return containing_file 

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

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

286 return "" 

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

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

289 return "" 

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

291 if self.parent: 

292 return self.parent.filename 

293 return EMPTY_STR 

294 

295 @override 

296 @property 

297 def extended_end_offset(self) -> int: 

298 try: 

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

300 end_offset = self._end_offset 

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

302 # but expressions (without the semicolon) 

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

304 content = self.root.binary_file_content() 

305 while ( 

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

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

308 end_offset += 1 

309 return end_offset 

310 except Exception: 

311 return 0 

312 

313 def _is_statement_or_declaration(self): 

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

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

316 

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

318 @override 

319 @property 

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

321 return matches_node_kind(self, node) 

322 

323 @override 

324 @property 

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

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

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

328 properties = { 

329 k: ClangJsonASTNode._remove_ids(v) 

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

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

332 } 

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

334 properties["macro_expansion"] = self.text 

335 # matching name through props 

336 if is_clang_declaration_reference(self): 

337 properties["name"] = self.name 

338 

339 return properties 

340 

341 @override 

342 @property 

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

344 if self.inserted: 

345 return [] 

346 self.translation_unit.lazy_create_references(self) 

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

348 definition_node_id = self._get_function_definition() 

349 if definition_node_id: 

350 # try to find the definition which might have references 

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

352 return [ 

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

354 for ref in ref_by 

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

356 ] 

357 

358 def _get_function_definition(self): 

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

360 for ref in refs: 

361 if ref.ref_kind == "previousDecl": 

362 return ref.node_id 

363 return None 

364 

365 @override 

366 @property 

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

368 if self.inserted: 

369 return [] 

370 self.translation_unit.lazy_create_references(self) 

371 

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

373 definition_node_id = self._get_function_definition() 

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

375 if definition_node_id: 

376 # try to find the definition which might have references 

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

378 # remove duplicates 

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

380 

381 return [ 

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

383 for ref in refs 

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

385 ] 

386 

387 @override 

388 @property 

389 def is_statement(self) -> bool: 

390 return ( 

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

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

393 

394 def _derive_name(self) -> str: 

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

396 if name: 

397 return name 

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

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

400 if kind == "CallExpr": 

401 # equalize with libclang 

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

403 if decl_ref_child: 

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

405 if kind == "DeclRefExpr": 

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

407 if kind == "StringLiteral": 

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

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

410 

411 def __derive_start_offset(self) -> int: 

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

413 if offset == -1: 

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

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

416 return offset 

417 

418 def __derive_end_offset(self) -> int: 

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

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

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

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

423 if offset == -1: 

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

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

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

427 

428 return offset + tok_len 

429 

430 def __derive_kind(self) -> str: 

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

432 

433 @staticmethod 

434 def _remove_wrapper(node): 

435 try: 

436 if ClangJsonASTNode._is_wrapped(node): 

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

438 except Exception: 

439 pass 

440 return node 

441 

442 @staticmethod 

443 def _remove_ids(json_node): 

444 if not isinstance(json_node, dict): 

445 return json_node 

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

447 

448 @staticmethod 

449 def _is_reference(json_node): 

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

451 

452 @staticmethod 

453 @cache 

454 def __is_property(key): 

455 return key not in [ 

456 "id", 

457 "inner", 

458 "loc", 

459 "range", 

460 "kind", 

461 "name", 

462 "isUsed", 

463 "isReferenced", 

464 "referencedDecl", 

465 "mangledName", 

466 *ON_NODE_ID_TAGS, 

467 ] 

468 

469 @staticmethod 

470 def _is_wrapped(node): 

471 """Check if a node is wrapped. 

472 

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

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

475 2. The node has exactly one inner node. 

476 """ 

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

478 

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

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

481 

482 @staticmethod 

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

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

485 try: 

486 for p in path: 

487 target = target[p] 

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

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

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

491 except Exception: 

492 return default 

493 

494 @property 

495 def is_implicit(self): 

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

497 self.is_part_of_translation_unit() 

498 

499 

500class ReferenceHelper: 

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

502 

503 @staticmethod 

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

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

506 assert isinstance(ast_node, ClangJsonASTNode), ( 

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

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

509 if ast_node.inserted: 

510 return 

511 references = [] 

512 node_id = ast_node.node["id"] 

513 ast_node.translation_unit._references[node_id] = references 

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

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

516 refs[k] = ast_node.node 

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

518 

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

520 if ast_node.semantic_kind is SemanticKind.CALL: 

521 for n in ast_node.children: 

522 if is_clang_declaration_reference(n): 

523 ref_child = { 

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

525 } 

526 refs.update(ref_child) 

527 

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

529 for ref_id in ReferenceHelper._get_reference_ids(ref): 

530 if ref_id == node_id: 

531 continue 

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

533 reference = ClangJsonASTReference(ref_id, kind, properties) 

534 referenced_by = ClangJsonASTReference(node_id, kind, properties) 

535 try: 

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

537 except Exception: 

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

539 references.append(reference) 

540 

541 @staticmethod 

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

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

544 

545 Hence these references are created in this method. 

546 

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

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

549 

550 Args: 

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

552 

553 Raises: 

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

555 

556 """ 

557 assert isinstance(ast_node, ClangJsonASTNode), ( 

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

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

560 if ast_node.inserted: 

561 return 

562 

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

564 if not bases: 

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

566 if not bases: 

567 return 

568 node_id = ast_node.node["id"] 

569 for base in bases: 

570 ref_ids = ReferenceHelper._get_record_decl(ast_node, base) 

571 for kind, ref_id in ref_ids: 

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

573 reference = ClangJsonASTReference(ref_id, kind, properties) 

574 referenced_by = ClangJsonASTReference(node_id, kind, properties) 

575 try: 

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

577 except Exception: 

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

579 try: 

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

581 except Exception: 

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

583 

584 @staticmethod 

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

586 try: 

587 tp = base["type"] 

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

589 # split desugaredQualType to derive the parent namespaces 

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

591 else: 

592 namespaces = [] 

593 qual_type = tp["qualType"] 

594 node_ids = [] 

595 ctor_type = EMPTY_STR 

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

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

598 

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

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

601 parent = node.parent 

602 matches = True 

603 for ns in namespaces: 

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

605 matches = False 

606 parent = parent.parent 

607 if matches: 

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

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

610 # link all matching 

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

612 if matches: 

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

614 return node_ids 

615 except Exception: 

616 pass 

617 return [] 

618 

619 @staticmethod 

620 def _get_reference_ids(json_node): 

621 result = [] 

622 if not isinstance(json_node, dict): 

623 return result 

624 for key in ID_TAGS: 

625 value = json_node.get(key) 

626 if value is not None: 

627 result.append(value) 

628 return result 

629 

630 @staticmethod 

631 @cache 

632 def _is_child_node(key): 

633 return key == "inner"