| 1678 | } |
| 1679 | |
| 1680 | static void resolve_member_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) { |
| 1681 | TSNode obj = ts_node_child_by_field_name(call, "object", 6); |
| 1682 | TSNode name_node = ts_node_child_by_field_name(call, "name", 4); |
| 1683 | if (ts_node_is_null(name_node)) |
| 1684 | return; |
| 1685 | if (kind == CBM_RESOLVED_CALL_REFERENCE && |
| 1686 | php_callable_reference_has_dynamic_member_name(ctx, call)) { |
| 1687 | return; |
| 1688 | } |
| 1689 | char *method_name = php_node_text(ctx, name_node); |
| 1690 | if (!method_name) |
| 1691 | return; |
| 1692 | if (ts_node_is_null(obj)) |
| 1693 | return; |
| 1694 | |
| 1695 | const CBMType *recv = php_eval_expr_type(ctx, obj); |
| 1696 | if (!recv) |
| 1697 | return; |
| 1698 | const char *class_qn = NULL; |
| 1699 | if (recv->kind == CBM_TYPE_NAMED) { |
| 1700 | class_qn = recv->data.named.qualified_name; |
| 1701 | } else if (recv->kind == CBM_TYPE_TEMPLATE) { |
| 1702 | /* `Collection<User>` resolves methods on `Collection`. */ |
| 1703 | class_qn = recv->data.template_type.template_name; |
| 1704 | } |
| 1705 | if (!class_qn) |
| 1706 | return; /* unknown receiver; defer */ |
| 1707 | const CBMRegisteredFunc *f = php_lookup_method(ctx, class_qn, method_name); |
| 1708 | if (f) { |
| 1709 | const char *strategy = (f->receiver_type && strcmp(f->receiver_type, class_qn) == 0) |
| 1710 | ? "php_method_typed" |
| 1711 | : "php_method_inherited"; |
| 1712 | emit_resolved_kind(ctx, f->qualified_name, strategy, 0.95f, kind); |
| 1713 | return; |
| 1714 | } |
| 1715 | /* Receiver known but method missing — magic __call? The call dispatches to |
| 1716 | * the class's __call handler, so resolve to <class>.__call (a real node). |
| 1717 | * The textual callee is the dynamic method name (`anything`), not `__call`, |
| 1718 | * so stash it in reason for the join (lsp_resolve.h, php_method_dynamic). |
| 1719 | * Emit above the join's confidence floor — dispatch to __call is certain. */ |
| 1720 | if (class_has_magic_call(ctx, class_qn, false)) { |
| 1721 | emit_resolved_reason_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.__call", class_qn), |
| 1722 | "php_method_dynamic", 0.85f, method_name, kind); |
| 1723 | return; |
| 1724 | } |
| 1725 | /* Receiver known but class not in registry (e.g. vendor type not indexed, |
| 1726 | * or method genuinely missing). Emit a synthetic resolved call pointing |
| 1727 | * at "<class_qn>.<method>" so the pipeline bridge can BLOCK the unified |
| 1728 | * extractor's likely-incorrect short-name fallback. The bridge filters |
| 1729 | * unknown targets and yields no edge — better than a wrong edge. */ |
| 1730 | emit_resolved_kind(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, method_name), |
| 1731 | "php_method_typed_unindexed", 0.55f, kind); |
| 1732 | } |
| 1733 | |
| 1734 | static void resolve_static_call(PHPLSPContext *ctx, TSNode call, CBMResolvedKind kind) { |
| 1735 | TSNode scope = ts_node_child_by_field_name(call, "scope", 5); |
no test coverage detected