Skip to content

Commit e8ce13c

Browse files
committed
Fix race condition in laravel config parsing
1 parent 40c8c50 commit e8ce13c

2 files changed

Lines changed: 35 additions & 15 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. 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.
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/` fill in any keys the project's own config file leaves unset, so a partially published `config/app.php` still resolves the framework defaults it does not override. 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/virtual_members/laravel/config_values.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,27 @@ impl ConfigNode {
232232
ConfigNode::Array(_) => None,
233233
}
234234
}
235+
236+
/// Fill in keys present in `defaults` but absent here, recursing into
237+
/// nested arrays. Keys already present keep their higher-precedence
238+
/// value, so a project config file wins over the framework defaults it
239+
/// only partially overrides. A leaf on either side is authoritative: an
240+
/// existing leaf is never replaced by a default array, and a default leaf
241+
/// never merges into an existing array.
242+
fn merge_defaults(&mut self, defaults: ConfigNode) {
243+
let ConfigNode::Array(target) = self else {
244+
return;
245+
};
246+
let ConfigNode::Array(source) = defaults else {
247+
return;
248+
};
249+
for (key, default_child) in source {
250+
match target.iter_mut().find(|(k, _)| *k == key) {
251+
Some((_, existing)) => existing.merge_defaults(default_child),
252+
None => target.push((key, default_child)),
253+
}
254+
}
255+
}
235256
}
236257

237258
/// Parse a `config/*.php` file's returned array into an owned [`ConfigNode`].
@@ -478,8 +499,16 @@ impl Backend {
478499
fn enumerate_config_trees(&self) -> Vec<(String, ConfigNode)> {
479500
use crate::virtual_members::laravel::laravel_config_prefix_from_uri;
480501

481-
let mut trees = Vec::new();
482-
let mut seen_prefixes = std::collections::HashSet::new();
502+
// Lower-precedence sources only fill keys the higher-precedence tree
503+
// for the same prefix leaves unset, so a project `config/app.php` that
504+
// publishes just a handful of keys still inherits the framework
505+
// defaults for everything it does not override.
506+
let mut trees: Vec<(String, ConfigNode)> = Vec::new();
507+
let mut merge =
508+
|prefix: String, tree: ConfigNode| match trees.iter_mut().find(|(p, _)| *p == prefix) {
509+
Some((_, existing)) => existing.merge_defaults(tree),
510+
None => trees.push((prefix, tree)),
511+
};
483512

484513
// 1. Project config files take highest precedence.
485514
let snapshot = self.user_file_symbol_maps();
@@ -491,21 +520,16 @@ impl Backend {
491520
continue;
492521
};
493522
if let Some(tree) = parse_config_tree(&content) {
494-
seen_prefixes.insert(prefix.clone());
495-
trees.push((prefix, tree));
523+
merge(prefix, tree);
496524
}
497525
}
498526

499527
// 2. Package config files from service providers.
500528
for res in &self.laravel_provider_resources.read().config_files {
501-
if seen_prefixes.contains(&res.namespace) {
502-
continue;
503-
}
504529
if let Ok(content) = std::fs::read_to_string(&res.path)
505530
&& let Some(tree) = parse_config_tree(&content)
506531
{
507-
seen_prefixes.insert(res.namespace.clone());
508-
trees.push((res.namespace.clone(), tree));
532+
merge(res.namespace.clone(), tree);
509533
}
510534
}
511535

@@ -523,14 +547,10 @@ impl Backend {
523547
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
524548
continue;
525549
};
526-
let prefix = stem.to_string();
527-
if seen_prefixes.contains(&prefix) {
528-
continue;
529-
}
530550
if let Ok(content) = std::fs::read_to_string(&path)
531551
&& let Some(tree) = parse_config_tree(&content)
532552
{
533-
trees.push((prefix, tree));
553+
merge(stem.to_string(), tree);
534554
}
535555
}
536556
}

0 commit comments

Comments
 (0)