Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 33 additions & 30 deletions docs/CHANGELOG.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions examples/php/completion.php
Original file line number Diff line number Diff line change
Expand Up @@ -4925,6 +4925,27 @@ public function demo(): void
$this->getDefaultDriver(); // resolves Router::getDefaultDriver()
});

// A union binding offers members from both possible contexts.
$router->eachContext(function () {
// Try: $this-> offers Route::prefix() and Resource::only().
if ($this instanceof Scaffolding\ScaffoldingClosureThisRoute) {
$this->prefix('/union'); // narrowed to Route
} else {
$this->only('index'); // narrowed to Resource
}
});

$router->eachContext(function () {
// Try: self::next()-> offers members from both contexts.
self::next()->withContext(function () {
// The nested callback keeps both possible bindings too.
if ($this instanceof Scaffolding\ScaffoldingClosureThisRoute) {
return;
}
$this->only('index'); // early return leaves only Resource
});
});

// The tag names the base class, so an assertion is how a closure
// body says which subclass it was actually bound to. Narrowing
// refines the tag rather than being overruled by it.
Expand Down
29 changes: 29 additions & 0 deletions examples/php/scaffolding/assertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,35 @@ function runDemoAssertions(): void
$ctExt = $ctRouter->extend('redis', function () {});
assert($ctExt instanceof Scaffolding\ScaffoldingClosureThisRouter, 'Router::extend() must return self');

$ctContexts = [];
$ctRouter->eachContext(function () use (&$ctContexts) {
if ($this instanceof Scaffolding\ScaffoldingClosureThisRoute) {
$ctContexts[] = $this->prefix('/union');
} else {
$ctContexts[] = $this->only('index');
}
});
assert(count($ctContexts) === 2, 'eachContext() must invoke both union alternatives');
assert($ctContexts[0] instanceof Scaffolding\ScaffoldingClosureThisRoute);
assert($ctContexts[1] instanceof Scaffolding\ScaffoldingClosureThisResource);

$ctChainedContexts = [];
$ctRemainingContexts = [];
$ctRouter->eachContext(function () use (&$ctChainedContexts, &$ctRemainingContexts) {
$ctChainedContexts[] = self::next();
self::next()->withContext(function () use (&$ctRemainingContexts) {
if ($this instanceof Scaffolding\ScaffoldingClosureThisRoute) {
return;
}
$ctRemainingContexts[] = $this->only('index');
});
});
assert(count($ctChainedContexts) === 2);
assert($ctChainedContexts[0] instanceof Scaffolding\ScaffoldingClosureThisRoute);
assert($ctChainedContexts[1] instanceof Scaffolding\ScaffoldingClosureThisResource);
assert(count($ctRemainingContexts) === 1, 'early return must exclude the Route binding');
assert($ctRemainingContexts[0] instanceof Scaffolding\ScaffoldingClosureThisResource);

// Nested @param-closure-this: the innermost binding is the one in
// effect, and the inner call's receiver is the outer binding.
$ctInner = null;
Expand Down
25 changes: 25 additions & 0 deletions examples/php/scaffolding/scaffolding.php
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,14 @@ public function through(array $pipes): static { return $this; }
// binds the callback with Closure::call() so the runtime matches the tag.
class ScaffoldingClosureThisRoute
{
public static function next(): self { return new self(); }

/** @param-closure-this self $callback */
public function withContext(\Closure $callback): void
{
$callback->call($this);
}

public function middleware(string $m): self { return $this; }
public function prefix(string $p): self { return $this; }

Expand All @@ -962,6 +970,14 @@ public function resource(string $name, \Closure $callback): void

class ScaffoldingClosureThisResource
{
public static function next(): self { return new self(); }

/** @param-closure-this self $callback */
public function withContext(\Closure $callback): void
{
$callback->call($this);
}

public function only(string $action): self { return $this; }
}

Expand All @@ -984,6 +1000,15 @@ public function group(\Closure $callback): void
$callback->call(new ScaffoldingClosureThisRoute());
}

/**
* @param-closure-this ScaffoldingClosureThisRoute|ScaffoldingClosureThisResource $callback
*/
public function eachContext(\Closure $callback): void
{
$callback->call(new ScaffoldingClosureThisRoute());
$callback->call(new ScaffoldingClosureThisResource());
}

/**
* Declares the base class but binds a subclass, the way Pest's `test()`
* declares `PHPUnit\Framework\TestCase` and binds whatever
Expand Down
42 changes: 23 additions & 19 deletions src/definition/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,29 @@ impl Backend {
.map(|loc| vec![loc])
}

SymbolKind::SelfStaticParent(ssp_kind) => self
.resolve_self_static_parent(uri, content, position, *ssp_kind)
.map(|loc| vec![loc]),
SymbolKind::SelfStaticParent(ssp_kind) => {
if *ssp_kind == SelfStaticParentKind::This
&& let Some(classes) =
self.resolve_closure_this_override(uri, content, cursor_offset)
{
return Some(
classes
.iter()
.filter_map(|class| {
self.resolve_class_reference(
uri,
content,
&class.fqn(),
true,
cursor_offset,
)
})
.collect(),
);
}
self.resolve_self_static_parent(uri, content, position, *ssp_kind)
.map(|loc| vec![loc])
}

SymbolKind::ClassReference { name, is_fqn, .. } => self
.resolve_class_reference(uri, content, name, *is_fqn, cursor_offset)
Expand Down Expand Up @@ -1169,22 +1189,6 @@ impl Backend {
ssp_kind,
SelfStaticParentKind::Self_ | SelfStaticParentKind::Static | SelfStaticParentKind::This
) {
// For `$this`, check `@param-closure-this` override first:
// when the cursor is inside a closure whose enclosing call
// site declares `@param-closure-this`, jump to the
// overridden class definition instead of the lexical class.
if ssp_kind == SelfStaticParentKind::This
&& let Some(override_cls) =
self.resolve_closure_this_override(uri, content, cursor_offset)
{
let fqn = override_cls.fqn();
if let Some(loc) =
self.resolve_class_reference(uri, content, &fqn, true, cursor_offset)
{
return Some(loc);
}
}

// Jump to the enclosing class definition in the current file.
if current_class.keyword_offset == 0 {
return None;
Expand Down
7 changes: 5 additions & 2 deletions src/definition/type_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,13 @@ impl Backend {
.map(|cc| vec![PhpType::named(atom(cc.name.as_ref()))])
.unwrap_or_default(),
SelfStaticParentKind::This => {
if let Some(override_cls) =
if let Some(override_classes) =
self.resolve_closure_this_override(uri, content, offset)
{
vec![PhpType::named(override_cls.fqn())]
override_classes
.into_iter()
.map(|class| PhpType::named(class.fqn()))
.collect()
} else {
current_class
.map(|cc| vec![PhpType::named(atom(cc.name.as_ref()))])
Expand Down
23 changes: 20 additions & 3 deletions src/hover/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,26 @@ impl Backend {
SelfStaticParentKind::Self_ | SelfStaticParentKind::Static => {
current_class.cloned()
}
SelfStaticParentKind::This => self
.resolve_closure_this_override(uri, content, cursor_offset)
.or_else(|| current_class.cloned()),
SelfStaticParentKind::This => {
if let Some(classes) =
self.resolve_closure_this_override(uri, content, cursor_offset)
{
if classes.len() > 1 {
let ty = PhpType::union(
classes
.iter()
.map(|class| PhpType::named(class.fqn()))
.collect(),
);
return Some(make_hover(format!(
"```php\n<?php\n$this = {ty}\n```"
)));
}
classes.into_iter().next().map(Arc::unwrap_or_clone)
} else {
current_class.cloned()
}
}
SelfStaticParentKind::Parent => current_class
.and_then(|cc| cc.parent_class.as_ref())
.and_then(|parent_name| {
Expand Down
8 changes: 4 additions & 4 deletions src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,7 +1488,7 @@ impl Backend {

/// Check whether `cursor_offset` is inside a closure whose
/// enclosing call site declares `@param-closure-this`, and if so
/// return the overridden class.
/// return the overridden class alternatives.
///
/// This is a convenience wrapper that builds the [`ResolutionCtx`]
/// and calls [`find_closure_this_override`] so that callers (hover,
Expand All @@ -1499,7 +1499,7 @@ impl Backend {
uri: &str,
content: &str,
cursor_offset: u32,
) -> Option<ClassInfo> {
) -> Option<Vec<Arc<ClassInfo>>> {
use crate::class_lookup::find_class_at_offset;
use crate::type_engine::resolver::ResolutionCtx;

Expand All @@ -1510,13 +1510,13 @@ impl Backend {
let target = self.facade_macro_concrete(&target).unwrap_or(target);
if let Some(class) = self.find_or_load_class(&target) {
let class_loader = self.class_loader(&ctx);
return Some(Arc::unwrap_or_clone(
return Some(vec![
crate::virtual_members::resolve_class_fully_maybe_cached(
&class,
&class_loader,
Some(&self.resolved_class_cache),
),
));
]);
}
}
let current_class = find_class_at_offset(&ctx.classes, cursor_offset);
Expand Down
48 changes: 43 additions & 5 deletions src/type_engine/call_resolution/callable_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,16 +259,54 @@ impl Backend {
/// Resolve a static class reference + method name to a
/// [`ResolvedCallableTarget`].
///
/// Resolves the class via [`crate::type_engine::resolver::resolve_static_owner_class`], merges
/// Resolves the class via [`crate::type_engine::resolver::resolve_static_owner_classes`], merges
/// via `resolve_class_fully`, and looks up `method_name`.
fn resolve_static_method_callable(
class: &str,
method_name: &str,
rctx: &ResolutionCtx<'_>,
args_text: Option<&str>,
) -> Option<ResolvedCallableTarget> {
let owner = crate::type_engine::resolver::resolve_static_owner_class(class, rctx)?;
let owners = crate::type_engine::resolver::resolve_static_owner_classes(class, rctx);
let mut result: Option<ResolvedCallableTarget> = None;
let mut returns = Vec::new();
for owner in &owners {
if let Some(target) = Self::resolve_static_method_callable_for_owner(
class,
method_name,
owner,
rctx,
args_text,
) {
if owners.len() > 1
&& let Some(ret) = &target.return_type
{
returns.push(ret.replace_self(&owner.fqn()));
}
if result.is_none() {
result = Some(target);
}
}
}
if let Some(target) = &mut result
&& !returns.is_empty()
{
target.return_type = if returns.len() == 1 {
returns.pop()
} else {
Some(PhpType::union(returns))
};
}
result
}

fn resolve_static_method_callable_for_owner(
class: &str,
method_name: &str,
owner: &Arc<ClassInfo>,
rctx: &ResolutionCtx<'_>,
args_text: Option<&str>,
) -> Option<ResolvedCallableTarget> {
// When the class has template params, try to substitute them with
// concrete types. For `parent::` calls, use the child's @extends
// generics to get the concrete type arguments. Otherwise fall back
Expand All @@ -287,16 +325,16 @@ impl Backend {
} else {
None
};
let args = type_args.unwrap_or_else(|| crate::inheritance::default_type_args(&owner));
let args = type_args.unwrap_or_else(|| crate::inheritance::default_type_args(owner));
crate::virtual_members::resolve_class_fully_with_type_args(
&owner,
owner,
rctx.class_loader,
rctx.resolved_class_cache,
&args,
)
} else {
crate::virtual_members::resolve_class_fully_maybe_cached(
&owner,
owner,
rctx.class_loader,
rctx.resolved_class_cache,
)
Expand Down
Loading