Skip to content

Commit 852aa6b

Browse files
committed
fix: resolve conditional return string branch for interpolated strings
The AST-based conditional return resolver only matched Expression::Literal(Literal::String) when checking `$key is string`, so interpolated strings like `"{$prefix}.host"` (parsed as Expression::CompositeString) fell through to the else branch and resolved as null. Add CompositeString to the string match so the correct branch is taken. Closes #269
1 parent 86a35b9 commit 852aa6b

3 files changed

Lines changed: 40 additions & 2 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4545
### Fixed
4646

4747
- **Completion works after closing a multi-line closure argument on the same line as the next chain operator.** Typing `->` immediately after a call like `->map(function (...) { ... })->` now resolves the full receiver instead of returning no member suggestions until the operator is moved to a new line. Contributed by @calebdw.
48+
- **Conditional return types recognize interpolated strings as strings.** A function with a conditional return type like `($key is string ? mixed : null)` now correctly resolves the `string` branch when called with an interpolated string argument (e.g. `config("{$prefix}.host")`). Previously the interpolated string was not recognized as a string literal, causing the return type to fall through to the else branch and resolve as `null`. Contributed by @calebdw.
4849
- **Deprecated enum cases are recognized.** Enum cases annotated with `@deprecated` or `#[Deprecated]` now carry deprecation metadata, so usages like `self::Low` can be highlighted as deprecated in contextual semantic-token mode. Contributed by @calebdw.
4950
- **Class constant accesses use constant semantic highlighting.** `self::CONSTANT`, `static::CONSTANT`, `parent::CONSTANT`, and `ClassName::CONSTANT` now emit the `enumMember` semantic token instead of being colored as properties, while `ClassName::$property` still emits `property`. Contributed by @calebdw.
5051
- **PHP attributes use decorator semantic highlighting.** Attribute class names in `#[...]` now emit the `decorator` semantic token instead of `class`, so editor themes can color attributes differently from normal class references. Contributed by @calebdw.

src/type_engine/types/conditional.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1435,7 +1435,13 @@ pub fn resolve_conditional_with_args_and_defaults<'b>(
14351435
} else {
14361436
// IsType equivalent: check scalar types from AST literals.
14371437
if condition_is_scalar_type(condition.as_ref(), "string")
1438-
&& matches!(arg_expr, Some(Expression::Literal(Literal::String(_))))
1438+
&& matches!(
1439+
arg_expr,
1440+
Some(
1441+
Expression::Literal(Literal::String(_))
1442+
| Expression::CompositeString(_)
1443+
)
1444+
)
14391445
{
14401446
let take_then = !*negated;
14411447
return resolve_conditional_with_args_and_defaults(

tests/integration/hover.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Integration tests for `textDocument/hover`.
22
33
use crate::common::{
4-
create_psr4_workspace, create_test_backend, create_test_backend_with_closure_stub,
4+
create_psr4_workspace create_test_backend, create_test_backend_with_closure_stub,
55
create_test_backend_with_full_stubs, create_test_backend_with_function_stubs,
66
create_test_backend_with_stdclass_stub,
77
};
@@ -154,6 +154,37 @@ class Service {
154154
assert!(text.contains("Order"), "should resolve to Order: {}", text);
155155
}
156156

157+
#[test]
158+
fn hover_conditional_return_with_interpolated_string_resolves_string_branch() {
159+
let backend = create_test_backend();
160+
let uri = "file:///test.php";
161+
let content = r#"<?php
162+
/**
163+
* @param string|null $key
164+
* @return ($key is null ? \stdClass : ($key is string ? mixed : null))
165+
*/
166+
function config($key = null, $default = null) {}
167+
168+
function test(string $cache): void {
169+
$redisHost = config("{$cache}.host", 'localhost');
170+
171+
if ($redisHost === null) {
172+
return;
173+
}
174+
175+
$redisHost;
176+
}
177+
"#;
178+
179+
let hover = hover_at(&backend, uri, content, 14, 6).expect("expected hover on $redisHost");
180+
let text = hover_text(&hover);
181+
assert!(
182+
text.contains("mixed"),
183+
"interpolated string arg should resolve `$key is string` to the `mixed` branch, \
184+
not fall through to `null`: {text}"
185+
);
186+
}
187+
157188
#[test]
158189
fn hover_variable_without_type() {
159190
let backend = create_test_backend();

0 commit comments

Comments
 (0)