diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 4625810998..df9b8abd66 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -264,6 +264,10 @@ jobs: cd e2e/bug-11857 composer install ../../bin/phpstan + - script: | + cd e2e/bug-12585 + composer install + ../../bin/phpstan - script: | cd e2e/result-cache-meta-extension composer install diff --git a/e2e/bug-12585/.gitignore b/e2e/bug-12585/.gitignore new file mode 100644 index 0000000000..8b7ef35032 --- /dev/null +++ b/e2e/bug-12585/.gitignore @@ -0,0 +1,2 @@ +/vendor +composer.lock diff --git a/e2e/bug-12585/composer.json b/e2e/bug-12585/composer.json new file mode 100644 index 0000000000..a072011fe8 --- /dev/null +++ b/e2e/bug-12585/composer.json @@ -0,0 +1,5 @@ +{ + "autoload-dev": { + "classmap": ["src/"] + } +} diff --git a/e2e/bug-12585/phpstan.neon b/e2e/bug-12585/phpstan.neon new file mode 100644 index 0000000000..a463d65106 --- /dev/null +++ b/e2e/bug-12585/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + level: 8 + paths: + - src + +services: + - + class: Bug12585\EloquentBuilderRelationParameterExtension + tags: + - phpstan.dynamicMethodParameterTypeExtension diff --git a/e2e/bug-12585/src/extension.php b/e2e/bug-12585/src/extension.php new file mode 100644 index 0000000000..285e732b1a --- /dev/null +++ b/e2e/bug-12585/src/extension.php @@ -0,0 +1,255 @@ + */ + private array $methods = ['whereHas', 'withWhereHas']; + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + if (! $methodReflection->getDeclaringClass()->is(Builder::class)) { + return false; + } + + if (! in_array($methodReflection->getName(), $this->methods, strict: true)) { + return false; + } + + return $parameter->getName() === 'callback'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): Type|null + { + $method = $methodReflection->getName(); + $relations = $this->getRelationsFromMethodCall($methodCall, $scope); + $models = $this->getModelsFromRelations($relations); + + if (count($models) === 0) { + return null; + } + + $type = $this->getBuilderTypeForModels($models); + + if ($method === 'withWhereHas') { + $type = TypeCombinator::union($type, ...$relations); + } + + return new ClosureType([new ClosureQueryParameter('query', $type)], new MixedType(), false); + } + + /** + * @param array $relations + * @return array + */ + private function getModelsFromRelations(array $relations): array + { + $models = []; + + foreach ($relations as $relation) { + $classNames = $relation->getTemplateType(Relation::class, 'TRelatedModel')->getObjectClassNames(); + foreach ($classNames as $className) { + $models[] = $className; + } + } + + return $models; + } + + /** @return array */ + private function getRelationsFromMethodCall(MethodCall $methodCall, Scope $scope): array + { + $relationType = null; + + foreach ($methodCall->args as $arg) { + if ($arg instanceof VariadicPlaceholder) { + continue; + } + + if ($arg->name === null || $arg->name->toString() === 'relation') { + $relationType = $scope->getType($arg->value); + break; + } + } + + if ($relationType === null) { + return []; + } + + $calledOnModels = $scope->getType($methodCall->var) + ->getTemplateType(Builder::class, 'TModel') + ->getObjectClassNames(); + + $values = array_map(fn ($type) => $type->getValue(), $relationType->getConstantStrings()); + $relationTypes = [$relationType]; + + foreach ($values as $relation) { + $relationTypes = array_merge( + $relationTypes, + $this->getRelationTypeFromString($calledOnModels, explode('.', $relation), $scope) + ); + } + + return array_values(array_filter( + $relationTypes, + static fn ($r) => (new ObjectType(Relation::class))->isSuperTypeOf($r)->yes() + )); + } + + /** + * @param list $calledOnModels + * @param list $relationParts + * @return list + */ + private function getRelationTypeFromString(array $calledOnModels, array $relationParts, Scope $scope): array + { + $relations = []; + + while ($relationName = array_shift($relationParts)) { + $relations = []; + $relatedModels = []; + + foreach ($calledOnModels as $model) { + $modelType = new ObjectType($model); + + if (! $modelType->hasMethod($relationName)->yes()) { + continue; + } + + $relationType = $modelType->getMethod($relationName, $scope)->getVariants()[0]->getReturnType(); + + if (! (new ObjectType(Relation::class))->isSuperTypeOf($relationType)->yes()) { + continue; + } + + $relations[] = $relationType; + + array_push($relatedModels, ...$relationType->getTemplateType(Relation::class, 'TRelatedModel')->getObjectClassNames()); + } + + $calledOnModels = $relatedModels; + } + + return $relations; + } + + private function determineBuilderName(string $modelClassName): string + { + $method = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('query'); + + $returnType = $method->getVariants()[0]->getReturnType(); + + if (in_array(Builder::class, $returnType->getReferencedClasses(), true)) { + return Builder::class; + } + + $classNames = $returnType->getObjectClassNames(); + + if (count($classNames) === 1) { + return $classNames[0]; + } + + return $returnType->describe(VerbosityLevel::value()); + } + + /** + * @param array|string|TypeWithClassName $models + * @return ($models is array ? Type : ObjectType) + */ + private function getBuilderTypeForModels(array|string|TypeWithClassName $models): Type + { + $models = is_array($models) ? $models : [$models]; + $models = array_unique($models, SORT_REGULAR); + + $mappedModels = []; + foreach ($models as $model) { + if (is_string($model)) { + $mappedModels[$model] = new ObjectType($model); + } else { + $mappedModels[$model->getClassName()] = $model; + } + } + + $groupedByBuilder = []; + foreach ($mappedModels as $class => $type) { + $builderName = $this->determineBuilderName($class); + $groupedByBuilder[$builderName][] = $type; + } + + $builderTypes = []; + foreach ($groupedByBuilder as $builder => $models) { + $builderReflection = $this->reflectionProvider->getClass($builder); + + $builderTypes[] = $builderReflection->isGeneric() + ? new GenericObjectType($builder, [TypeCombinator::union(...$models)]) + : new ObjectType($builder); + } + + return TypeCombinator::union(...$builderTypes); + } +} + +final class ClosureQueryParameter implements ParameterReflection +{ + public function __construct(private string $name, private Type $type) + { + } + + public function getName(): string + { + return $this->name; + } + + public function isOptional(): bool + { + return false; + } + + public function getType(): Type + { + return $this->type; + } + + public function passedByReference(): PassedByReference + { + return PassedByReference::createNo(); + } + + public function isVariadic(): bool + { + return false; + } + + public function getDefaultValue(): Type|null + { + return null; + } +} diff --git a/e2e/bug-12585/src/test.php b/e2e/bug-12585/src/test.php new file mode 100644 index 0000000000..57abe4977e --- /dev/null +++ b/e2e/bug-12585/src/test.php @@ -0,0 +1,157 @@ + $related + * @return BelongsTo + */ + public function belongsTo(string $related): BelongsTo + { + return new BelongsTo(); // @phpstan-ignore return.type + } + + /** + * @template T of Model + * @param class-string $related + * @return HasMany + */ + public function hasMany(string $related): HasMany + { + return new HasMany(); // @phpstan-ignore return.type + } + + /** @return Builder */ + public static function query(): Builder + { + return new Builder(new static()); + } +} + +/** @template TModel of Model */ +class Builder +{ + /** @param TModel $model */ + final public function __construct(protected Model $model) + { + } + + /** + * @param (\Closure(static): mixed)|string $column + * @return $this + */ + public function where(Closure|string $column, mixed $value = null) + { + return $this; + } + + /** + * @template TRelatedModel of Model + * + * @param Relation|string $relation + * @param (\Closure(Builder): mixed)|null $callback + * @return $this + */ + public function whereHas($relation, ?Closure $callback = null) + { + return $this; + } + + /** + * @param string $relation + * @param (\Closure(Builder<*>|Relation<*, *>): mixed)|null $callback + * @return $this + */ + public function withWhereHas($relation, ?Closure $callback = null) + { + return $this; + } +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + * @mixin Builder + */ +abstract class Relation +{ +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + * @extends Relation + */ +class BelongsTo extends Relation +{ +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + * @extends Relation + */ +class HasMany extends Relation +{ +} + +final class User extends Model +{ + /** @return HasMany */ + public function posts(): HasMany + { + return $this->hasMany(Post::class); + } +} + +final class Post extends Model +{ + /** @return BelongsTo */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public static function query(): PostBuilder + { + return new PostBuilder(new self()); + } +} + +/** @extends Builder */ +class PostBuilder extends Builder +{ +} + +function test(): void +{ + User::query()->whereHas('posts', function ($query) { + assertType('Bug12585\PostBuilder', $query); + }); + User::query()->whereHas('posts', function (Builder $query) { + return $query->where('name', 'test'); + }); + User::query()->whereHas('posts', function (PostBuilder $query) { + return $query->where('name', 'test'); + }); + User::query()->whereHas('posts', fn (Builder $q) => $q->where('name', 'test')); + User::query()->whereHas('posts', fn (PostBuilder $q) => $q->where('name', 'test')); + + Post::query()->whereHas('user', fn ($q) => $q->where('name', 'test')); + Post::query()->withWhereHas('user', function ($query) { + assertType('Bug12585\BelongsTo|Bug12585\Builder', $query); + }); + Post::query()->withWhereHas('user', fn (Builder|Relation $q) => $q->where('name', 'test')); + Post::query()->withWhereHas('user', fn (Builder|BelongsTo $q) => $q->where('name', 'test')); +} diff --git a/e2e/parameter-type-extension/.gitignore b/e2e/parameter-type-extension/.gitignore new file mode 100644 index 0000000000..de4a392c33 --- /dev/null +++ b/e2e/parameter-type-extension/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/e2e/parameter-type-extension/composer.json b/e2e/parameter-type-extension/composer.json new file mode 100644 index 0000000000..f8a4e6ebed --- /dev/null +++ b/e2e/parameter-type-extension/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "App\\": "src/" + } + } +} diff --git a/e2e/parameter-type-extension/phpstan.neon.dist b/e2e/parameter-type-extension/phpstan.neon.dist new file mode 100644 index 0000000000..399cb98181 --- /dev/null +++ b/e2e/parameter-type-extension/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + level: 9 + paths: + - src +services: + - + class: App\ParameterTypeExtension + tags: + - phpstan.dynamicMethodParameterTypeExtension diff --git a/e2e/parameter-type-extension/src/ParameterTypeExtension.php b/e2e/parameter-type-extension/src/ParameterTypeExtension.php new file mode 100644 index 0000000000..4920ab3a2a --- /dev/null +++ b/e2e/parameter-type-extension/src/ParameterTypeExtension.php @@ -0,0 +1,94 @@ +getDeclaringClass()->is(Builder::class)) { + return false; + } + + return $methodReflection->getName() === 'with'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + ParameterReflection $parameter, + Scope $scope, + ): Type|null { + $arg = $methodCall->getArgs()[0] ?? null; + if (!$arg) { + return null; + } + + $type = $scope->getType($arg->value)->getConstantArrays()[0] ?? null; + if (!$type) { + return null; + } + + $model = $scope->getType($methodCall->var) + ->getTemplateType(Builder::class, 'TModel') + ->getObjectClassNames()[0] ?? null; + if (!$model) { + return null; + } + + foreach ($type->getKeyTypes() as $keyType) { + $relationType = $this->getRelationTypeFromModel($model, (string) $keyType->getValue(), $scope); + if (!$relationType) { + continue; + } + + $newType = new ClosureType([ + /** @phpstan-ignore phpstanApi.constructor */ + new NativeParameterReflection('test', false, $relationType, PassedByReference::createNo(), false, null), + ], new MixedType(), false); + + $type = $type->setOffsetValueType($keyType, $newType, false); + } + + return $type; + } + + public function getRelationTypeFromModel(string $model, string $relation, Scope $scope): ?Type + { + $modelType = new ObjectType($model); + + if (! $modelType->hasMethod($relation)->yes()) { + return null; + } + + $relationType = $modelType->getMethod($relation, $scope)->getVariants()[0]->getReturnType(); + + if (! (new ObjectType(Relation::class))->isSuperTypeOf($relationType)->yes()) { + return null; + } + + return $relationType; + } +} diff --git a/e2e/parameter-type-extension/src/test.php b/e2e/parameter-type-extension/src/test.php new file mode 100644 index 0000000000..51fc3c2b63 --- /dev/null +++ b/e2e/parameter-type-extension/src/test.php @@ -0,0 +1,88 @@ + */ + public function car(): HasOne + { + return new HasOne(); // @phpstan-ignore return.type + } + + /** @return MorphTo */ + public function monitorable(): MorphTo + { + return new MorphTo(); // @phpstan-ignore return.type + } +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + * @template TResult + */ +class Relation { + /** + * @param list $columns + * @return $this + */ + public function select(array $columns): static + { + return $this; + } +} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + * @extends Relation + */ +class HasOne extends Relation {} + +/** + * @template TRelatedModel of Model + * @template TDeclaringModel of Model + * @extends Relation + */ +class MorphTo extends Relation { + /** @return $this */ + public function morphWith(): static + { + return $this; + } +} + +/** @template TModel of Model */ +class Builder +{ + /** + * @param array): mixed> $relations + * @return $this + */ + public function with(array $relations): static + { + return $this; + } +} + +/** @param Builder $query */ +function test(Builder $query): void +{ + $query->with([ + 'car' => function ($r) { assertType('App\HasOne', $r); }, + 'monitorable' => function ($r) { assertType('App\MorphTo', $r); }, + ]); + $query->with([ + 'car' => fn (HasOne $q) => $q->select(['id']), + 'monitorable' => fn (MorphTo $q) => $q->morphWith(), + ]); +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fa4815cbd9..0d5451ab54 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -42,6 +42,60 @@ parameters: count: 1 path: src/Analyser/MutatingScope.php + - + rawMessage: 'Call to method getFunctionParameterClosureTypeExtensions() of deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method getMethodParameterClosureTypeExtensions() of deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method getStaticMethodParameterClosureTypeExtensions() of deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method getTypeFromFunctionCall() of deprecated interface PHPStan\Type\FunctionParameterClosureTypeExtension.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method getTypeFromMethodCall() of deprecated interface PHPStan\Type\MethodParameterClosureTypeExtension.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method getTypeFromStaticMethodCall() of deprecated interface PHPStan\Type\StaticMethodParameterClosureTypeExtension.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method isFunctionSupported() of deprecated interface PHPStan\Type\FunctionParameterClosureTypeExtension.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method isMethodSupported() of deprecated interface PHPStan\Type\MethodParameterClosureTypeExtension.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Call to method isStaticMethodSupported() of deprecated interface PHPStan\Type\StaticMethodParameterClosureTypeExtension.' + identifier: method.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType @@ -54,6 +108,18 @@ parameters: count: 1 path: src/Analyser/NodeScopeResolver.php + - + rawMessage: 'Parameter $parameterClosureTypeExtensionProvider of method PHPStan\Analyser\NodeScopeResolver::__construct() has typehint with deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider.' + identifier: parameter.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + + - + rawMessage: Property $parameterClosureTypeExtensionProvider references deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider in its type. + identifier: property.deprecatedInterface + count: 1 + path: src/Analyser/NodeScopeResolver.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.' identifier: phpstanApi.instanceofType @@ -216,6 +282,42 @@ parameters: count: 1 path: src/DependencyInjection/NeonAdapter.php + - + rawMessage: Access to constant on deprecated interface PHPStan\Type\FunctionParameterClosureTypeExtension. + identifier: classConstant.deprecatedInterface + count: 1 + path: src/DependencyInjection/ValidateServiceTagsExtension.php + + - + rawMessage: Access to constant on deprecated interface PHPStan\Type\MethodParameterClosureTypeExtension. + identifier: classConstant.deprecatedInterface + count: 1 + path: src/DependencyInjection/ValidateServiceTagsExtension.php + + - + rawMessage: Access to constant on deprecated interface PHPStan\Type\StaticMethodParameterClosureTypeExtension. + identifier: classConstant.deprecatedInterface + count: 1 + path: src/DependencyInjection/ValidateServiceTagsExtension.php + + - + rawMessage: Fetching class constant FUNCTION_TAG of deprecated class PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider. + identifier: classConstant.deprecatedClass + count: 1 + path: src/DependencyInjection/ValidateServiceTagsExtension.php + + - + rawMessage: Fetching class constant METHOD_TAG of deprecated class PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider. + identifier: classConstant.deprecatedClass + count: 1 + path: src/DependencyInjection/ValidateServiceTagsExtension.php + + - + rawMessage: Fetching class constant STATIC_METHOD_TAG of deprecated class PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider. + identifier: classConstant.deprecatedClass + count: 1 + path: src/DependencyInjection/ValidateServiceTagsExtension.php + - rawMessage: Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead. identifier: phpstanApi.runtimeReflection @@ -771,6 +873,18 @@ parameters: count: 1 path: src/Testing/PHPStanTestCase.php + - + rawMessage: Access to constant on deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider. + identifier: classConstant.deprecatedInterface + count: 1 + path: src/Testing/RuleTestCase.php + + - + rawMessage: Access to constant on deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider. + identifier: classConstant.deprecatedInterface + count: 1 + path: src/Testing/TypeInferenceTestCase.php + - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' identifier: phpstanApi.instanceofType @@ -1653,6 +1767,12 @@ parameters: count: 1 path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php + - + rawMessage: Class PHPStan\Type\Php\PregReplaceCallbackClosureTypeExtension implements deprecated interface PHPStan\Type\FunctionParameterClosureTypeExtension. + identifier: class.implementsDeprecatedInterface + count: 1 + path: src/Type/Php/PregReplaceCallbackClosureTypeExtension.php + - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType @@ -1881,6 +2001,12 @@ parameters: count: 2 path: src/Type/VoidType.php + - + rawMessage: Access to constant on deprecated interface PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider. + identifier: classConstant.deprecatedInterface + count: 1 + path: tests/PHPStan/Analyser/AnalyserTest.php + - rawMessage: Unreachable statement - code above always terminates. identifier: deadCode.unreachable diff --git a/src/Analyser/ExpressionContext.php b/src/Analyser/ExpressionContext.php index c910b0cc3d..5ed6b40997 100644 --- a/src/Analyser/ExpressionContext.php +++ b/src/Analyser/ExpressionContext.php @@ -12,18 +12,19 @@ private function __construct( private ?string $inAssignRightSideVariableName, private ?Type $inAssignRightSideType, private ?Type $inAssignRightSideNativeType, + private ?Type $overriddenType = null, ) { } public static function createTopLevel(): self { - return new self(false, null, null, null); + return new self(false, null, null, null, null); } public static function createDeep(): self { - return new self(true, null, null, null); + return new self(true, null, null, null, null); } public function enterDeep(): self @@ -32,7 +33,7 @@ public function enterDeep(): self return $this; } - return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType); + return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType, $this->overriddenType); } public function isDeep(): bool @@ -42,7 +43,7 @@ public function isDeep(): bool public function enterRightSideAssign(string $variableName, Type $type, Type $nativeType): self { - return new self($this->isDeep, $variableName, $type, $nativeType); + return new self($this->isDeep, $variableName, $type, $nativeType, $this->overriddenType); } public function getInAssignRightSideVariableName(): ?string @@ -60,4 +61,14 @@ public function getInAssignRightSideNativeType(): ?Type return $this->inAssignRightSideNativeType; } + public function withOverriddenType(?Type $type): self + { + return new self($this->isDeep, $this->inAssignRightSideVariableName, $this->inAssignRightSideType, $this->inAssignRightSideNativeType, $type); + } + + public function getOverriddenType(): ?Type + { + return $this->overriddenType; + } + } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 0fbac39d6b..6c7676741a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -66,6 +66,7 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; @@ -274,6 +275,7 @@ public function __construct( private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider, private readonly ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider, + private readonly DynamicParameterTypeExtensionProvider $dynamicParameterTypeExtensionProvider, private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider, private readonly ScopeFactory $scopeFactory, #[AutowiredParameter] @@ -3269,7 +3271,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope = $result->getScope(); } } elseif ($expr instanceof Expr\Closure) { - $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); + $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context); return new ExpressionResult( $processClosureResult->getScope(), @@ -3279,7 +3281,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { [], ); } elseif ($expr instanceof Expr\ArrowFunction) { - $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); + $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, $context); return new ExpressionResult( $result->getScope(), $result->hasYield(), @@ -3352,10 +3354,14 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints = []; $impurePoints = []; $isAlwaysTerminating = false; + $overriddenType = $context->getOverriddenType(); + $nextAutoIndex = 0; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); $nodeCallback($arrayItem, $scope); + $keyType = new ConstantIntegerType($nextAutoIndex); if ($arrayItem->key !== null) { + $keyType = $scope->getType($arrayItem->key); $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $keyResult->hasYield(); $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); @@ -3364,7 +3370,18 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope = $keyResult->getScope(); } - $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $valueContext = $context->enterDeep(); + if ($overriddenType !== null && $overriddenType->hasOffsetValueType($keyType)->yes()) { + $valueContext = $valueContext->withOverriddenType($overriddenType->getOffsetValueType($keyType)); + } + + if ($arrayItem->key === null) { + $nextAutoIndex++; + } elseif ($keyType instanceof ConstantIntegerType) { + $nextAutoIndex = $keyType->getValue() + 1; + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $valueContext); $hasYield = $hasYield || $valueResult->hasYield(); $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); @@ -4657,7 +4674,6 @@ private function processClosureNode( MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, - ?Type $passedToType, ): ProcessClosureResult { foreach ($expr->params as $param) { @@ -4671,7 +4687,7 @@ private function processClosureNode( $scope, $expr, $closureCallArgs, - $passedToType, + $context->getOverriddenType(), ); $useScope = $scope; @@ -4782,6 +4798,7 @@ private function processClosureNode( $statementResult, $executionEnds, array_merge($statementResult->getImpurePoints(), $closureImpurePoints), + $context->getOverriddenType(), ), $closureScope); return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); @@ -4828,6 +4845,7 @@ private function processClosureNode( $statementResult, $executionEnds, array_merge($statementResult->getImpurePoints(), $closureImpurePoints), + $context->getOverriddenType(), ), $closureScope); return new ProcessClosureResult($scope->processClosureScope($closureResultScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating); @@ -4874,7 +4892,7 @@ private function processArrowFunctionNode( Expr\ArrowFunction $expr, MutatingScope $scope, callable $nodeCallback, - ?Type $passedToType, + ExpressionContext $context, ): ExpressionResult { foreach ($expr->params as $param) { @@ -4889,13 +4907,13 @@ private function processArrowFunctionNode( $scope, $expr, $arrowFunctionCallArgs, - $passedToType, + $context->getOverriddenType(), )); $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); if (!$arrowFunctionType instanceof ClosureType) { throw new ShouldNotHappenException(); } - $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope); + $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr, $context->getOverriddenType()), $arrowFunctionScope); $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints()); @@ -4905,7 +4923,7 @@ private function processArrowFunctionNode( * @param Node\Arg[] $args * @return ParameterReflection[]|null */ - public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $overriddenType): ?array { $callableParameters = null; if ($args !== null) { @@ -4935,19 +4953,19 @@ public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array ); } } - } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { - if ($passedToType instanceof UnionType) { - $passedToType = TypeCombinator::union(...array_filter( - $passedToType->getTypes(), + } elseif ($overriddenType !== null && !$overriddenType->isCallable()->no()) { + if ($overriddenType instanceof UnionType) { + $overriddenType = TypeCombinator::union(...array_filter( + $overriddenType->getTypes(), static fn (Type $type) => $type->isCallable()->yes(), )); - if ($passedToType->isCallable()->no()) { + if ($overriddenType->isCallable()->no()) { return null; } } - $acceptors = $passedToType->getCallableParametersAcceptors($scope); + $acceptors = $overriddenType->getCallableParametersAcceptors($scope); if (count($acceptors) > 0) { foreach ($acceptors as $acceptor) { if ($callableParameters === null) { @@ -5230,6 +5248,7 @@ private function processArgs( $assignByReference = false; $parameter = null; $parameterType = null; + $overwritingParameterType = null; $parameterNativeType = null; if (isset($parameters) && $parametersAcceptor !== null) { if (isset($parameters[$i])) { @@ -5252,6 +5271,13 @@ private function processArgs( } } + if ($parameter !== null && $calleeReflection !== null) { + $overwritingParameterType = $this->getParameterTypeFromDynamicParameterTypeExtension($callLike, $calleeReflection, $parameter, $scope); + if ($overwritingParameterType !== null) { + $parameterType = $overwritingParameterType; + } + } + $lookForUnset = false; if ($assignByReference) { $isBuiltin = false; @@ -5312,6 +5338,7 @@ private function processArgs( } } + // @todo remove once the closure type extensions are removed if ($parameter !== null) { $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); @@ -5321,7 +5348,7 @@ private function processArgs( } $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->withOverriddenType($parameterType)); if ($callCallbackImmediately) { $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); @@ -5367,6 +5394,7 @@ private function processArgs( } } + // @todo remove once the closure type extensions are removed if ($parameter !== null) { $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); @@ -5376,7 +5404,7 @@ private function processArgs( } $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); + $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->withOverriddenType($parameterType)); if ($callCallbackImmediately) { $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints())); $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints()); @@ -5384,7 +5412,7 @@ private function processArgs( } } else { $exprType = $scope->getType($arg->value); - $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()->withOverriddenType($parameterType)); $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); @@ -5517,6 +5545,46 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints); } + /** + * @param MethodReflection|FunctionReflection|null $calleeReflection + */ + private function getParameterTypeFromDynamicParameterTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type + { + if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->dynamicParameterTypeExtensionProvider->getDynamicFunctionParameterTypeExtensions() as $dynamicFunctionParameterTypeExtension) { + if (!$dynamicFunctionParameterTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { + continue; + } + $type = $dynamicFunctionParameterTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->dynamicParameterTypeExtensionProvider->getDynamicStaticMethodParameterTypeExtensions() as $dynamicStaticMethodParameterTypeExtension) { + if (!$dynamicStaticMethodParameterTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $dynamicStaticMethodParameterTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->dynamicParameterTypeExtensionProvider->getDynamicMethodParameterTypeExtensions() as $dynamicMethodParameterTypeExtension) { + if (!$dynamicMethodParameterTypeExtension->isMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $dynamicMethodParameterTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } + + return null; + } + /** * @param MethodReflection|FunctionReflection|null $calleeReflection */ diff --git a/src/DependencyInjection/Type/DynamicParameterTypeExtensionProvider.php b/src/DependencyInjection/Type/DynamicParameterTypeExtensionProvider.php new file mode 100644 index 0000000000..c6bc6fd2a2 --- /dev/null +++ b/src/DependencyInjection/Type/DynamicParameterTypeExtensionProvider.php @@ -0,0 +1,21 @@ +container->getServicesByTag(self::FUNCTION_TAG); + } + + public function getDynamicMethodParameterTypeExtensions(): array + { + return $this->container->getServicesByTag(self::METHOD_TAG); + } + + public function getDynamicStaticMethodParameterTypeExtensions(): array + { + return $this->container->getServicesByTag(self::STATIC_METHOD_TAG); + } + +} diff --git a/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php index ecc30869f5..6941ee78de 100644 --- a/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyParameterClosureTypeExtensionProvider.php @@ -5,6 +5,10 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\DependencyInjection\Container; +/** + * @deprecated + * @see \PHPStan\DependencyInjection\Type\LazyDynamicParameterTypeExtensionProvider + */ #[AutowiredService(as: ParameterClosureTypeExtensionProvider::class)] final class LazyParameterClosureTypeExtensionProvider implements ParameterClosureTypeExtensionProvider { diff --git a/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php b/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php index 817bd6acf1..93ac91971c 100644 --- a/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/ParameterClosureTypeExtensionProvider.php @@ -6,6 +6,10 @@ use PHPStan\Type\MethodParameterClosureTypeExtension; use PHPStan\Type\StaticMethodParameterClosureTypeExtension; +/** + * @deprecated + * @see \PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider + */ interface ParameterClosureTypeExtensionProvider { diff --git a/src/DependencyInjection/ValidateServiceTagsExtension.php b/src/DependencyInjection/ValidateServiceTagsExtension.php index 0180baae09..0bbe48a2ca 100644 --- a/src/DependencyInjection/ValidateServiceTagsExtension.php +++ b/src/DependencyInjection/ValidateServiceTagsExtension.php @@ -10,6 +10,7 @@ use PHPStan\Broker\BrokerFactory; use PHPStan\Collectors\Collector; use PHPStan\Collectors\RegistryFactory as CollectorRegistryFactory; +use PHPStan\DependencyInjection\Type\LazyDynamicParameterTypeExtensionProvider; use PHPStan\DependencyInjection\Type\LazyDynamicThrowTypeExtensionProvider; use PHPStan\DependencyInjection\Type\LazyParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\LazyParameterClosureTypeExtensionProvider; @@ -41,10 +42,13 @@ use PHPStan\Rules\RestrictedUsage\RestrictedPropertyUsageExtension; use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\DynamicFunctionParameterTypeExtension; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\DynamicFunctionThrowTypeExtension; +use PHPStan\Type\DynamicMethodParameterTypeExtension; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\DynamicMethodThrowTypeExtension; +use PHPStan\Type\DynamicStaticMethodParameterTypeExtension; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\DynamicStaticMethodThrowTypeExtension; use PHPStan\Type\ExpressionTypeResolverExtension; @@ -94,6 +98,9 @@ final class ValidateServiceTagsExtension extends CompilerExtension FunctionParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::FUNCTION_TAG, MethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::METHOD_TAG, StaticMethodParameterClosureThisExtension::class => LazyParameterClosureThisExtensionProvider::STATIC_METHOD_TAG, + DynamicFunctionParameterTypeExtension::class => LazyDynamicParameterTypeExtensionProvider::FUNCTION_TAG, + DynamicMethodParameterTypeExtension::class => LazyDynamicParameterTypeExtensionProvider::METHOD_TAG, + DynamicStaticMethodParameterTypeExtension::class => LazyDynamicParameterTypeExtensionProvider::STATIC_METHOD_TAG, FunctionParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::FUNCTION_TAG, MethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::METHOD_TAG, StaticMethodParameterClosureTypeExtension::class => LazyParameterClosureTypeExtensionProvider::STATIC_METHOD_TAG, diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 87b63a60a4..8af87b65a7 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -10,6 +10,7 @@ use PhpParser\NodeAbstract; use PHPStan\Analyser\ImpurePoint; use PHPStan\Analyser\StatementResult; +use PHPStan\Type\Type; use function count; /** @@ -33,6 +34,7 @@ public function __construct( private StatementResult $statementResult, private array $executionEnds, private array $impurePoints, + private ?Type $overriddenType = null, ) { parent::__construct($closureExpr->getAttributes()); @@ -84,6 +86,11 @@ public function returnsByRef(): bool return $this->closureExpr->byRef; } + public function getOverriddenType(): ?Type + { + return $this->overriddenType; + } + #[Override] public function getType(): string { diff --git a/src/Node/InArrowFunctionNode.php b/src/Node/InArrowFunctionNode.php index 6876978cec..c92fb487d5 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -7,6 +7,7 @@ use PhpParser\Node\Expr\ArrowFunction; use PhpParser\NodeAbstract; use PHPStan\Type\ClosureType; +use PHPStan\Type\Type; /** * @api @@ -16,7 +17,11 @@ final class InArrowFunctionNode extends NodeAbstract implements VirtualNode private Node\Expr\ArrowFunction $originalNode; - public function __construct(private ClosureType $closureType, ArrowFunction $originalNode) + public function __construct( + private ClosureType $closureType, + ArrowFunction $originalNode, + private ?Type $overriddenType = null, + ) { parent::__construct($originalNode->getAttributes()); $this->originalNode = $originalNode; @@ -32,6 +37,11 @@ public function getOriginalNode(): Node\Expr\ArrowFunction return $this->originalNode; } + public function getOverriddenType(): ?Type + { + return $this->overriddenType; + } + #[Override] public function getType(): string { diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 36b3c6faa6..430ca73ee7 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -139,6 +139,7 @@ public function check( ), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), + null, new New_($attribute->name, $attribute->args, $nodeAttributes), 'attribute', $attributeConstructor->acceptsNamedArguments(), diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 6f8b322975..110f93c016 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -240,6 +240,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ ), $scope, $constructorReflection->getDeclaringClass()->isBuiltin(), + null, $node, 'new', $constructorReflection->acceptsNamedArguments(), diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 57ae028018..4ed8125b3a 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -8,7 +8,10 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ResolvedFunctionVariant; @@ -46,6 +49,7 @@ public function __construct( private NullsafeCheck $nullsafeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, private PropertyReflectionFinder $propertyReflectionFinder, + private DynamicParameterTypeExtensionProvider $dynamicParameterTypeExtensionProvider, #[AutowiredParameter(ref: '%checkFunctionArgumentTypes%')] private bool $checkArgumentTypes, #[AutowiredParameter] @@ -66,6 +70,7 @@ public function check( ParametersAcceptor $parametersAcceptor, Scope $scope, bool $isBuiltin, + MethodReflection|FunctionReflection|null $calleeReflection, Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, string $nodeType, TrinaryLogic $acceptsNamedArguments, @@ -349,6 +354,13 @@ public function check( if ($this->checkArgumentTypes) { $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); + if (! $funcCall instanceof Node\Expr\New_) { + $overriddenType = $this->getParameterTypeFromDynamicExtension($funcCall, $calleeReflection, $parameter, $scope); + if ($overriddenType !== null) { + $parameterType = $overriddenType; + } + } + if ( !$parameter->passedByReference()->createsNewVariable() || (!$isBuiltin && !$argumentValueType instanceof ErrorType) @@ -681,4 +693,50 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu return implode(' ', $parts); } + private function getParameterTypeFromDynamicExtension( + Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall $funcCall, + MethodReflection|FunctionReflection|null $calleeReflection, + ParameterReflection $parameter, + Scope $scope, + ): ?Type + { + if ($calleeReflection === null) { + return null; + } + + if ($funcCall instanceof Node\Expr\FuncCall && $calleeReflection instanceof FunctionReflection) { + foreach ($this->dynamicParameterTypeExtensionProvider->getDynamicFunctionParameterTypeExtensions() as $extension) { + if (!$extension->isFunctionSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getTypeFromFunctionCall($calleeReflection, $funcCall, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($funcCall instanceof Node\Expr\StaticCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->dynamicParameterTypeExtensionProvider->getDynamicStaticMethodParameterTypeExtensions() as $extension) { + if (!$extension->isStaticMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getTypeFromStaticMethodCall($calleeReflection, $funcCall, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } elseif ($funcCall instanceof Node\Expr\MethodCall && $calleeReflection instanceof MethodReflection) { + foreach ($this->dynamicParameterTypeExtensionProvider->getDynamicMethodParameterTypeExtensions() as $extension) { + if (!$extension->isMethodSupported($calleeReflection, $parameter)) { + continue; + } + $type = $extension->getTypeFromMethodCall($calleeReflection, $funcCall, $parameter, $scope); + if ($type !== null) { + return $type; + } + } + } + + return null; + } + } diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index ab1fecfe73..70f2786cd9 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -12,6 +12,8 @@ use PHPStan\ShouldNotHappenException; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\TypeCombinator; +use function array_map; /** * @implements Rule @@ -37,6 +39,13 @@ public function processNode(Node $node, Scope $scope): array $returnType = $scope->getAnonymousFunctionReturnType(); $generatorType = new ObjectType(Generator::class); + $overriddenType = $node->getOverriddenType(); + if ($overriddenType !== null && $overriddenType->isCallable()->yes()) { + $returnType = TypeCombinator::union(...array_map( + static fn ($a) => $a->getReturnType(), + $overriddenType->getCallableParametersAcceptors($scope), + )); + } $originalNode = $node->getOriginalNode(); $isVoidSuperType = $returnType->isVoid(); diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 4ab0af4b01..c7104fd2dd 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -121,6 +121,7 @@ public function processNode( $parametersAcceptor, $scope, false, + null, $node, 'callable', $acceptsNamedArguments, diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 39f6f7cfea..3f8e95595c 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -50,6 +50,7 @@ public function processNode(Node $node, Scope $scope): array ), $scope, $function->isBuiltin(), + $function, $node, 'function', $function->acceptsNamedArguments(), diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 3dae092b52..8a73e4a41c 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -66,6 +66,7 @@ public function processNode(Node $node, Scope $scope): array $parametersAcceptor, $scope, false, + null, $funcCall, 'function', $acceptsNamedArguments, diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index abeb7c3866..abfe196e39 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -9,6 +9,7 @@ use PHPStan\Rules\FunctionReturnTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Type\TypeCombinator; +use function array_map; /** * @implements Rule @@ -33,6 +34,13 @@ public function processNode(Node $node, Scope $scope): array } $returnType = $scope->getAnonymousFunctionReturnType(); + $overriddenType = $node->getOverriddenType(); + if ($overriddenType !== null && $overriddenType->isCallable()->yes()) { + $returnType = TypeCombinator::union(...array_map( + static fn ($a) => $a->getReturnType(), + $overriddenType->getCallableParametersAcceptors($scope), + )); + } $containsNull = TypeCombinator::containsNull($returnType); $hasNativeTypehint = $node->getClosureExpr()->returnType !== null; diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 1f042288f0..719c4b93e9 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -81,6 +81,7 @@ private function processSingleMethodCall(Scope $scope, MethodCall $node, string ), $scope, $declaringClass->isBuiltin(), + $methodReflection, $node, 'method', $methodReflection->acceptsNamedArguments(), diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index c9430abd81..02594d78d0 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -90,6 +90,7 @@ private function processSingleMethodCall(Scope $scope, StaticCall $node, string ), $scope, $method->getDeclaringClass()->isBuiltin(), + $method, $node, 'staticMethod', $method->acceptsNamedArguments(), diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 47c8a997e9..8c09b8b40a 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -16,6 +16,7 @@ use PHPStan\Collectors\Collector; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; @@ -107,6 +108,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), $readWritePropertiesExtensions !== [] ? new DirectReadWritePropertiesExtensionProvider($readWritePropertiesExtensions) : self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), $this->shouldPolluteScopeWithLoopInitialAssignments(), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index be8065f52a..0e37c2e7f2 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; @@ -85,6 +86,7 @@ public static function processFile( self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'), diff --git a/src/Type/DynamicFunctionParameterTypeExtension.php b/src/Type/DynamicFunctionParameterTypeExtension.php new file mode 100644 index 0000000000..63d4c0389c --- /dev/null +++ b/src/Type/DynamicFunctionParameterTypeExtension.php @@ -0,0 +1,32 @@ +getByType(DynamicThrowTypeExtensionProvider::class), self::getContainer()->getByType(ReadWritePropertiesExtensionProvider::class), self::getContainer()->getByType(ParameterClosureThisExtensionProvider::class), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), self::getContainer()->getByType(ParameterClosureTypeExtensionProvider::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), false, diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index a7342e7506..d5bb33d170 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Methods\CallMethodsRule; use PHPStan\Rules\Methods\MethodCallCheck; @@ -24,7 +25,7 @@ protected function getRule(): Rule $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false, true); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, true), ); } diff --git a/tests/PHPStan/Analyser/DynamicParameterTypeExtensionArraysTest.php b/tests/PHPStan/Analyser/DynamicParameterTypeExtensionArraysTest.php new file mode 100644 index 0000000000..00a1e28ec5 --- /dev/null +++ b/tests/PHPStan/Analyser/DynamicParameterTypeExtensionArraysTest.php @@ -0,0 +1,28 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/dynamic-parameter-type-extension-arrays.neon']; + } + +} diff --git a/tests/PHPStan/Analyser/DynamicParameterTypeExtensionClosuresTest.php b/tests/PHPStan/Analyser/DynamicParameterTypeExtensionClosuresTest.php new file mode 100644 index 0000000000..e8c8a52829 --- /dev/null +++ b/tests/PHPStan/Analyser/DynamicParameterTypeExtensionClosuresTest.php @@ -0,0 +1,28 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/dynamic-parameter-type-extension-closures.neon']; + } + +} diff --git a/tests/PHPStan/Analyser/data/dynamic-parameter-type-extension-arrays.php b/tests/PHPStan/Analyser/data/dynamic-parameter-type-extension-arrays.php new file mode 100644 index 0000000000..8898adb444 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-parameter-type-extension-arrays.php @@ -0,0 +1,181 @@ +getType($methodReflection, $methodCall, $parameter, $scope); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return $this->getType($methodReflection, $methodCall, $parameter, $scope); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return $this->getType($functionReflection, $functionCall, $parameter, $scope); + } + + private function getType( + FunctionReflection|MethodReflection $functionReflection, + FuncCall|MethodCall|StaticCall $call, + ParameterReflection $parameter, + Scope $scope, + ): ?Type + { + $arg = $call->getArgs()[0] ?? null; + if (!$arg) { + return null; + } + + $type = $scope->getType($arg->value)->getConstantArrays()[0] ?? null; + if (!$type) { + return null; + } + + $replacements = [ + 'a' => new IntegerType(), + 'b' => new StringType(), + 0 => new IntegerType(), + 1 => new StringType(), + 2 => new FloatType(), + ]; + + foreach ($replacements as $key => $value) { + $keyType = is_int($key) ? new ConstantIntegerType($key) : new ConstantStringType($key); + if ($type->hasOffsetValueType($keyType)->no()) { + continue; + } + + $newType = new CallableType([ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [$value]), PassedByReference::createNo(), false, null), + ], new MixedType(), false); + + $type = $type->setOffsetValueType($keyType, $newType, false); + } + + return $type; + } +} + +class Foo +{ + + /** @param array)> $array */ + public function methodWithArray($array) {} + + public static function staticMethodWithArray(array $array) {} + +} + +/** @template T */ +class Generic +{ + public function __construct( + /** @var T */ + private mixed $value, + ) { + } + + /** @return T */ + public function getValue() + { + return $this->value; + } +} + +/** @param array)> $array */ +function functionWithArray(array $array): void {} + +/** @param list)> $list */ +function functionWithNumericArray(array $list): void {} + +function test(Foo $foo): void +{ + functionWithArray([ + fn ($i) => assertType('int', $i->getValue()), + fn ($i) => assertType('string', $i->getValue()), + fn ($i) => assertType('float', $i->getValue()), + ]); + + functionWithArray([ + 0 => fn ($i) => assertType('int', $i->getValue()), + 1 => fn ($i) => assertType('string', $i->getValue()), + 2 => fn ($i) => assertType('float', $i->getValue()), + ]); + + functionWithArray([ + 'a' => fn ($i) => assertType('int', $i->getValue()), + 'b' => fn ($i) => assertType('string', $i->getValue()), + 'c' => fn (int $i) => assertType('int', $i), + ]); + $foo->methodWithArray([ + 'a' => fn ($i) => assertType('int', $i->getValue()), + 'b' => fn ($i) => assertType('string', $i->getValue()), + 'c' => fn (int $i) => assertType('int', $i), + ]); + Foo::staticMethodWithArray([ + 'a' => fn ($i) => assertType('int', $i->getValue()), + 'b' => fn ($i) => assertType('string', $i->getValue()), + 'c' => fn (int $i) => assertType('int', $i), + ]); + + functionWithArray([ + 'a' => function ($i) { assertType('int', $i->getValue()); }, + 'b' => function ($i) { assertType('string', $i->getValue()); }, + 'c' => function (int $i) { assertType('int', $i); }, + ]); + $foo->methodWithArray([ + 'a' => function ($i) { assertType('int', $i->getValue()); }, + 'b' => function ($i) { assertType('string', $i->getValue()); }, + 'c' => function (int $i) { assertType('int', $i); }, + ]); + Foo::staticMethodWithArray([ + 'a' => function ($i) { assertType('int', $i->getValue()); }, + 'b' => function ($i) { assertType('string', $i->getValue()); }, + 'c' => function (int $i) { assertType('int', $i); }, + ]); +} diff --git a/tests/PHPStan/Analyser/data/dynamic-parameter-type-extension-closures.php b/tests/PHPStan/Analyser/data/dynamic-parameter-type-extension-closures.php new file mode 100644 index 0000000000..eb407c5db6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/dynamic-parameter-type-extension-closures.php @@ -0,0 +1,157 @@ +getName() === 'DynamicParameterTypeExtensionClosures\functionWithCallable'; + } + + public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && + $parameter->getName() === 'callback' && + $methodReflection->getName() === 'methodWithCallable'; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool + { + return $methodReflection->getDeclaringClass()->getName() === Foo::class && $methodReflection->getName() === 'staticMethodWithCallable'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return $this->getType($functionReflection, $functionCall, $parameter, $scope); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return $this->getType($methodReflection, $methodCall, $parameter, $scope); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type + { + return new CallableType( + [ + new NativeParameterReflection('test', false, new FloatType(), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + private function getType( + FunctionReflection|MethodReflection $methodReflection, + FuncCall|MethodCall $methodCall, + ParameterReflection $parameter, + Scope $scope, + ): ?Type { + $args = $methodCall->getArgs(); + + if (count($args) < 2) { + return null; + } + + $integer = $scope->getType($args[0]->value)->getConstantScalarValues()[0]; + + if ($integer === 1) { + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new IntegerType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } + + return new CallableType( + [ + new NativeParameterReflection('test', false, new GenericObjectType(Generic::class, [new StringType()]), PassedByReference::createNo(), false, null), + ], + new MixedType() + ); + } +} + +class Foo +{ + + /** + * @param int $foo + * @param callable(Generic) $callback + * + * @return void + */ + public function methodWithCallable(int $foo, callable $callback) {} + + /** @return void */ + public static function staticMethodWithCallable(callable $callback) {} + +} + +/** @template T */ +class Generic +{ + private $value; + + /** @param T $value */ + public function __construct($value) + { + $this->value = $value; + } + + /** @return T */ + public function getValue() + { + return $this->value; + } +} + +/** + * @param int $foo + * @param callable(Generic) $callback + * + * @return void + */ +function functionWithCallable(int $foo, callable $callback) {} + +function test(Foo $foo): void +{ + + // arrow functions + $foo->methodWithCallable(1, fn ($i) => assertType('int', $i->getValue())); + (new Foo)->methodWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); + Foo::staticMethodWithCallable(fn ($i) => assertType('float', $i)); + functionWithCallable(1, fn ($i) => assertType('int', $i->getValue())); + functionWithCallable(2, fn (Generic $i) => assertType('string', $i->getValue())); + + + // closures + $foo->methodWithCallable(1, function ($i) { assertType('int', $i->getValue()); }); + (new Foo)->methodWithCallable(2, function (Generic $i) { assertType('string', $i->getValue()); }); + Foo::staticMethodWithCallable(function ($i) { assertType('float', $i); }); + functionWithCallable(1, function ($i) { assertType('int', $i->getValue()); }); + functionWithCallable(2, function (Generic $i) { assertType('string', $i->getValue()); }); +} diff --git a/tests/PHPStan/Analyser/dynamic-parameter-type-extension-arrays.neon b/tests/PHPStan/Analyser/dynamic-parameter-type-extension-arrays.neon new file mode 100644 index 0000000000..82f6e9cb59 --- /dev/null +++ b/tests/PHPStan/Analyser/dynamic-parameter-type-extension-arrays.neon @@ -0,0 +1,7 @@ +services: + - + class: DynamicParameterTypeExtensionArrays\DynamicParameterTypeExtension + tags: + - phpstan.dynamicFunctionParameterTypeExtension + - phpstan.dynamicMethodParameterTypeExtension + - phpstan.dynamicStaticMethodParameterTypeExtension diff --git a/tests/PHPStan/Analyser/dynamic-parameter-type-extension-closures.neon b/tests/PHPStan/Analyser/dynamic-parameter-type-extension-closures.neon new file mode 100644 index 0000000000..13c6ef79d7 --- /dev/null +++ b/tests/PHPStan/Analyser/dynamic-parameter-type-extension-closures.neon @@ -0,0 +1,7 @@ +services: + - + class: DynamicParameterTypeExtensionClosures\DynamicParameterTypeExtension + tags: + - phpstan.dynamicFunctionParameterTypeExtension + - phpstan.dynamicMethodParameterTypeExtension + - phpstan.dynamicStaticMethodParameterTypeExtension diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index c85ac4f704..bb8637fa85 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -36,6 +37,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index a2a1f5328b..7b07fce868 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index cf4d172f92..ad7319bc20 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -26,7 +27,7 @@ protected function getRule(): Rule return new InstantiationRule( self::getContainer(), $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 945f08e533..527d968053 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -26,7 +27,7 @@ protected function getRule(): Rule return new InstantiationRule( self::getContainer(), $reflectionProvider, - new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 1028c66778..48ae188241 100644 --- a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\EnumCases; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -32,6 +33,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 64ce4730fe..0e0b7e2fdd 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index c82080b9ed..37e6370afc 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -30,6 +31,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 9a9cc2f6e5..15e29c1190 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -29,7 +30,7 @@ protected function getRule(): Rule $broker = self::createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, true), ); } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index ba44a5bc0f..2089961bec 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -20,7 +21,7 @@ class CallUserFuncRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); - return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true)); + return new CallUserFuncRule($reflectionProvider, new FunctionCallParametersCheck(new RuleLevelHelper($reflectionProvider, true, false, true, true, false, false, true), new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, true)); } #[RequiresPhp('>= 8.0')] diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index 659659449e..b2a92e06c5 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 0ad48b3e98..2dca477e53 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 0b0296bc6a..c04da4d81e 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 73b0e4aee1..5642673ab2 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; @@ -35,7 +36,7 @@ protected function getRule(): Rule $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true); return new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, true), ); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index bd53eca6e0..53a3dfd76b 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -52,6 +53,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 2245a8cb2a..343a7f2c5a 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index e5eab80521..fb19acb381 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Properties; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -30,6 +31,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php index ef9b16df15..1c18db2691 100644 --- a/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyHookAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Properties; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -31,6 +32,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true, diff --git a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php index d8b9c54d0c..76ca2e182d 100644 --- a/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Traits/TraitAttributesRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Traits; +use PHPStan\DependencyInjection\Type\DynamicParameterTypeExtensionProvider; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; @@ -36,6 +37,7 @@ protected function getRule(): Rule new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), + self::getContainer()->getByType(DynamicParameterTypeExtensionProvider::class), true, true, true,