Skip to content

Commit a7ccd32

Browse files
committed
feat: infer config() return types from project config files
Parse Laravel config/*.php files into typed trees and use them to infer return types for config(), Config::get(), and Repository::get() calls with static string keys. Scalar values widen to their base type (string, int, bool), env() defaults resolve through the fallback argument, and nested arrays become array shapes. The parsed trees are cached alongside config keys and invalidated when config files change, so no filesystem access happens on the hot completion/hover path.
1 parent 2438e0e commit a7ccd32

14 files changed

Lines changed: 319 additions & 2 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +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.
1213
- **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.
1314
- **`@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.
1415
- **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/analyse/run.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,11 @@ pub async fn run(options: AnalyseOptions) -> i32 {
305305
let class_loader = backend.class_loader(&file_ctx);
306306
let function_loader_cl = backend.function_loader(&file_ctx);
307307
let constant_loader_cl = backend.constant_loader();
308+
let config_resolver = |key: &str| backend.resolve_config_type(key);
308309
let loaders = crate::type_engine::resolver::Loaders {
309310
function_loader: Some(&function_loader_cl),
310311
constant_loader: Some(&constant_loader_cl),
312+
config_resolver: Some(&config_resolver),
311313
};
312314
crate::type_engine::variable::forward_walk::build_diagnostic_scopes(
313315
content,

src/code_actions/extract_function/scope.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ pub(crate) fn resolve_var_type(
2828
let class_loader = backend.class_loader(&ctx);
2929
let function_loader = backend.function_loader(&ctx);
3030
let constant_loader = backend.constant_loader();
31+
let config_resolver = |key: &str| backend.resolve_config_type(key);
3132
let loaders = Loaders {
3233
function_loader: Some(
3334
&function_loader as &dyn Fn(&str, u32) -> Option<crate::types::FunctionInfo>,
3435
),
3536
constant_loader: Some(&constant_loader),
37+
config_resolver: Some(&config_resolver),
3638
};
3739

3840
let current_class = find_class_at_offset(&ctx.classes, cursor_offset);

src/diagnostics/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,11 @@ impl Backend {
289289
let class_loader = self.class_loader(&ctx.file);
290290
let function_loader_cl = self.function_loader(&ctx.file);
291291
let constant_loader_cl = self.constant_loader();
292+
let config_resolver = |key: &str| self.resolve_config_type(key);
292293
let loaders = crate::type_engine::resolver::Loaders {
293294
function_loader: Some(&function_loader_cl),
294295
constant_loader: Some(&constant_loader_cl),
296+
config_resolver: Some(&config_resolver),
295297
};
296298
crate::type_engine::variable::forward_walk::build_diagnostic_scopes(
297299
content,

src/diagnostics/property_type_errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,11 @@ fn check_expression_for_property_assignment(expr: &Expression<'_>, ctx: &mut Pro
344344
let rhs_start = rhs_span.start.offset as usize;
345345
let rhs_end = rhs_span.end.offset as usize;
346346

347+
let config_resolver = |key: &str| ctx.backend.resolve_config_type(key);
347348
let loaders = Loaders {
348349
function_loader: Some(ctx.function_loader),
349350
constant_loader: Some(ctx.constant_loader),
351+
config_resolver: Some(&config_resolver),
350352
};
351353

352354
let var_ctx = VarResolutionCtx {

src/diagnostics/return_type_errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,11 @@ fn process_top_level_statement(
477477
let enclosing = find_innermost_enclosing_class(&file_ctx.classes, func_offset);
478478
let current_class = enclosing.unwrap_or(default_class);
479479

480+
let config_resolver = |key: &str| backend.resolve_config_type(key);
480481
let loaders = Loaders {
481482
function_loader: Some(function_loader),
482483
constant_loader: Some(constant_loader),
484+
config_resolver: Some(&config_resolver),
483485
};
484486

485487
for (maybe_expr, start, end) in returns {
@@ -613,9 +615,11 @@ fn process_class_member(
613615
}
614616
});
615617

618+
let config_resolver = |key: &str| backend.resolve_config_type(key);
616619
let loaders = Loaders {
617620
function_loader: Some(function_loader),
618621
constant_loader: Some(constant_loader),
622+
config_resolver: Some(&config_resolver),
619623
};
620624

621625
for (maybe_expr, start, end) in returns {

src/diagnostics/type_errors/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,11 @@ impl Backend {
311311
let enclosing = find_innermost_enclosing_class(&file_ctx.classes, *args_start);
312312
let current_class_info = enclosing.unwrap_or(&default_class);
313313

314+
let config_resolver = |key: &str| self.resolve_config_type(key);
314315
let loaders = Loaders {
315316
function_loader: Some(&function_loader_cl),
316317
constant_loader: Some(&constant_loader_cl),
318+
config_resolver: Some(&config_resolver),
317319
};
318320

319321
let var_ctx = VarResolutionCtx {

src/hover/variable.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,11 @@ impl Backend {
7070
let class_loader = self.class_loader(ctx);
7171
let function_loader = self.function_loader(ctx);
7272
let constant_loader = self.constant_loader();
73+
let config_resolver = |key: &str| self.resolve_config_type(key);
7374
let loaders = crate::type_engine::resolver::Loaders {
7475
function_loader: Some(&function_loader as &dyn Fn(&str, u32) -> Option<FunctionInfo>),
7576
constant_loader: Some(&constant_loader),
77+
config_resolver: Some(&config_resolver),
7678
};
7779

7880
// Try the type-string path first. This preserves generic

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ pub(crate) struct LaravelStringKeyCache {
314314
pub config_keys: Option<Vec<String>>,
315315
pub view_names: Option<Vec<String>>,
316316
pub trans_keys: Option<Vec<String>>,
317+
pub config_trees: Option<
318+
Vec<(
319+
String,
320+
crate::virtual_members::laravel::config_values::ConfigNode,
321+
)>,
322+
>,
317323
}
318324

319325
impl LaravelStringKeyCache {
@@ -323,6 +329,7 @@ impl LaravelStringKeyCache {
323329
}
324330
if uri.contains("/config/") {
325331
self.config_keys = None;
332+
self.config_trees = None;
326333
}
327334
// View roots are configurable via `config/view.php`, so a Blade
328335
// file may live outside `resources/views/`. Invalidate on any

src/server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,6 +2414,7 @@ impl Backend {
24142414
if config_count + view_count + trans_count + route_count > 0 {
24152415
let mut cache = self.laravel_string_key_cache.write();
24162416
cache.config_keys = None;
2417+
cache.config_trees = None;
24172418
cache.view_names = None;
24182419
cache.trans_keys = None;
24192420
cache.route_names = None;

0 commit comments

Comments
 (0)