Skip to content

Commit c27ab05

Browse files
committed
Refactor signature help to use AST for callable variable extraction
1 parent 8e7bbae commit c27ab05

5 files changed

Lines changed: 264 additions & 149 deletions

File tree

docs/todo/bugs.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,20 @@ handle this automatically.
2222
`tests/psalm_assertions/method_call.php` (out of scope until
2323
upstream stubs land).
2424

25+
26+
## B17. Double-`$` prefix in assignment dependency tracking
27+
28+
`src/completion/variable/forward_walk.rs` L5972-5977
29+
(`collect_expr_assignment_deps`) and L5985-5987
30+
(`collect_rhs_variables`) use `format!("${}", dv.name)` to
31+
construct variable names. Since `dv.name` already includes the
32+
`$` prefix (e.g. `"$fn"`), this produces `"$$fn"`. The bug is
33+
latent because both the LHS key and the RHS set values are
34+
consistently double-prefixed, so dependency tracking still
35+
works. However, any code that compares these names against
36+
normally-prefixed variable names (single `$`) would fail to
37+
match.
38+
39+
**Fix:** Remove the `format!("${}", ...)` wrapper and use
40+
`dv.name.to_string()` directly in both functions.
41+

docs/todo/refactor.md

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,6 @@ Each item must include:
214214

215215
## Outstanding items
216216

217-
### R2. Mini expression resolver in `call_resolution.rs` 🔴
218-
219-
`src/completion/call_resolution.rs` L2528 has `resolve_expression_to_type`, a
220-
separate expression-to-`PhpType` resolver for conditional return type argument
221-
matching. Handles `SubjectExpr::PropertyChain` and `SubjectExpr::Variable`
222-
through different code paths than the forward walker's `resolve_rhs_expression`.
223-
Fewer cases, different path, will diverge as features are added.
224-
225-
**Action:** Route through the shared `resolve_rhs_expression` /
226-
`resolve_expression_type` pipeline.
227-
228-
**Files:** `src/completion/call_resolution.rs`
229-
230217
### R3. Backward `@var` scanner in `docblock/tags.rs` 🟠
231218

232219
`src/docblock/tags.rs` L520–620 (`find_var_raw_type_in_source`) and L1000–1100
@@ -243,17 +230,6 @@ unnecessary.
243230
**Files:** `src/docblock/tags.rs`, `src/completion/call_resolution.rs`,
244231
`src/completion/array_shape.rs`
245232

246-
### R4. Backward callable scan in `signature_help.rs` 🟠
247-
248-
`src/signature_help.rs` L438–470 (`extract_callable_target_from_variable`) uses
249-
raw `rfind` backward text scan to find `$fn = someTarget(...)` assignments,
250-
bypassing the forward walker which already tracks variable assignments.
251-
252-
**Action:** Use the forward walker's assignment tracking instead of raw text
253-
scanning.
254-
255-
**Files:** `src/signature_help.rs`
256-
257233
---
258234

259235
## Intentional overlap (reference, not actionable)

src/completion/call_resolution.rs

Lines changed: 9 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,110 +2512,17 @@ impl Backend {
25122512

25132513
/// Resolve an arbitrary expression to a [`PhpType`].
25142514
///
2515-
/// Unlike [`super::resolver::resolve_target_classes`] (which returns
2516-
/// class-bearing `ResolvedType` values and drops scalars), this function
2517-
/// returns the *type* of the expression — including scalars like `string`,
2518-
/// `int`, `bool`, etc.
2519-
///
2520-
/// Strategy:
2521-
/// 1. Parse the expression with [`SubjectExpr::parse`].
2522-
/// 2. For property chains (`$var->prop`), resolve the base to classes
2523-
/// and look up the property's type hint directly.
2524-
/// 3. For method calls (`$var->method()`), resolve the base and look
2525-
/// up the method's return type.
2526-
/// 4. For everything else, fall back to [`super::resolver::resolve_target_classes`]
2527-
/// which handles variables, `$this`, `new …`, etc.
2515+
/// Delegates to [`super::resolver::resolve_target_classes`] which
2516+
/// handles all expression patterns (variables, property chains,
2517+
/// method calls, static accesses, etc.) and preserves scalar types
2518+
/// through the `type_string` field of [`ResolvedType`].
25282519
fn resolve_expression_to_type(text: &str, ctx: &ResolutionCtx<'_>) -> Option<PhpType> {
2529-
let expr = SubjectExpr::parse(text);
2530-
2531-
match &expr {
2532-
// $var->prop or $this->prop — resolve base to classes, then
2533-
// look up the property's type hint (which preserves scalars).
2534-
SubjectExpr::PropertyChain { base, property } => {
2535-
let base_text = base.to_subject_text();
2536-
let base_classes = ResolvedType::into_arced_classes(
2537-
super::resolver::resolve_target_classes(&base_text, crate::AccessKind::Arrow, ctx),
2538-
);
2539-
for cls in &base_classes {
2540-
if let Some(hint) =
2541-
crate::inheritance::resolve_property_type_hint(cls, property, ctx.class_loader)
2542-
{
2543-
return Some(hint);
2544-
}
2545-
}
2546-
None
2547-
}
2548-
2549-
// $var->method() or $this->helper->getText() — resolve the
2550-
// full call expression to return-type classes first; if that
2551-
// yields nothing class-like, resolve the raw return type
2552-
// (which preserves scalars).
2553-
SubjectExpr::CallExpr { callee, args_text } => {
2554-
// Try class-bearing resolution first (handles non-scalar returns).
2555-
// Use the _with_hint variant to capture the raw return type
2556-
// (which preserves generic parameters like `ASTArtifactList<ASTClass>`).
2557-
// Without this, generic args are lost and downstream template
2558-
// substitution resolves to unsubstituted template params.
2559-
let mut return_hint: Option<PhpType> = None;
2560-
let classes = Backend::resolve_call_return_types_expr_with_hint(
2561-
callee,
2562-
args_text,
2563-
ctx,
2564-
Some(&mut return_hint),
2565-
);
2566-
if let Some(first) = classes.first() {
2567-
// Prefer the return type hint (preserves generics) over
2568-
// the bare class FQN. Fall back to FQN when no hint
2569-
// was captured or when the hint is a raw template name.
2570-
if let Some(ref hint) = return_hint
2571-
&& !hint.is_untyped()
2572-
&& !hint.is_mixed()
2573-
{
2574-
return Some(hint.clone());
2575-
}
2576-
return Some(PhpType::Named(first.fqn().to_string()));
2577-
}
2578-
2579-
// Fall back to raw return type for scalar returns.
2580-
if let SubjectExpr::MethodCall { base, method } = callee.as_ref() {
2581-
let base_text = base.to_subject_text();
2582-
let base_classes =
2583-
ResolvedType::into_arced_classes(super::resolver::resolve_target_classes(
2584-
&base_text,
2585-
crate::AccessKind::Arrow,
2586-
ctx,
2587-
));
2588-
for cls in &base_classes {
2589-
if let Some(rt) = crate::inheritance::resolve_method_return_type(
2590-
cls,
2591-
method,
2592-
ctx.class_loader,
2593-
) {
2594-
return Some(rt);
2595-
}
2596-
}
2597-
}
2598-
if let SubjectExpr::StaticMethodCall { class, method } = callee.as_ref()
2599-
&& let Some(owner) = super::resolver::resolve_static_owner_class(class, ctx)
2600-
&& let Some(rt) =
2601-
crate::inheritance::resolve_method_return_type(&owner, method, ctx.class_loader)
2602-
{
2603-
return Some(rt);
2604-
}
2605-
None
2606-
}
2607-
2608-
// Everything else ($var, $this, ClassName, new X, etc.) —
2609-
// use the standard resolver which returns class-bearing types.
2610-
_ => {
2611-
let resolved =
2612-
super::resolver::resolve_target_classes(text, crate::types::AccessKind::Arrow, ctx);
2613-
if let Some(first) = resolved.first() {
2614-
return Some(first.type_string.clone());
2615-
}
2616-
None
2617-
}
2520+
let results =
2521+
super::resolver::resolve_target_classes(text, crate::types::AccessKind::Arrow, ctx);
2522+
if let Some(first) = results.first() {
2523+
return Some(first.type_string.clone());
26182524
}
2525+
None
26192526
}
26202527

26212528
/// Resolve a `ClassName::Member` expression to a type.

0 commit comments

Comments
 (0)