-
Notifications
You must be signed in to change notification settings - Fork 0
Fix impact graphs and add a layered 3D view for large graphs #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8aa49d0
d3821f7
b00004d
449b98f
a83ca77
7987cf9
f3ba74b
bcbc085
f24e689
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| SERIALIZER_BASES = {"Serializer", "ModelSerializer", "HyperlinkedModelSerializer", "ListSerializer"} | ||||||||||||||||||||||||||||||||||
| FORM_BASES = {"Form", "ModelForm", "BaseForm", "BaseModelForm"} | ||||||||||||||||||||||||||||||||||
| MODEL_BASES = {"Model"} | ||||||||||||||||||||||||||||||||||
| ADMIN_BASES = {"ModelAdmin", "StackedInline", "TabularInline"} | ||||||||||||||||||||||||||||||||||
| CELERY_DECORATORS = {"shared_task", "task", "periodic_task"} | ||||||||||||||||||||||||||||||||||
|
|
@@ -141,7 +142,18 @@ def _app_from_path(rel: str) -> str | None: | |||||||||||||||||||||||||||||||||
| if idx > 0: | ||||||||||||||||||||||||||||||||||
| return parts[idx - 1] | ||||||||||||||||||||||||||||||||||
| for i, part in enumerate(parts): | ||||||||||||||||||||||||||||||||||
| if part in {"models.py", "views.py", "serializers.py", "urls.py", "signals.py", "tasks.py", "admin.py", "apps.py"}: | ||||||||||||||||||||||||||||||||||
| if part in { | ||||||||||||||||||||||||||||||||||
| "models.py", | ||||||||||||||||||||||||||||||||||
| "views.py", | ||||||||||||||||||||||||||||||||||
| "serializers.py", | ||||||||||||||||||||||||||||||||||
| "urls.py", | ||||||||||||||||||||||||||||||||||
| "signals.py", | ||||||||||||||||||||||||||||||||||
| "signal_handlers.py", | ||||||||||||||||||||||||||||||||||
| "forms.py", | ||||||||||||||||||||||||||||||||||
| "tasks.py", | ||||||||||||||||||||||||||||||||||
| "admin.py", | ||||||||||||||||||||||||||||||||||
| "apps.py", | ||||||||||||||||||||||||||||||||||
| }: | ||||||||||||||||||||||||||||||||||
| return parts[i - 1] if i > 0 else None | ||||||||||||||||||||||||||||||||||
| if part == "management" and i > 0: | ||||||||||||||||||||||||||||||||||
| return parts[i - 1] | ||||||||||||||||||||||||||||||||||
|
|
@@ -216,6 +228,12 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: | |||||||||||||||||||||||||||||||||
| self._model(node) | ||||||||||||||||||||||||||||||||||
| elif _has_base(node, SERIALIZER_BASES): | ||||||||||||||||||||||||||||||||||
| self._serializer(node) | ||||||||||||||||||||||||||||||||||
| elif _has_base(node, FORM_BASES) or ( | ||||||||||||||||||||||||||||||||||
| node.name.endswith("Form") | ||||||||||||||||||||||||||||||||||
| and not node.name.startswith("Test") | ||||||||||||||||||||||||||||||||||
| and not _has_base(node, {"TestCase", "SimpleTestCase", "TransactionTestCase", "LiveServerTestCase", "APITestCase"}) | ||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||
| self._serializer(node, ntype=NodeType.FORM) | ||||||||||||||||||||||||||||||||||
| elif _has_base(node, DJANGO_VIEW_BASES) or node.name.endswith(("View", "ViewSet")): | ||||||||||||||||||||||||||||||||||
| self._view(node) | ||||||||||||||||||||||||||||||||||
| elif any(b.split(".")[-1] in {"BaseCommand", "AppCommand", "LabelCommand"} for b in _bases(node)): | ||||||||||||||||||||||||||||||||||
|
|
@@ -240,6 +258,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: | |||||||||||||||||||||||||||||||||
| def visit_FunctionDef(self, node: ast.FunctionDef) -> None: | ||||||||||||||||||||||||||||||||||
| self._maybe_task(node) | ||||||||||||||||||||||||||||||||||
| self._maybe_receiver(node) | ||||||||||||||||||||||||||||||||||
| self._maybe_plain_signal_handler(node) | ||||||||||||||||||||||||||||||||||
| self._maybe_command(node) | ||||||||||||||||||||||||||||||||||
| self._maybe_test(node) | ||||||||||||||||||||||||||||||||||
| self._maybe_service_fn(node) | ||||||||||||||||||||||||||||||||||
|
|
@@ -282,8 +301,14 @@ def visit_Call(self, node: ast.Call) -> None: | |||||||||||||||||||||||||||||||||
| self.graph.residuals.append(f"Raw SQL ({fname}) in {self.rel_path}:{node.lineno}") | ||||||||||||||||||||||||||||||||||
| elif short in {"select_related", "prefetch_related"}: | ||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||
| elif short == "connect" and any(s in fname for s in SIGNAL_NAMES | {"signal"}): | ||||||||||||||||||||||||||||||||||
| self._signal_connect(node, fname) | ||||||||||||||||||||||||||||||||||
| elif short == "connect": | ||||||||||||||||||||||||||||||||||
| handler = node.args[0] if node.args else _kw(node, "receiver") | ||||||||||||||||||||||||||||||||||
| stem = Path(self.rel_path).stem | ||||||||||||||||||||||||||||||||||
| looks_signal = stem in {"apps", "signals", "signal_handlers", "handlers"} or any( | ||||||||||||||||||||||||||||||||||
| token in fname.lower() for token in {n.lower() for n in SIGNAL_NAMES} | {"signal"} | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| if looks_signal and isinstance(handler, (ast.Name, ast.Attribute)): | ||||||||||||||||||||||||||||||||||
| self._signal_connect(node, fname) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+304
to
+311
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Record a residual for an unresolvable signal handler. Line 310 prevents Proposed fix- if looks_signal and isinstance(handler, (ast.Name, ast.Attribute)):
+ if looks_signal:
self._signal_connect(node, fname)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| elif short == "reverse": | ||||||||||||||||||||||||||||||||||
| self._reverse(node) | ||||||||||||||||||||||||||||||||||
| self.generic_visit(node) | ||||||||||||||||||||||||||||||||||
|
|
@@ -344,9 +369,12 @@ def _model(self, node: ast.ClassDef) -> None: | |||||||||||||||||||||||||||||||||
| if extra.get("on_delete") == "CASCADE": | ||||||||||||||||||||||||||||||||||
| self.add_edge(model.id, rel_id, EdgeType.RELATES_TO, extra={"cascade": True}) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _serializer(self, node: ast.ClassDef) -> None: | ||||||||||||||||||||||||||||||||||
| def _serializer(self, node: ast.ClassDef, ntype: NodeType = NodeType.SERIALIZER) -> None: | ||||||||||||||||||||||||||||||||||
| qname = f"{self.app}.{node.name}" | ||||||||||||||||||||||||||||||||||
| ser = self.add_node(NodeType.SERIALIZER, node.name, qname, node.lineno, {"app": self.app}) | ||||||||||||||||||||||||||||||||||
| extra: dict = {"app": self.app} | ||||||||||||||||||||||||||||||||||
| if ntype is NodeType.FORM: | ||||||||||||||||||||||||||||||||||
| extra["django_form"] = True | ||||||||||||||||||||||||||||||||||
| ser = self.add_node(ntype, node.name, qname, node.lineno, extra) | ||||||||||||||||||||||||||||||||||
| meta_model = None | ||||||||||||||||||||||||||||||||||
| meta_fields: list[str] | None = None | ||||||||||||||||||||||||||||||||||
| meta_exclude: list[str] | None = None | ||||||||||||||||||||||||||||||||||
|
|
@@ -805,6 +833,27 @@ def _maybe_receiver(self, node: ast.FunctionDef) -> None: | |||||||||||||||||||||||||||||||||
| model_q = sender if "." in sender else f"{self.app}.{sender.split('.')[-1]}" | ||||||||||||||||||||||||||||||||||
| self.add_edge(recv.id, node_id(NodeType.MODEL, model_q), EdgeType.EMITS_SIGNAL) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _maybe_plain_signal_handler(self, node: ast.FunctionDef) -> None: | ||||||||||||||||||||||||||||||||||
| if self.class_stack: | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| if Path(self.rel_path).stem not in {"signals", "signal_handlers", "handlers"}: | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| for dec in node.decorator_list: | ||||||||||||||||||||||||||||||||||
| dname = _name(dec.func if isinstance(dec, ast.Call) else dec) or "" | ||||||||||||||||||||||||||||||||||
| if dname.split(".")[-1] == "receiver": | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| args = [a.arg for a in node.args.args] | ||||||||||||||||||||||||||||||||||
| if not (node.args.kwarg or "instance" in args or "sender" in args): | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| qname = f"{self.app}.{node.name}" | ||||||||||||||||||||||||||||||||||
| self.add_node( | ||||||||||||||||||||||||||||||||||
| NodeType.RECEIVER, | ||||||||||||||||||||||||||||||||||
| node.name, | ||||||||||||||||||||||||||||||||||
| qname, | ||||||||||||||||||||||||||||||||||
| node.lineno, | ||||||||||||||||||||||||||||||||||
| {"app": self.app, "plain_handler": True}, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _maybe_command(self, node: ast.FunctionDef) -> None: | ||||||||||||||||||||||||||||||||||
| if "management/commands" in self.rel_path and node.name == "handle": | ||||||||||||||||||||||||||||||||||
| cmd = Path(self.rel_path).stem | ||||||||||||||||||||||||||||||||||
|
|
@@ -833,7 +882,7 @@ def _maybe_test(self, node: ast.FunctionDef) -> None: | |||||||||||||||||||||||||||||||||
| # crude: referenced class names in the test become tested_by | ||||||||||||||||||||||||||||||||||
| for child in ast.walk(node): | ||||||||||||||||||||||||||||||||||
| if isinstance(child, ast.Name) and child.id[:1].isupper(): | ||||||||||||||||||||||||||||||||||
| for ntype in (NodeType.SERIALIZER, NodeType.VIEW, NodeType.MODEL, NodeType.SERVICE): | ||||||||||||||||||||||||||||||||||
| for ntype in (NodeType.SERIALIZER, NodeType.FORM, NodeType.VIEW, NodeType.MODEL, NodeType.SERVICE, NodeType.RECEIVER): | ||||||||||||||||||||||||||||||||||
| self.add_edge( | ||||||||||||||||||||||||||||||||||
| node_id(ntype, f"{self.app}.{child.id}"), | ||||||||||||||||||||||||||||||||||
| test.id, | ||||||||||||||||||||||||||||||||||
|
|
@@ -915,7 +964,39 @@ def _get_model(self, node: ast.Call) -> None: | |||||||||||||||||||||||||||||||||
| self.add_node(NodeType.MODEL, label.split(".")[-1], label, node.lineno, {"string_ref": True}) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _signal_connect(self, node: ast.Call, fname: str) -> None: | ||||||||||||||||||||||||||||||||||
| self.graph.residuals.append(f"signal.connect() at {self.rel_path}:{node.lineno} ({fname})") | ||||||||||||||||||||||||||||||||||
| handler_ast = node.args[0] if node.args else _kw(node, "receiver") | ||||||||||||||||||||||||||||||||||
| handler = _name(handler_ast) | ||||||||||||||||||||||||||||||||||
| signal = fname.rsplit(".", 1)[0] if "." in fname else None | ||||||||||||||||||||||||||||||||||
| sender = _name(_kw(node, "sender")) | ||||||||||||||||||||||||||||||||||
| if not handler: | ||||||||||||||||||||||||||||||||||
| self.graph.residuals.append(f"signal.connect() at {self.rel_path}:{node.lineno} ({fname})") | ||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||
| handler_short = handler.split(".")[-1] | ||||||||||||||||||||||||||||||||||
| qname = handler if "." in handler else f"{self.app}.{handler_short}" | ||||||||||||||||||||||||||||||||||
| extra = { | ||||||||||||||||||||||||||||||||||
| "app": self.app, | ||||||||||||||||||||||||||||||||||
| "signal": signal, | ||||||||||||||||||||||||||||||||||
| "sender": sender, | ||||||||||||||||||||||||||||||||||
| "referenced": True, | ||||||||||||||||||||||||||||||||||
| "via": "connect", | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| recv = self.add_node(NodeType.RECEIVER, handler_short, qname, node.lineno, extra) | ||||||||||||||||||||||||||||||||||
| if signal: | ||||||||||||||||||||||||||||||||||
| sig_short = signal.split(".")[-1] | ||||||||||||||||||||||||||||||||||
| sig_id = node_id(NodeType.SIGNAL, sig_short) | ||||||||||||||||||||||||||||||||||
| self.graph.nodes.append( | ||||||||||||||||||||||||||||||||||
| Node( | ||||||||||||||||||||||||||||||||||
| id=sig_id, | ||||||||||||||||||||||||||||||||||
| type=NodeType.SIGNAL, | ||||||||||||||||||||||||||||||||||
| name=sig_short, | ||||||||||||||||||||||||||||||||||
| qualified_name=sig_short, | ||||||||||||||||||||||||||||||||||
| extra={"referenced": True}, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| self.add_edge(sig_id, recv.id, EdgeType.RECEIVES) | ||||||||||||||||||||||||||||||||||
| if sender: | ||||||||||||||||||||||||||||||||||
| model_q = sender if "." in sender else f"{self.app}.{sender.split('.')[-1]}" | ||||||||||||||||||||||||||||||||||
| self.add_edge(recv.id, node_id(NodeType.MODEL, model_q), EdgeType.EMITS_SIGNAL) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+967
to
+999
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find likely Django signal registrations with a positional sender.
rg -nP --type py '\.connect\(\s*[^,\n]+,\s*[^,\n\)]+' .Repository: Modsofthenation/PR-Reviewer Length of output: 165 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- target implementation ---'
sed -n '920,1020p' src/loadpath/extractors/django.py
printf '%s\n' '--- helper and call-site references ---'
rg -n -C 5 '_signal_connect|visit_Call|EMITS_SIGNAL|residuals' src/loadpath/extractors/django.py tests 2>/dev/null || true
printf '%s\n' '--- all connect call shapes, including multiline calls ---'
rg -n -C 2 '(^|[^[:alnum:]_])connect[[:space:]]*\(' --glob '*.py' . || trueRepository: Modsofthenation/PR-Reviewer Length of output: 36624 🏁 Script executed: set -euo pipefail
printf '%s\n' '--- existing signal tests ---'
sed -n '425,475p' tests/unit/test_django_extractors.py
printf '%s\n' '--- standalone AST argument-shape probe ---'
python3 - <<'PY'
import ast
source = """
post_save.connect(sync_invoice, Invoice)
post_save.connect(receiver=sync_invoice, sender=Invoice)
post_save.connect(sync_invoice, sender=Invoice)
"""
tree = ast.parse(source)
for call in [n for n in ast.walk(tree) if isinstance(n, ast.Call)]:
positional = [ast.unparse(arg) for arg in call.args]
keywords = {kw.arg: ast.unparse(kw.value) for kw in call.keywords if kw.arg is not None}
print(f"positional={positional} keywords={keywords}")
sender_from_current_code = keywords.get("sender")
sender_with_positional_fallback = sender_from_current_code
if sender_with_positional_fallback is None and len(positional) > 1:
sender_with_positional_fallback = positional[1]
print(
f"current_sender={sender_from_current_code!r} "
f"fallback_sender={sender_with_positional_fallback!r}"
)
PYRepository: Modsofthenation/PR-Reviewer Length of output: 2343 🌐 Web query:
💡 Result: In Django, the Signal.connect method is used to register a receiver function to handle a specific signal [1][2]. The signature for the connect method is: Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None) [1][3] Parameters: - receiver: The callable function or method that will be executed when the signal is sent [1]. - sender: (Optional) A specific sender to filter signals from. If provided, the receiver will only be called when the signal is sent by this specific sender [1][2]. - weak: (Optional) A boolean that defaults to True. If True, Django stores the receiver as a weak reference, which can cause the receiver to be garbage collected if it is a local function. Set to False to keep the reference alive [1][2]. - dispatch_uid: (Optional) A unique identifier to prevent duplicate signals if the same receiver is connected multiple times [1][3]. Regarding the receiver function itself, Django enforces that all signal receivers must accept a sender argument and wildcard keyword arguments (kwargs) [1][2]. Even if a signal is documented as sending no additional information, your receiver function must be defined to accept these arguments to remain compatible with future changes [1][3]: def my_receiver(sender, kwargs): # Your logic here pass Attempting to define a receiver that does not accept kwargs may result in a ValueError, as Django's dispatcher explicitly checks for this requirement [4]. While the sender argument is technically a positional argument in the receiver's definition, signals are dispatched such that these values are passed to the receiver correctly [1]. Citations:
Handle positional Django supports 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def _reverse(self, node: ast.Call) -> None: | ||||||||||||||||||||||||||||||||||
| name = _const_str(node.args[0]) if node.args else None | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Exclude test-path
manage.pyfiles from the fallback.managesstill contains test-path files. If the repository has no non-testapps.pyormanage.py, Line 149 selects a nested test project asdjango_root. Filter_is_testish(...)paths before sorting.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents