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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

#### Type inference

- **`self` parameters in traits.** A parameter typed `self` inside a trait now sees the class its `@phpstan-require-extends` or `-implements` names, as `$this` already did, so a model trait's `static::saving(static function (self $model) { $model->getTable(); })` no longer reports `getTable()` as unknown.
- **Constants passed to calls.** Namespaced and imported constants keep their values and types when used as arguments. Contributed by @shuvroroy.
- **Constants defined from namespaced or imported constants.** A class constant or namespaced constant such as `const MAX = LIMIT;` now holds the value of the constant its own file means, whether imported with `use const`, declared in its namespace, or global, when it is read from another file. A parameter default that names a constant is read the same way when the argument is left out.
- **Constants holding a class name or an enum case.** A global or namespaced constant such as `const DEFAULT = Widget::class;` or `const SUIT = Suit::Hearts;` now has that type, including when it is passed to a templated function or used as a parameter default.
Expand Down
23 changes: 22 additions & 1 deletion src/type_engine/variable/forward_walk/param_seeding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use mago_span::HasSpan;
use crate::atom::bytes_to_str;
use crate::parser::extract_hint_type;
use crate::php_type::PhpType;
use crate::types::{MethodInfo, ResolvedType};
use crate::types::{ClassInfo, MethodInfo, ResolvedType};

/// What seeding a function-like body needs to know about its declaration.
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -794,12 +794,33 @@ pub(crate) fn declared_param_type(
}
}

// A `self` parameter in a trait holds a class using the trait, so it gets `$this`'s bounds.
if is_the_current_trait(&param_results, ctx.current_class) {
crate::type_engine::trait_context::extend_this_with_trait_bounds(
&mut param_results,
ctx.current_class,
ctx.file.all_classes,
ctx.file.class_loader,
ctx.file.backend,
);
}

DeclaredParam {
effective: effective_type,
results: param_results,
}
}

/// Whether a parameter's resolved type is exactly the trait being analysed,
/// which is what a `self` hint inside a trait resolves to.
fn is_the_current_trait(param_results: &[ResolvedType], current_class: &ClassInfo) -> bool {
current_class.kind == crate::types::ClassLikeKind::Trait
&& matches!(
param_results,
[only] if only.class_info.as_ref().is_some_and(|ci| ci.fqn() == current_class.fqn())
)
}

/// Turn a variadic parameter's element types into the array it receives.
///
/// Named arguments land in it under their names, so its keys are
Expand Down
46 changes: 46 additions & 0 deletions tests/integration/diagnostics_unknown_members.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8313,6 +8313,52 @@ trait MyTrait {
);
}

/// A parameter typed `self` inside a trait is an instance of a class using
/// the trait, so it sees the `@phpstan-require-extends` class's members the
/// way `$this` does, in a method and in a closure (Laravel model events).
#[test]
fn self_typed_parameter_inside_trait_sees_required_class() {
let backend = create_test_backend();
let uri = "file:///test.php";
let text = r#"<?php
abstract class Model {
public function getTable(): string { return ''; }
public static function saving(\Closure $callback): void {}
}

/** @phpstan-require-extends Model */
trait Stamps {
public static function bootStamps(): void {
static::saving(static function (self $model): void {
$model->getTable();
$model->notOnModel();
});
}

public static function viaParameter(self $model): string {
return $model->getTable();
}
}

class Post extends Model {
use Stamps;
}
"#;
let diags = unknown_member_diagnostics(&backend, uri, text);
let messages: Vec<String> = diags
.iter()
.map(|d| message_text(&d.message).to_string())
.collect();
assert!(
!messages.iter().any(|m| m.contains("getTable")),
"getTable() comes from the required class, got: {messages:?}"
);
assert!(
messages.iter().any(|m| m.contains("notOnModel")),
"a member neither the trait nor the required class has must still be flagged, got: {messages:?}"
);
}

/// `$this->` and `static::` inside a closure nested within a trait
/// method should be suppressed just like direct trait method bodies.
#[test]
Expand Down