diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 62903d0af76..60bac79ce80 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -50,6 +50,7 @@ use function array_values; use function count; use function get_class; +use function in_array; use function reset; use function strtolower; @@ -295,45 +296,22 @@ public static function analyze( } } - $naive_method_exists = false; - // @mixin attributes are an absolute pain! Lots of complexity here, // as they can redefine the called class, method id etc. - if ($class_storage->templatedMixins - && $lhs_type_part instanceof TGenericObject - && $class_storage->template_types - ) { - [$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name] - = self::handleTemplatedMixins( - $class_storage, - $lhs_type_part, - $method_name_lc, - $codebase, - $context, - $method_id, - $source, - $stmt, - $statements_analyzer, - $fq_class_name, - ); - } elseif ($class_storage->mixin_declaring_fqcln - && $class_storage->namedMixins - ) { - [$lhs_type_part, $class_storage, $naive_method_exists, $method_id, $fq_class_name] - = self::handleRegularMixins( - $class_storage, - $lhs_type_part, - $method_name_lc, - $codebase, - $context, - $method_id, - $source, - $stmt, - $statements_analyzer, - $fq_class_name, - $lhs_var_id, - ); - } + [$lhs_type_part, $class_storage, , $naive_method_exists, $method_id, $fq_class_name] + = self::handleMixins( + $class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $method_id, + $source, + $stmt, + $statements_analyzer, + $fq_class_name, + $lhs_var_id, + ); } $all_intersection_return_type = null; @@ -725,7 +703,81 @@ private static function handleInvalidClass( /** * @param lowercase-string $method_name_lc - * @return array{TNamedObject, ClassLikeStorage, bool, MethodIdentifier, string} + * @param string[] $ignore_mixins + * @return array{TNamedObject, ClassLikeStorage, bool, bool, MethodIdentifier, string} + */ + private static function handleMixins( + ClassLikeStorage $class_storage, + TNamedObject $lhs_type_part, + string $method_name_lc, + Codebase $codebase, + Context $context, + MethodIdentifier $method_id, + StatementsSource $source, + PhpParser\Node\Expr\MethodCall $stmt, + StatementsAnalyzer $statements_analyzer, + string $fq_class_name, + ?string $lhs_var_id, + array $ignore_mixins = [] + ): array { + $method_exists = false; + $naive_method_exists = false; + + // @mixin attributes are an absolute pain! Lots of complexity here, + // as they can redefine the called class, method id etc. + if ($class_storage->templatedMixins + && $lhs_type_part instanceof TGenericObject + && $class_storage->template_types + ) { + [$lhs_type_part, $class_storage, $method_exists, $naive_method_exists, $method_id, $fq_class_name] + = self::handleTemplatedMixins( + $class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $method_id, + $source, + $stmt, + $statements_analyzer, + $fq_class_name, + $lhs_var_id, + $ignore_mixins, + ); + } elseif ($class_storage->mixin_declaring_fqcln + && $class_storage->namedMixins + ) { + [$lhs_type_part, $class_storage, $method_exists, $naive_method_exists, $method_id, $fq_class_name] + = self::handleRegularMixins( + $class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $method_id, + $source, + $stmt, + $statements_analyzer, + $fq_class_name, + $lhs_var_id, + $ignore_mixins, + ); + } + + return [ + $lhs_type_part, + $class_storage, + $method_exists, + $naive_method_exists, + $method_id, + $fq_class_name, + ]; + } + + /** + * @param lowercase-string $method_name_lc + * @param string[] $ignore_mixins + * @return array{TNamedObject, ClassLikeStorage, bool, bool, MethodIdentifier, string} */ private static function handleTemplatedMixins( ClassLikeStorage $class_storage, @@ -737,10 +789,15 @@ private static function handleTemplatedMixins( StatementsSource $source, PhpParser\Node\Expr\MethodCall $stmt, StatementsAnalyzer $statements_analyzer, - string $fq_class_name + string $fq_class_name, + ?string $lhs_var_id, + array $ignore_mixins ): array { + $method_exists = false; $naive_method_exists = false; + $ignore_mixins[] = $fq_class_name; + if ($class_storage->templatedMixins && $lhs_type_part instanceof TGenericObject && $class_storage->template_types @@ -773,6 +830,8 @@ private static function handleTemplatedMixins( $lhs_type_part_new->value, ); + $mixin_fq_class_name = $mixin_class_storage->name; + if ($codebase->methods->methodExists( $new_method_id, $context->calling_method_id, @@ -787,15 +846,44 @@ private static function handleTemplatedMixins( true, $context->insideUse(), )) { - $lhs_type_part = $lhs_type_part_new; - $class_storage = $mixin_class_storage; - + $method_exists = true; $naive_method_exists = true; - $method_id = $new_method_id; - } elseif (isset($mixin_class_storage->pseudo_methods[$method_name_lc])) { + } + + if (!$method_exists) { + $method_exists = isset($mixin_class_storage->pseudo_methods[$method_name_lc]); + } + + if (!$method_exists && !in_array($mixin_fq_class_name, $ignore_mixins)) { + [ + $lhs_type_part_new, + $mixin_class_storage, + $method_exists, + $naive_method_exists, + $new_method_id, + $mixin_fq_class_name, + ] = self::handleMixins( + $mixin_class_storage, + $lhs_type_part_new, + $method_name_lc, + $codebase, + $context, + $new_method_id, + $source, + $stmt, + $statements_analyzer, + $mixin_fq_class_name, + $lhs_var_id, + $ignore_mixins, + ); + } + + if ($method_exists) { $lhs_type_part = $lhs_type_part_new; $class_storage = $mixin_class_storage; $method_id = $new_method_id; + $fq_class_name = $mixin_fq_class_name; + break; } } } @@ -806,6 +894,7 @@ private static function handleTemplatedMixins( return [ $lhs_type_part, $class_storage, + $method_exists, $naive_method_exists, $method_id, $fq_class_name, @@ -814,7 +903,8 @@ private static function handleTemplatedMixins( /** * @param lowercase-string $method_name_lc - * @return array{TNamedObject, ClassLikeStorage, bool, MethodIdentifier, string} + * @param string[] $ignore_mixins + * @return array{TNamedObject, ClassLikeStorage, bool, bool, MethodIdentifier, string} */ private static function handleRegularMixins( ClassLikeStorage $class_storage, @@ -827,14 +917,59 @@ private static function handleRegularMixins( PhpParser\Node\Expr\MethodCall $stmt, StatementsAnalyzer $statements_analyzer, string $fq_class_name, - ?string $lhs_var_id + ?string $lhs_var_id, + array $ignore_mixins ): array { + $method_exists = false; $naive_method_exists = false; + $ignore_mixins[] = $fq_class_name; + foreach ($class_storage->namedMixins as $mixin) { if (!$class_storage->mixin_declaring_fqcln) { continue; } + if (!$codebase->classlike_storage_provider->has($mixin->value)) { + continue; + } + + $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( + $class_storage->mixin_declaring_fqcln, + ); + + $mixin_class_template_params = ClassTemplateParamCollector::collect( + $codebase, + $mixin_declaring_class_storage, + $codebase->classlike_storage_provider->get($fq_class_name), + null, + $lhs_type_part, + $lhs_var_id === '$this', + ); + + $mixin_lhs_type_part = $mixin->replaceTemplateTypesWithArgTypes( + new TemplateResult([], $mixin_class_template_params ?: []), + $codebase, + ); + + $lhs_type_expanded = TypeExpander::expandUnion( + $codebase, + new Union([$mixin_lhs_type_part]), + $mixin_declaring_class_storage->name, + $fq_class_name, + $class_storage->parent_class, + true, + false, + $class_storage->final, + ); + + $_lhs_type_part = $lhs_type_expanded->getSingleAtomic(); + if ($_lhs_type_part instanceof TNamedObject) { + $mixin_lhs_type_part = $_lhs_type_part; + } + + $mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value); + + $mixin_fq_class_name = $mixin_class_storage->name; $new_method_id = new MethodIdentifier( $mixin->value, @@ -855,54 +990,48 @@ private static function handleRegularMixins( true, $context->insideUse(), )) { - $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( - $class_storage->mixin_declaring_fqcln, - ); - - $mixin_class_template_params = ClassTemplateParamCollector::collect( - $codebase, - $mixin_declaring_class_storage, - $codebase->classlike_storage_provider->get($fq_class_name), - null, - $lhs_type_part, - $lhs_var_id === '$this', - ); - - $lhs_type_part = $mixin->replaceTemplateTypesWithArgTypes( - new TemplateResult([], $mixin_class_template_params ?: []), - $codebase, - ); + $method_exists = true; + $naive_method_exists = true; + } - $lhs_type_expanded = TypeExpander::expandUnion( + if (!$method_exists && !in_array($mixin_fq_class_name, $ignore_mixins)) { + [ + $mixin_lhs_type_part, + $mixin_class_storage, + $method_exists, + $naive_method_exists, + $new_method_id, + $mixin_fq_class_name, + ] = self::handleMixins( + $mixin_class_storage, + $mixin_lhs_type_part, + $method_name_lc, $codebase, - new Union([$lhs_type_part]), - $mixin_declaring_class_storage->name, - $fq_class_name, - $class_storage->parent_class, - true, - false, - $class_storage->final, + $context, + $new_method_id, + $source, + $stmt, + $statements_analyzer, + $mixin_fq_class_name, + $lhs_var_id, + $ignore_mixins, ); + } - $new_lhs_type_part = $lhs_type_expanded->getSingleAtomic(); - - if ($new_lhs_type_part instanceof TNamedObject) { - $lhs_type_part = $new_lhs_type_part; - } - - $mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value); - - $fq_class_name = $mixin_class_storage->name; + if ($method_exists) { + $lhs_type_part = $mixin_lhs_type_part; + $fq_class_name = $mixin_fq_class_name; $mixin_class_storage->mixin_declaring_fqcln = $class_storage->mixin_declaring_fqcln; $class_storage = $mixin_class_storage; - $naive_method_exists = true; $method_id = $new_method_id; + break; } } return [ $lhs_type_part, $class_storage, + $method_exists, $naive_method_exists, $method_id, $fq_class_name, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index c9dce4d1460..d571f83f3d3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -384,97 +384,29 @@ private static function handleNamedCall( $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); - if (!$naive_method_exists - && $class_storage->mixin_declaring_fqcln - && $class_storage->namedMixins - ) { - foreach ($class_storage->namedMixins as $mixin) { - $new_method_id = new MethodIdentifier( - $mixin->value, - $method_name_lc, - ); + if (!$naive_method_exists) { + $new_lhs_type = self::handleRegularMixins( + $class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $stmt_name, + $statements_analyzer, + $fq_class_name, + ); + if ($new_lhs_type !== null) { + $mixin_context = clone $context; + $mixin_context->vars_in_scope['$__tmp_mixin_var__'] = $new_lhs_type; - if ($codebase->methods->methodExists( - $new_method_id, - $context->calling_method_id, - $codebase->collect_locations - ? new CodeLocation($statements_analyzer, $stmt_name) - : null, - !$context->collect_initializations - && !$context->collect_mutations - ? $statements_analyzer - : null, - $statements_analyzer->getFilePath(), + return self::forwardCallToInstanceMethod( + $statements_analyzer, + $stmt, + $stmt_name, + $mixin_context, + '__tmp_mixin_var__', true, - $context->insideUse(), - )) { - $mixin_candidates = []; - foreach ($class_storage->templatedMixins as $mixin_candidate) { - $mixin_candidates[] = $mixin_candidate; - } - - foreach ($class_storage->namedMixins as $mixin_candidate) { - $mixin_candidates[] = $mixin_candidate; - } - - $mixin_candidates_no_generic = array_filter( - $mixin_candidates, - static fn(Atomic $check): bool => !($check instanceof TGenericObject), - ); - - // $mixin_candidates_no_generic will only be empty when there are TGenericObject entries. - // In that case, Union will be initialized with an empty array but - // replaced with non-empty types in the following loop. - /** @psalm-suppress ArgumentTypeCoercion */ - $mixin_candidate_type = new Union($mixin_candidates_no_generic); - - foreach ($mixin_candidates as $tGenericMixin) { - if (!($tGenericMixin instanceof TGenericObject)) { - continue; - } - - $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( - $class_storage->mixin_declaring_fqcln, - ); - - $new_mixin_candidate_type = AtomicPropertyFetchAnalyzer::localizePropertyType( - $codebase, - new Union([$lhs_type_part]), - $tGenericMixin, - $class_storage, - $mixin_declaring_class_storage, - )->getBuilder(); - - foreach ($mixin_candidate_type->getAtomicTypes() as $type) { - $new_mixin_candidate_type->addType($type); - } - - $mixin_candidate_type = $new_mixin_candidate_type->freeze(); - } - - $new_lhs_type = TypeExpander::expandUnion( - $codebase, - $mixin_candidate_type, - $fq_class_name, - $fq_class_name, - $class_storage->parent_class, - true, - false, - $class_storage->final, - ); - - $mixin_context = clone $context; - $mixin_context->vars_in_scope['$__tmp_mixin_var__'] = $new_lhs_type; - - return self::forwardCallToInstanceMethod( - $statements_analyzer, - $stmt, - $stmt_name, - $mixin_context, - '__tmp_mixin_var__', - true, - ); - } + ); } } @@ -991,6 +923,132 @@ private static function checkPseudoMethod( return null; } + /** + * @param lowercase-string $method_name_lc + * @param string[] $ignore_mixins + */ + private static function handleRegularMixins( + ClassLikeStorage $class_storage, + Atomic $lhs_type_part, + string $method_name_lc, + Codebase $codebase, + Context $context, + PhpParser\Node\Identifier $stmt_name, + StatementsAnalyzer $statements_analyzer, + string $fq_class_name, + array $ignore_mixins = [] + ): ?Union { + if ($class_storage->mixin_declaring_fqcln === null) { + return null; + } + + $ignore_mixins[] = $fq_class_name; + + foreach ($class_storage->namedMixins as $mixin) { + $new_method_id = new MethodIdentifier( + $mixin->value, + $method_name_lc, + ); + + if ($codebase->methods->methodExists( + $new_method_id, + $context->calling_method_id, + $codebase->collect_locations + ? new CodeLocation($statements_analyzer, $stmt_name) + : null, + !$context->collect_initializations + && !$context->collect_mutations + ? $statements_analyzer + : null, + $statements_analyzer->getFilePath(), + true, + $context->insideUse(), + )) { + $mixin_candidates = []; + foreach ($class_storage->templatedMixins as $mixin_candidate) { + $mixin_candidates[] = $mixin_candidate; + } + + foreach ($class_storage->namedMixins as $mixin_candidate) { + $mixin_candidates[] = $mixin_candidate; + } + + $mixin_candidates_no_generic = array_filter( + $mixin_candidates, + static fn(Atomic $check): bool => !($check instanceof TGenericObject), + ); + + // $mixin_candidates_no_generic will only be empty when there are TGenericObject entries. + // In that case, Union will be initialized with an empty array but + // replaced with non-empty types in the following loop. + /** @psalm-suppress ArgumentTypeCoercion */ + $mixin_candidate_type = new Union($mixin_candidates_no_generic); + + foreach ($mixin_candidates as $tGenericMixin) { + if (!($tGenericMixin instanceof TGenericObject)) { + continue; + } + + $mixin_declaring_class_storage = $codebase->classlike_storage_provider->get( + $class_storage->mixin_declaring_fqcln, + ); + + $new_mixin_candidate_type = AtomicPropertyFetchAnalyzer::localizePropertyType( + $codebase, + new Union([$lhs_type_part]), + $tGenericMixin, + $class_storage, + $mixin_declaring_class_storage, + )->getBuilder(); + + foreach ($mixin_candidate_type->getAtomicTypes() as $type) { + $new_mixin_candidate_type->addType($type); + } + + $mixin_candidate_type = $new_mixin_candidate_type->freeze(); + } + + $new_lhs_type = TypeExpander::expandUnion( + $codebase, + $mixin_candidate_type, + $fq_class_name, + $fq_class_name, + $class_storage->parent_class, + true, + false, + $class_storage->final, + ); + + return $new_lhs_type; + } + } + foreach ($class_storage->namedMixins as $mixin) { + if (!$codebase->classlike_storage_provider->has($mixin->value)) { + continue; + } + $mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value); + $mixin_fq_class_name = $mixin_class_storage->name; + if (in_array($mixin_fq_class_name, $ignore_mixins)) { + continue; + } + $new_lhs_type = self::handleRegularMixins( + $mixin_class_storage, + $lhs_type_part, + $method_name_lc, + $codebase, + $context, + $stmt_name, + $statements_analyzer, + $mixin_fq_class_name, + $ignore_mixins, + ); + if ($new_lhs_type) { + return $new_lhs_type; + } + } + return null; + } + public static function handleNonObjectCall( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\StaticCall $stmt, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 6ad6983b76e..b788567553b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -265,38 +265,19 @@ public static function analyze( if (!$naive_property_exists) { if ($class_storage->namedMixins) { - foreach ($class_storage->namedMixins as $mixin) { - $new_property_id = $mixin->value . '::$' . $prop_name; - - try { - $new_class_storage = $codebase->classlike_storage_provider->get($mixin->value); - } catch (InvalidArgumentException $e) { - $new_class_storage = null; - } - - if ($new_class_storage - && ($codebase->properties->propertyExists( - $new_property_id, - !$in_assignment, - $statements_analyzer, - $context, - $codebase->collect_locations - ? new CodeLocation($statements_analyzer->getSource(), $stmt) - : null, - ) - || isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) - ) { - $fq_class_name = $mixin->value; - $lhs_type_part = $mixin; - $class_storage = $new_class_storage; - - if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) { - $naive_property_exists = true; - } - - $property_id = $new_property_id; - } - } + [$lhs_type_part, $class_storage, , $naive_property_exists, $property_id, $fq_class_name] + = self::handleRegularMixins( + $class_storage, + $lhs_type_part, + $prop_name, + $in_assignment, + $codebase, + $context, + $property_id, + $stmt, + $statements_analyzer, + $fq_class_name, + ); } elseif ($intersection_types !== [] && !$class_storage->final) { foreach ($intersection_types as $intersection_type) { self::analyze( @@ -1277,6 +1258,114 @@ private static function handleNonExistentProperty( ); } + /** + * @param string[] $ignore_mixins + * @return array{TNamedObject, ClassLikeStorage, bool, bool, string, string} + */ + private static function handleRegularMixins( + ClassLikeStorage $class_storage, + TNamedObject $lhs_type_part, + string $prop_name, + bool $in_assignment, + Codebase $codebase, + Context $context, + string $property_id, + PhpParser\Node\Expr\PropertyFetch $stmt, + StatementsAnalyzer $statements_analyzer, + string $fq_class_name, + array $ignore_mixins = [] + ): array { + $property_exists = false; + $naive_property_exists = false; + + $ignore_mixins[] = $fq_class_name; + + foreach ($class_storage->namedMixins as $mixin) { + $new_property_id = $mixin->value . '::$' . $prop_name; + + try { + $new_class_storage = $codebase->classlike_storage_provider->get($mixin->value); + } catch (InvalidArgumentException $e) { + $new_class_storage = null; + } + + if ($new_class_storage + && ($codebase->properties->propertyExists( + $new_property_id, + !$in_assignment, + $statements_analyzer, + $context, + $codebase->collect_locations + ? new CodeLocation($statements_analyzer->getSource(), $stmt) + : null, + ) + || isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) + ) { + $fq_class_name = $mixin->value; + $lhs_type_part = $mixin; + $class_storage = $new_class_storage; + + $property_exists = true; + if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) { + $naive_property_exists = true; + } + + $property_id = $new_property_id; + } + } + if (!$property_exists) { + foreach ($class_storage->namedMixins as $mixin) { + try { + $mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value); + } catch (InvalidArgumentException $e) { + continue; + } + $mixin_lhs_type_part = $mixin; + $mixin_fq_class_name = $mixin_class_storage->name; + if (in_array($mixin_fq_class_name, $ignore_mixins)) { + continue; + } + + [ + $new_lhs_type_part, + $new_class_storage, + $property_exists, + $naive_property_exists, + $new_property_id, + $new_fq_class_name, + ] = self::handleRegularMixins( + $mixin_class_storage, + $mixin_lhs_type_part, + $prop_name, + $in_assignment, + $codebase, + $context, + $property_id, + $stmt, + $statements_analyzer, + $mixin_fq_class_name, + $ignore_mixins, + ); + + if ($property_exists) { + $fq_class_name = $new_fq_class_name; + $lhs_type_part = $new_lhs_type_part; + $class_storage = $new_class_storage; + $property_id = $new_property_id; + break; + } + } + } + return [ + $lhs_type_part, + $class_storage, + $property_exists, + $naive_property_exists, + $property_id, + $fq_class_name, + ]; + } + private static function getClassPropertyType( StatementsAnalyzer $statements_analyzer, Codebase $codebase, diff --git a/tests/MixinAnnotationTest.php b/tests/MixinAnnotationTest.php index 3c0c11b26bb..41a03937f49 100644 --- a/tests/MixinAnnotationTest.php +++ b/tests/MixinAnnotationTest.php @@ -603,12 +603,14 @@ public function providerInvalidCodeParse(): iterable { return [ 'undefinedMixinClass' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedDocblockClass', ], 'undefinedMixinClassWithPropertyFetch' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedPropertyFetch', ], 'undefinedMixinClassWithPropertyFetch_WithMagicMethod' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedMagicPropertyFetch', ], 'undefinedMixinClassWithPropertyAssignment' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedPropertyAssignment', ], 'undefinedMixinClassWithPropertyAssignment_WithMagicMethod' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedMagicPropertyAssignment', ], 'undefinedMixinClassWithMethodCall' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedMethod', ], 'undefinedMixinClassWithMethodCall_WithMagicMethod' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedMagicMethod', ], 'undefinedMixinClassWithStaticMethodCall' => [ + // Similar test in MixinsDeepTest.php 'code' => ' 'UndefinedMethod', ], 'undefinedMixinClassWithStaticMethodCall_WithMagicMethod' => [ + // Similar test in MixinsDeepTest.php 'code' => ' [ + 'code' => <<<'PHP' + getString(); + $b = $baz->getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'NamedMixinsWithoutT_WithStaticMethods' => [ + 'code' => <<<'PHP' + [ + '$a' => 'string', + '$b' => 'int', + ], + 'ignored_issues' => ['InvalidReturnType'], + ], + 'NamedMixinsWithoutT_WithObjectProperties' => [ + 'code' => <<<'PHP' + propString; + $b = $baz->propInt; + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'NamedMixinsWithT_WithObjectMethods' => [ + 'code' => <<<'PHP' + + */ + abstract class Bar { + /** + * @return T2 + */ + abstract public function getInt(); + + public function __call(string $name, array $arguments) {} + } + + /** + * @template T1 + * @template T2 + * @mixin Bar + */ + class Baz { + public function __call(string $name, array $arguments) {} + } + + /** @var Baz */ + $baz = new Baz(); + $a = $baz->getString(); + $b = $baz->getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'NamedMixinsWithT_WithStaticMethods' => [ + 'code' => <<<'PHP' + + */ + abstract class Bar { + /** + * @return T2 + */ + public static function getInt() {} + + public static function __callStatic(string $name, array $arguments) {} + } + + /** + * @template T1 + * @template T2 + * @mixin Bar + */ + class Baz { + public static function __callStatic(string $name, array $arguments) {} + } + + /** @mixin Baz */ + class Bat { + public static function __callStatic(string $name, array $arguments) {} + } + $a = Bat::getString(); + $b = Bat::getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + 'ignored_issues' => ['InvalidReturnType'], + ], + 'TemplatedMixins_WithObjectMethods' => [ + 'code' => <<<'PHP' + > */ + $baz = new Baz(); + $a = $baz->getString(); + $b = $baz->getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'CombineNamedAndTemplatedMixins_WithObjectMethods' => [ + 'code' => <<<'PHP' + + */ + class Baz { + public function __call(string $name, array $arguments) {} + } + + /** @var Baz */ + $baz = new Baz(); + $a = $baz->getString(); + $b = $baz->getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'CombineTemplatedAndNamedMixinsWithoutT_WithObjectMethods' => [ + 'code' => <<<'PHP' + $baz */ + $baz = new Baz(); + $a = $baz->getString(); + $b = $baz->getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'CombineTemplatedAndNamedMixinsWithT_WithObjectMethods' => [ + 'code' => <<<'PHP' + + */ + abstract class Bar { + abstract public function getInt(): int; + public function __call(string $name, array $arguments) {} + } + + /** + * @template T + * @mixin T + */ + class Baz { + public function __call(string $name, array $arguments) {} + } + + /** @var Baz $baz */ + $baz = new Baz(); + $a = $baz->getString(); + $b = $baz->getInt(); + PHP, + 'assertions' => [ + '$a' => 'string', + '$b' => 'int', + ], + ], + 'LowMixinCollision_WithObjectMethods' => [ + 'code' => <<<'PHP' + notExistsMethod(); + PHP, + 'assertions' => [ + '$a' => 'mixed', + ], + 'ignored_issues' => ['MixedAssignment'], + ], + 'DeepMixinCollision_WithObjectMethods' => [ + 'code' => <<<'PHP' + notExistsMethod(); + PHP, + 'assertions' => [ + '$a' => 'mixed', + ], + 'ignored_issues' => ['MixedAssignment'], + ], + 'LowMixinCollision_WithStaticMethods' => [ + 'code' => <<<'PHP' + [ + '$a' => 'mixed', + ], + 'ignored_issues' => ['MixedAssignment'], + ], + 'DeepMixinCollision_WithStaticMethods' => [ + 'code' => <<<'PHP' + [ + '$a' => 'mixed', + ], + 'ignored_issues' => ['MixedAssignment'], + ], + 'LowMixinCollision_WithProperties' => [ + 'code' => <<<'PHP' + notExistsProp; + PHP, + 'assertions' => [ + '$a' => 'mixed', + ], + 'ignored_issues' => ['MixedAssignment'], + ], + 'DeepMixinCollision_WithProperties' => [ + 'code' => <<<'PHP' + notExistsProp; + PHP, + 'assertions' => [ + '$a' => 'mixed', + ], + 'ignored_issues' => ['MixedAssignment'], + ], + ]; + } + + public function providerInvalidCodeParse(): iterable + { + return [ + 'undefinedMixinClass' => [ + // Similar test in MixinAnnotationTest.php + 'code' => ' 'UndefinedDocblockClass', + ], + 'undefinedMixinClassWithPropertyFetch' => [ + // Similar test in MixinAnnotationTest.php + 'code' => 'foo;', + 'error_message' => 'UndefinedPropertyFetch', + ], + 'undefinedMixinClassWithPropertyFetch_WithMagicMethod' => [ + // Similar test in MixinAnnotationTest.php + 'code' => 'foo;', + 'error_message' => 'UndefinedMagicPropertyFetch', + ], + 'undefinedMixinClassWithPropertyAssignment' => [ + // Similar test in MixinAnnotationTest.php + 'code' => 'foo = "bar";', + 'error_message' => 'UndefinedPropertyAssignment', + ], + 'undefinedMixinClassWithPropertyAssignment_WithMagicMethod' => [ + // Similar test in MixinAnnotationTest.php + 'code' => 'foo = "bar";', + 'error_message' => 'UndefinedMagicPropertyAssignment', + ], + 'undefinedMixinClassWithMethodCall' => [ + // Similar test in MixinAnnotationTest.php + 'code' => 'foo();', + 'error_message' => 'UndefinedMethod', + ], + 'undefinedMixinClassWithMethodCall_WithMagicMethod' => [ + // Similar test in MixinAnnotationTest.php + 'code' => 'foo();', + 'error_message' => 'UndefinedMagicMethod', + ], + 'undefinedMixinClassWithStaticMethodCall' => [ + // Similar test in MixinAnnotationTest.php + 'code' => ' 'UndefinedMethod', + ], + 'undefinedMixinClassWithStaticMethodCall_WithMagicMethod' => [ + // Similar test in MixinAnnotationTest.php + 'code' => ' 'UndefinedMagicMethod', + ], + ]; + } +}