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
« 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
7"""AI: ASTNode implementation backed by Clang's JSON AST dump output."""
9# create a class that inherits syntax tree ASTNode
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
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
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]
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)
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)
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
89class ClangJsonASTReference:
90 """AI: Represent a reference from one clang-json AST node to another by id."""
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
99class ClangJsonTranslationUnit:
100 """AI: Wrap a parsed clang JSON AST dump with lazily-built reference caches."""
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] = {}
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
123class ClangJsonASTNode(ASTNode[dict[str, Any], ClangJsonTranslationUnit]):
124 """AI: ASTNode implementation backed by clang's JSON AST dump format."""
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
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
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]
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 )
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]
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
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
313 except Exception as e:
314 print("Call to clang failed. Did you install clang?, is it on the env path?")
315 raise e
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)
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
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
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}
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)
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
389 return properties
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 ]
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
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)
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())
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 ]
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?
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)
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
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)
478 return offset + tok_len
480 def __derive_kind(self) -> str:
481 return self.node.get("kind", EMPTY_STR)
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
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}
498 @staticmethod
499 def _is_reference(json_node) -> bool:
500 return len(ReferenceHelper._get_reference_ids(json_node)) > 0
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 ]
519 @staticmethod
520 def _is_wrapped(node) -> bool:
521 """Check if a node is wrapped.
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
529 def _get[T](self, path: Sequence[str], default: T) -> T:
530 return self._get_property(self.node, path, default)
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
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()
550class ReferenceHelper:
551 """AI: Static helpers that populate reference caches between clang-json AST nodes."""
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
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)
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)
591 @staticmethod
592 def add_record_references(ast_node: ClangJsonASTNode) -> None:
593 """JSON does not contain direct references between classes and their base classes.
595 Hence these references are created in this method.
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.
600 Args:
601 ast_node (ClangJsonASTNode): The AST node to process.
603 Raises:
604 AssertionError: If the provided ast_node is not an instance of ClangJsonASTNode.
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
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]
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)
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 []
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
680 @staticmethod
681 @cache
682 def _is_child_node(key) -> bool:
683 return key == "inner"