Skip to content

Commit c7cd659

Browse files
committed
feat: scan vendor framework config files as fallback
Scan vendor/laravel/framework/config/ for default config values when the project does not override them with its own config/ file. Project configs take precedence, then package service provider configs, then framework defaults. Also adds the same vendor scanning to config key enumeration so completion and diagnostics know about framework default keys.
1 parent a7ccd32 commit c7cd659

4 files changed

Lines changed: 107 additions & 2 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- **Config return type inference.** `config('database.default')`, `Config::get('app.name')`, and `$repository->get('mail.from')` now infer their return type from the project's `config/*.php` files. Scalar values resolve to their base type (`string`, `int`, `bool`), `env()` defaults resolve through their fallback argument, and nested arrays resolve to array shapes with typed keys. Parsed config trees are cached and invalidated when config files change. Contributed by @calebdw.
12+
- **Config return type inference.** `config('database.default')`, `Config::get('app.name')`, and `$repository->get('mail.from')` now infer their return type from the project's `config/*.php` files. Scalar values resolve to their base type (`string`, `int`, `bool`), `env()` defaults resolve through their fallback argument, and nested arrays resolve to array shapes with typed keys. Framework default configs from `vendor/laravel/framework/config/` are scanned as a fallback when the project does not override them. Parsed config trees are cached and invalidated when config files change. Contributed by @calebdw.
1313
- **Semantic token modes.** `.phpantom.toml` now supports `[semantic_tokens] mode = "contextual" | "full" | "off"`. The default `contextual` mode emits only context-sensitive highlighting that complements editor syntax grammars, while `full` keeps the previous broad semantic-token stream and `off` disables semantic tokens. Contributed by @calebdw.
1414
- **`@phpstan-ignore` identifiers are highlighted and completed.** PHPStan ignore comments now highlight the `@phpstan-ignore` tag and each listed error identifier in both docblocks and ordinary `//` comments. Identifier completion works inside the comma-separated ignore list, using PHPStan diagnostic codes already seen in the current file while staying out of per-code parenthesized reasons. Contributed by @calebdw.
1515
- **Parent member override completion.** Typing a method name after `function` in a class body (for example `protected function get`) now suggests public and protected methods from parent classes and interfaces that can still be overridden or implemented, inserting a full signature snippet. The same flow suggests parent properties after `$` (for example `protected $tit`) and parent constants after `const`. Snippets insert `#[\Override]` above methods on PHP 8.3+, properties on PHP 8.5+, and constants on PHP 8.6+ (from `composer.json` / `config.platform.php`). Private members and ones already defined on the class are omitted. Contributed by @calebdw.

src/completion/laravel_string_keys.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,30 @@ impl Backend {
324324
}
325325
}
326326

327+
if let Some(root) = self.workspace.workspace_root.read().clone() {
328+
let framework_config = root.join("vendor/laravel/framework/config");
329+
if framework_config.is_dir()
330+
&& let Ok(entries) = std::fs::read_dir(&framework_config)
331+
{
332+
for entry in entries.flatten() {
333+
let path = entry.path();
334+
if !path.extension().is_some_and(|e| e == "php") {
335+
continue;
336+
}
337+
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
338+
continue;
339+
};
340+
let prefix = stem.to_string();
341+
if let Ok(content) = std::fs::read_to_string(&path) {
342+
let decls = collect_laravel_config_declarations(&content, &prefix);
343+
for d in decls {
344+
keys.push(d.key);
345+
}
346+
}
347+
}
348+
}
349+
}
350+
327351
keys.sort();
328352
keys.dedup();
329353
keys

src/virtual_members/laravel/config_values.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,9 +478,11 @@ impl Backend {
478478
fn enumerate_config_trees(&self) -> Vec<(String, ConfigNode)> {
479479
use crate::virtual_members::laravel::laravel_config_prefix_from_uri;
480480

481-
let snapshot = self.user_file_symbol_maps();
482481
let mut trees = Vec::new();
482+
let mut seen_prefixes = std::collections::HashSet::new();
483483

484+
// 1. Project config files take highest precedence.
485+
let snapshot = self.user_file_symbol_maps();
484486
for (file_uri, _) in &snapshot {
485487
let Some(prefix) = laravel_config_prefix_from_uri(file_uri) else {
486488
continue;
@@ -489,18 +491,51 @@ impl Backend {
489491
continue;
490492
};
491493
if let Some(tree) = parse_config_tree(&content) {
494+
seen_prefixes.insert(prefix.clone());
492495
trees.push((prefix, tree));
493496
}
494497
}
495498

499+
// 2. Package config files from service providers.
496500
for res in &self.laravel_provider_resources.read().config_files {
501+
if seen_prefixes.contains(&res.namespace) {
502+
continue;
503+
}
497504
if let Ok(content) = std::fs::read_to_string(&res.path)
498505
&& let Some(tree) = parse_config_tree(&content)
499506
{
507+
seen_prefixes.insert(res.namespace.clone());
500508
trees.push((res.namespace.clone(), tree));
501509
}
502510
}
503511

512+
// 3. Laravel framework default configs from vendor.
513+
if let Some(root) = self.workspace.workspace_root.read().clone() {
514+
let framework_config = root.join("vendor/laravel/framework/config");
515+
if framework_config.is_dir()
516+
&& let Ok(entries) = std::fs::read_dir(&framework_config)
517+
{
518+
for entry in entries.flatten() {
519+
let path = entry.path();
520+
if !path.extension().is_some_and(|e| e == "php") {
521+
continue;
522+
}
523+
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
524+
continue;
525+
};
526+
let prefix = stem.to_string();
527+
if seen_prefixes.contains(&prefix) {
528+
continue;
529+
}
530+
if let Ok(content) = std::fs::read_to_string(&path)
531+
&& let Some(tree) = parse_config_tree(&content)
532+
{
533+
trees.push((prefix, tree));
534+
}
535+
}
536+
}
537+
}
538+
504539
trees
505540
}
506541
}

tests/integration/hover.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,52 @@ function test(): void {
248248
);
249249
}
250250

251+
#[test]
252+
fn hover_config_vendor_framework_fallback() {
253+
let (backend, _dir) = create_psr4_workspace(
254+
r#"{ "autoload": { "psr-4": { "App\\": "src/" } } }"#,
255+
&[
256+
(
257+
"config/app.php",
258+
"<?php\nreturn [\n 'name' => env('APP_NAME', 'Laravel'),\n];\n",
259+
),
260+
(
261+
"vendor/laravel/framework/config/app.php",
262+
"<?php\nreturn [\n 'name' => env('APP_NAME', 'Laravel'),\n 'faker_locale' => 'en_US',\n 'timezone' => 'UTC',\n];\n",
263+
),
264+
(
265+
"vendor/laravel/framework/config/cache.php",
266+
"<?php\nreturn [\n 'default' => env('CACHE_STORE', 'database'),\n 'prefix' => env('CACHE_PREFIX', ''),\n];\n",
267+
),
268+
],
269+
);
270+
271+
let uri = "file:///test_vendor_config.php";
272+
let content = r#"<?php
273+
function test(): void {
274+
$locale = config('app.faker_locale');
275+
$locale;
276+
$cache = config('cache.default');
277+
$cache;
278+
}
279+
"#;
280+
backend.update_ast(uri, content);
281+
282+
let h = hover_at(&backend, uri, content, 3, 6).expect("hover on $locale");
283+
let text = hover_text(&h);
284+
assert!(
285+
text.contains("string"),
286+
"vendor framework config should provide app.faker_locale as string: {text}"
287+
);
288+
289+
let h = hover_at(&backend, uri, content, 5, 6).expect("hover on $cache");
290+
let text = hover_text(&h);
291+
assert!(
292+
text.contains("string"),
293+
"vendor framework config should provide cache.default as string: {text}"
294+
);
295+
}
296+
251297
#[test]
252298
fn hover_variable_without_type() {
253299
let backend = create_test_backend();

0 commit comments

Comments
 (0)