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
« 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."""
3# create a class that inherits syntax tree ASTNode
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
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
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]
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
45class ClangJsonASTReference:
46 """AI: Represent a reference from one clang-json AST node to another by id."""
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
55class ClangJsonTranslationUnit:
56 """AI: Wrap a parsed clang JSON AST dump with lazily-built reference caches."""
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] = {}
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
79class ClangJsonASTNode(ASTNode[dict[str, Any], ClangJsonTranslationUnit]):
80 """AI: ASTNode implementation backed by clang's JSON AST dump format."""
82 parse_args = [
83 "-fparse-all-comments",
84 "-ferror-limit=0",
85 "-Xclang",
86 "-ast-dump=json",
87 "-fsyntax-only",
88 ]
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]
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 )
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]
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
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
263 except Exception as e:
264 print("Call to clang failed. Did you install clang?, is it on the env path?")
265 raise e
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)
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
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
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}
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)
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
339 return properties
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 ]
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
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)
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())
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 ]
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?
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)
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
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)
428 return offset + tok_len
430 def __derive_kind(self) -> str:
431 return self.node.get("kind", EMPTY_STR)
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
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}
448 @staticmethod
449 def _is_reference(json_node):
450 return len(ReferenceHelper._get_reference_ids(json_node)) > 0
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 ]
469 @staticmethod
470 def _is_wrapped(node):
471 """Check if a node is wrapped.
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
479 def _get[T](self, path: Sequence[str], default: T) -> T:
480 return self._get_property(self.node, path, default)
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
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()
500class ReferenceHelper:
501 """AI: Static helpers that populate reference caches between clang-json AST nodes."""
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
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)
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)
541 @staticmethod
542 def add_record_references(ast_node: ClangJsonASTNode) -> None:
543 """JSON does not contain direct references between classes and their base classes.
545 Hence these references are created in this method.
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.
550 Args:
551 ast_node (ClangJsonASTNode): The AST node to process.
553 Raises:
554 AssertionError: If the provided ast_node is not an instance of ClangJsonASTNode.
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
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]
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)
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 []
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
630 @staticmethod
631 @cache
632 def _is_child_node(key):
633 return key == "inner"