Skip to content

Commit 2be2b05

Browse files
committed
Complete for user constants
1 parent b395311 commit 2be2b05

3 files changed

Lines changed: 915 additions & 0 deletions

File tree

example.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2195,3 +2195,29 @@ public function inTernary(User $user, bool $flag): void
21952195
$result->getEmail(); // Resolved: string (both User and AdminUser have it)
21962196
}
21972197
}
2198+
2199+
// ─── Standalone Constant Go-to-Definition ───────────────────────────────────
2200+
//
2201+
// Go-to-definition now works for standalone constants defined via `define()`.
2202+
// Clicking on a constant name jumps to the `define('NAME', ...)` call site,
2203+
// whether it's in the same file or a different file.
2204+
2205+
define('APP_VERSION', '1.0.0');
2206+
define('MAX_RETRIES', 3);
2207+
define('DEFAULT_TIMEOUT', 30);
2208+
2209+
// Ctrl+Click / Go-to-definition on any of these constant names jumps to
2210+
// the corresponding define() call above:
2211+
echo APP_VERSION; // → jumps to define('APP_VERSION', ...)
2212+
$retries = MAX_RETRIES; // → jumps to define('MAX_RETRIES', ...)
2213+
2214+
function getTimeout(): int
2215+
{
2216+
// Works inside function bodies too
2217+
return DEFAULT_TIMEOUT; // → jumps to define('DEFAULT_TIMEOUT', ...)
2218+
}
2219+
2220+
// Constants used in expressions, comparisons, and array indices:
2221+
if (MAX_RETRIES > 0) { // → jumps to define('MAX_RETRIES', ...)
2222+
echo APP_VERSION; // → jumps to define('APP_VERSION', ...)
2223+
}

src/definition/resolve.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,90 @@ impl Backend {
160160
return Some(location);
161161
}
162162

163+
// 8. Try standalone constant lookup (define() constants).
164+
if let Some(location) = self.resolve_constant_definition(&func_candidates) {
165+
return Some(location);
166+
}
167+
168+
None
169+
}
170+
171+
// ─── Constant Definition Resolution ─────────────────────────────────────
172+
173+
/// Resolve a standalone constant to its `define('NAME', …)` call site.
174+
///
175+
/// Checks `global_defines` (user-defined constants discovered from parsed
176+
/// files) for a matching constant name, reads the source file, and returns
177+
/// a `Location` pointing at the `define(` call. Built-in constants from
178+
/// `stub_constant_index` are not navigable (they have no real file).
179+
fn resolve_constant_definition(&self, candidates: &[String]) -> Option<Location> {
180+
// Look up the constant in global_defines.
181+
let file_uri = {
182+
let dmap = self.global_defines.lock().ok()?;
183+
let mut result = None;
184+
for candidate in candidates {
185+
if let Some(uri) = dmap.get(candidate.as_str()) {
186+
result = Some((candidate.clone(), uri.clone()));
187+
break;
188+
}
189+
}
190+
result
191+
};
192+
193+
let (const_name, file_uri) = file_uri?;
194+
195+
// Read the file content (try open files first, then disk).
196+
let file_content = self
197+
.open_files
198+
.lock()
199+
.ok()
200+
.and_then(|files| files.get(&file_uri).cloned())
201+
.or_else(|| {
202+
let path = Url::parse(&file_uri).ok()?.to_file_path().ok()?;
203+
std::fs::read_to_string(path).ok()
204+
})?;
205+
206+
let position = Self::find_define_position(&file_content, &const_name)?;
207+
let parsed_uri = Url::parse(&file_uri).ok()?;
208+
209+
Some(Location {
210+
uri: parsed_uri,
211+
range: Range {
212+
start: position,
213+
end: position,
214+
},
215+
})
216+
}
217+
218+
/// Find the position of a `define('NAME'` or `define("NAME"` call in
219+
/// file content.
220+
///
221+
/// Searches each line for a `define(` keyword followed (possibly with
222+
/// whitespace) by a string literal containing the constant name.
223+
/// Returns the position of the `define` keyword on the matching line.
224+
fn find_define_position(content: &str, constant_name: &str) -> Option<Position> {
225+
// Patterns: `'NAME'` and `"NAME"` — we search for these after
226+
// the `define(` token, allowing optional whitespace.
227+
let single_q = format!("'{}'", constant_name);
228+
let double_q = format!("\"{}\"", constant_name);
229+
230+
for (line_idx, line) in content.lines().enumerate() {
231+
// Find `define(` anywhere on the line.
232+
let Some(def_pos) = line.find("define(") else {
233+
continue;
234+
};
235+
236+
// Extract the text after `define(` and trim leading whitespace
237+
// to allow `define( 'NAME'` with spaces.
238+
let after_paren = line[def_pos + 7..].trim_start();
239+
if after_paren.starts_with(&single_q) || after_paren.starts_with(&double_q) {
240+
return Some(Position {
241+
line: line_idx as u32,
242+
character: def_pos as u32,
243+
});
244+
}
245+
}
246+
163247
None
164248
}
165249

0 commit comments

Comments
 (0)