Skip to content

Commit 73bdea0

Browse files
MingJenAJenbo
andauthored
feat: complete Laravel config navigation with config() helper and typed accessors
Co-authored-by: Anders Jenbo <anders@jenbo.dk>
1 parent 55bb333 commit 73bdea0

4 files changed

Lines changed: 640 additions & 15 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- **Unused variable diagnostics.** Variables assigned but never read are flagged with hint severity and rendered as dimmed text. Covers local variables, foreach key/value bindings, catch variables, and closure `use` bindings. Variables named `$_` or prefixed with `$_` are exempt.
1515
- **Mago diagnostic proxy.** Mago lint and analyze diagnostics are surfaced as LSP diagnostics with quick-fix code actions. Configurable under `[mago]` in `.phpantom.toml`. Requires Mago 1.15+.
1616
- **PHPCS diagnostic proxy.** PHP_CodeSniffer violations are surfaced as LSP diagnostics with sniff-name diagnostic codes and severity mapping. Configurable under `[phpcs]` in `.phpantom.toml`.
17-
- **Laravel config and env key navigation.** "Go to Definition" and "Find All References" now work for string-literal config keys and env variables (`config('app.name')`, `Config::get(...)`, `env('APP_KEY')`). Contributed by @MingJen in https://github.com/AJenbo/phpantom_lsp/pull/93.
17+
- **Laravel config and env key navigation.** "Go to Definition" and "Find All References" now work for string-literal config keys and env variables (`config('app.name')`, `Config::get(...)`, `config()->string(...)`, `env('APP_KEY')`). Covers the `Config` facade, the `config()` helper (including fully-qualified `\config()`), and all typed accessors (`string`, `integer`, `float`, `boolean`, `array`, `collection`, `set`, `prepend`, `push`). Contributed by @MingJen in https://github.com/AJenbo/phpantom_lsp/pull/93 and https://github.com/AJenbo/phpantom_lsp/pull/96.
1818
- **Namespace renaming.** Renaming a namespace segment updates all namespace declarations, use statements, group use declarations, and fully-qualified name references across the workspace. When a PSR-4 autoload mapping exists, the corresponding directory is moved to keep the filesystem consistent.
1919
- **Linked editing ranges.** Place the cursor on a variable and all occurrences within its definition region enter linked editing mode. Typing a new name updates every occurrence simultaneously.
2020
- **Import all missing classes.** A bulk code action that imports every unresolved class name in the file at once. Only names with a single unambiguous candidate are imported; ambiguous names are left for manual resolution.

src/symbol_map/extraction.rs

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,13 @@ fn extract_from_expression<'a>(
16251625

16261626
if let ClassLikeMemberSelector::Identifier(ident) = &method_call.method {
16271627
let member_name = ident.value.to_string();
1628+
if is_laravel_config_repository_call(method_call.object, &member_name) {
1629+
try_emit_config_key_span(
1630+
&method_call.argument_list,
1631+
ctx.content,
1632+
&mut ctx.spans,
1633+
);
1634+
}
16281635
// Emit call site for method call: `$subject->method(...)`
16291636
emit_call_site(
16301637
format!("{}->{}", &subject_text, &member_name),
@@ -1652,6 +1659,13 @@ fn extract_from_expression<'a>(
16521659

16531660
if let ClassLikeMemberSelector::Identifier(ident) = &method_call.method {
16541661
let member_name = ident.value.to_string();
1662+
if is_laravel_config_repository_call(method_call.object, &member_name) {
1663+
try_emit_config_key_span(
1664+
&method_call.argument_list,
1665+
ctx.content,
1666+
&mut ctx.spans,
1667+
);
1668+
}
16551669
// Emit call site for null-safe method call.
16561670
// Use `->` so resolve_callable handles it the same
16571671
// as regular method calls.
@@ -1703,20 +1717,7 @@ fn extract_from_expression<'a>(
17031717
if (clean_subject.eq_ignore_ascii_case("Config")
17041718
|| clean_subject
17051719
.eq_ignore_ascii_case("Illuminate\\Support\\Facades\\Config"))
1706-
&& matches!(
1707-
member_name.to_ascii_lowercase().as_str(),
1708-
"has"
1709-
| "get"
1710-
| "string"
1711-
| "integer"
1712-
| "float"
1713-
| "boolean"
1714-
| "array"
1715-
| "collection"
1716-
| "set"
1717-
| "prepend"
1718-
| "push"
1719-
)
1720+
&& is_config_repository_method(&member_name)
17201721
{
17211722
try_emit_config_key_span(
17221723
&static_call.argument_list,
@@ -2938,6 +2939,43 @@ fn try_emit_config_key_span(
29382939
});
29392940
}
29402941

2942+
/// Returns `true` if `name` is a method on Laravel's `Repository` config contract
2943+
/// that accepts a config key as its first argument.
2944+
fn is_config_repository_method(name: &str) -> bool {
2945+
matches!(
2946+
name.to_ascii_lowercase().as_str(),
2947+
"has"
2948+
| "get"
2949+
| "string"
2950+
| "integer"
2951+
| "float"
2952+
| "boolean"
2953+
| "array"
2954+
| "collection"
2955+
| "set"
2956+
| "prepend"
2957+
| "push"
2958+
)
2959+
}
2960+
2961+
/// Returns `true` if `object` is a `config()` (or `\config()`) helper call and
2962+
/// `member_name` is a config-key-accepting method, e.g. `config()->get('app.name')`.
2963+
fn is_laravel_config_repository_call(object: &Expression<'_>, member_name: &str) -> bool {
2964+
if !is_config_repository_method(member_name) {
2965+
return false;
2966+
}
2967+
2968+
match object {
2969+
Expression::Call(Call::Function(func_call)) => match func_call.function {
2970+
Expression::Identifier(ident) => {
2971+
strip_fqn_prefix(ident.value()).eq_ignore_ascii_case("config")
2972+
}
2973+
_ => false,
2974+
},
2975+
_ => false,
2976+
}
2977+
}
2978+
29412979
/// Recursively check whether an expression contains an `instanceof` operator.
29422980
fn arg_contains_instanceof(expr: &Expression<'_>) -> bool {
29432981
match expr {

src/virtual_members/laravel/helpers_tests.rs

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,290 @@ fn legacy_accessor_prop_name_three_words() {
121121
"getFullLegalNameAttribute"
122122
);
123123
}
124+
125+
// ── walk_all_php_expressions ─────────────────────────────────────────────────
126+
127+
fn collect_strings(php: &str) -> Vec<String> {
128+
let mut found = Vec::new();
129+
walk_all_php_expressions(php, &mut |expr| {
130+
if let Some((s, _, _)) = extract_string_literal(expr, php) {
131+
found.push(s.to_string());
132+
}
133+
ControlFlow::Continue(())
134+
});
135+
found
136+
}
137+
138+
fn has(php: &str, needle: &str) -> bool {
139+
collect_strings(php).iter().any(|s| s == needle)
140+
}
141+
142+
// ── Statement branches ───────────────────────────────────────────────────────
143+
144+
#[test]
145+
fn walker_return_stmt() {
146+
assert!(has("<?php return 'ret';", "ret"));
147+
}
148+
149+
#[test]
150+
fn walker_echo_stmt() {
151+
let php = "<?php echo 'ea', 'eb';";
152+
assert!(has(php, "ea"));
153+
assert!(has(php, "eb"));
154+
}
155+
156+
#[test]
157+
fn walker_namespace_stmt() {
158+
assert!(has("<?php namespace Foo; return 'ns';", "ns"));
159+
}
160+
161+
#[test]
162+
fn walker_block_stmt() {
163+
assert!(has("<?php { return 'blk'; }", "blk"));
164+
}
165+
166+
#[test]
167+
fn walker_if_stmt() {
168+
let php =
169+
"<?php if (true) { return 'th'; } elseif (false) { return 'ei'; } else { return 'el'; }";
170+
assert!(has(php, "th"));
171+
assert!(has(php, "ei"));
172+
assert!(has(php, "el"));
173+
}
174+
175+
#[test]
176+
fn walker_while_stmt() {
177+
assert!(has("<?php while (true) { return 'wb'; }", "wb"));
178+
}
179+
180+
#[test]
181+
fn walker_do_while_stmt() {
182+
assert!(has("<?php do { return 'dw'; } while (false);", "dw"));
183+
}
184+
185+
#[test]
186+
fn walker_for_stmt() {
187+
let php = "<?php for (foo('fi'); foo('fc'); foo('fu')) { foo('fb'); }";
188+
assert!(has(php, "fi"));
189+
assert!(has(php, "fc"));
190+
assert!(has(php, "fu"));
191+
assert!(has(php, "fb"));
192+
}
193+
194+
#[test]
195+
fn walker_foreach_stmt() {
196+
let php = "<?php foreach (['item'] as $v) { return 'fv'; }";
197+
assert!(has(php, "item"));
198+
assert!(has(php, "fv"));
199+
}
200+
201+
#[test]
202+
fn walker_try_catch_finally() {
203+
let php = "<?php try { return 'tv'; } catch (\\Exception $e) { return 'cv'; } finally { return 'fv'; }";
204+
assert!(has(php, "tv"));
205+
assert!(has(php, "cv"));
206+
assert!(has(php, "fv"));
207+
}
208+
209+
#[test]
210+
fn walker_switch_stmt() {
211+
let php = "<?php switch ('sw') { case 'cs': return 'rv'; default: return 'dv'; }";
212+
assert!(has(php, "sw"));
213+
assert!(has(php, "cs"));
214+
assert!(has(php, "rv"));
215+
assert!(has(php, "dv"));
216+
}
217+
218+
#[test]
219+
fn walker_function_body() {
220+
assert!(has("<?php function foo() { return 'fn'; }", "fn"));
221+
}
222+
223+
#[test]
224+
fn walker_class_method_body() {
225+
assert!(has("<?php class C { function m() { return 'cm'; } }", "cm"));
226+
}
227+
228+
#[test]
229+
fn walker_class_property() {
230+
assert!(has("<?php class C { public $x = 'pv'; }", "pv"));
231+
}
232+
233+
#[test]
234+
fn walker_class_constant() {
235+
assert!(has("<?php class C { const K = 'ck'; }", "ck"));
236+
}
237+
238+
#[test]
239+
fn walker_interface_constant() {
240+
assert!(has("<?php interface I { const K = 'ik'; }", "ik"));
241+
}
242+
243+
#[test]
244+
fn walker_trait_method_body() {
245+
assert!(has("<?php trait T { function m() { return 'tm'; } }", "tm"));
246+
}
247+
248+
#[test]
249+
fn walker_enum_backed_case() {
250+
assert!(has("<?php enum S: string { case A = 'ev'; }", "ev"));
251+
}
252+
253+
#[test]
254+
fn walker_enum_constant() {
255+
assert!(has("<?php enum S { const K = 'ec'; }", "ec"));
256+
}
257+
258+
#[test]
259+
fn walker_static_var() {
260+
assert!(has("<?php function f() { static $x = 'sv'; }", "sv"));
261+
}
262+
263+
#[test]
264+
fn walker_unset_stmt() {
265+
// unset args are variable expressions; verify the walker reaches code after it
266+
assert!(has("<?php unset($x); return 'au';", "au"));
267+
}
268+
269+
// ── Expression branches ──────────────────────────────────────────────────────
270+
271+
#[test]
272+
fn walker_method_call() {
273+
assert!(has("<?php $o->m('ma');", "ma"));
274+
}
275+
276+
#[test]
277+
fn walker_null_safe_method_call() {
278+
assert!(has("<?php $o?->m('na');", "na"));
279+
}
280+
281+
#[test]
282+
fn walker_binary_expr() {
283+
let php = "<?php return 'lhs' . 'rhs';";
284+
assert!(has(php, "lhs"));
285+
assert!(has(php, "rhs"));
286+
}
287+
288+
#[test]
289+
fn walker_unary_prefix() {
290+
assert!(has("<?php $x = !true; return 'up';", "up"));
291+
}
292+
293+
#[test]
294+
fn walker_unary_postfix() {
295+
assert!(has("<?php $i = 0; $i++; return 'upo';", "upo"));
296+
}
297+
298+
#[test]
299+
fn walker_parenthesized() {
300+
assert!(has("<?php return ('pv');", "pv"));
301+
}
302+
303+
#[test]
304+
fn walker_assignment() {
305+
assert!(has("<?php $x = 'av';", "av"));
306+
}
307+
308+
#[test]
309+
fn walker_conditional_ternary() {
310+
let php = "<?php return true ? 'th' : 'el';";
311+
assert!(has(php, "th"));
312+
assert!(has(php, "el"));
313+
}
314+
315+
#[test]
316+
fn walker_conditional_elvis() {
317+
let php = "<?php return 'cv' ?: 'ev';";
318+
assert!(has(php, "cv"));
319+
assert!(has(php, "ev"));
320+
}
321+
322+
#[test]
323+
fn walker_array_literal() {
324+
let php = "<?php return ['v1', 'k' => 'v2'];";
325+
assert!(has(php, "v1"));
326+
assert!(has(php, "k"));
327+
assert!(has(php, "v2"));
328+
}
329+
330+
#[test]
331+
fn walker_legacy_array() {
332+
assert!(has("<?php return array('la1', 'la2');", "la1"));
333+
}
334+
335+
#[test]
336+
fn walker_variadic_array_element() {
337+
assert!(has("<?php return [...$a, 'vae'];", "vae"));
338+
}
339+
340+
#[test]
341+
fn walker_array_access() {
342+
assert!(has("<?php return $a['ki'];", "ki"));
343+
}
344+
345+
#[test]
346+
fn walker_closure_body() {
347+
assert!(has("<?php $f = function() { return 'clv'; };", "clv"));
348+
}
349+
350+
#[test]
351+
fn walker_arrow_function() {
352+
assert!(has("<?php $f = fn() => 'afv';", "afv"));
353+
}
354+
355+
#[test]
356+
fn walker_match_expr() {
357+
let php = "<?php return match ('ms') { 'ma' => 'mv', default => 'md' };";
358+
assert!(has(php, "ms"));
359+
assert!(has(php, "ma"));
360+
assert!(has(php, "mv"));
361+
assert!(has(php, "md"));
362+
}
363+
364+
#[test]
365+
fn walker_throw_expr() {
366+
assert!(has("<?php throw new \\Exception('te');", "te"));
367+
}
368+
369+
#[test]
370+
fn walker_yield_value() {
371+
assert!(has("<?php function g() { yield 'yv'; }", "yv"));
372+
}
373+
374+
#[test]
375+
fn walker_yield_pair() {
376+
let php = "<?php function g() { yield 'yk' => 'yp'; }";
377+
assert!(has(php, "yk"));
378+
assert!(has(php, "yp"));
379+
}
380+
381+
#[test]
382+
fn walker_yield_from() {
383+
assert!(has("<?php function g() { yield from ['yf']; }", "yf"));
384+
}
385+
386+
#[test]
387+
fn walker_clone() {
388+
assert!(has("<?php $b = clone $a; return 'after';", "after"));
389+
}
390+
391+
#[test]
392+
fn walker_instantiation_args() {
393+
assert!(has("<?php new Foo('ca');", "ca"));
394+
}
395+
396+
// ── ControlFlow early exit ───────────────────────────────────────────────────
397+
398+
#[test]
399+
fn walker_early_exit_stops_after_break() {
400+
let php = "<?php foo('a'); foo('b'); foo('c');";
401+
let mut visited: Vec<String> = Vec::new();
402+
walk_all_php_expressions(php, &mut |expr| {
403+
if let Some((s, _, _)) = extract_string_literal(expr, php) {
404+
visited.push(s.to_string());
405+
return ControlFlow::Break(());
406+
}
407+
ControlFlow::Continue(())
408+
});
409+
assert_eq!(visited, vec!["a".to_string()]);
410+
}

0 commit comments

Comments
 (0)