@@ -752,6 +752,35 @@ class EdgeInfo:
752752 ".yaml": "yaml",
753753}
754754
755+ # ``.h`` is shared by C and C++. Keep C as the extension default, then promote
756+ # a header only when the C++ grammar finds syntax that C cannot express. Weak
757+ # compatibility markers such as ``__cplusplus`` and ``extern "C"`` are
758+ # deliberately excluded because they are common in otherwise-C headers.
759+ _CPP_HEADER_EVIDENCE_TYPES = frozenset({
760+ "access_specifier",
761+ "alias_declaration",
762+ "base_class_clause",
763+ "class_specifier",
764+ "concept_definition",
765+ "lambda_expression",
766+ "namespace_definition",
767+ "noexcept",
768+ "template_declaration",
769+ "trailing_return_type",
770+ "using_declaration",
771+ })
772+
773+ _CPP_HEADER_EVIDENCE_QUALIFIERS = frozenset({b"consteval", b"constinit"})
774+
775+ _CPP_QT_STRUCTURAL_MACRO_REPLACEMENTS = {
776+ b"QT_BEGIN_NAMESPACE": b" " * len(b"QT_BEGIN_NAMESPACE"),
777+ b"QT_END_NAMESPACE": b" " * len(b"QT_END_NAMESPACE"),
778+ b"Q_OBJECT": b" " * len(b"Q_OBJECT"),
779+ b"Q_SIGNALS": b"public" + b" " * (len(b"Q_SIGNALS") - len(b"public")),
780+ b"Q_SLOTS": b" " * len(b"Q_SLOTS"),
781+ b"Q_EMIT": b" " * len(b"Q_EMIT"),
782+ }
783+
755784# Shebang interpreter → language mapping for extension-less Unix scripts.
756785# Each key is the **basename** of the interpreter path as it appears after
757786# ``#!`` (or after ``#!/usr/bin/env``). Only languages already registered
@@ -943,13 +972,22 @@ class EdgeInfo:
943972 "rust": ["function_item", "function_signature_item"],
944973 "java": ["method_declaration", "constructor_declaration"],
945974 "c": ["function_definition"],
946- "cpp": ["function_definition"],
975+ "cpp": ["function_definition", "declaration", "field_declaration" ],
947976 "csharp": ["method_declaration", "constructor_declaration"],
948977 "ruby": ["method", "singleton_method"],
949978 "r": ["function_definition"],
950979 "perl": ["subroutine_declaration_statement", "method_declaration_statement"],
951980 "kotlin": ["function_declaration"],
952- "swift": ["function_declaration"],
981+ # Swift: initializers, deinitializers and subscripts are separate node
982+ # types, not `function_declaration`s, so they need listing alongside it —
983+ # the same way java/csharp list `constructor_declaration`. Their names come
984+ # from the `_get_name` Swift branch (the grammar has no usable name field).
985+ "swift": [
986+ "function_declaration",
987+ "init_declaration",
988+ "deinit_declaration",
989+ "subscript_declaration",
990+ ],
953991 "php": ["function_definition", "method_declaration"],
954992 "scala": ["function_definition", "function_declaration"],
955993 # Solidity: events and modifiers use kind="Function" because the graph
@@ -2522,6 +2560,22 @@ def parse_bytes(self, path: Path, source: bytes) -> tuple[list[NodeInfo], list[E
25222560 if not language:
25232561 return [], []
25242562
2563+ parser = None
2564+ tree = None
2565+ parse_source = source
2566+ if language == "c" and path.suffix.lower() == ".h":
2567+ cpp_parser = self._get_parser("cpp")
2568+ if cpp_parser is not None:
2569+ cpp_source = self._mask_cpp_qt_macros(source)
2570+ cpp_tree = cpp_parser.parse(cpp_source)
2571+ if self._has_cpp_header_evidence(cpp_tree.root_node):
2572+ language = "cpp"
2573+ parser = cpp_parser
2574+ tree = cpp_tree
2575+ parse_source = cpp_source
2576+ elif language == "cpp":
2577+ parse_source = self._mask_cpp_qt_macros(source)
2578+
25252579 if language == "blade":
25262580 return self._parse_blade(path, source)
25272581
@@ -2592,11 +2646,13 @@ def parse_bytes(self, path: Path, source: bytes) -> tuple[list[NodeInfo], list[E
25922646 if language == "yaml":
25932647 return [], []
25942648
2595- parser = self._get_parser(language)
2649+ if parser is None:
2650+ parser = self._get_parser(language)
25962651 if not parser:
25972652 return [], []
25982653
2599- tree = parser.parse(source)
2654+ if tree is None:
2655+ tree = parser.parse(parse_source)
26002656 nodes: list[NodeInfo] = []
26012657 edges: list[EdgeInfo] = []
26022658 file_path_str = str(path)
@@ -2684,6 +2740,140 @@ def parse_bytes(self, path: Path, source: bytes) -> tuple[list[NodeInfo], list[E
26842740
26852741 return nodes, edges
26862742
2743+ @staticmethod
2744+ def _has_cpp_header_evidence(root) -> bool:
2745+ """Return whether a parsed ``.h`` tree contains C++-only syntax."""
2746+ pending = [root]
2747+ while pending:
2748+ node = pending.pop()
2749+ if node.type == "ERROR" or _is_in_static_dead_guard(node):
2750+ continue
2751+
2752+ previous = node.prev_named_sibling
2753+ recovered_after_error = (
2754+ previous is not None
2755+ and previous.type == "ERROR"
2756+ and previous.end_byte == node.start_byte
2757+ )
2758+ if recovered_after_error:
2759+ continue
2760+
2761+ if node.type in _CPP_HEADER_EVIDENCE_TYPES:
2762+ return True
2763+ if (
2764+ node.type == "enum_specifier"
2765+ and any(child.type in ("class", "struct") for child in node.children)
2766+ ):
2767+ return True
2768+ if (
2769+ node.type == "type_qualifier"
2770+ and node.text in _CPP_HEADER_EVIDENCE_QUALIFIERS
2771+ ):
2772+ return True
2773+ pending.extend(node.named_children)
2774+ return False
2775+
2776+ @staticmethod
2777+ def _mask_cpp_qt_macros(source: bytes) -> bytes:
2778+ """Shield structural Qt macros without changing byte or line offsets."""
2779+ masked = bytearray(source)
2780+ length = len(source)
2781+ index = 0
2782+ line_has_code = False
2783+
2784+ def skip_quoted(start: int, quote: int) -> int:
2785+ cursor = start + 1
2786+ while cursor < length:
2787+ if source[cursor] == ord("\\"):
2788+ cursor += 2
2789+ elif source[cursor] == quote:
2790+ return cursor + 1
2791+ else:
2792+ cursor += 1
2793+ return length
2794+
2795+ def raw_string_end(start: int) -> Optional[int]:
2796+ for prefix in (b'u8R"', b'LR"', b'UR"', b'uR"', b'R"'):
2797+ if not source.startswith(prefix, start):
2798+ continue
2799+ delimiter_start = start + len(prefix)
2800+ opening = source.find(b"(", delimiter_start, delimiter_start + 17)
2801+ if opening == -1:
2802+ return None
2803+ delimiter = source[delimiter_start:opening]
2804+ if any(byte in b" ()\\\t\r\n" for byte in delimiter):
2805+ return None
2806+ closing = source.find(b")" + delimiter + b'"', opening + 1)
2807+ return length if closing == -1 else closing + len(delimiter) + 2
2808+ return None
2809+
2810+ while index < length:
2811+ byte = source[index]
2812+ if byte == ord("\n"):
2813+ line_has_code = False
2814+ index += 1
2815+ continue
2816+
2817+ if source.startswith(b"//", index):
2818+ newline = source.find(b"\n", index + 2)
2819+ index = length if newline == -1 else newline
2820+ continue
2821+ if source.startswith(b"/*", index):
2822+ closing = source.find(b"*/", index + 2)
2823+ comment_end = length if closing == -1 else closing + 2
2824+ if b"\n" in source[index:comment_end]:
2825+ line_has_code = False
2826+ index = comment_end
2827+ continue
2828+
2829+ if byte == ord("#") and not line_has_code:
2830+ cursor = index
2831+ while cursor < length:
2832+ newline = source.find(b"\n", cursor)
2833+ if newline == -1:
2834+ cursor = length
2835+ break
2836+ previous = newline - 1
2837+ if previous >= cursor and source[previous] == ord("\r"):
2838+ previous -= 1
2839+ if previous < cursor or source[previous] != ord("\\"):
2840+ cursor = newline
2841+ break
2842+ cursor = newline + 1
2843+ index = cursor
2844+ continue
2845+
2846+ raw_end = raw_string_end(index)
2847+ if raw_end is not None:
2848+ line_has_code = True
2849+ index = raw_end
2850+ continue
2851+ if byte in (ord('"'), ord("'")):
2852+ line_has_code = True
2853+ index = skip_quoted(index, byte)
2854+ continue
2855+
2856+ if byte == ord("_") or chr(byte).isalpha():
2857+ end = index + 1
2858+ while end < length:
2859+ candidate = source[end]
2860+ if candidate != ord("_") and not chr(candidate).isalnum():
2861+ break
2862+ end += 1
2863+ token = source[index:end]
2864+ replacement = _CPP_QT_STRUCTURAL_MACRO_REPLACEMENTS.get(token)
2865+ if replacement is not None:
2866+ masked[index:end] = replacement
2867+ line_has_code = True
2868+ index = end
2869+ continue
2870+
2871+ if byte not in b" \t\v\f\r":
2872+ line_has_code = True
2873+ index += 1
2874+
2875+ return bytes(masked)
2876+
26872877 @classmethod
26882878 def _mask_blade_comments(cls, text: str) -> str:
26892879 """Mask Blade comments while preserving offsets and line numbers."""
@@ -14064,6 +14254,13 @@ def _get_name(self, node, language: str, kind: str) -> Optional[str]:
1406414254
1406514255 if language == "cpp" and kind == "function":
1406614256 declarator = node.child_by_field_name("declarator")
14257+ if node.type in ("declaration", "field_declaration"):
14258+ if (
14259+ not self._cpp_declaration_has_callable_scope(node)
14260+ or not self._cpp_is_callable_declaration(declarator)
14261+ ):
14262+ return None
14263+ return self._cpp_callable_name(declarator)
1406714264 cpp_name = self._cpp_callable_name(declarator)
1406814265 if cpp_name:
1406914266 return cpp_name
@@ -14157,6 +14354,18 @@ def _leaf_name(qi):
1415714354 for child in node.children:
1415814355 if child.type == "identifier":
1415914356 return child.text.decode("utf-8", errors="replace")
14357+ # Swift init/deinit/subscript: the grammar gives none of them a usable
14358+ # name. `init_declaration`'s name field is the `init` keyword itself,
14359+ # `deinit_declaration` has no name field at all (so the generic loop
14360+ # returns None and the node is dropped), and `subscript_declaration`'s
14361+ # name field is the *return type* (`subscript(i: Int) -> String` would
14362+ # be named "String"). Name each after its Swift declaration keyword.
14363+ if language == "swift" and node.type in (
14364+ "init_declaration",
14365+ "deinit_declaration",
14366+ "subscript_declaration",
14367+ ):
14368+ return node.type.removesuffix("_declaration")
1416014369 # Swift extensions: name is inside user_type > type_identifier
1416114370 # (e.g. `extension MyClass: Protocol { ... }`)
1416214371 if language == "swift" and node.type == "class_declaration":
@@ -14426,6 +14635,11 @@ def _cpp_find_function_declarator(self, declarator):
1442614635 if declarator is None:
1442714636 return None
1442814637 if declarator.type in ("function_declarator", "abstract_function_declarator"):
14638+ nested = self._cpp_find_function_declarator(
14639+ declarator.child_by_field_name("declarator"),
14640+ )
14641+ if nested is not None:
14642+ return nested
1442914643 return declarator
1443014644 for child in declarator.named_children:
1443114645 if child.type in ("parameter_list", "template_argument_list"):
@@ -14435,6 +14649,44 @@ def _cpp_find_function_declarator(self, declarator):
1443514649 return found
1443614650 return None
1443714651
14652+ def _cpp_is_callable_declaration(self, declarator) -> bool:
14653+ """Return whether a declaration names a function, not a function pointer."""
14654+ function_declarator = self._cpp_find_function_declarator(declarator)
14655+ if function_declarator is None:
14656+ return False
14657+
14658+ callable_declarator = function_declarator.child_by_field_name("declarator")
14659+ if (
14660+ callable_declarator is None
14661+ or callable_declarator.type != "parenthesized_declarator"
14662+ ):
14663+ return True
14664+
14665+ # ``void (*callback)(int)`` has no nested function declarator inside
14666+ # the parentheses. A real function returning a function pointer,
14667+ # such as ``void (*factory())(int)``, does.
14668+ return self._cpp_find_function_declarator(callable_declarator) is not None
14669+
14670+ @staticmethod
14671+ def _cpp_declaration_has_callable_scope(declaration) -> bool:
14672+ """Limit callable declarations to file, namespace, and class scopes."""
14673+ scope = declaration.parent
14674+ while scope is not None:
14675+ if scope.type in (
14676+ "translation_unit",
14677+ "namespace_definition",
14678+ "field_declaration_list",
14679+ ):
14680+ return not _is_in_static_dead_guard(declaration)
14681+ if scope.type in (
14682+ "compound_statement",
14683+ "function_definition",
14684+ "lambda_expression",
14685+ ):
14686+ return False
14687+ scope = scope.parent
14688+ return False
14689+
1443814690 def _cpp_find_qualified_identifier(self, declarator):
1443914691 """Find the callable's qualified identifier outside its parameters."""
1444014692 if declarator is None:
0 commit comments