Skip to content

Commit fdeca41

Browse files
authored
Merge branch 'main' into dependabot/pip/mypy-gte-1.10-and-lt-3
2 parents 91c22a3 + f88f26e commit fdeca41

9 files changed

Lines changed: 740 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818

1919
### Fixed
2020

21+
- Swift initializers, deinitializers and subscripts now emit `Function` nodes
22+
(#786). `init_declaration`, `deinit_declaration` and `subscript_declaration`
23+
are node types distinct from `function_declaration`, so the walker skipped
24+
them and attributed the calls in their bodies to the enclosing *File* node —
25+
a change inside one initializer read as file-wide blast radius, and
26+
`get_impact_radius` on a callee could not trace back to the initializer that
27+
calls it. Each is named after its declaration keyword, since the grammar
28+
gives none of the three a usable name field (`subscript`'s would be its
29+
return type, `deinit`'s is absent).
2130
- GitHub Copilot auto-detection now requires the Copilot extension and also
2231
recognizes the extension bundled with released VS Code installations.
2332
- C# methods with a non-generic return type are no longer named after that

code_review_graph/parser.py

Lines changed: 256 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#include "MyWidget.h"
2+
3+
MyWidget::MyWidget(QWidget* parent) : QMainWindow(parent) {}
4+
MyWidget::~MyWidget() {}
5+
void MyWidget::doSomething() { onReset(); }
6+
int MyWidget::calculateValue(int a, int b) { return a + b; }
7+
void MyWidget::onButtonClicked() { Q_EMIT dataReady(calculateValue(1, 2)); }
8+
void MyWidget::onDataReceived(int value) {
9+
if (value < 0) { Q_EMIT errorOccurred("err"); return; }
10+
doSomething();
11+
}
12+
void MyWidget::onReset() { Q_EMIT dataReady(0); }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma once
2+
#include <QMainWindow>
3+
4+
QT_BEGIN_NAMESPACE namespace Ui { class MyWidgetClass; };
5+
QT_END_NAMESPACE
6+
7+
class MyWidget : public QMainWindow {
8+
Q_OBJECT
9+
10+
public:
11+
MyWidget(QWidget* parent = nullptr);
12+
~MyWidget();
13+
void doSomething();
14+
int calculateValue(int a, int b);
15+
16+
protected Q_SLOTS:
17+
void onButtonClicked();
18+
void onDataReceived(int value);
19+
20+
public Q_SLOTS:
21+
void onReset();
22+
23+
Q_SIGNALS:
24+
void dataReady(int result);
25+
void errorOccurred(const QString& msg);
26+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include "MyWidgetPlain.h"
2+
3+
MyWidgetPlain::MyWidgetPlain() {}
4+
MyWidgetPlain::~MyWidgetPlain() {}
5+
6+
void MyWidgetPlain::doSomething() { onReset(); }
7+
int MyWidgetPlain::calculateValue(int a, int b) { return a + b; }
8+
void MyWidgetPlain::onButtonClicked() { int result = calculateValue(1, 2); }
9+
void MyWidgetPlain::onDataReceived(int value) { if (value < 0) return; doSomething(); }
10+
void MyWidgetPlain::onReset() {}

0 commit comments

Comments
 (0)