Skip to content

Commit 92a6b99

Browse files
committed
Support goto in new objects
1 parent ba7b5bf commit 92a6b99

6 files changed

Lines changed: 703 additions & 15 deletions

File tree

src/completion/resolver.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ impl Backend {
4545
#[allow(clippy::too_many_arguments)]
4646
pub fn resolve_target_class(
4747
subject: &str,
48-
access_kind: AccessKind,
48+
_access_kind: AccessKind,
4949
current_class: Option<&ClassInfo>,
5050
all_classes: &[ClassInfo],
5151
content: &str,
@@ -74,8 +74,12 @@ impl Backend {
7474
return None;
7575
}
7676

77-
// ── Bare class name (for `::`) ──
78-
if access_kind == AccessKind::DoubleColon && !subject.starts_with('$') {
77+
// ── Bare class name (for `::` or `->` from `new ClassName()`) ──
78+
if !subject.starts_with('$')
79+
&& !subject.contains("->")
80+
&& !subject.contains("::")
81+
&& !subject.ends_with("()")
82+
{
7983
let lookup = subject.rsplit('\\').next().unwrap_or(subject);
8084
if let Some(cls) = all_classes.iter().find(|c| c.name == lookup) {
8185
return Some(cls.clone());

src/completion/target.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use tower_lsp::lsp_types::*;
77

88
use crate::Backend;
99
use crate::types::*;
10-
use crate::util::skip_balanced_parens_back;
10+
use crate::util::{
11+
check_new_keyword_before, extract_new_expression_inside_parens, skip_balanced_parens_back,
12+
};
1113

1214
impl Backend {
1315
/// Detect the access operator before the cursor position and extract
@@ -68,6 +70,8 @@ impl Backend {
6870
/// - `app()->` (function call)
6971
/// - `$this->getService()->` (method call chain)
7072
/// - `ClassName::make()->` (static method call)
73+
/// - `new ClassName()->` (instantiation, PHP 8.4+)
74+
/// - `(new ClassName())->` (parenthesized instantiation)
7175
fn extract_arrow_subject(chars: &[char], arrow_pos: usize) -> String {
7276
// Position just before the `->`
7377
let end = arrow_pos;
@@ -78,8 +82,9 @@ impl Backend {
7882
i -= 1;
7983
}
8084

81-
// ── Function / method call: detect `)` before the operator ──
82-
// e.g. `app()->`, `$this->getService()->`, `Class::make()->`
85+
// ── Function / method call or `new` expression: detect `)` ──
86+
// e.g. `app()->`, `$this->getService()->`, `Class::make()->`,
87+
// `new Foo()->`, `(new Foo())->`
8388
if i > 0
8489
&& chars[i - 1] == ')'
8590
&& let Some(call_subject) = Self::extract_call_subject(chars, i)
@@ -128,6 +133,7 @@ impl Backend {
128133
/// - `"app()"` for a standalone function call
129134
/// - `"$this->getService()"` for an instance method call
130135
/// - `"ClassName::make()"` for a static method call
136+
/// - `"ClassName"` for `new ClassName()` instantiation
131137
fn extract_call_subject(chars: &[char], paren_end: usize) -> Option<String> {
132138
let open = skip_balanced_parens_back(chars, paren_end)?;
133139
if open == 0 {
@@ -136,15 +142,24 @@ impl Backend {
136142

137143
// Read the function / method name before `(`
138144
let mut i = open;
139-
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
145+
while i > 0
146+
&& (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\')
147+
{
140148
i -= 1;
141149
}
142150
if i == open {
143-
// No identifier before `(` — can't resolve
144-
return None;
151+
// No identifier before `(` — check if the contents inside the
152+
// balanced parens form a `(new ClassName(...))` expression.
153+
return extract_new_expression_inside_parens(chars, open, paren_end);
145154
}
146155
let func_name: String = chars[i..open].iter().collect();
147156

157+
// ── `new ClassName()` instantiation ──
158+
// Check if the `new` keyword immediately precedes the class name.
159+
if let Some(class_name) = check_new_keyword_before(chars, i, &func_name) {
160+
return Some(class_name);
161+
}
162+
148163
// Check what precedes the function name to determine the kind of
149164
// call expression.
150165

src/definition/resolve.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ use tower_lsp::lsp_types::*;
2020
use crate::Backend;
2121
use crate::composer;
2222
use crate::types::*;
23-
use crate::util::skip_balanced_parens_back;
23+
use crate::util::{
24+
check_new_keyword_before, extract_new_expression_inside_parens, skip_balanced_parens_back,
25+
};
2426

2527
/// The kind of class member being resolved.
2628
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -450,8 +452,9 @@ impl Backend {
450452
i -= 1;
451453
}
452454

453-
// ── Function / method call: detect `)` before the operator ──
454-
// e.g. `app()->`, `$this->getService()->`, `Class::make()->`
455+
// ── Function / method call or `new` expression: detect `)` ──
456+
// e.g. `app()->`, `$this->getService()->`, `Class::make()->`,
457+
// `new Foo()->`, `(new Foo())->`
455458
if i > 0
456459
&& chars[i - 1] == ')'
457460
&& let Some(call_subject) = Self::extract_call_subject_for_definition(chars, i)
@@ -497,6 +500,7 @@ impl Backend {
497500
/// - `"app()"` for a standalone function call
498501
/// - `"$this->getService()"` for an instance method call
499502
/// - `"ClassName::make()"` for a static method call
503+
/// - `"ClassName"` for `new ClassName()` instantiation
500504
fn extract_call_subject_for_definition(chars: &[char], paren_end: usize) -> Option<String> {
501505
let open = skip_balanced_parens_back(chars, paren_end)?;
502506
if open == 0 {
@@ -505,14 +509,24 @@ impl Backend {
505509

506510
// Read the function / method name before `(`
507511
let mut i = open;
508-
while i > 0 && (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_') {
512+
while i > 0
513+
&& (chars[i - 1].is_alphanumeric() || chars[i - 1] == '_' || chars[i - 1] == '\\')
514+
{
509515
i -= 1;
510516
}
511517
if i == open {
512-
return None;
518+
// No identifier before `(` — check if the contents inside the
519+
// balanced parens form a `(new ClassName(...))` expression.
520+
return extract_new_expression_inside_parens(chars, open, paren_end);
513521
}
514522
let func_name: String = chars[i..open].iter().collect();
515523

524+
// ── `new ClassName()` instantiation ──
525+
// Check if the `new` keyword immediately precedes the class name.
526+
if let Some(class_name) = check_new_keyword_before(chars, i, &func_name) {
527+
return Some(class_name);
528+
}
529+
516530
// Instance method call: `$this->method()` / `$var->method()`
517531
if i >= 2 && chars[i - 2] == '-' && chars[i - 1] == '>' {
518532
let inner_subject = Self::extract_simple_var_before(chars, i - 2);

src/util.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,90 @@ pub(crate) fn skip_balanced_parens_back(chars: &[char], pos: usize) -> Option<us
3939
None
4040
}
4141

42+
/// Check if the `new` keyword (followed by whitespace) appears immediately
43+
/// before the identifier starting at position `ident_start`.
44+
///
45+
/// Returns the class name (possibly with namespace) if `new` is found.
46+
pub(crate) fn check_new_keyword_before(
47+
chars: &[char],
48+
ident_start: usize,
49+
class_name: &str,
50+
) -> Option<String> {
51+
let mut j = ident_start;
52+
// Skip whitespace between `new` and the class name.
53+
while j > 0 && chars[j - 1] == ' ' {
54+
j -= 1;
55+
}
56+
// Check for the `new` keyword.
57+
if j >= 3 && chars[j - 3] == 'n' && chars[j - 2] == 'e' && chars[j - 1] == 'w' {
58+
// Verify word boundary before `new` (start of line, whitespace, `(`, etc.).
59+
let before_ok = j == 3 || {
60+
let prev = chars[j - 4];
61+
!prev.is_alphanumeric() && prev != '_'
62+
};
63+
if before_ok {
64+
// Strip leading `\` from FQN if present.
65+
let name = class_name.strip_prefix('\\').unwrap_or(class_name);
66+
return Some(name.to_string());
67+
}
68+
}
69+
None
70+
}
71+
72+
/// Try to extract a class name from a parenthesized `new` expression:
73+
/// `(new ClassName(...))`.
74+
///
75+
/// `open` is the position of the outer `(`, `close` is one past the
76+
/// outer `)`. The function looks inside for the pattern
77+
/// `new ClassName(...)`.
78+
pub(crate) fn extract_new_expression_inside_parens(
79+
chars: &[char],
80+
open: usize,
81+
close: usize,
82+
) -> Option<String> {
83+
// Content is chars[open+1 .. close-1].
84+
let inner_start = open + 1;
85+
let inner_end = close - 1;
86+
if inner_start >= inner_end {
87+
return None;
88+
}
89+
90+
// Skip whitespace inside the opening `(`.
91+
let mut k = inner_start;
92+
while k < inner_end && chars[k] == ' ' {
93+
k += 1;
94+
}
95+
96+
// Check for `new` keyword.
97+
if k + 3 >= inner_end {
98+
return None;
99+
}
100+
if chars[k] != 'n' || chars[k + 1] != 'e' || chars[k + 2] != 'w' {
101+
return None;
102+
}
103+
k += 3;
104+
105+
// Must be followed by whitespace.
106+
if k >= inner_end || chars[k] != ' ' {
107+
return None;
108+
}
109+
while k < inner_end && chars[k] == ' ' {
110+
k += 1;
111+
}
112+
113+
// Read the class name (may include `\` for namespaces).
114+
let name_start = k;
115+
while k < inner_end && (chars[k].is_alphanumeric() || chars[k] == '_' || chars[k] == '\\') {
116+
k += 1;
117+
}
118+
if k == name_start {
119+
return None;
120+
}
121+
let class_name: String = chars[name_start..k].iter().collect();
122+
let name = class_name.strip_prefix('\\').unwrap_or(&class_name);
123+
Some(name.to_string())
124+
}
125+
42126
impl Backend {
43127
/// Convert an LSP Position (line, character) to a byte offset in content.
44128
pub(crate) fn position_to_offset(content: &str, position: Position) -> Option<u32> {

0 commit comments

Comments
 (0)