Skip to content

Commit d22bbc4

Browse files
committed
Fix top level context tracking
1 parent a36b973 commit d22bbc4

4 files changed

Lines changed: 663 additions & 34 deletions

File tree

src/completion/resolver.rs

Lines changed: 106 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ struct VarResolutionCtx<'a> {
5757
function_loader: FunctionLoaderFn<'a>,
5858
}
5959

60+
/// Bundles the common parameters threaded through call-expression
61+
/// return-type resolution.
62+
///
63+
/// This keeps the argument count of [`resolve_call_return_types`] under
64+
/// clippy's `too_many_arguments` threshold.
65+
struct CallResolutionCtx<'a> {
66+
current_class: Option<&'a ClassInfo>,
67+
all_classes: &'a [ClassInfo],
68+
content: &'a str,
69+
cursor_offset: u32,
70+
class_loader: &'a dyn Fn(&str) -> Option<ClassInfo>,
71+
function_loader: FunctionLoaderFn<'a>,
72+
}
73+
6074
impl Backend {
6175
/// Determine which class (if any) the completion subject refers to.
6276
///
@@ -129,6 +143,21 @@ impl Backend {
129143
return vec![];
130144
}
131145

146+
// ── Enum case / static member access: `ClassName::CaseName` ──
147+
// When an enum case or static member is used with `->`, resolve to
148+
// the class/enum itself (e.g. `Status::Active->label()` → `Status`).
149+
if !subject.starts_with('$')
150+
&& subject.contains("::")
151+
&& !subject.ends_with(')')
152+
&& let Some((class_part, _case_part)) = subject.split_once("::")
153+
{
154+
let lookup = class_part.rsplit('\\').next().unwrap_or(class_part);
155+
if let Some(cls) = all_classes.iter().find(|c| c.name == lookup) {
156+
return vec![cls.clone()];
157+
}
158+
return class_loader(class_part).into_iter().collect();
159+
}
160+
132161
// ── Bare class name (for `::` or `->` from `new ClassName()`) ──
133162
if !subject.starts_with('$')
134163
&& !subject.contains("->")
@@ -150,14 +179,15 @@ impl Backend {
150179
if subject.ends_with(')')
151180
&& let Some((call_body, args_text)) = split_call_subject(subject)
152181
{
153-
return Self::resolve_call_return_types(
154-
call_body,
155-
args_text,
182+
let ctx = CallResolutionCtx {
156183
current_class,
157184
all_classes,
185+
content,
186+
cursor_offset,
158187
class_loader,
159188
function_loader,
160-
);
189+
};
190+
return Self::resolve_call_return_types(call_body, args_text, &ctx);
161191
}
162192

163193
// ── Property-chain: $this->prop or $this?->prop ──
@@ -177,18 +207,38 @@ impl Backend {
177207

178208
// ── Variable like `$var` — resolve via assignments / parameter hints ──
179209
if subject.starts_with('$') {
180-
if let Some(cc) = current_class {
181-
return Self::resolve_variable_types(
182-
subject,
183-
cc,
184-
all_classes,
185-
content,
186-
cursor_offset,
187-
class_loader,
188-
function_loader,
189-
);
190-
}
191-
return vec![];
210+
// When the cursor is inside a class, use the enclosing class
211+
// for `self`/`static` resolution in type hints. When in
212+
// top-level code (`current_class` is `None`), use a dummy
213+
// empty class so that assignment scanning still works.
214+
let dummy_class;
215+
let effective_class = match current_class {
216+
Some(cc) => cc,
217+
None => {
218+
dummy_class = ClassInfo {
219+
name: String::new(),
220+
methods: vec![],
221+
properties: vec![],
222+
constants: vec![],
223+
start_offset: 0,
224+
end_offset: 0,
225+
parent_class: None,
226+
used_traits: vec![],
227+
mixins: vec![],
228+
is_final: false,
229+
};
230+
&dummy_class
231+
}
232+
};
233+
return Self::resolve_variable_types(
234+
subject,
235+
effective_class,
236+
all_classes,
237+
content,
238+
cursor_offset,
239+
class_loader,
240+
function_loader,
241+
);
192242
}
193243

194244
vec![]
@@ -209,11 +259,12 @@ impl Backend {
209259
fn resolve_call_return_types(
210260
call_body: &str,
211261
text_args: &str,
212-
current_class: Option<&ClassInfo>,
213-
all_classes: &[ClassInfo],
214-
class_loader: &dyn Fn(&str) -> Option<ClassInfo>,
215-
function_loader: FunctionLoaderFn<'_>,
262+
ctx: &CallResolutionCtx<'_>,
216263
) -> Vec<ClassInfo> {
264+
let current_class = ctx.current_class;
265+
let all_classes = ctx.all_classes;
266+
let class_loader = ctx.class_loader;
267+
let function_loader = ctx.function_loader;
217268
// ── Instance method call: $this->method / $var->method ──
218269
if let Some(pos) = call_body.rfind("->") {
219270
let lhs = &call_body[..pos];
@@ -235,14 +286,7 @@ impl Backend {
235286
// `$this->getFactory()->create(…)`).
236287
// Recursively resolve it.
237288
if let Some((inner_body, inner_args)) = split_call_subject(lhs) {
238-
Self::resolve_call_return_types(
239-
inner_body,
240-
inner_args,
241-
current_class,
242-
all_classes,
243-
class_loader,
244-
function_loader,
245-
)
289+
Self::resolve_call_return_types(inner_body, inner_args, ctx)
246290
} else {
247291
vec![]
248292
}
@@ -253,8 +297,23 @@ impl Backend {
253297
current_class
254298
.map(|cc| Self::resolve_property_types(prop, cc, all_classes, class_loader))
255299
.unwrap_or_default()
300+
} else if lhs.starts_with('$') {
301+
// Bare variable like `$profile` — resolve its type via
302+
// assignment scanning so that chains like
303+
// `$profile->getUser()->getEmail()` work in both
304+
// class-method and top-level contexts.
305+
Self::resolve_target_classes(
306+
lhs,
307+
AccessKind::Arrow,
308+
ctx.current_class,
309+
ctx.all_classes,
310+
ctx.content,
311+
ctx.cursor_offset,
312+
ctx.class_loader,
313+
ctx.function_loader,
314+
)
256315
} else {
257-
// Could be a variable — for now, skip complex chains
316+
// Unknown LHS form — skip
258317
vec![]
259318
};
260319

@@ -536,7 +595,11 @@ impl Backend {
536595
statements: impl Iterator<Item = &'b Statement<'b>>,
537596
ctx: &VarResolutionCtx<'_>,
538597
) -> Vec<ClassInfo> {
539-
for stmt in statements {
598+
// Collect so we can iterate twice: once to check class bodies,
599+
// once (if needed) to walk top-level statements.
600+
let stmts: Vec<&Statement> = statements.collect();
601+
602+
for &stmt in &stmts {
540603
match stmt {
541604
Statement::Class(class) => {
542605
let start = class.left_brace.start.offset;
@@ -580,7 +643,13 @@ impl Backend {
580643
_ => {}
581644
}
582645
}
583-
vec![]
646+
647+
// The cursor is not inside any class/interface/enum body — it must
648+
// be in top-level code. Walk all top-level statements to find
649+
// variable assignments (e.g. `$user = new User(…);`).
650+
let mut results: Vec<ClassInfo> = Vec::new();
651+
Self::walk_statements_for_assignments(stmts.into_iter(), ctx, &mut results, false);
652+
results
584653
}
585654

586655
/// Resolve a variable's type by scanning class-like members for parameter
@@ -1163,13 +1232,16 @@ impl Backend {
11631232
{
11641233
let current_class =
11651234
all_classes.iter().find(|c| c.name == current_class_name);
1166-
let resolved = Self::resolve_call_return_types(
1167-
call_body,
1168-
args_text,
1235+
let call_ctx = CallResolutionCtx {
11691236
current_class,
11701237
all_classes,
1238+
content,
1239+
cursor_offset: ctx.cursor_offset,
11711240
class_loader,
11721241
function_loader,
1242+
};
1243+
let resolved = Self::resolve_call_return_types(
1244+
call_body, args_text, &call_ctx,
11731245
);
11741246
push_results(results, resolved, conditional);
11751247
}

src/completion/target.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ impl Backend {
133133
}
134134
}
135135

136+
// Check if preceded by `::` (enum case or static member access,
137+
// e.g. `Status::Active->`)
138+
if i >= 2 && chars[i - 2] == ':' && chars[i - 1] == ':' {
139+
let class_subject = Self::extract_double_colon_subject_raw(chars, i - 2);
140+
if !class_subject.is_empty() {
141+
let ident: String = chars[ident_start..ident_end].iter().collect();
142+
return format!("{}::{}", class_subject, ident);
143+
}
144+
}
145+
136146
// Otherwise treat the whole thing as a simple variable like `$this` or `$var`
137147
Self::extract_simple_variable(chars, end)
138148
}

0 commit comments

Comments
 (0)