|
22 | 22 |
|
23 | 23 | log = logging.getLogger(__name__) |
24 | 24 |
|
| 25 | + |
| 26 | +def _coerce_id_list(raw: Any) -> list[str]: |
| 27 | + """Normalize Kuzu ``collect(DISTINCT ...)`` list results to string ids.""" |
| 28 | + if raw is None: |
| 29 | + return [] |
| 30 | + if isinstance(raw, list): |
| 31 | + return [str(x) for x in raw if x is not None and str(x) != ""] |
| 32 | + s = str(raw) |
| 33 | + return [s] if s else [] |
| 34 | + |
| 35 | + |
25 | 36 | __all__ = [ |
26 | 37 | "KuzuGraph", |
27 | 38 | "resolve_kuzu_path", |
@@ -618,6 +629,72 @@ def member_edge_rollup_for(self, type_id: str) -> dict[str, dict[str, int]]: |
618 | 629 | rollup[key] = {"in": 0, "out": n} |
619 | 630 | return rollup |
620 | 631 |
|
| 632 | + def _edge_row_count_from_method_ids(self, method_ids: list[str], rel: str) -> int: |
| 633 | + """Count outgoing ``rel`` edges from method symbols (describe rollup helper).""" |
| 634 | + total = 0 |
| 635 | + for mid in method_ids: |
| 636 | + rows = self._rows( |
| 637 | + f"MATCH (x:Symbol {{id: $mid}})-[e:{rel}]->() RETURN count(e) AS n", |
| 638 | + {"mid": mid}, |
| 639 | + ) |
| 640 | + total += int(rows[0].get("n") or 0) if rows else 0 |
| 641 | + return total |
| 642 | + |
| 643 | + def override_axis_rollup_for(self, method_id: str) -> dict[str, dict[str, int]]: |
| 644 | + """Dispatch-axis composed keys for method Symbols (describe-time only). |
| 645 | +
|
| 646 | + Uses one-hop ``IMPLEMENTS`` / ``EXTENDS`` class edges plus ``Symbol.signature`` |
| 647 | + equality. Omits keys with zero counts (same convention as ``edge_counts_for``). |
| 648 | + Returns ``{}`` for non-methods, constructors (caller should skip), and static methods. |
| 649 | + """ |
| 650 | + params = {"id": method_id} |
| 651 | + gate = self._rows( |
| 652 | + "MATCH (m:Symbol {id: $id}) " |
| 653 | + "WHERE m.kind = 'method' " |
| 654 | + "AND NOT list_contains(COALESCE(m.modifiers, []), 'static') " |
| 655 | + "RETURN 1 AS ok LIMIT 1", |
| 656 | + params, |
| 657 | + ) |
| 658 | + if not gate: |
| 659 | + return {} |
| 660 | + |
| 661 | + rollup: dict[str, dict[str, int]] = {} |
| 662 | + |
| 663 | + down_rows = self._rows( |
| 664 | + "MATCH (m:Symbol {id: $id})<-[:DECLARES]-(t:Symbol) " |
| 665 | + "MATCH (impl:Symbol)-[:IMPLEMENTS|EXTENDS]->(t) " |
| 666 | + "MATCH (impl)-[:DECLARES]->(mover:Symbol) " |
| 667 | + "WHERE mover.signature = m.signature AND mover.id <> m.id " |
| 668 | + "RETURN collect(DISTINCT mover.id) AS ids", |
| 669 | + params, |
| 670 | + ) |
| 671 | + impl_ids = _coerce_id_list(down_rows[0].get("ids") if down_rows else None) |
| 672 | + |
| 673 | + if impl_ids: |
| 674 | + distinct_impl = list(dict.fromkeys(impl_ids)) |
| 675 | + rollup["OVERRIDDEN_BY"] = {"in": 0, "out": len(distinct_impl)} |
| 676 | + n_dc = self._edge_row_count_from_method_ids(distinct_impl, "DECLARES_CLIENT") |
| 677 | + if n_dc > 0: |
| 678 | + rollup["OVERRIDDEN_BY.DECLARES_CLIENT"] = {"in": 0, "out": n_dc} |
| 679 | + n_ex = self._edge_row_count_from_method_ids(distinct_impl, "EXPOSES") |
| 680 | + if n_ex > 0: |
| 681 | + rollup["OVERRIDDEN_BY.EXPOSES"] = {"in": 0, "out": n_ex} |
| 682 | + |
| 683 | + up_rows = self._rows( |
| 684 | + "MATCH (m:Symbol {id: $id})<-[:DECLARES]-(impl:Symbol) " |
| 685 | + "MATCH (impl)-[:IMPLEMENTS|EXTENDS]->(parent:Symbol) " |
| 686 | + "MATCH (parent)-[:DECLARES]->(decl_m:Symbol) " |
| 687 | + "WHERE decl_m.signature = m.signature AND decl_m.id <> m.id " |
| 688 | + "RETURN collect(DISTINCT decl_m.id) AS ids", |
| 689 | + params, |
| 690 | + ) |
| 691 | + decl_ids = _coerce_id_list(up_rows[0].get("ids") if up_rows else None) |
| 692 | + if decl_ids: |
| 693 | + distinct_decl = list(dict.fromkeys(decl_ids)) |
| 694 | + rollup["OVERRIDES"] = {"in": 0, "out": len(distinct_decl)} |
| 695 | + |
| 696 | + return rollup |
| 697 | + |
621 | 698 | def _scope_counts(self, column: str) -> dict[str, int]: |
622 | 699 | """Generic helper: count resolved type symbols grouped by `column`. |
623 | 700 |
|
|
0 commit comments