diff --git a/.github/workflows/bcc.yml b/.github/workflows/bcc.yml index 8bc58be9bb3..5cc8c319458 100644 --- a/.github/workflows/bcc.yml +++ b/.github/workflows/bcc.yml @@ -13,7 +13,7 @@ jobs: tools: composer:v2 coverage: none - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -24,7 +24,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.gitignore b/.gitignore index a060c7ed342..3d4eb36dc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /tests/fixtures/symlinktest/* .idea/ +.vscode/ diff --git a/UPGRADING.md b/UPGRADING.md index 9f2ad3f0dd1..6e176c69ebd 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,14 @@ # Upgrading from Psalm 4 to Psalm 5 ## Changed +- [BC] `Psalm\Type\Union`s are now partially immutable, mutator methods were removed and moved into `Psalm\Type\MutableUnion`. + To modify a union type, use the new `Psalm\Type\Union::getBuilder` method to turn a `Psalm\Type\Union` into a `Psalm\Type\MutableUnion`: once you're done, use `Psalm\Type\MutableUnion::freeze` to get a new `Psalm\Type\Union`. + Methods removed from `Psalm\Type\Union` and moved into `Psalm\Type\MutableUnion`: + - `replaceTypes` + - `addType` + - `removeType` + - `substitute` + - `replaceClassLike` + - [BC] TPositiveInt has been removed and replaced by TIntRange - [BC] The parameter `$php_version` of `Psalm\Type\Atomic::create()` renamed diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 95396607e4d..a1dcc3a49a3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -68,7 +68,7 @@ - + $assertion->rule[0] $assertion->rule[0] $assertion->rule[0] @@ -90,12 +90,6 @@ $expr->getArgs()[0] $expr->getArgs()[0] $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] $expr->getArgs()[1] $expr->getArgs()[1] $get_debug_type_expr->getArgs()[0] @@ -112,6 +106,9 @@ + + verifyType + $non_existent_method_ids[0] $parts[1] @@ -119,9 +116,8 @@ - + $arg_function_params[$argument_offset][0] - $array_type->getGenericArrayType()->getChildNodes()[0] @@ -253,10 +249,9 @@ - + $l[4] $r[4] - $var_line_parts[0] @@ -289,6 +284,26 @@ $cs[0] + + + $callable + + + TCallable|TClosure|null + + + + + get + get + get + getClassTemplateTypes + has + + + $candidate_param_type->from_template_default + + $combination->array_type_params[1] @@ -311,47 +326,153 @@ array_keys($template_type_map[$template_param_name])[0] - - - VirtualClass - - - - - VirtualFunction - - - - - VirtualInterface - - - - - VirtualTrait - - - - - VirtualConst - - + + classExtendsOrImplements + classExtendsOrImplements + classExtendsOrImplements + classOrInterfaceExists + classOrInterfaceExists + classOrInterfaceExists + getMappedGenericTypeParams + interfaceExtends + interfaceExtends + interfaceExtends + array_keys($template_type_map[$value])[0] + + + replace + replace + replace + replace + + + + getMappedGenericTypeParams + replace + replace + $this->type_params[1] + + + getMostSpecificTypeFromBounds + + + + + replace + + + + + getString + getString + replace + replace + + + $cloned->value_param + + + + + replace + + + + + combine + combine + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + replace + replace + + + $key_type->possibly_undefined + $value_type->possibly_undefined + $value_type->possibly_undefined + + + + + replace + replace + + + $cloned->type_param + + + + + replace + replace + + + $type->possibly_undefined + $type->possibly_undefined + + + + + replace + + + + + replace + + + + + replace + + + + + $allow_mutations + $by_ref + $failed_reconciliation + $from_template_default + $has_mutations + $initialized_class + $reference_free + + $type[0] $type[0][0] + + + $ignore_isset + + + + + allFloatLiterals + allFloatLiterals + + + + + UndefinedMethod + + $subNodes['expr'] diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 70b11d39fd9..0afbe3de4e2 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -485,11 +485,14 @@ public function update( if ((!$new_type || !$old_type->equals($new_type)) && ($new_type || count($existing_type->getAtomicTypes()) > 1) ) { - $existing_type->substitute($old_type, $new_type); + $existing_type = $existing_type + ->getBuilder() + ->substitute($old_type, $new_type); if ($new_type && $new_type->from_docblock) { - $existing_type->setFromDocblock(); + $existing_type = $existing_type->setFromDocblock(); } + $existing_type = $existing_type->freeze(); $updated_vars[$var_id] = true; } @@ -770,18 +773,23 @@ public function removeDescendents( $statements_analyzer ); - foreach ($this->vars_in_scope as $var_id => $type) { + foreach ($this->vars_in_scope as $var_id => &$type) { if (preg_match('/' . preg_quote($remove_var_id, '/') . '[\]\[\-]/', $var_id)) { $this->remove($var_id, false); } + $builder = null; foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof DependentType && $atomic_type->getVarId() === $remove_var_id ) { - $type->addType($atomic_type->getReplacement()); + $builder ??= $type->getBuilder(); + $builder->addType($atomic_type->getReplacement()); } } + if ($builder) { + $type = $builder->freeze(); + } } } diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 943d0b235ed..d3eaf7107a4 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -295,8 +295,7 @@ public function analyze( $mixins = array_merge($storage->templatedMixins, $storage->namedMixins); $union = new Union($mixins); - $static_self = new TNamedObject($storage->name); - $static_self->is_static = true; + $static_self = new TNamedObject($storage->name, true); $union = TypeExpander::expandUnion( $codebase, @@ -768,7 +767,7 @@ public static function addContextProperties( // Get actual types used for templates (to support @template-covariant) $template_standins = new TemplateResult($lower_bounds, []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $guide_property_type, $template_standins, $codebase, @@ -801,12 +800,12 @@ public static function addContextProperties( $template_result = new TemplateResult([], $lower_bounds); - TemplateInferredTypeReplacer::replace( + $guide_property_type = TemplateInferredTypeReplacer::replace( $guide_property_type, $template_result, $codebase ); - TemplateInferredTypeReplacer::replace( + $property_type = TemplateInferredTypeReplacer::replace( $property_type, $template_result, $codebase @@ -1233,8 +1232,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $method_context->collect_nonprivate_initializations = !$uninitialized_private_properties; $method_context->self = $fq_class_name; - $this_atomic_object_type = new TNamedObject($fq_class_name); - $this_atomic_object_type->is_static = !$storage->final; + $this_atomic_object_type = new TNamedObject($fq_class_name, !$storage->final); $method_context->vars_in_scope['$this'] = new Union([$this_atomic_object_type]); $method_context->vars_possibly_in_scope['$this'] = true; @@ -1304,8 +1302,12 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { ); } elseif (!$property_storage->has_default) { if (isset($this->inferred_property_types[$property_name])) { - $this->inferred_property_types[$property_name]->addType(new TNull()); - $this->inferred_property_types[$property_name]->setFromDocblock(); + $this->inferred_property_types[$property_name] = + $this->inferred_property_types[$property_name] + ->getBuilder() + ->addType(new TNull()) + ->setFromDocblock(true) + ->freeze(); } } } @@ -1553,7 +1555,7 @@ private function analyzeProperty( } if ($suggested_type && !$property_storage->has_default && $property_storage->is_static) { - $suggested_type->addType(new TNull()); + $suggested_type = $suggested_type->getBuilder()->addType(new TNull())->freeze(); } if ($suggested_type && !$suggested_type->isNull()) { diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 4a3ce8a2fc3..59d1165c014 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -92,8 +92,7 @@ public static function analyzeExpression( /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this']; } elseif ($context->self) { - $this_atomic = new TNamedObject($context->self); - $this_atomic->is_static = true; + $this_atomic = new TNamedObject($context->self, true); $use_context->vars_in_scope['$this'] = new Union([$this_atomic]); } diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 5af3a43e8a3..925af7f35d6 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -159,7 +159,8 @@ public static function arrayToDocblocks( $var_type_tokens, null, $template_type_map ?: [], - $type_aliases ?: [] + $type_aliases ?: [], + true ); } catch (TypeParseTreeException $e) { throw new DocblockParseException( @@ -173,8 +174,6 @@ public static function arrayToDocblocks( ); } - $defined_type->setFromDocblock(); - $var_comment = new VarDocblockComment(); $var_comment->type = $defined_type; $var_comment->var_id = $var_id; @@ -375,7 +374,7 @@ public static function splitDocLine(string $return_block): array $remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); if ($remaining) { - return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining)); + return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining) ?: []); } return [$type]; diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 76e23c93eae..7e18ff9a250 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -168,8 +168,7 @@ public static function verifyReturnType( // only add null if we have a return statement elsewhere and it wasn't void foreach ($inferred_return_type_parts as $inferred_return_type_part) { if (!$inferred_return_type_part->isVoid()) { - $atomic_null = new TNull(); - $atomic_null->from_docblock = true; + $atomic_null = new TNull(true); $inferred_return_type_parts[] = new Union([$atomic_null]); break; } @@ -577,15 +576,13 @@ public static function verifyReturnType( return false; } } - } elseif (!$inferred_return_type->hasMixed() - && !UnionTypeComparator::isContainedBy( - $codebase, - $declared_return_type, - $inferred_return_type, - false, - false - ) - ) { + } elseif (!UnionTypeComparator::isContainedBy( + $codebase, + $declared_return_type, + $inferred_return_type, + false, + false + )) { if ($codebase->alter_code) { if (isset($project_analyzer->getIssuesToFix()['LessSpecificReturnType']) && !in_array('LessSpecificReturnType', $suppressed_issues) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 56b669a81fb..8b51864ea13 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -623,7 +623,7 @@ public function analyze( /** * @var TClosure */ - $closure_atomic = $function_type->getSingleAtomic(); + $closure_atomic = clone $function_type->getSingleAtomic(); if (($storage->return_type === $storage->signature_return_type) && (!$storage->return_type @@ -634,10 +634,14 @@ public function analyze( $storage->return_type )) ) { + /** @psalm-suppress InaccessibleProperty Acting on clone */ $closure_atomic->return_type = $closure_return_type; } + /** @psalm-suppress InaccessibleProperty Acting on clone */ $closure_atomic->is_pure = !$this->inferred_impure; + + $statements_analyzer->node_data->setType($this->function, new Union([$closure_atomic])); } } @@ -1012,8 +1016,9 @@ private function processParams( if ($signature_type && $signature_type_location && $signature_type->hasObjectType()) { $referenced_type = $signature_type; if ($referenced_type->isNullable()) { - $referenced_type = clone $referenced_type; + $referenced_type = $referenced_type->getBuilder(); $referenced_type->removeType('null'); + $referenced_type = $referenced_type->freeze(); } [$start, $end] = $signature_type_location->getSelectionBounds(); $codebase->analyzer->addOffsetReference( @@ -1845,18 +1850,21 @@ private function getFunctionInformation( $this_object_type = new TGenericObject( $context->self, - $template_params + $template_params, + false, + !$storage->final ); } else { - $this_object_type = new TNamedObject($context->self); + $this_object_type = new TNamedObject( + $context->self, + !$storage->final + ); } - $this_object_type->is_static = !$storage->final; - if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Union([$this_object_type]), $template_result, $codebase, @@ -1864,9 +1872,9 @@ private function getFunctionInformation( $this->storage->if_this_is_type ); - foreach ($context->vars_in_scope as $var_name => $var_type) { + foreach ($context->vars_in_scope as $var_name => &$var_type) { if (0 === mb_strpos($var_name, '$this->')) { - TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); + $var_type = TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); } } @@ -2003,14 +2011,11 @@ private function getFunctionInformation( $closure_type = new TClosure( 'Closure', $storage->params, - $closure_return_type + $closure_return_type, + $storage instanceof FunctionStorage ? $storage->pure : null, + $storage instanceof FunctionStorage ? $storage->byref_uses : [], ); - if ($storage instanceof FunctionStorage) { - $closure_type->byref_uses = $storage->byref_uses; - $closure_type->is_pure = $storage->pure; - } - $type_provider->setType( $this->function, new Union([ diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 58476245364..f6bfe8db4b9 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -362,7 +362,7 @@ private static function compareMethodParams( $guide_param_signature_type = $guide_param->type; $or_null_guide_param_signature_type = $guide_param->signature_type - ? clone $guide_param->signature_type + ? $guide_param->signature_type->getBuilder() : null; if ($or_null_guide_param_signature_type) { @@ -729,29 +729,34 @@ private static function compareMethodDocblockParams( } } - foreach ($implementer_method_storage_param_type->getAtomicTypes() as $k => $t) { + $builder = $implementer_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { if ($t instanceof TTemplateParam && strpos($t->defining_class, 'fn-') === 0 ) { - $implementer_method_storage_param_type->removeType($k); + $builder->removeType($k); foreach ($t->as->getAtomicTypes() as $as_t) { - $implementer_method_storage_param_type->addType($as_t); + $builder->addType($as_t); } } } + $implementer_method_storage_param_type = $builder->freeze(); - foreach ($guide_method_storage_param_type->getAtomicTypes() as $k => $t) { + $builder = $guide_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { if ($t instanceof TTemplateParam && strpos($t->defining_class, 'fn-') === 0 ) { - $guide_method_storage_param_type->removeType($k); + $builder->removeType($k); foreach ($t->as->getAtomicTypes() as $as_t) { - $guide_method_storage_param_type->addType($as_t); + $builder->addType($as_t); } } } + $guide_method_storage_param_type = $builder->freeze(); + unset($builder); if ($implementer_classlike_storage->template_extended_params) { self::transformTemplates( @@ -1055,7 +1060,7 @@ private static function compareMethodDocblockReturnTypes( private static function transformTemplates( array $template_extended_params, string $base_class_name, - Union $templated_type, + Union &$templated_type, Codebase $codebase ): void { if (isset($template_extended_params[$base_class_name])) { @@ -1092,7 +1097,7 @@ private static function transformTemplates( $template_result = new TemplateResult([], $template_types); - TemplateInferredTypeReplacer::replace( + $templated_type = TemplateInferredTypeReplacer::replace( $templated_type, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 209080eefef..6939ec53888 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -550,11 +550,8 @@ public static function checkIteratorType( } } elseif ($iterator_atomic_type instanceof TIterable) { if ($iterator_atomic_type->extra_types) { - $iterator_atomic_type_copy = clone $iterator_atomic_type; - $iterator_atomic_type_copy->extra_types = []; - $iterator_atomic_types = [$iterator_atomic_type_copy]; $iterator_atomic_types = array_merge( - $iterator_atomic_types, + [$iterator_atomic_type->setIntersectionTypes([])], $iterator_atomic_type->extra_types ); } else { @@ -736,10 +733,10 @@ public static function handleIterable( bool &$has_valid_iterator ): void { if ($iterator_atomic_type->extra_types) { - $iterator_atomic_type_copy = clone $iterator_atomic_type; - $iterator_atomic_type_copy->extra_types = []; - $iterator_atomic_types = [$iterator_atomic_type_copy]; - $iterator_atomic_types = array_merge($iterator_atomic_types, $iterator_atomic_type->extra_types); + $iterator_atomic_types = array_merge( + [$iterator_atomic_type->setIntersectionTypes([])], + $iterator_atomic_type->extra_types + ); } else { $iterator_atomic_types = [$iterator_atomic_type]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 1160f615394..68fc8398a9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -273,7 +273,7 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { && $codebase->interfaceExists($fq_catch_class) && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') ) { - $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); + return $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); } return $catch_class_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index f33c0d82ad4..52eb2c41d08 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -120,14 +120,14 @@ public static function analyze( // if this array looks like an object-like array, let's return that instead if (count($array_creation_info->property_types) !== 0) { - $atomic_type = new TKeyedArray($array_creation_info->property_types, $array_creation_info->class_strings); - if ($array_creation_info->can_create_objectlike) { - $atomic_type->sealed = true; - } else { - $atomic_type->previous_key_type = $item_key_type ?? Type::getArrayKey(); - $atomic_type->previous_value_type = $item_value_type ?? Type::getMixed(); - } - $atomic_type->is_list = $array_creation_info->all_list; + $atomic_type = new TKeyedArray( + $array_creation_info->property_types, + $array_creation_info->class_strings, + $array_creation_info->can_create_objectlike, + $array_creation_info->can_create_objectlike ? null : ($item_key_type ?? Type::getArrayKey()), + $array_creation_info->can_create_objectlike ? null : ($item_value_type ?? Type::getMixed()), + $array_creation_info->all_list + ); $stmt_type = new Union([$atomic_type]); @@ -225,10 +225,10 @@ public static function analyze( } if ($bad_types && $good_types) { - $item_key_type->substitute( + $item_key_type = $item_key_type->getBuilder()->substitute( TypeCombiner::combine($bad_types, $codebase), TypeCombiner::combine($good_types, $codebase) - ); + )->freeze(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 09e411a90c4..670fae607b4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -68,6 +68,7 @@ use Psalm\Storage\Assertion\NonEmptyCountable; use Psalm\Storage\Assertion\NotNonEmptyCountable; use Psalm\Storage\Assertion\Truthy; +use Psalm\Storage\Possibilities; use Psalm\Storage\PropertyStorage; use Psalm\Type; use Psalm\Type\Atomic; @@ -805,10 +806,7 @@ public static function processFunctionCall( } } elseif ($class_exists_check_type = self::hasClassExistsCheck($expr)) { if ($first_var_name) { - $class_string_type = new TClassString(); - if ($class_exists_check_type === 1) { - $class_string_type->is_loaded = true; - } + $class_string_type = new TClassString('object', null, $class_exists_check_type === 1); $if_types[$first_var_name] = [[new IsType($class_string_type)]]; } } elseif ($class_exists_check_type = self::hasTraitExistsCheck($expr)) { @@ -821,14 +819,12 @@ public static function processFunctionCall( } } elseif (self::hasEnumExistsCheck($expr)) { if ($first_var_name) { - $class_string = new TClassString(); - $class_string->is_enum = true; + $class_string = new TClassString('object', null, false, false, true); $if_types[$first_var_name] = [[new IsType($class_string)]]; } } elseif (self::hasInterfaceExistsCheck($expr)) { if ($first_var_name) { - $class_string = new TClassString(); - $class_string->is_interface = true; + $class_string = new TClassString('object', null, false, true, false); $if_types[$first_var_name] = [[new IsType($class_string)]]; } } elseif (self::hasFunctionExistsCheck($expr)) { @@ -962,15 +958,15 @@ protected static function processCustomAssertion( foreach ($if_true_assertions as $assertion) { $if_types = []; - $assertion = clone $assertion; + $newRules = []; - foreach ($assertion->rule as $i => $rule) { + foreach ($assertion->rule as $rule) { $rule_type = $rule->getAtomicType(); if ($rule_type instanceof TClassConstant) { $codebase = $source->getCodebase(); - $assertion->rule[$i]->setAtomicType( + $newRules[] = $rule->setAtomicType( TypeExpander::expandAtomic( $codebase, $rule_type, @@ -979,9 +975,13 @@ protected static function processCustomAssertion( null )[0] ); + } else { + $newRules []= $rule; } } + $assertion = new Possibilities($assertion->var_id, $newRules); + if (is_int($assertion->var_id) && isset($expr->getArgs()[$assertion->var_id])) { if ($assertion->var_id === 0) { $var_name = $first_var_name; @@ -994,7 +994,7 @@ protected static function processCustomAssertion( } if ($var_name) { - $if_types[$var_name] = [[clone $assertion->rule[0]]]; + $if_types[$var_name] = [[$assertion->rule[0]]]; } } elseif ($assertion->var_id === '$this') { if (!$expr instanceof PhpParser\Node\Expr\MethodCall) { @@ -1014,7 +1014,7 @@ protected static function processCustomAssertion( ); if ($var_id) { - $if_types[$var_id] = [[clone $assertion->rule[0]]]; + $if_types[$var_id] = [[$assertion->rule[0]]]; } } elseif (is_string($assertion->var_id)) { $is_function = substr($assertion->var_id, -2) === '()'; @@ -1083,7 +1083,7 @@ protected static function processCustomAssertion( ); continue; } - $if_types[$assertion_var_id] = [[clone $assertion->rule[0]]]; + $if_types[$assertion_var_id] = [[$assertion->rule[0]]]; } if ($if_types) { @@ -1096,15 +1096,15 @@ protected static function processCustomAssertion( foreach ($if_false_assertions as $assertion) { $if_types = []; - $assertion = clone $assertion; + $newRules = []; - foreach ($assertion->rule as $i => $rule) { + foreach ($assertion->rule as $rule) { $rule_type = $rule->getAtomicType(); if ($rule_type instanceof TClassConstant) { $codebase = $source->getCodebase(); - $assertion->rule[$i]->setAtomicType( + $newRules []= $rule->setAtomicType( TypeExpander::expandAtomic( $codebase, $rule_type, @@ -1113,9 +1113,13 @@ protected static function processCustomAssertion( null )[0] ); + } else { + $newRules []= $rule; } } + $assertion = new Possibilities($assertion->var_id, $newRules); + if (is_int($assertion->var_id) && isset($expr->getArgs()[$assertion->var_id])) { if ($assertion->var_id === 0) { $var_name = $first_var_name; @@ -1128,7 +1132,7 @@ protected static function processCustomAssertion( } if ($var_name) { - $if_types[$var_name] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_name] = [[$assertion->rule[0]->getNegation()]]; } } elseif ($assertion->var_id === '$this' && $expr instanceof PhpParser\Node\Expr\MethodCall) { $var_id = ExpressionIdentifier::getExtendedVarId( @@ -1138,7 +1142,7 @@ protected static function processCustomAssertion( ); if ($var_id) { - $if_types[$var_id] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_id] = [[$assertion->rule[0]->getNegation()]]; } } elseif (is_string($assertion->var_id)) { $is_function = substr($assertion->var_id, -2) === '()'; @@ -1190,7 +1194,7 @@ protected static function processCustomAssertion( } } - $rule = clone $assertion->rule[0]->getNegation(); + $rule = $assertion->rule[0]->getNegation(); $assertion_var_id = str_replace($var_id, $arg_var_id, $assertion->var_id); @@ -1200,7 +1204,7 @@ protected static function processCustomAssertion( if (strpos($var_id, 'self::') === 0) { $var_id = $this_class_name.'::'.substr($var_id, 6); } - $if_types[$var_id] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_id] = [[$assertion->rule[0]->getNegation()]]; } else { IssueBuffer::maybeAdd( new InvalidDocblock( @@ -1245,10 +1249,10 @@ protected static function getInstanceOfAssertions( if ($this_class_name && (in_array(strtolower($stmt->class->parts[0]), ['self', 'static'], true))) { - $named_object =new TNamedObject($this_class_name); + $is_static = $stmt->class->parts[0] === 'static'; + $named_object = new TNamedObject($this_class_name, $is_static); - if ($stmt->class->parts[0] === 'static') { - $named_object->is_static = true; + if ($is_static) { return [new IsIdentical($named_object)]; } @@ -3345,7 +3349,7 @@ private static function getGetclassEqualityAssertions( new IsIdentical(new TTemplateParam( $type_part->param_name, $type_part->as_type - ? new Union([clone $type_part->as_type]) + ? new Union([$type_part->as_type]) : Type::getObject(), $type_part->defining_class )) @@ -3533,8 +3537,7 @@ private static function getIsaAssertions( if ($class_node->parts === ['static']) { if ($this_class_name) { - $object = new TNamedObject($this_class_name); - $object->is_static = true; + $object = new TNamedObject($this_class_name, true); $if_types[$first_var_name] = [[new IsAClass($object, $third_arg_value === 'true')]]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index c78f72ef44a..fecc1cda373 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -218,7 +218,9 @@ public static function updateArrayType( $new_child_type = $root_type; } + $new_child_type = $new_child_type->getBuilder(); $new_child_type->removeType('null'); + $new_child_type = $new_child_type->freeze(); if (!$root_type->hasObjectType()) { $root_type = $new_child_type; @@ -295,63 +297,74 @@ private static function updateTypeWithKeyValues( $has_matching_objectlike_property = false; $has_matching_string = false; - $child_stmt_type = clone $child_stmt_type; - + $changed = false; + $types = []; foreach ($child_stmt_type->getAtomicTypes() as $type) { + $old_type = $type; if ($type instanceof TTemplateParam) { - $type->as = self::updateTypeWithKeyValues( + $type = $type->replaceAs(self::updateTypeWithKeyValues( $codebase, $type->as, $current_type, $key_values - ); - + )); $has_matching_objectlike_property = true; - - $child_stmt_type->substitute(new Union([$type]), $type->as); - - continue; - } - - foreach ($key_values as $key_value) { - if ($type instanceof TKeyedArray) { - if (isset($type->properties[$key_value->value])) { + } elseif ($type instanceof TKeyedArray) { + $properties = $type->properties; + foreach ($key_values as $key_value) { + if (isset($properties[$key_value->value])) { $has_matching_objectlike_property = true; - $type->properties[$key_value->value] = clone $current_type; + $properties[$key_value->value] = clone $current_type; } - } elseif ($type instanceof TString - && $key_value instanceof TLiteralInt - ) { - $has_matching_string = true; - - if ($type instanceof TLiteralString - && $current_type->isSingleStringLiteral() - ) { - $new_char = $current_type->getSingleStringLiteral()->value; - - if (strlen($new_char) === 1) { - $type->value[0] = $new_char; + } + $type = $type->setProperties($properties); + } elseif ($type instanceof TString) { + foreach ($key_values as $key_value) { + if ($key_value instanceof TLiteralInt) { + $has_matching_string = true; + + if ($type instanceof TLiteralString + && $current_type->isSingleStringLiteral() + ) { + $new_char = $current_type->getSingleStringLiteral()->value; + + if (strlen($new_char) === 1 && $type->value[0] !== $new_char) { + $v = $type->value; + $v[0] = $new_char; + $changed = true; + $type = new TLiteralString($v); + break; + } } } - } elseif ($type instanceof TNonEmptyList - && $key_value instanceof TLiteralInt - && count($key_values) === 1 - ) { + } + } elseif ($type instanceof TNonEmptyList + && count($key_values) === 1 + && $key_values[0] instanceof TLiteralInt + ) { + $key_value = $key_values[0]; + $count = ($type->count ?? $type->min_count) ?? 1; + if ($key_value->value < $count) { $has_matching_objectlike_property = true; - $type->type_param = Type::combineUnionTypes( + $changed = true; + $type = $type->replaceTypeParam(Type::combineUnionTypes( clone $current_type, $type->type_param, $codebase, true, false - ); + )); } } + $types[$type->getKey()] = $type; + $changed = $changed || $old_type !== $type; } - $child_stmt_type->bustCache(); + if ($changed) { + $child_stmt_type = $child_stmt_type->getBuilder()->setTypes($types)->freeze(); + } if (!$has_matching_objectlike_property && !$has_matching_string) { if (count($key_values) === 1) { @@ -361,11 +374,10 @@ private static function updateTypeWithKeyValues( [$key_value->value => clone $current_type], $key_value instanceof TLiteralClassString ? [$key_value->value => true] - : null + : null, + true ); - $object_like->sealed = true; - $array_assignment_type = new Union([ $object_like, ]); @@ -511,9 +523,7 @@ private static function updateArrayAssignmentChildType( } } - $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts( - $key_type - ); + $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts($key_type); } else { $array_atomic_key_type = Type::getArrayKey(); } @@ -557,7 +567,7 @@ private static function updateArrayAssignmentChildType( ] ); - TemplateInferredTypeReplacer::replace( + $value_type = TemplateInferredTypeReplacer::replace( $value_type, $template_result, $codebase @@ -600,10 +610,12 @@ private static function updateArrayAssignmentChildType( } elseif ($atomic_root_types['array'] instanceof TNonEmptyArray || $atomic_root_types['array'] instanceof TNonEmptyList ) { + /** @psalm-suppress InaccessibleProperty We just created this object */ $array_atomic_type->count = $atomic_root_types['array']->count; } elseif ($atomic_root_types['array'] instanceof TKeyedArray && $atomic_root_types['array']->sealed ) { + /** @psalm-suppress InaccessibleProperty We just created this object */ $array_atomic_type->count = count($atomic_root_types['array']->properties); $from_countable_object_like = true; @@ -654,7 +666,9 @@ private static function updateArrayAssignmentChildType( || $atomic_root_types['array'] instanceof TNonEmptyList) && $atomic_root_types['array']->count !== null ) { - $atomic_root_types['array']->count++; + $atomic_root_types['array'] = + $atomic_root_types['array']->setCount($atomic_root_types['array']->count+1); + $new_child_type = new Union($atomic_root_types); } } @@ -742,17 +756,21 @@ private static function analyzeNestedArrayAssignment( $is_last = $i === count($child_stmts) - 1; + $child_stmt_dim_type_or_int = $child_stmt_dim_type ?? Type::getInt(); $child_stmt_type = ArrayFetchAnalyzer::getArrayAccessTypeGivenOffset( $statements_analyzer, $child_stmt, $array_type, - $child_stmt_dim_type ?? Type::getInt(), + $child_stmt_dim_type_or_int, true, $extended_var_id, $context, $assign_value, !$is_last ? null : $assignment_type ); + if ($child_stmt->dim) { + $statements_analyzer->node_data->setType($child_stmt->dim, $child_stmt_dim_type_or_int); + } $statements_analyzer->node_data->setType( $child_stmt, @@ -886,8 +904,10 @@ private static function analyzeNestedArrayAssignment( ); } + $new_child_type = $new_child_type->getBuilder(); $new_child_type->removeType('null'); $new_child_type->possibly_undefined = false; + $new_child_type = $new_child_type->freeze(); if (!$child_stmt_type->hasObjectType()) { $child_stmt_type = $new_child_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index cbbf381a410..ea9bf51f9ff 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -875,6 +875,8 @@ private static function analyzeRegularAssignment( /** * @param list $invalid_assignment_types + * + * @psalm-suppress ComplexMethod Unavoidably complex method */ private static function analyzeAtomicAssignment( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c787b2443c9..da243bb3e9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -716,7 +716,7 @@ public static function assignTypeFromVarDocblock( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $var_comment_type->check( $statements_analyzer, @@ -1497,7 +1497,7 @@ private static function analyzeDestructuringAssignment( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $new_assign_type = $var_comment_type; break; @@ -1561,7 +1561,10 @@ private static function analyzeDestructuringAssignment( if (($context->error_suppressing && ($offset || $can_be_empty)) || $has_null ) { - $context->vars_in_scope[$list_var_id]->addType(new TNull); + $context->vars_in_scope[$list_var_id] = $context->vars_in_scope[$list_var_id] + ->getBuilder() + ->addType(new TNull) + ->freeze(); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 7a85011e61d..4e4668dd4ac 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -292,6 +292,8 @@ private static function getNumericalType($result): Union /** * @param string[] $invalid_left_messages * @param string[] $invalid_right_messages + * + * @psalm-suppress ComplexMethod Unavoidably complex method. */ private static function analyzeOperands( ?StatementsSource $statements_source, @@ -576,8 +578,11 @@ private static function analyzeOperands( } } - $new_keyed_array = new TKeyedArray($properties); - $new_keyed_array->sealed = $left_type_part->sealed && $right_type_part->sealed; + $new_keyed_array = new TKeyedArray( + $properties, + null, + $left_type_part->sealed && $right_type_part->sealed + ); $result_type_member = new Union([$new_keyed_array]); } else { $result_type_member = TypeCombiner::combine( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index 09e1c693993..67f0efb888f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -36,6 +36,7 @@ use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -189,9 +190,11 @@ public static function analyze( } if (!$literal_concat) { - $numeric_type = Type::getNumericString(); - $numeric_type->addType(new TInt()); - $numeric_type->addType(new TFloat()); + $numeric_type = new Union([ + new TNumericString, + new TInt, + new TFloat + ]); $left_is_numeric = UnionTypeComparator::isContainedBy( $codebase, $left_type, @@ -220,8 +223,7 @@ public static function analyze( } } - $lowercase_type = clone $numeric_type; - $lowercase_type->addType(new TLowercaseString()); + $lowercase_type = $numeric_type->getBuilder()->addType(new TLowercaseString())->freeze(); $all_lowercase = UnionTypeComparator::isContainedBy( $codebase, @@ -233,8 +235,7 @@ public static function analyze( $lowercase_type ); - $non_empty_string = clone $numeric_type; - $non_empty_string->addType(new TNonEmptyString()); + $non_empty_string = $numeric_type->getBuilder()->addType(new TNonEmptyString())->freeze(); $left_non_empty = UnionTypeComparator::isContainedBy( $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php index 878dc11eca8..0aae409e996 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -44,12 +44,13 @@ public static function analyze( $unacceptable_type = null; $has_valid_operand = false; + $stmt_expr_type = $stmt_expr_type->getBuilder(); foreach ($stmt_expr_type->getAtomicTypes() as $type_string => $type_part) { if ($type_part instanceof TInt || $type_part instanceof TString) { if ($type_part instanceof TLiteralInt) { - $type_part->value = ~$type_part->value; + $type_part = new TLiteralInt(~$type_part->value); } elseif ($type_part instanceof TLiteralString) { - $type_part->value = ~$type_part->value; + $type_part = new TLiteralString(~$type_part->value); } $acceptable_types[] = $type_part; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index fc3d6eb73fd..6a2078b02d9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -33,12 +33,11 @@ public static function analyze( $stmt_type = Type::getBool(); if ($expr_type) { if ($expr_type->isAlwaysTruthy()) { - $stmt_type = Type::getFalse(); + $stmt_type = Type::getFalse($expr_type->from_docblock); } elseif ($expr_type->isAlwaysFalsy()) { - $stmt_type = Type::getTrue(); + $stmt_type = Type::getTrue($expr_type->from_docblock); } - $stmt_type->from_docblock = $expr_type->from_docblock; $stmt_type->parent_nodes = $expr_type->parent_nodes; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 31c8ddc4bd2..5342ed14419 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -58,6 +58,7 @@ use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; use function count; @@ -834,6 +835,7 @@ public static function verifyType( if ($param_type->hasCallableType() && $param_type->isSingle()) { // we do this replacement early because later we don't have access to the // $statements_analyzer, which is necessary to understand string function names + $input_type = $input_type->getBuilder(); foreach ($input_type->getAtomicTypes() as $key => $atomic_type) { if (!$atomic_type instanceof TLiteralString || InternalCallMapHandler::inCallMap($atomic_type->value) @@ -854,6 +856,7 @@ public static function verifyType( $input_type->addType($candidate_callable); } } + $input_type = $input_type->freeze(); } $union_comparison_results = new TypeComparisonResult(); @@ -1349,21 +1352,30 @@ private static function coerceValueAfterGatekeeperArgument( } if (!$input_type_changed && $param_type->from_docblock && !$input_type->hasMixed()) { - $input_type = clone $input_type; - + $types = $input_type->getAtomicTypes(); foreach ($param_type->getAtomicTypes() as $param_atomic_type) { if ($param_atomic_type instanceof TGenericObject) { - foreach ($input_type->getAtomicTypes() as $input_atomic_type) { + foreach ($types as &$input_atomic_type) { if ($input_atomic_type instanceof TGenericObject && $input_atomic_type->value === $param_atomic_type->value ) { + $new_type_params = []; foreach ($input_atomic_type->type_params as $i => $type_param) { if ($type_param->isNever() && isset($param_atomic_type->type_params[$i])) { $input_type_changed = true; - $input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i]; + $new_type_params[$i] = $param_atomic_type->type_params[$i]; } } + if ($new_type_params) { + $input_atomic_type = new TGenericObject( + $input_atomic_type->value, + [...$input_atomic_type->type_params, ...$new_type_params], + $input_atomic_type->remapped_params, + false, + $input_atomic_type->extra_types + ); + } } } } @@ -1372,6 +1384,8 @@ private static function coerceValueAfterGatekeeperArgument( if (!$input_type_changed) { return; } + + $input_type = new Union($types); } $var_id = ExpressionIdentifier::getVarId( @@ -1384,23 +1398,15 @@ private static function coerceValueAfterGatekeeperArgument( $was_cloned = false; if ($input_type->isNullable() && !$param_type->isNullable()) { - $input_type = clone $input_type; + $input_type = $input_type->getBuilder(); $was_cloned = true; $input_type->removeType('null'); + $input_type = $input_type->freeze(); } if ($input_type->getId() === $param_type->getId()) { if ($input_type->from_docblock) { - if (!$was_cloned) { - $was_cloned = true; - $input_type = clone $input_type; - } - - $input_type->from_docblock = false; - - foreach ($input_type->getAtomicTypes() as $atomic_type) { - $atomic_type->from_docblock = false; - } + $input_type = $input_type->setFromDocblock(false); } } elseif ($input_type->hasMixed() && $signature_param_type) { $was_cloned = true; @@ -1426,20 +1432,24 @@ private static function coerceValueAfterGatekeeperArgument( if ($unpack) { if ($unpacked_atomic_array instanceof TList) { - $unpacked_atomic_array = clone $unpacked_atomic_array; - $unpacked_atomic_array->type_param = $input_type; + $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParam($input_type); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TArray) { - $unpacked_atomic_array = clone $unpacked_atomic_array; - $unpacked_atomic_array->type_params[1] = $input_type; + $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParams([ + $unpacked_atomic_array->type_params[0], + $input_type + ]); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TKeyedArray && $unpacked_atomic_array->is_list ) { - $unpacked_atomic_array = $unpacked_atomic_array->getList(); - $unpacked_atomic_array->type_param = $input_type; + if ($unpacked_atomic_array->isNonEmpty()) { + $unpacked_atomic_array = new TNonEmptyList($input_type); + } else { + $unpacked_atomic_array = new TList($input_type); + } $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 68b90dc676d..a3d5f7f46ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -268,7 +268,7 @@ public static function analyze( if (null !== $inferred_arg_type && null !== $template_result && null !== $param && null !== $param->type) { $codebase = $statements_analyzer->getCodebase(); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, @@ -308,19 +308,6 @@ private static function handleArrayMapFilterArrayArg( ): void { $codebase = $statements_analyzer->getCodebase(); - $generic_param_type = new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TTemplateParam( - 'ArrayValue' . $argument_offset, - Type::getMixed(), - $method_id - ) - ]) - ]) - ]); - $template_types = ['ArrayValue' . $argument_offset => [$method_id => Type::getMixed()]]; $replace_template_result = new TemplateResult( @@ -330,8 +317,19 @@ private static function handleArrayMapFilterArrayArg( $existing_type = $statements_analyzer->node_data->getType($arg->value); - TemplateStandinTypeReplacer::replace( - $generic_param_type, + TemplateStandinTypeReplacer::fillTemplateResult( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TTemplateParam( + 'ArrayValue' . $argument_offset, + Type::getMixed(), + $method_id + ) + ]) + ]) + ]), $replace_template_result, $codebase, $statements_analyzer, @@ -494,7 +492,7 @@ private static function handleHighOrderFuncCallArg( // The map function expects callable(A):B as second param // We know that previous arg type is list where the int is the A template. // Then we can replace callable(A): B to callable(int):B using $inferred_template_result. - TemplateInferredTypeReplacer::replace( + $replaced_container_hof_atomic = TemplateInferredTypeReplacer::replace( $replaced_container_hof_atomic, $inferred_template_result, $codebase @@ -515,7 +513,7 @@ private static function handleHighOrderFuncCallArg( $actual_func_param->type->getTemplateTypes() && isset($container_hof_atomic->params[$offset]) ) { - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $actual_func_param->type, $high_order_template_result, $codebase, @@ -553,16 +551,18 @@ private static function handleClosureArg( $function_like_params = []; foreach ($template_result->lower_bounds as $template_name => $_) { + $t = new Union([ + new TTemplateParam( + $template_name, + Type::getMixed(), + $method_id + ) + ]); $function_like_params[] = new FunctionLikeParameter( 'function', false, - new Union([ - new TTemplateParam( - $template_name, - Type::getMixed(), - $method_id - ) - ]) + $t, + $t ); } @@ -601,7 +601,7 @@ private static function handleClosureArg( $context->calling_method_id ?: $context->calling_function_id ); - TemplateInferredTypeReplacer::replace( + $replaced_type = TemplateInferredTypeReplacer::replace( $replaced_type, $replace_template_result, $codebase @@ -726,7 +726,7 @@ public static function checkArgumentsMatch( $codebase = $statements_analyzer->getCodebase(); if ($method_id) { - if (!$in_call_map && $method_id instanceof MethodIdentifier) { + if ($method_id instanceof MethodIdentifier) { $fq_class_name = $method_id->fq_class_name; } @@ -769,7 +769,7 @@ public static function checkArgumentsMatch( } } - if ($function_params) { + if ($function_params && !$is_variadic) { foreach ($function_params as $function_param) { $is_variadic = $is_variadic || $function_param->is_variadic; } @@ -897,7 +897,8 @@ public static function checkArgumentsMatch( $array_type = $arg_value_type->getAtomicTypes()['array']; if ($array_type instanceof TKeyedArray) { - $key_types = $array_type->getGenericArrayType()->getChildNodes()[0]->getChildNodes(); + $array_type = $array_type->getGenericArrayType(); + $key_types = $array_type->type_params[0]->getAtomicTypes(); foreach ($key_types as $key_type) { if (!$key_type instanceof TLiteralString @@ -1234,7 +1235,7 @@ private static function handlePossiblyMatchingByRefParam( ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_type = TemplateInferredTypeReplacer::replace( $original_by_ref_type, $template_result, $codebase @@ -1259,7 +1260,7 @@ private static function handlePossiblyMatchingByRefParam( ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_out_type = TemplateInferredTypeReplacer::replace( $original_by_ref_out_type, $template_result, $codebase @@ -1386,16 +1387,18 @@ private static function evaluateArbitraryParam( $statements_analyzer ); - foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $type) { + $t = $context->vars_in_scope[$var_id]->getBuilder(); + foreach ($t->getAtomicTypes() as $type) { if ($type instanceof TArray && $type->isEmptyArray()) { - $context->vars_in_scope[$var_id]->removeType('array'); - $context->vars_in_scope[$var_id]->addType( + $t->removeType('array'); + $t->addType( new TArray( [Type::getArrayKey(), Type::getMixed()] ) ); } } + $context->vars_in_scope[$var_id] = $t->freeze(); } } @@ -1614,7 +1617,7 @@ private static function getProvisionalTemplateResultForFunctionLike( $calling_class_storage->final ?? false ); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $fleshed_out_param_type, $template_result, $codebase, @@ -1794,7 +1797,7 @@ private static function checkArgCount( $default_type = new Union([$default_type_atomic]); } - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index d39fc69dce5..87174eab776 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; @@ -35,7 +36,6 @@ use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; -use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; @@ -115,6 +115,7 @@ public static function checkArgumentsMatch( $max_closure_param_count = count($args) > 2 ? 2 : 1; } + $new = []; foreach ($closure_arg_type->getAtomicTypes() as $closure_type) { self::checkClosureType( $statements_analyzer, @@ -127,7 +128,13 @@ public static function checkArgumentsMatch( $array_arg_types, $check_functions ); + $new []= $closure_type; } + + $statements_analyzer->node_data->setType( + $closure_arg->value, + $closure_arg_type->getBuilder()->setTypes($new)->freeze() + ); } } @@ -266,7 +273,7 @@ public static function handleAddition( new Union([new TArray([$new_offset_type, Type::getMixed()])]) ); } elseif ($arg->unpack) { - $arg_value_type = clone $arg_value_type; + $arg_value_type = $arg_value_type->getBuilder(); foreach ($arg_value_type->getAtomicTypes() as $arg_value_atomic_type) { if ($arg_value_atomic_type instanceof TKeyedArray) { @@ -285,6 +292,7 @@ public static function handleAddition( $arg_value_type->addType($arg_value_atomic_type); } } + $arg_value_type = $arg_value_type->freeze(); $by_ref_type = Type::combineUnionTypes( $by_ref_type, @@ -292,9 +300,10 @@ public static function handleAddition( ); } else { if ($objectlike_list) { - array_unshift($objectlike_list->properties, $arg_value_type); + $properties = $objectlike_list->properties; + array_unshift($properties, $arg_value_type); - $by_ref_type = new Union([$objectlike_list]); + $by_ref_type = new Union([$objectlike_list->setProperties($properties)]); } elseif ($array_type instanceof TList) { $by_ref_type = Type::combineUnionTypes( $by_ref_type, @@ -508,33 +517,24 @@ public static function handleByRefArrayAdjustment( $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer); if (isset($context->vars_in_scope[$var_id])) { - $array_type = clone $context->vars_in_scope[$var_id]; + $array_atomic_types = []; - $array_atomic_types = $array_type->getAtomicTypes(); - - foreach ($array_atomic_types as $array_atomic_type) { + foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $array_atomic_type) { if ($array_atomic_type instanceof TKeyedArray) { if ($is_array_shift && $array_atomic_type->is_list) { - $array_atomic_type = clone $array_atomic_type; - $array_properties = $array_atomic_type->properties; array_shift($array_properties); if (!$array_properties) { - $array_atomic_type = new TList( - $array_atomic_type->previous_value_type ?: Type::getMixed() - ); - - $array_type->addType($array_atomic_type); + $array_atomic_types []= new TList(Type::getNever()); } else { - $array_atomic_type->properties = $array_properties; + $array_atomic_types []= $array_atomic_type->setProperties($array_properties); } + continue; } - if ($array_atomic_type instanceof TKeyedArray) { - $array_atomic_type = $array_atomic_type->getGenericArrayType(); - } + $array_atomic_type = $array_atomic_type->getGenericArrayType(); } if ($array_atomic_type instanceof TNonEmptyArray) { @@ -542,38 +542,39 @@ public static function handleByRefArrayAdjustment( if ($array_atomic_type->count === 1) { $array_atomic_type = new TArray( [ - new Union([new TNever]), - new Union([new TNever]), + Type::getNever(), + Type::getNever(), ] ); } else { - $array_atomic_type->count--; + $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); } } else { $array_atomic_type = new TArray($array_atomic_type->type_params); } - $array_type->addType($array_atomic_type); + $array_atomic_types[] = $array_atomic_type; } elseif ($array_atomic_type instanceof TNonEmptyList) { if (!$context->inside_loop && $array_atomic_type->count !== null) { if ($array_atomic_type->count === 1) { - $array_atomic_type = new TArray( - [ - new Union([new TNever]), - new Union([new TNever]), - ] - ); + $array_atomic_type = new TList(Type::getNever()); } else { - $array_atomic_type->count--; + $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); } } else { $array_atomic_type = new TList($array_atomic_type->type_param); } - $array_type->addType($array_atomic_type); + $array_atomic_types[] = $array_atomic_type; + } else { + $array_atomic_types[] = $array_atomic_type; } } + if (!$array_atomic_types) { + throw new AssertionError("We must have some types here!"); + } + $array_type = new Union($array_atomic_types); $context->removeDescendents($var_id, $array_type); $context->vars_in_scope[$var_id] = $array_type; } @@ -582,13 +583,12 @@ public static function handleByRefArrayAdjustment( /** * @param (TArray|null)[] $array_arg_types - * */ private static function checkClosureType( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -724,10 +724,10 @@ private static function checkClosureType( } } } else { - $closure_types = [$closure_type]; + $closure_types = [&$closure_type]; } - foreach ($closure_types as $closure_type) { + foreach ($closure_types as &$closure_type) { if ($closure_type->params === null) { continue; } @@ -753,7 +753,7 @@ private static function checkClosureTypeArgs( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -836,9 +836,6 @@ private static function checkClosureTypeArgs( && $closure_type->return_type && $closure_param_type->hasTemplate() ) { - $closure_param_type = clone $closure_param_type; - $closure_type->return_type = clone $closure_type->return_type; - $template_result = new TemplateResult( [], [] @@ -861,7 +858,7 @@ private static function checkClosureTypeArgs( $context->calling_method_id ?: $context->calling_function_id ); - $closure_type->replaceTemplateTypesWithArgTypes( + $closure_type = $closure_type->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index d498beff791..30022eb2a82 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -240,8 +240,7 @@ private static function resolveTemplateParam( } } else { if ($template_result !== null) { - $type_extends_atomic = clone $type_extends_atomic; - $type_extends_atomic->replaceTemplateTypesWithArgTypes( + $type_extends_atomic = $type_extends_atomic->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index ff7e03d122c..a86f701f2f7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -564,7 +564,7 @@ private static function handleNamedFunction( } } catch (UnexpectedValueException $e) { $function_call_info->function_params = [ - new FunctionLikeParameter('args', false, null, null, null, false, false, true) + new FunctionLikeParameter('args', false, null, null, null, null, false, false, true) ]; } } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index ed207aded41..a084fb7380a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -41,6 +41,7 @@ use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use UnexpectedValueException; @@ -172,7 +173,7 @@ public static function fetch( null ); - TemplateInferredTypeReplacer::replace( + $return_type = TemplateInferredTypeReplacer::replace( $return_type, $template_result, $codebase @@ -327,9 +328,7 @@ private static function getReturnTypeFromCallMapWithArgs( $keyed_array = new TKeyedArray([ Type::getInt(), Type::getInt() - ]); - $keyed_array->sealed = true; - $keyed_array->is_list = true; + ], null, true, null, null, true); return new Union([$keyed_array]); case 'get_called_class': @@ -440,9 +439,7 @@ private static function getReturnTypeFromCallMapWithArgs( $keyed_array = new TKeyedArray([ Type::getInt(), Type::getInt() - ]); - $keyed_array->sealed = true; - $keyed_array->is_list = true; + ], null, true, null, null, true); if ((string) $first_arg_type === 'false') { return new Union([$keyed_array]); @@ -501,8 +498,10 @@ private static function getReturnTypeFromCallMapWithArgs( break; case 'fgetcsv': - $string_type = Type::getString(); - $string_type->addType(new TNull); + $string_type = new Union([ + new TString, + new TNull + ]); $string_type->ignore_nullable_issues = true; $call_map_return_type = new Union([ @@ -609,10 +608,8 @@ private static function taintReturnType( $conditionally_removed_taints = []; foreach ($function_storage->conditionally_removed_taints as $conditionally_removed_taint) { - $conditionally_removed_taint = clone $conditionally_removed_taint; - - TemplateInferredTypeReplacer::replace( - $conditionally_removed_taint, + $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( + clone $conditionally_removed_taint, $template_result, $codebase ); 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 1c9edaf7607..70ecf984149 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -89,15 +89,15 @@ public static function analyze( $lhs_type_part->as->getAtomicTypes() )[0]; - $lhs_type_part->from_docblock = true; - if ($lhs_type_part instanceof TNamedObject) { - $lhs_type_part->extra_types = $extra_types; + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types)->setFromDocblock(true); } elseif ($lhs_type_part instanceof TObject && $extra_types) { - $lhs_type_part = array_shift($extra_types); + $lhs_type_part = array_shift($extra_types)->setFromDocblock(true); if ($extra_types) { - $lhs_type_part->extra_types = $extra_types; + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types); } + } else { + $lhs_type_part = $lhs_type_part->setFromDocblock(true); } $result->has_mixed_method_call = true; @@ -849,9 +849,7 @@ private static function handleRegularMixins( $lhs_var_id === '$this' ); - $lhs_type_part = clone $mixin; - - $lhs_type_part->replaceTemplateTypesWithArgTypes( + $lhs_type_part = $mixin->replaceTemplateTypesWithArgTypes( new TemplateResult([], $mixin_class_template_params ?: []), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index b16fadae07a..795b6b1c02c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -207,7 +207,7 @@ public static function analyze( if ($method_storage && $method_storage->if_this_is_type) { $method_template_result = new TemplateResult($method_storage->template_types ?: [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $method_storage->if_this_is_type, $method_template_result, $codebase, @@ -300,9 +300,11 @@ public static function analyze( if ($method_storage) { if ($method_storage->if_this_is_type) { $class_type = new Union([$lhs_type_part]); - $if_this_is_type = clone $method_storage->if_this_is_type; - - TemplateInferredTypeReplacer::replace($if_this_is_type, $template_result, $codebase); + $if_this_is_type = TemplateInferredTypeReplacer::replace( + clone $method_storage->if_this_is_type, + $template_result, + $codebase + ); if (!UnionTypeComparator::isContainedBy($codebase, $class_type, $if_this_is_type)) { IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index 2ed17b4bc67..d62ee2d694f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -618,7 +618,7 @@ public static function replaceTemplateTypes( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 086a8d255dd..ac2346d95ee 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -149,7 +149,7 @@ public static function handleMagicMethod( $return_type_candidate = clone $pseudo_method_storage->return_type; if ($found_generic_params) { - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, new TemplateResult([], $found_generic_params), $codebase @@ -315,7 +315,7 @@ public static function handleMissingOrMagicMethod( $return_type_candidate = clone $pseudo_method_storage->return_type; if ($found_generic_params) { - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, new TemplateResult([], $found_generic_params), $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index a755115db7f..864b353b725 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; @@ -398,27 +399,24 @@ public static function analyze( && ($class_type->from_docblock || $class_type->isNullable()) && $real_method_call ) { - $keys_to_remove = []; + $types = $class_type->getAtomicTypes(); - $class_type = clone $class_type; - - foreach ($class_type->getAtomicTypes() as $key => $type) { + foreach ($types as $key => &$type) { if (!$type instanceof TNamedObject) { - $keys_to_remove[] = $key; + unset($types[$key]); } else { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } } - - foreach ($keys_to_remove as $key) { - $class_type->removeType($key); + if (!$types) { + throw new AssertionError("We must have some types here!"); } - $class_type->from_docblock = false; - $context->removeVarFromConflictingClauses($lhs_var_id, null, $statements_analyzer); - $context->vars_in_scope[$lhs_var_id] = $class_type; + $class_type = $class_type->getBuilder()->setTypes($types); + $class_type->from_docblock = false; + $context->vars_in_scope[$lhs_var_id] = $class_type->freeze(); } return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 324861eada9..d15daac482f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -525,11 +525,11 @@ private static function analyzeNamedConstructor( if ($generic_param_types) { $result_atomic_type = new TGenericObject( $fq_class_name, - $generic_param_types + $generic_param_types, + false, + $from_static ); - $result_atomic_type->is_static = $from_static; - $statements_analyzer->node_data->setType( $stmt, new Union([$result_atomic_type]) @@ -552,11 +552,11 @@ private static function analyzeNamedConstructor( static fn($map) => clone reset($map), $storage->template_types ) - ) + ), + false, + $from_static ); - $result_atomic_type->is_static = $from_static; - $statements_analyzer->node_data->setType( $stmt, new Union([$result_atomic_type]) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 772ad322c0c..e1754c01c93 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -298,10 +298,8 @@ public static function taintReturnType( if ($method_storage && $template_result) { foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) { - $conditionally_removed_taint = clone $conditionally_removed_taint; - - TemplateInferredTypeReplacer::replace( - $conditionally_removed_taint, + $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( + clone $conditionally_removed_taint, $template_result, $codebase ); 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 7f02b1441a3..c1afc0fa770 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -446,13 +446,13 @@ private static function handleNamedCall( $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; + $mixin_candidate_type = $new_mixin_candidate_type->freeze(); } $new_lhs_type = TypeExpander::expandUnion( @@ -744,7 +744,7 @@ private static function handleNamedCall( if (isset($context->vars_in_scope['$this']) && $method_call_type = $statements_analyzer->node_data->getType($stmt) ) { - $method_call_type = clone $method_call_type; + $method_call_type = $method_call_type->getBuilder(); foreach ($method_call_type->getAtomicTypes() as $name => $type) { if ($type instanceof TNamedObject && $type->is_static && $type->value === $fq_class_name) { @@ -754,7 +754,7 @@ private static function handleNamedCall( } } - $statements_analyzer->node_data->setType($stmt, $method_call_type); + $statements_analyzer->node_data->setType($stmt, $method_call_type->freeze()); } return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 06ccbdaaab3..8cdfd6c7560 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -21,6 +21,7 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\TypeVisitor\ContainsStaticVisitor; use Psalm\Issue\AbstractMethodCall; use Psalm\Issue\ImpureMethodCall; use Psalm\IssueBuffer; @@ -575,7 +576,7 @@ private static function getMethodReturnType( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase @@ -630,16 +631,8 @@ private static function getMethodReturnType( */ private static function hasStaticInType(Type\TypeNode $type): bool { - if ($type instanceof TNamedObject && ($type->value === 'static' || $type->is_static)) { - return true; - } - - foreach ($type->getChildNodes() as $child_type) { - if (self::hasStaticInType($child_type)) { - return true; - } - } - - return false; + $visitor = new ContainsStaticVisitor; + $visitor->traverse($type); + return $visitor->matches(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 619d2ac00e6..a3f34607a6b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -578,16 +578,14 @@ public static function getFunctionIdsFromCallableArg( if ($type_part instanceof TNamedObject) { $method_id = $type_part->value . '::' . $method_name_arg->value; - if ($type_part->extra_types) { - foreach ($type_part->extra_types as $extra_type) { - if ($extra_type instanceof TTemplateParam - || $extra_type instanceof TObjectWithProperties - ) { - throw new UnexpectedValueException('Shouldn’t get a generic param here'); - } - - $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; + foreach ($type_part->extra_types as $extra_type) { + if ($extra_type instanceof TTemplateParam + || $extra_type instanceof TObjectWithProperties + ) { + throw new UnexpectedValueException('Shouldn’t get a generic param here'); } + + $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; } $method_ids[] = '$' . $method_id; @@ -756,9 +754,8 @@ public static function applyAssertionsToContext( $assertion_type_atomic = $assertion_rule->getAtomicType(); if ($assertion_type_atomic) { - $assertion_type = new Union([clone $assertion_type_atomic]); - TemplateInferredTypeReplacer::replace( - $assertion_type, + $assertion_type = TemplateInferredTypeReplacer::replace( + new Union([clone $assertion_type_atomic]), $template_result, $codebase ); @@ -771,8 +768,7 @@ public static function applyAssertionsToContext( continue; } - $assertion_rule = clone $assertion_rule; - $assertion_rule->setAtomicType($atomic_type); + $assertion_rule = $assertion_rule->setAtomicType($atomic_type); $orred_rules[] = $assertion_rule; } } elseif (isset($context->vars_in_scope[$assertion_var_id])) { @@ -940,19 +936,7 @@ public static function applyAssertionsToContext( ); } - $op_vars_in_scope[$var_id]->from_docblock = true; - - foreach ($op_vars_in_scope[$var_id]->getAtomicTypes() as $changed_atomic_type) { - $changed_atomic_type->from_docblock = true; - - if ($changed_atomic_type instanceof TNamedObject - && $changed_atomic_type->extra_types - ) { - foreach ($changed_atomic_type->extra_types as $extra_type) { - $extra_type->from_docblock = true; - } - } - } + $op_vars_in_scope[$var_id] = $op_vars_in_scope[$var_id]->setFromDocblock(true); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 10258732bd0..a435018739f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -244,9 +244,7 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type) { if ($type instanceof Scalar) { - $keyed_array = new TKeyedArray([new Union([$type])]); - $keyed_array->is_list = true; - $keyed_array->sealed = true; + $keyed_array = new TKeyedArray([new Union([$type])], null, true, null, null, true); $permissible_atomic_types[] = $keyed_array; } elseif ($type instanceof TNull) { $permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 923829c562c..8360fb5ead0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -169,8 +169,7 @@ public static function analyzeFetch( } if ($first_part_lc === 'static') { - $static_named_object = new TNamedObject($fq_class_name); - $static_named_object->is_static = true; + $static_named_object = new TNamedObject($fq_class_name, true); $statements_analyzer->node_data->setType( $stmt, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 57bfbdce1f4..5b45cdc1d74 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -80,6 +80,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Atomic\TTrue; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use UnexpectedValueException; @@ -271,7 +272,7 @@ public static function analyze( && !$const_array_key_type->hasMixed() && !$stmt_dim_type->hasMixed() ) { - $new_offset_type = clone $stmt_dim_type; + $new_offset_type = $stmt_dim_type->getBuilder(); $const_array_key_atomic_types = $const_array_key_type->getAtomicTypes(); foreach ($new_offset_type->getAtomicTypes() as $offset_key => $offset_atomic_type) { @@ -295,6 +296,8 @@ public static function analyze( $new_offset_type->removeType($offset_key); } } + + $new_offset_type = $new_offset_type->freeze(); } } } @@ -456,14 +459,16 @@ public static function taintArrayFetch( public static function getArrayAccessTypeGivenOffset( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, - Union $offset_type, + Union &$array_type, + Union &$offset_type_original, bool $in_assignment, ?string $extended_var_id, Context $context, PhpParser\Node\Expr $assign_value = null, Union $replacement_type = null ): Union { + $offset_type = $offset_type_original->getBuilder(); + $codebase = $statements_analyzer->getCodebase(); $has_array_access = false; @@ -549,7 +554,10 @@ public static function getArrayAccessTypeGivenOffset( $has_valid_absolute_offset = true; } - foreach ($array_type->getAtomicTypes() as $type_string => $type) { + $types = $array_type->getAtomicTypes(); + $changed = false; + foreach ($types as $type_string => $type) { + $original_type_real = $type; $original_type = $type; if ($type instanceof TMixed @@ -574,6 +582,7 @@ public static function getArrayAccessTypeGivenOffset( } $type = clone $type->as->getSingleAtomic(); + $original_type = $type; } if ($type instanceof TNull) { @@ -623,12 +632,11 @@ public static function getArrayAccessTypeGivenOffset( $in_assignment, $type, $key_values, - $array_type, - $type_string, + $array_type->hasMixed(), $stmt, $replacement_type, $offset_type, - $original_type, + $original_type_real, $codebase, $extended_var_id, $context, @@ -639,6 +647,12 @@ public static function getArrayAccessTypeGivenOffset( $has_valid_expected_offset ); + if ($type !== $original_type) { + $changed = true; + unset($types[$type_string]); + $types[$type->getKey()] = $type; + } + continue; } @@ -689,6 +703,9 @@ public static function getArrayAccessTypeGivenOffset( $non_array_types[] = (string)$type; } } + if ($changed) { + $array_type = $array_type->getBuilder()->setTypes($types)->freeze(); + } if ($non_array_types) { if ($has_array_access) { @@ -847,6 +864,8 @@ public static function getArrayAccessTypeGivenOffset( } } + $offset_type_original = $offset_type->freeze(); + if ($array_access_type === null) { // shouldn’t happen, but don’t crash return Type::getMixed(); @@ -856,15 +875,11 @@ public static function getArrayAccessTypeGivenOffset( $array_access_type->by_ref = true; } - if ($in_assignment) { - $array_type->bustCache(); - } - return $array_access_type; } private static function checkLiteralIntArrayOffset( - Union $offset_type, + MutableUnion $offset_type, Union $expected_offset_type, ?string $extended_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, @@ -912,7 +927,7 @@ private static function checkLiteralIntArrayOffset( } private static function checkLiteralStringArrayOffset( - Union $offset_type, + MutableUnion $offset_type, Union $expected_offset_type, ?string $extended_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, @@ -961,26 +976,16 @@ private static function checkLiteralStringArrayOffset( public static function replaceOffsetTypeWithInts(Union $offset_type): Union { + $offset_type = $offset_type->getBuilder(); $offset_types = $offset_type->getAtomicTypes(); - $cloned = false; - foreach ($offset_types as $key => $offset_type_part) { if ($offset_type_part instanceof TLiteralString) { if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) { - if (!$cloned) { - $offset_type = clone $offset_type; - $cloned = true; - } $offset_type->addType(new TLiteralInt((int) $offset_type_part->value)); $offset_type->removeType($key); } } elseif ($offset_type_part instanceof TBool) { - if (!$cloned) { - $offset_type = clone $offset_type; - $cloned = true; - } - if ($offset_type_part instanceof TFalse) { if (!$offset_type->ignore_falsable_issues) { $offset_type->addType(new TLiteralInt(0)); @@ -997,7 +1002,7 @@ public static function replaceOffsetTypeWithInts(Union $offset_type): Union } } - return $offset_type; + return $offset_type->freeze(); } /** @@ -1079,17 +1084,19 @@ public static function handleMixedArrayAccess( /** * @param list $expected_offset_types * @param TArray|TKeyedArray|TList|TClassStringMap $type + * @param-out TArray|TKeyedArray|TList|TClassStringMap $type * @param list $key_values + * + * @psalm-suppress ConflictingReferenceConstraint Ignore */ private static function handleArrayAccessOnArray( bool $in_assignment, Atomic &$type, array &$key_values, - Union $array_type, - string $type_string, + bool $hasMixed, PhpParser\Node\Expr\ArrayDimFetch $stmt, ?Union $replacement_type, - Union &$offset_type, + MutableUnion $offset_type, Atomic $original_type, Codebase $codebase, ?string $extended_var_id, @@ -1113,25 +1120,19 @@ private static function handleArrayAccessOnArray( [$previous_key_type, $previous_value_type] = $type->type_params; // ok, type becomes an TKeyedArray - $array_type->removeType($type_string); - $type = new TKeyedArray([ - $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever() - ]); - if ($single_atomic instanceof TLiteralClassString) { - $type->class_strings[$single_atomic->value] = true; - } - - $type->sealed = $from_empty_array; - - if (!$from_empty_array) { - $type->previous_value_type = clone $previous_value_type; - $type->previous_key_type = clone $previous_key_type; - } - - $array_type->addType($type); + $type = new TKeyedArray( + [ + $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever(), + ], + $single_atomic instanceof TLiteralClassString ? [ + $single_atomic->value => true + ] : null, + $from_empty_array, + $from_empty_array ? null : $previous_key_type, + $from_empty_array ? null : $previous_value_type, + ); } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { - $array_type->removeType($type_string); - $array_type->addType(new TNonEmptyList($replacement_type)); + $type = new TNonEmptyList($replacement_type); return; } } elseif ($type instanceof TKeyedArray @@ -1139,27 +1140,42 @@ private static function handleArrayAccessOnArray( && $type->previous_value_type->isMixed() && count($key_values) === 1 ) { - $type->properties[$key_values[0]->value] = Type::getMixed(); + $properties = $type->properties; + $properties[$key_values[0]->value] = Type::getMixed(); + $type = $type->setProperties($properties); } } - $offset_type = self::replaceOffsetTypeWithInts($offset_type); + $offset_type = self::replaceOffsetTypeWithInts($offset_type->freeze())->getBuilder(); if ($type instanceof TList && (($in_assignment && $stmt->dim) || $original_type instanceof TTemplateParam || !$offset_type->isInt()) ) { - $type = new TArray([Type::getInt(), $type->type_param]); - } - - if ($type instanceof TArray) { + $temp = new TArray([Type::getInt(), $type->type_param]); self::handleArrayAccessOnTArray( $statements_analyzer, $codebase, $context, $stmt, - $array_type, + $hasMixed, + $extended_var_id, + $temp, + $offset_type, + $in_assignment, + $expected_offset_types, + $array_access_type, + $original_type, + $has_valid_offset + ); + } elseif ($type instanceof TArray) { + self::handleArrayAccessOnTArray( + $statements_analyzer, + $codebase, + $context, + $stmt, + $hasMixed, $extended_var_id, $type, $offset_type, @@ -1206,9 +1222,8 @@ private static function handleArrayAccessOnArray( $extended_var_id, $context, $type, - $array_type, + $hasMixed, $expected_offset_types, - $type_string, $has_valid_offset ); } @@ -1220,16 +1235,17 @@ private static function handleArrayAccessOnArray( /** * @param list $expected_offset_types + * @param-out TArray $type */ private static function handleArrayAccessOnTArray( StatementsAnalyzer $statements_analyzer, Codebase $codebase, Context $context, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, + bool $hasMixed, ?string $extended_var_id, - TArray $type, - Union $offset_type, + TArray &$type, + MutableUnion $offset_type, bool $in_assignment, array &$expected_offset_types, ?Union &$array_access_type, @@ -1239,9 +1255,12 @@ private static function handleArrayAccessOnTArray( // if we're assigning to an empty array with a key offset, refashion that array if ($in_assignment) { if ($type->isEmptyArray()) { - $type->type_params[0] = $offset_type->isMixed() - ? Type::getArrayKey() - : $offset_type; + $type = $type->replaceTypeParams([ + $offset_type->isMixed() + ? Type::getArrayKey() + : $offset_type->freeze(), + $type->type_params[1] + ]); } } elseif (!$type->isEmptyArray()) { $expected_offset_type = $type->type_params[0]->hasMixed() @@ -1264,12 +1283,15 @@ private static function handleArrayAccessOnTArray( && $offset_as->param_name === $original_type->param_name && $offset_as->defining_class === $original_type->defining_class ) { - $type->type_params[1] = new Union([ - new TTemplateIndexedAccess( - $offset_as->param_name, - $templated_offset_type->param_name, - $offset_as->defining_class - ) + $type = $type->replaceTypeParams([ + $type->type_params[0], + new Union([ + new TTemplateIndexedAccess( + $offset_as->param_name, + $templated_offset_type->param_name, + $offset_as->defining_class + ) + ]) ]); $has_valid_offset = true; @@ -1278,7 +1300,7 @@ private static function handleArrayAccessOnTArray( } else { $offset_type_contained_by_expected = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type, true, $offset_type->ignore_falsable_issues, @@ -1336,7 +1358,7 @@ private static function handleArrayAccessOnTArray( if (UnionTypeComparator::canExpressionTypesBeIdentical( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type )) { $has_valid_offset = true; @@ -1348,7 +1370,7 @@ private static function handleArrayAccessOnTArray( } if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) { - $type->count++; + $type = $type->setCount($type->count+1); } $array_access_type = Type::combineUnionTypes( @@ -1357,7 +1379,7 @@ private static function handleArrayAccessOnTArray( ); if ($array_access_type->isNever() - && !$array_type->hasMixed() + && !$hasMixed && !$in_assignment && !$context->inside_isset ) { @@ -1377,8 +1399,8 @@ private static function handleArrayAccessOnTArray( private static function handleArrayAccessOnClassStringMap( Codebase $codebase, - TClassStringMap $type, - Union $offset_type, + TClassStringMap &$type, + MutableUnion $offset_type, ?Union $replacement_type, ?Union &$array_access_type ): void { @@ -1438,27 +1460,27 @@ private static function handleArrayAccessOnClassStringMap( ); } - $expected_value_param_get = clone $type->value_param; - - TemplateInferredTypeReplacer::replace( - $expected_value_param_get, + $expected_value_param_get = TemplateInferredTypeReplacer::replace( + $type->value_param, $template_result_get, $codebase ); if ($replacement_type) { - $expected_value_param_set = clone $type->value_param; - - TemplateInferredTypeReplacer::replace( + $replacement_type = TemplateInferredTypeReplacer::replace( $replacement_type, $template_result_set, $codebase ); - $type->value_param = Type::combineUnionTypes( - $replacement_type, - $expected_value_param_set, - $codebase + $type = new TClassStringMap( + $type->param_name, + $type->as_type, + Type::combineUnionTypes( + $replacement_type, + $type->value_param, + $codebase + ) ); } @@ -1474,6 +1496,7 @@ private static function handleArrayAccessOnClassStringMap( /** * @param list $expected_offset_types * @param list $key_values + * @param-out TArray|TKeyedArray|TList $type */ private static function handleArrayAccessOnKeyedArray( StatementsAnalyzer $statements_analyzer, @@ -1483,13 +1506,12 @@ private static function handleArrayAccessOnKeyedArray( ?Union &$array_access_type, bool $in_assignment, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $offset_type, + MutableUnion $offset_type, ?string $extended_var_id, Context $context, - TKeyedArray $type, - Union $array_type, + TKeyedArray &$type, + bool $hasMixed, array &$expected_offset_types, - string $type_string, bool &$has_valid_offset ): void { $generic_key_type = $type->getGenericKeyType(); @@ -1499,27 +1521,28 @@ private static function handleArrayAccessOnKeyedArray( } if ($key_values) { + $properties = $type->properties; foreach ($key_values as $key_value) { - if (isset($type->properties[$key_value->value]) || $replacement_type) { + if (isset($properties[$key_value->value]) || $replacement_type) { $has_valid_offset = true; if ($replacement_type) { - $type->properties[$key_value->value] = Type::combineUnionTypes( - $type->properties[$key_value->value] ?? null, + $properties[$key_value->value] = Type::combineUnionTypes( + $properties[$key_value->value] ?? null, $replacement_type ); } $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $type->properties[$key_value->value] + clone $properties[$key_value->value] ); } elseif ($in_assignment) { - $type->properties[$key_value->value] = new Union([new TNever]); + $properties[$key_value->value] = new Union([new TNever]); $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $type->properties[$key_value->value] + clone $properties[$key_value->value] ); } elseif ($type->previous_value_type) { if ($codebase->config->ensure_array_string_offsets_exist) { @@ -1544,16 +1567,16 @@ private static function handleArrayAccessOnKeyedArray( ); } - $type->properties[$key_value->value] = clone $type->previous_value_type; + $properties[$key_value->value] = clone $type->previous_value_type; $array_access_type = clone $type->previous_value_type; - } elseif ($array_type->hasMixed()) { + } elseif ($hasMixed) { $has_valid_offset = true; $array_access_type = Type::getMixed(); } else { if ($type->sealed || !$context->inside_isset) { - $object_like_keys = array_keys($type->properties); + $object_like_keys = array_keys($properties); $last_key = array_pop($object_like_keys); @@ -1580,6 +1603,8 @@ private static function handleArrayAccessOnKeyedArray( $array_access_type = Type::getMixed(); } } + + $type = $type->setProperties($properties); } else { $key_type = $generic_key_type->hasMixed() ? Type::getArrayKey() @@ -1589,7 +1614,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $key_type, true, $offset_type->ignore_falsable_issues, @@ -1600,7 +1625,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, $key_type, - $offset_type, + $offset_type->freeze(), true, $offset_type->ignore_falsable_issues ); @@ -1620,23 +1645,18 @@ private static function handleArrayAccessOnKeyedArray( $new_key_type = Type::combineUnionTypes( $generic_key_type, - $offset_type->isMixed() ? Type::getArrayKey() : $offset_type + $offset_type->isMixed() ? Type::getArrayKey() : $offset_type->freeze() ); $property_count = $type->sealed ? count($type->properties) : null; if (!$stmt->dim && $property_count) { ++$property_count; - $array_type->removeType($type_string); $type = new TNonEmptyArray([ $new_key_type, $generic_params, - ]); - $array_type->addType($type); - $type->count = $property_count; + ], $property_count); } else { - $array_type->removeType($type_string); - if (!$stmt->dim && $type->is_list) { $type = new TList($generic_params); } else { @@ -1645,8 +1665,6 @@ private static function handleArrayAccessOnKeyedArray( $generic_params, ]); } - - $array_type->addType($type); } $array_access_type = Type::combineUnionTypes( @@ -1676,13 +1694,14 @@ private static function handleArrayAccessOnKeyedArray( /** * @param list $expected_offset_types * @param list $key_values + * @param-out TList $type */ private static function handleArrayAccessOnList( StatementsAnalyzer $statements_analyzer, Codebase $codebase, PhpParser\Node\Expr\ArrayDimFetch $stmt, - TList $type, - Union $offset_type, + TList &$type, + MutableUnion $offset_type, ?string $extended_var_id, array $key_values, Context $context, @@ -1726,15 +1745,15 @@ private static function handleArrayAccessOnList( } if ($in_assignment && $type instanceof TNonEmptyList && $type->count !== null) { - $type->count++; + $type = $type->setCount($type->count+1); } if ($in_assignment && $replacement_type) { - $type->type_param = Type::combineUnionTypes( + $type = $type->replaceTypeParam(Type::combineUnionTypes( $type->type_param, $replacement_type, $codebase - ); + )); } $array_access_type = Type::combineUnionTypes( @@ -1897,7 +1916,7 @@ private static function handleArrayAccessOnString( Context $context, ?Union $replacement_type, TString $type, - Union $offset_type, + MutableUnion $offset_type, array &$expected_offset_types, ?Union &$array_access_type, bool &$has_valid_offset @@ -1960,7 +1979,7 @@ private static function handleArrayAccessOnString( if (!UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $valid_offset_type, true )) { @@ -1981,7 +2000,7 @@ private static function handleArrayAccessOnString( * @param Atomic[] $offset_types */ private static function checkArrayOffsetType( - Union $offset_type, + MutableUnion $offset_type, array $offset_types, Codebase $codebase ): bool { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index d9c83bdc4ee..ef24481d64e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -77,6 +77,8 @@ class AtomicPropertyFetchAnalyzer { /** * @param array $invalid_fetch_types $invalid_fetch_types + * + * @psalm-suppress ComplexMethod Unavoidably complex method. */ public static function analyze( StatementsAnalyzer $statements_analyzer, @@ -743,7 +745,7 @@ public static function localizePropertyType( } } - TemplateInferredTypeReplacer::replace( + $class_property_type = TemplateInferredTypeReplacer::replace( $class_property_type, new TemplateResult([], $template_types), $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index 195bccced82..2e25c2cd7e5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -262,7 +262,8 @@ public static function analyze( $stmt_type = $statements_analyzer->node_data->getType($stmt); if ($stmt_var_type->isNullable() && !$context->inside_isset && $stmt_type) { - $stmt_type->addType(new TNull); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); + $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($stmt_var_type->ignore_nullable_issues) { $stmt_type->ignore_nullable_issues = true; @@ -388,7 +389,10 @@ private static function handleScopedProperty( $statements_analyzer->getSuppressedIssues() ); - $stmt_type->addType(new TNull); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); + + $context->vars_in_scope[$var_id] = $stmt_type; + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 740daa859d7..ee327d72eeb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -668,89 +668,91 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ $bool_string_helper = new Union([new TBool(), new TString()]); $bool_string_helper->possibly_undefined = true; - $detailed_type = new TKeyedArray([ - // https://www.php.net/manual/en/reserved.variables.server.php - 'PHP_SELF' => $non_empty_string_helper, - 'argv' => $argv_helper, - 'argc' => $argc_helper, - 'GATEWAY_INTERFACE' => $non_empty_string_helper, - 'SERVER_ADDR' => $non_empty_string_helper, - 'SERVER_NAME' => $non_empty_string_helper, - 'SERVER_SOFTWARE' => $non_empty_string_helper, - 'SERVER_PROTOCOL' => $non_empty_string_helper, - 'REQUEST_METHOD' => $non_empty_string_helper, - 'REQUEST_TIME' => $request_time_helper, - 'REQUEST_TIME_FLOAT' => $request_time_float_helper, - 'QUERY_STRING' => $string_helper, - 'DOCUMENT_ROOT' => $non_empty_string_helper, - 'HTTP_ACCEPT' => $non_empty_string_helper, - 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, - 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, - 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, - 'HTTP_CONNECTION' => $non_empty_string_helper, - 'HTTP_HOST' => $non_empty_string_helper, - 'HTTP_REFERER' => $non_empty_string_helper, - 'HTTP_USER_AGENT' => $non_empty_string_helper, - 'HTTPS' => $string_helper, - 'REMOTE_ADDR' => $non_empty_string_helper, - 'REMOTE_HOST' => $non_empty_string_helper, - 'REMOTE_PORT' => $string_helper, - 'REMOTE_USER' => $non_empty_string_helper, - 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, - 'SCRIPT_FILENAME' => $non_empty_string_helper, - 'SERVER_ADMIN' => $non_empty_string_helper, - 'SERVER_PORT' => $non_empty_string_helper, - 'SERVER_SIGNATURE' => $non_empty_string_helper, - 'PATH_TRANSLATED' => $non_empty_string_helper, - 'SCRIPT_NAME' => $non_empty_string_helper, - 'REQUEST_URI' => $non_empty_string_helper, - 'PHP_AUTH_DIGEST' => $non_empty_string_helper, - 'PHP_AUTH_USER' => $non_empty_string_helper, - 'PHP_AUTH_PW' => $non_empty_string_helper, - 'AUTH_TYPE' => $non_empty_string_helper, - 'PATH_INFO' => $non_empty_string_helper, - 'ORIG_PATH_INFO' => $non_empty_string_helper, - // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html - 'CONTENT_LENGTH' => $string_helper, - 'CONTENT_TYPE' => $string_helper, - // common, misc stuff - 'FCGI_ROLE' => $non_empty_string_helper, - 'HOME' => $non_empty_string_helper, - 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, - 'HTTP_COOKIE' => $non_empty_string_helper, - 'HTTP_PRIORITY' => $non_empty_string_helper, - 'PATH' => $non_empty_string_helper, - 'REDIRECT_STATUS' => $non_empty_string_helper, - 'REQUEST_SCHEME' => $non_empty_string_helper, - 'USER' => $non_empty_string_helper, - // common, misc headers - 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, - 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, - 'HTTP_CLIENT_IP' => $non_empty_string_helper, - 'HTTP_X_REAL_IP' => $non_empty_string_helper, - 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, - 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, - 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, - 'HTTP_CF_VISITOR' => $non_empty_string_helper, - 'HTTP_CDN_LOOP' => $non_empty_string_helper, - // common, misc browser headers - 'HTTP_DNT' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, - 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, - 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, - 'HTTP_SEC_CH_UA' => $non_empty_string_helper, - // phpunit - 'APP_DEBUG' => $bool_string_helper, - 'APP_ENV' => $string_helper, - ]); - - // generic case for all other elements - $detailed_type->previous_key_type = Type::getNonEmptyString(); - $detailed_type->previous_value_type = Type::getString(); - + $detailed_type = new TKeyedArray( + [ + // https://www.php.net/manual/en/reserved.variables.server.php + 'PHP_SELF' => $non_empty_string_helper, + 'argv' => $argv_helper, + 'argc' => $argc_helper, + 'GATEWAY_INTERFACE' => $non_empty_string_helper, + 'SERVER_ADDR' => $non_empty_string_helper, + 'SERVER_NAME' => $non_empty_string_helper, + 'SERVER_SOFTWARE' => $non_empty_string_helper, + 'SERVER_PROTOCOL' => $non_empty_string_helper, + 'REQUEST_METHOD' => $non_empty_string_helper, + 'REQUEST_TIME' => $request_time_helper, + 'REQUEST_TIME_FLOAT' => $request_time_float_helper, + 'QUERY_STRING' => $string_helper, + 'DOCUMENT_ROOT' => $non_empty_string_helper, + 'HTTP_ACCEPT' => $non_empty_string_helper, + 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, + 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, + 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, + 'HTTP_CONNECTION' => $non_empty_string_helper, + 'HTTP_HOST' => $non_empty_string_helper, + 'HTTP_REFERER' => $non_empty_string_helper, + 'HTTP_USER_AGENT' => $non_empty_string_helper, + 'HTTPS' => $string_helper, + 'REMOTE_ADDR' => $non_empty_string_helper, + 'REMOTE_HOST' => $non_empty_string_helper, + 'REMOTE_PORT' => $string_helper, + 'REMOTE_USER' => $non_empty_string_helper, + 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, + 'SCRIPT_FILENAME' => $non_empty_string_helper, + 'SERVER_ADMIN' => $non_empty_string_helper, + 'SERVER_PORT' => $non_empty_string_helper, + 'SERVER_SIGNATURE' => $non_empty_string_helper, + 'PATH_TRANSLATED' => $non_empty_string_helper, + 'SCRIPT_NAME' => $non_empty_string_helper, + 'REQUEST_URI' => $non_empty_string_helper, + 'PHP_AUTH_DIGEST' => $non_empty_string_helper, + 'PHP_AUTH_USER' => $non_empty_string_helper, + 'PHP_AUTH_PW' => $non_empty_string_helper, + 'AUTH_TYPE' => $non_empty_string_helper, + 'PATH_INFO' => $non_empty_string_helper, + 'ORIG_PATH_INFO' => $non_empty_string_helper, + // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html + 'CONTENT_LENGTH' => $string_helper, + 'CONTENT_TYPE' => $string_helper, + // common, misc stuff + 'FCGI_ROLE' => $non_empty_string_helper, + 'HOME' => $non_empty_string_helper, + 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, + 'HTTP_COOKIE' => $non_empty_string_helper, + 'HTTP_PRIORITY' => $non_empty_string_helper, + 'PATH' => $non_empty_string_helper, + 'REDIRECT_STATUS' => $non_empty_string_helper, + 'REQUEST_SCHEME' => $non_empty_string_helper, + 'USER' => $non_empty_string_helper, + // common, misc headers + 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, + 'HTTP_CLIENT_IP' => $non_empty_string_helper, + 'HTTP_X_REAL_IP' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, + 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, + 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, + 'HTTP_CF_VISITOR' => $non_empty_string_helper, + 'HTTP_CDN_LOOP' => $non_empty_string_helper, + // common, misc browser headers + 'HTTP_DNT' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA' => $non_empty_string_helper, + // phpunit + 'APP_DEBUG' => $bool_string_helper, + 'APP_ENV' => $string_helper, + ], + null, + false, + Type::getNonEmptyString(), + Type::getString() + ); + return new Union([$detailed_type]); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 12e98ae5d9d..4b6c3dae06f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Config; @@ -30,6 +31,7 @@ use function implode; use function in_array; use function is_string; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -379,6 +381,9 @@ public static function resolveIncludePath(string $file_name, string $current_dir ? preg_split('#(?getBuilder(); $invalidTypes->removeType('string'); $invalidTypes->removeType('int'); $invalidTypes->removeType('float'); @@ -430,19 +430,21 @@ public static function infer( return null; } + $new_types = []; foreach ($type_to_invert->getAtomicTypes() as $type_part) { if ($type_part instanceof TLiteralInt && $stmt instanceof PhpParser\Node\Expr\UnaryMinus ) { - $type_part->value = -$type_part->value; + $new_types []= new TLiteralInt(-$type_part->value); } elseif ($type_part instanceof TLiteralFloat && $stmt instanceof PhpParser\Node\Expr\UnaryMinus ) { - $type_part->value = -$type_part->value; + $new_types []= new TLiteralFloat(-$type_part->value); + } else { + $new_types []= $type_part; } } - - return $type_to_invert; + return new Union($new_types); } if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) { @@ -574,10 +576,12 @@ private static function inferArrayType( ) { $objectlike = new TKeyedArray( $array_creation_info->property_types, - $array_creation_info->class_strings + $array_creation_info->class_strings, + true, + null, + null, + $array_creation_info->all_list ); - $objectlike->sealed = true; - $objectlike->is_list = $array_creation_info->all_list; return new Union([$objectlike]); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php index b46ac3df06c..60b8e3f6593 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php @@ -18,6 +18,7 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TString; use Psalm\Type\Union; +use RuntimeException; /** * @internal @@ -45,39 +46,37 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type_part) { if ($type_part instanceof TInt || $type_part instanceof TFloat) { - if ($type_part instanceof TLiteralInt - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { - $type_part->value = -$type_part->value; - } elseif ($type_part instanceof TLiteralFloat - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { - $type_part->value = -$type_part->value; + if (!$stmt instanceof PhpParser\Node\Expr\UnaryMinus) { + $acceptable_types []= $type_part; + continue; } - - if ($type_part instanceof TIntRange - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { + if ($type_part instanceof TLiteralInt) { + $type_part = new TLiteralInt(-$type_part->value); + } elseif ($type_part instanceof TLiteralFloat) { + $type_part = new TLiteralFloat(-$type_part->value); + } elseif ($type_part instanceof TIntRange) { //we'll have to inverse min and max bound and negate any literal $old_min_bound = $type_part->min_bound; $old_max_bound = $type_part->max_bound; if ($old_min_bound === null) { //min bound is null, max bound will be null - $type_part->max_bound = null; + $new_max_bound = null; } elseif ($old_min_bound === 0) { - $type_part->max_bound = 0; + $new_max_bound = 0; } else { - $type_part->max_bound = -$old_min_bound; + $new_max_bound = -$old_min_bound; } if ($old_max_bound === null) { //max bound is null, min bound will be null - $type_part->min_bound = null; + $new_min_bound = null; } elseif ($old_max_bound === 0) { - $type_part->min_bound = 0; + $new_min_bound = 0; } else { - $type_part->min_bound = -$old_max_bound; + $new_min_bound = -$old_max_bound; } + + $type_part = new TIntRange($new_min_bound, $new_max_bound); } $acceptable_types[] = $type_part; @@ -89,6 +88,10 @@ public static function analyze( } } + if (!$acceptable_types) { + throw new RuntimeException("Impossible!"); + } + $statements_analyzer->node_data->setType($stmt, new Union($acceptable_types)); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index 9715f8ade7e..afb4da37625 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -216,7 +216,7 @@ public static function analyze( } if ($yield_type) { - $expression_type->substitute($expression_type, $yield_type); + $expression_type = $expression_type->getBuilder()->substitute($expression_type, $yield_type)->freeze(); } $statements_analyzer->node_data->setType($stmt, $expression_type); diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index c063431a38f..2cf90031d27 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -284,10 +284,8 @@ public static function analyze( unset($found_generic_params[$template_name][$fq_class_name]); } - $local_return_type = clone $local_return_type; - - TemplateInferredTypeReplacer::replace( - $local_return_type, + $local_return_type = TemplateInferredTypeReplacer::replace( + clone $local_return_type, new TemplateResult([], $found_generic_params), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index d537e8ca870..b3882121827 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -99,7 +99,7 @@ public static function analyze( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $var_comment_type->check( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 2c9a1644902..3916fcccbb5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -57,76 +57,91 @@ public static function analyze( $statements_analyzer ); - if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) { - $root_type = clone $context->vars_in_scope[$root_var_id]; + $key_type = $statements_analyzer->node_data->getType($var->dim); + if ($root_var_id && isset($context->vars_in_scope[$root_var_id]) && $key_type) { + $root_types = []; - foreach ($root_type->getAtomicTypes() as $atomic_root_type) { + foreach ($context->vars_in_scope[$root_var_id]->getAtomicTypes() as $atomic_root_type) { if ($atomic_root_type instanceof TKeyedArray) { - if ($var->dim instanceof PhpParser\Node\Scalar\String_ - || $var->dim instanceof PhpParser\Node\Scalar\LNumber - ) { - if (isset($atomic_root_type->properties[$var->dim->value])) { - if ($atomic_root_type->is_list - && $var->dim->value !== count($atomic_root_type->properties)-1 + $key_value = null; + if ($key_type->isSingleIntLiteral()) { + $key_value = $key_type->getSingleIntLiteral()->value; + } elseif ($key_type->isSingleStringLiteral()) { + $key_value = $key_type->getSingleStringLiteral()->value; + } + if ($key_value !== null) { + $properties = $atomic_root_type->properties; + $is_list = $atomic_root_type->is_list; + if (isset($properties[$key_value])) { + if ($is_list + && $key_value !== count($properties)-1 ) { - $atomic_root_type->is_list = false; + $is_list = false; } - unset($atomic_root_type->properties[$var->dim->value]); - $root_type->bustCache(); //remove id cache + unset($properties[$key_value]); } - if (!$atomic_root_type->properties) { + /** @psalm-suppress DocblockTypeContradiction https://github.com/vimeo/psalm/issues/8518 */ + if (!$properties) { if ($atomic_root_type->previous_value_type) { - $root_type->addType( + $root_types [] = new TArray([ $atomic_root_type->previous_key_type ? clone $atomic_root_type->previous_key_type : new Union([new TArrayKey]), clone $atomic_root_type->previous_value_type, ]) - ); + ; } else { - $root_type->addType( + $root_types [] = new TArray([ new Union([new TNever]), new Union([new TNever]), ]) - ); + ; } + } else { + $root_types []= new TKeyedArray( + $properties, + null, + $atomic_root_type->sealed, + $atomic_root_type->previous_key_type, + $atomic_root_type->previous_value_type, + $is_list + ); } } else { + $properties = []; foreach ($atomic_root_type->properties as $key => $type) { - $atomic_root_type->properties[$key] = clone $type; - $atomic_root_type->properties[$key]->possibly_undefined = true; + $properties[$key] = clone $type; + $properties[$key]->possibly_undefined = true; } - - $atomic_root_type->sealed = false; - - $root_type->addType( - $atomic_root_type->getGenericArrayType(false) + $root_types []= new TKeyedArray( + $properties, + null, + false, + $atomic_root_type->previous_key_type, + $atomic_root_type->previous_value_type, + false, ); - - $atomic_root_type->is_list = false; } } elseif ($atomic_root_type instanceof TNonEmptyArray) { - $root_type->addType( - new TArray($atomic_root_type->type_params) - ); + $root_types []= new TArray($atomic_root_type->type_params); } elseif ($atomic_root_type instanceof TNonEmptyMixed) { - $root_type->addType( - new TMixed() - ); + $root_types []= new TMixed(); } elseif ($atomic_root_type instanceof TList) { - $root_type->addType( + $root_types []= new TArray([ Type::getInt(), $atomic_root_type->type_param ]) - ); + ; + } else { + $root_types []= $atomic_root_type; } } - $context->vars_in_scope[$root_var_id] = $root_type; + $context->vars_in_scope[$root_var_id] = new Union($root_types); $context->removeVarFromConflictingClauses( $root_var_id, diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index bfbe749fb61..1e9f815ba89 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use AssertionError; use Composer\XdebugHandler\XdebugHandler; use Psalm\Config; use Psalm\Exception\UnsupportedIssueToFixException; @@ -48,6 +49,7 @@ use function is_string; use function microtime; use function pathinfo; +use function preg_last_error_msg; use function preg_replace; use function preg_split; use function realpath; @@ -499,6 +501,9 @@ private static function loadCodeowners(Providers $providers): array $codeowner_lines = array_map( static function (string $line): array { $line_parts = preg_split('/\s+/', $line); + if ($line_parts === false) { + throw new AssertionError("An error occurred: ".preg_last_error_msg()); + } $file_selector = substr(array_shift($line_parts), 1); return [$file_selector, $line_parts]; diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index e6dd58f0efd..9010b7c53b8 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use AssertionError; use Composer\XdebugHandler\XdebugHandler; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\CliUtils; @@ -39,6 +40,7 @@ use function is_string; use function max; use function microtime; +use function preg_last_error_msg; use function preg_replace; use function preg_split; use function realpath; @@ -241,6 +243,9 @@ static function (string $arg) use ($valid_long_options): void { if ($operation === 'move_into') { $last_arg_parts = preg_split('/, ?/', $last_arg); + if ($last_arg_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } foreach ($last_arg_parts as $last_arg_part) { if (strpos($last_arg_part, '::')) { diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index 12592a39030..05dea134a7a 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -12,6 +12,7 @@ use Psalm\Exception\ConfigNotFoundException; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Report; +use RuntimeException; use function array_slice; use function assert; @@ -30,6 +31,7 @@ use function is_dir; use function is_string; use function json_decode; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -298,6 +300,9 @@ public static function getPathsToCheck($f_paths): ?array stream_set_blocking(STDIN, false); if ($stdin = fgets(STDIN)) { $filtered_input_paths = preg_split('/\s+/', trim($stdin)); + if ($filtered_input_paths === false) { + throw new RuntimeException('Invalid paths: '.preg_last_error_msg()); + } } $blocked = $meta['blocked']; stream_set_blocking(STDIN, $blocked); diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index cae25acbaa4..cb4ed8ffd50 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1455,9 +1455,10 @@ public function handleDocblockTypeInMigration( foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) { if ($type->containsClassLike($old_fq_class_name)) { - $type = clone $type; - - $type->replaceClassLike($old_fq_class_name, $new_fq_class_name); + $type = $type->replaceClassLike( + $old_fq_class_name, + $new_fq_class_name + ); $bounds = $type_location->getSelectionBounds(); @@ -1495,9 +1496,10 @@ public function handleDocblockTypeInMigration( $destination_class = $codebase->classes_to_move[$fq_class_name_lc]; if ($type->containsClassLike($fq_class_name_lc)) { - $type = clone $type; - - $type->replaceClassLike($fq_class_name_lc, $destination_class); + $type = $type->replaceClassLike( + $fq_class_name_lc, + $destination_class + ); } $this->airliftClassDefinedDocblockType( diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 88c9e03b189..e6e0fcae640 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -139,8 +139,11 @@ public static function resolve( } if ($left instanceof TKeyedArray && $right instanceof TKeyedArray) { - $type = new TKeyedArray($left->properties + $right->properties); - $type->sealed = true; + $type = new TKeyedArray( + $left->properties + $right->properties, + null, + true + ); return $type; } @@ -263,10 +266,7 @@ public static function resolve( new Union([new TNever()]), ]); } else { - $resolved_type = new TKeyedArray($properties); - - $resolved_type->is_list = $is_list; - $resolved_type->sealed = true; + $resolved_type = new TKeyedArray($properties, null, true, null, null, $is_list); } return $resolved_type; @@ -352,9 +352,7 @@ public static function getLiteralTypeFromScalarValue($value, bool $sealed_array foreach ($value as $key => $val) { $types[$key] = new Union([self::getLiteralTypeFromScalarValue($val, $sealed_array)]); } - $type = new TKeyedArray($types); - $type->sealed = $sealed_array; - return $type; + return new TKeyedArray($types, null, $sealed_array); } if (is_string($value)) { diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index a41fa171f0c..5cd02209614 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -305,6 +305,7 @@ public static function getCallablesFromCallMap(string $function_id): ?array $arg_name, $by_reference, $param_type, + $param_type, null, null, $optional, diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index ba56971366e..e92134bcf82 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -19,29 +19,24 @@ use Psalm\Internal\Provider\MethodVisibilityProvider; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\TypeVisitor\TypeLocalizer; use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TEnumCase; -use Psalm\Type\Atomic\TGenericObject; -use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Union; use UnexpectedValueException; use function array_pop; -use function array_values; use function assert; use function count; use function explode; @@ -494,7 +489,7 @@ public function getMethodParams( if ($params[$i]->signature_type && $params[$i]->signature_type->isNullable() ) { - $params[$i]->type->addType(new TNull); + $params[$i]->type = $params[$i]->type->getBuilder()->addType(new TNull)->freeze(); } $params[$i]->type_location = $overridden_storage->params[$i]->type_location; @@ -520,105 +515,10 @@ public static function localizeType( return $type; } - $type = clone $type; - - foreach ($type->getAtomicTypes() as $key => $atomic_type) { - if ($atomic_type instanceof TTemplateParam - && ($atomic_type->defining_class === $base_fq_class_name - || isset($extends[$atomic_type->defining_class])) - ) { - $types_to_add = self::getExtendedTemplatedTypes( - $atomic_type, - $extends - ); - - if ($types_to_add) { - $type->removeType($key); - - foreach ($types_to_add as $extra_added_type) { - $type->addType($extra_added_type); - } - } - } - - if ($atomic_type instanceof TTemplateParamClass) { - if ($atomic_type->defining_class === $base_fq_class_name) { - if (isset($extends[$base_fq_class_name][$atomic_type->param_name])) { - $extended_param = $extends[$base_fq_class_name][$atomic_type->param_name]; - - $types = array_values($extended_param->getAtomicTypes()); - - if (count($types) === 1 && $types[0] instanceof TNamedObject) { - $atomic_type->as_type = $types[0]; - } else { - $atomic_type->as_type = null; - } - } - } - } - - if ($atomic_type instanceof TArray - || $atomic_type instanceof TIterable - || $atomic_type instanceof TGenericObject - ) { - foreach ($atomic_type->type_params as &$type_param) { - $type_param = self::localizeType( - $codebase, - $type_param, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - - if ($atomic_type instanceof TList) { - $atomic_type->type_param = self::localizeType( - $codebase, - $atomic_type->type_param, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - - if ($atomic_type instanceof TKeyedArray) { - foreach ($atomic_type->properties as &$property_type) { - $property_type = self::localizeType( - $codebase, - $property_type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - - if ($atomic_type instanceof TCallable - || $atomic_type instanceof TClosure - ) { - if ($atomic_type->params) { - foreach ($atomic_type->params as $param) { - if ($param->type) { - $param->type = self::localizeType( - $codebase, - $param->type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - } - - if ($atomic_type->return_type) { - $atomic_type->return_type = self::localizeType( - $codebase, - $atomic_type->return_type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - } - - $type->bustCache(); + (new TypeLocalizer( + $extends, + $base_fq_class_name + ))->traverse($type); return $type; } @@ -725,9 +625,7 @@ public function getMethodReturnType( $types[] = new Union([new TEnumCase($original_fq_class_name, $case_name)]); } - $list = new TKeyedArray($types); - $list->is_list = true; - $list->sealed = true; + $list = new TKeyedArray($types, null, true, null, null, true); return new Union([$list]); } } @@ -889,12 +787,17 @@ public function getMethodReturnType( if ((!$old_contained_by_new && !$new_contained_by_old) || ($old_contained_by_new && $new_contained_by_old) ) { + $attempted_intersection = null; if ($old_contained_by_new) { //implicitly $new_contained_by_old as well - $attempted_intersection = Type::intersectUnionTypes( - $candidate_type, - $overridden_storage->return_type, - $source_analyzer->getCodebase() - ); + try { + $attempted_intersection = Type::intersectUnionTypes( + $candidate_type, + $overridden_storage->return_type, + $source_analyzer->getCodebase() + ); + } catch (InvalidArgumentException $e) { + // TODO: fix + } } else { $attempted_intersection = Type::intersectUnionTypes( $overridden_storage->return_type, diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index 893a02b7ec4..7bd20893404 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -343,6 +343,7 @@ private function getReflectionParamData(ReflectionParameter $param): FunctionLik $param_name, $param->isPassedByReference(), $param_type, + $param_type, null, null, $is_optional, diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php index e1391b4c6eb..bb1759675b8 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php @@ -119,7 +119,7 @@ private function readMessages(string $buffer): int ++$emitted_messages; $this->emit('message', [$msg]); /** - * @psalm-suppress DocblockTypeContradiction + * @psalm-suppress TypeDoesNotContainType */ if (!$this->is_accepting_new_requests) { // If we fork, don't read any bytes in the input buffer from the worker process. diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index f84bde3f889..cf8febd6575 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -30,6 +30,7 @@ use function explode; use function implode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -66,6 +67,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg()); + } $template_name = array_shift($template_type); @@ -106,6 +110,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template-covariant'])) { foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg()); + } $template_name = array_shift($template_type); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index fb7808c2c6f..b67b7e18a54 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -499,9 +499,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $yield_type_tokens, null, $storage->template_types ?: [], - $this->type_aliases + $this->type_aliases, + true ); - $yield_type->setFromDocblock(); $yield_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -559,9 +559,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $pseudo_property_type_tokens, null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); - $pseudo_property_type->setFromDocblock(); $pseudo_property_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -660,7 +660,8 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); $mixin_type->queueClassLikesForScanning( @@ -669,8 +670,6 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->template_types ?: [] ); - $mixin_type->setFromDocblock(); - if ($mixin_type->isSingle()) { $mixin_type = $mixin_type->getSingleAtomic(); @@ -797,11 +796,10 @@ public function finish(PhpParser\Node\Stmt\ClassLike $node): ClassLikeStorage $type->replacement_tokens, null, [], - $this->type_aliases + $this->type_aliases, + true ); - $union->setFromDocblock(); - $converted_aliases[$key] = new ClassTypeAlias(array_values($union->getAtomicTypes())); } catch (Exception $e) { $classlike_storage->docblock_issues[] = new InvalidDocblock( @@ -924,7 +922,8 @@ private function extendTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -942,8 +941,6 @@ private function extendTemplatedType( ); } - $extended_union_type->setFromDocblock(); - $extended_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1008,7 +1005,8 @@ private function implementTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1028,8 +1026,6 @@ private function implementTemplatedType( return; } - $implemented_union_type->setFromDocblock(); - $implemented_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1094,7 +1090,8 @@ private function useTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1114,8 +1111,6 @@ private function useTemplatedType( return; } - $used_union_type->setFromDocblock(); - $used_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1543,7 +1538,6 @@ private function visitPropertyDeclaration( if ($doc_var_group_type) { $doc_var_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage); - $doc_var_group_type->setFromDocblock(); } foreach ($stmt->props as $property) { @@ -1616,6 +1610,7 @@ private function visitPropertyDeclaration( foreach ($property_storage->type->getAtomicTypes() as $key => $type) { if (isset($signature_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->from_docblock = false; } else { $all_typehint_types_match = false; @@ -1629,7 +1624,7 @@ private function visitPropertyDeclaration( if ($property_storage->signature_type->isNullable() && !$property_storage->type->isNullable() ) { - $property_storage->type->addType(new TNull()); + $property_storage->type = $property_storage->type->getBuilder()->addType(new TNull())->freeze(); } } @@ -1849,6 +1844,10 @@ private static function getTypeAliasesFromCommentLines( array_shift($var_line_parts); } + if (!isset($var_line_parts[0])) { + continue; + } + if ($var_line_parts[0] === '=') { array_shift($var_line_parts); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 2626f7742d5..efd06cc0a7d 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\PhpVisitor\Reflector; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\DocComment; @@ -20,6 +21,7 @@ use function explode; use function implode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -229,6 +231,9 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-taint-sink'])) { foreach ($parsed_docblock->tags['psalm-taint-sink'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if (count($param_parts) >= 2) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; @@ -240,6 +245,9 @@ public static function parse( if (isset($parsed_docblock->tags['param-taint'])) { foreach ($parsed_docblock->tags['param-taint'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if (count($param_parts) === 2) { $taint_type = $param_parts[1]; @@ -264,6 +272,9 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-taint-source'])) { foreach ($parsed_docblock->tags['psalm-taint-source'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if ($param_parts[0]) { $info->taint_source_types[] = $param_parts[0]; @@ -273,6 +284,9 @@ public static function parse( // support for MediaWiki taint plugin foreach ($parsed_docblock->tags['return-taint'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if ($param_parts[0]) { if ($param_parts[0] === 'tainted') { @@ -429,6 +443,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new AssertionError(preg_last_error_msg()); + } $template_name = array_shift($template_type); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 084a6ce1bb7..2b9316afa4c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\PhpVisitor\Reflector; +use AssertionError; use PhpParser; use Psalm\Aliases; use Psalm\CodeLocation; @@ -52,6 +53,7 @@ use function count; use function explode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -765,6 +767,7 @@ private static function improveParamsFromDocblock( null, null, null, + null, false, false, true, @@ -785,7 +788,8 @@ private static function improveParamsFromDocblock( ), null, $function_template_types + $class_template_types, - $type_aliases + $type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -797,7 +801,6 @@ private static function improveParamsFromDocblock( } $storage_param->has_docblock_type = true; - $new_param_type->setFromDocblock(); $new_param_type->queueClassLikesForScanning( $codebase, @@ -846,7 +849,7 @@ private static function improveParamsFromDocblock( && !$new_param_type->isNullable() && !$new_param_type->hasTemplate() ) { - $new_param_type->addType(new TNull()); + $new_param_type = $new_param_type->getBuilder()->addType(new TNull())->freeze(); } $config = Config::getInstance(); @@ -870,6 +873,7 @@ private static function improveParamsFromDocblock( foreach ($new_param_type->getAtomicTypes() as $key => $type) { if (isset($storage_param_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->from_docblock = false; if ($storage_param_atomic_types[$key] instanceof TArray @@ -888,7 +892,7 @@ private static function improveParamsFromDocblock( } if ($existing_param_type_nullable && !$new_param_type->isNullable()) { - $new_param_type->addType(new TNull()); + $new_param_type = $new_param_type->getBuilder()->addType(new TNull())->freeze(); } $storage_param->type = $new_param_type; @@ -966,11 +970,10 @@ private static function handleReturn( array_values($fixed_type_tokens), null, $function_template_types + $class_template_types, - $type_aliases + $type_aliases, + true ); - $storage->return_type->setFromDocblock(); - if ($storage instanceof MethodStorage) { $storage->has_docblock_return_type = true; } @@ -981,6 +984,7 @@ private static function handleReturn( foreach ($storage->return_type->getAtomicTypes() as $key => $type) { if (isset($signature_return_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this atomic type */ $type->from_docblock = false; } else { $all_typehint_types_match = false; @@ -1010,7 +1014,7 @@ private static function handleReturn( $storage->signature_return_type ) ) { - $storage->return_type->addType(new TNull()); + $storage->return_type = $storage->return_type->getBuilder()->addType(new TNull())->freeze(); } } } @@ -1079,6 +1083,9 @@ private static function handleTaintFlow( if ($source_param_string[0] === '(' && substr($source_param_string, -1) === ')') { $source_params = preg_split('/, ?/', substr($source_param_string, 1, -1)); + if ($source_params === false) { + throw new AssertionError(preg_last_error_msg()); + } foreach ($source_params as $source_param) { $source_param = substr($source_param, 1); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 0c04fe9262d..ef7c2f7f207 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -846,7 +846,7 @@ private function getTranslatedFunctionParam( ); if ($is_nullable) { - $param_type->addType(new TNull); + $param_type = $param_type->getBuilder()->addType(new TNull)->freeze(); } else { $is_nullable = $param_type->isNullable(); } @@ -884,6 +884,7 @@ private function getTranslatedFunctionParam( $param->var->name, $param->byRef, $param_type, + $param_type, new CodeLocation( $this->file_scanner, $fake_method ? $stmt : $param->var, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 9265d3c8b15..336d8984cff 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -165,11 +165,12 @@ public static function resolve( if ($type_string) { $atomic_type = $type->getSingleAtomic(); + /** @psalm-suppress InaccessibleProperty We just created this type */ $atomic_type->text = $type_string; } if ($is_nullable) { - $type->addType(new TNull); + $type = $type->getBuilder()->addType(new TNull)->freeze(); } return $type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index be8e069e7e6..759053d5cd0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -88,8 +88,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!isset($call_args[1]) && !$first_arg_array->previous_value_type) { $had_one = count($first_arg_array->properties) === 1; - $first_arg_array = clone $first_arg_array; - $new_properties = array_filter( array_map( static function ($keyed_type) use ($statements_source, $context) { @@ -119,12 +117,14 @@ static function ($keyed_type) use ($statements_source, $context) { return Type::getEmptyArray(); } - $first_arg_array->properties = $new_properties; - - $first_arg_array->is_list = $first_arg_array->is_list && $had_one; - $first_arg_array->sealed = false; - - return new Union([$first_arg_array]); + return new Union([new TKeyedArray( + $new_properties, + null, + false, + null, + null, + $first_arg_array->is_list && $had_one + )]); } } @@ -153,11 +153,11 @@ static function ($keyed_type) use ($statements_source, $context) { } if ($key_type->getLiteralStrings()) { - $key_type->addType(new TString); + $key_type = $key_type->getBuilder()->addType(new TString)->freeze(); } if ($key_type->getLiteralInts()) { - $key_type->addType(new TInt); + $key_type = $key_type->getBuilder()->addType(new TInt)->freeze(); } if ($inner_type->isUnionEmpty()) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 5c55b900ce6..46a6bf10bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -197,12 +197,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev array_map( static fn(Union $_): Union => clone $mapping_return_type, $array_arg_atomic_type->properties - ) + ), + null, + $array_arg_atomic_type->sealed, + $array_arg_atomic_type->previous_key_type, + $mapping_return_type, + $array_arg_atomic_type->is_list ); - $atomic_type->is_list = $array_arg_atomic_type->is_list; - $atomic_type->sealed = $array_arg_atomic_type->sealed; - $atomic_type->previous_key_type = $array_arg_atomic_type->previous_key_type; - $atomic_type->previous_value_type = $mapping_return_type; return new Union([$atomic_type]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 90257156ecd..36970468665 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -235,20 +235,14 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && ($generic_property_count < $max_keyed_array_size * 2 || $generic_property_count < 16) ) { - $objectlike = new TKeyedArray($generic_properties); - - if ($class_strings !== []) { - $objectlike->class_strings = $class_strings; - } - - if ($all_nonempty_lists || $all_int_offsets) { - $objectlike->is_list = true; - } - - if (!$all_keyed_arrays) { - $objectlike->previous_key_type = $inner_key_type; - $objectlike->previous_value_type = $inner_value_type; - } + $objectlike = new TKeyedArray( + $generic_properties, + $class_strings ?: null, + false, + $all_keyed_arrays ? null : $inner_key_type, + $all_keyed_arrays ? null : $inner_value_type, + $all_nonempty_lists || $all_int_offsets + ); return new Union([$objectlike]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index 5ac91d639da..f89b9012e08 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -86,7 +86,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($value_type->isNever()) { $value_type = Type::getFalse(); } elseif (($function_id !== 'reset' && $function_id !== 'end') || !$definitely_has_items) { - $value_type->addType(new TFalse); + $value_type = $value_type->getBuilder()->addType(new TFalse)->freeze(); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php index 0ea580949d7..64266953d8f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -85,7 +85,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($nullable) { - $value_type->addType(new TNull); + $value_type = $value_type->getBuilder()->addType(new TNull)->freeze(); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php index eb6ef09c806..766895b9c4e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php @@ -51,10 +51,8 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($first_arg_array instanceof TArray) { - $first_arg_array = clone $first_arg_array; - if ($first_arg_array instanceof TNonEmptyArray) { - $first_arg_array->count = null; + $first_arg_array = $first_arg_array->setCount(null); } return new Union([$first_arg_array]); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index e24f5a3e531..afa872b72ca 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -104,7 +104,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $options_array->properties['default'] ); } else { - $filter_type->addType(new TFalse); + $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); } if (isset($atomic_type->properties['flags']) @@ -116,20 +116,20 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($filter_type->hasBool() && $filter_flag_type->value === FILTER_NULL_ON_FAILURE ) { - $filter_type->addType(new TNull); + $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); } } } elseif ($atomic_type instanceof TLiteralInt) { if ($atomic_type->value === FILTER_NULL_ON_FAILURE) { $filter_null = true; - $filter_type->addType(new TNull); + $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); } } } } if (!$has_object_like && !$filter_null && $filter_type) { - $filter_type->addType(new TFalse); + $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php index 00f633bd858..2a3d18e8048 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; /** @@ -34,15 +35,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } - $return_type = Type::getString(); - if (($first_arg_type = $statements_source->node_data->getType($call_args[0]->value)) && $first_arg_type->isString() ) { - return $return_type; + return new Union([new TString]); } - $return_type->addType(new TNull); + $return_type = new Union([new TString, new TNull]); $return_type->ignore_nullable_issues = true; return $return_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php index a5176b1a1ef..f3732474502 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php @@ -109,7 +109,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $template_types = $key_type->getTemplateTypes(); $template_type = array_shift($template_types); if ($template_type->as->hasMixed()) { - $template_type->as = Type::getArrayKey(); + $template_type = $template_type->replaceAs(Type::getArrayKey()); $key_type = new Union([$template_type]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php index c8eba436197..75c6d475910 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php @@ -52,6 +52,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'mode', false, Type::getInt(), + Type::getInt(), null, null, false @@ -66,6 +67,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'colno', false, Type::getInt(), + Type::getInt(), null, null, false @@ -77,6 +79,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'classname', false, Type::getClassString(), + Type::getClassString(), null, null, false @@ -86,6 +89,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'ctorargs', false, Type::getArray(), + Type::getArray(), null, null, true @@ -97,6 +101,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'object', false, Type::getObject(), + Type::getObject(), null, null, false diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php index 6061ddf0d34..2dfa3681691 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use function count; @@ -50,7 +51,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $return_type = Type::getString(); if (in_array($function_id, ['preg_replace', 'preg_replace_callback'], true)) { - $return_type->addType(new TNull()); + $return_type = new Union([new TString, new TNull()]); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/ReferenceConstraint.php b/src/Psalm/Internal/ReferenceConstraint.php index 14bb1707b41..a77ec7ec648 100644 --- a/src/Psalm/Internal/ReferenceConstraint.php +++ b/src/Psalm/Internal/ReferenceConstraint.php @@ -18,19 +18,21 @@ class ReferenceConstraint public function __construct(?Union $type = null) { if ($type) { - $this->type = clone $type; + $type = $type->getBuilder(); - if ($this->type->getLiteralStrings()) { - $this->type->addType(new TString); + if ($type->getLiteralStrings()) { + $type->addType(new TString); } - if ($this->type->getLiteralInts()) { - $this->type->addType(new TInt); + if ($type->getLiteralInts()) { + $type->addType(new TInt); } - if ($this->type->getLiteralFloats()) { - $this->type->addType(new TFloat); + if ($type->getLiteralFloats()) { + $type->addType(new TFloat); } + + $this->type = $type->freeze(); } } } diff --git a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index e65c343324a..df5c8d40e7e 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -177,10 +177,6 @@ public static function getAll( continue; } - if ($type->isMixed()) { - continue; - } - $name_parts = explode('\\', $fq_name); $constant_name = array_pop($name_parts); diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 5782e8bdb4e..d39fe490e64 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -282,7 +282,7 @@ private static function refine( StatementsAnalyzer $statements_analyzer, Assertion $assertion, Atomic $new_type_part, - Union $existing_var_type, + Union &$existing_var_type, ?string $key, bool $negated, ?CodeLocation $code_location, @@ -344,9 +344,11 @@ private static function refine( } if ($acceptable_atomic_types) { - $new_type_part->as = new Union($acceptable_atomic_types); - - return new Union([$new_type_part]); + $acceptable_atomic_types = + count($acceptable_atomic_types) === count($existing_var_type->getAtomicTypes()) + ? $existing_var_type + : new Union($acceptable_atomic_types); + return new Union([$new_type_part->replaceAs($acceptable_atomic_types)]); } } @@ -359,13 +361,10 @@ private static function refine( $existing_var_type_part->properties, $new_type_part->properties )) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->properties = array_merge( + $acceptable_atomic_types[] = $existing_var_type_part->setProperties(array_merge( $existing_var_type_part->properties, $new_type_part->properties - ); - - $acceptable_atomic_types[] = $existing_var_type_part; + )); } } } @@ -402,14 +401,12 @@ private static function refine( && ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value)) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } if ($existing_var_type_part instanceof TTemplateParam) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } @@ -530,7 +527,7 @@ private static function refine( */ private static function filterTypeWithAnother( Codebase $codebase, - Union $existing_type, + Union &$existing_type, Union $new_type, bool &$any_scalar_type_match_found = false ): ?Union { @@ -538,8 +535,9 @@ private static function filterTypeWithAnother( $new_type = clone $new_type; + $existing_types = $existing_type->getAtomicTypes(); foreach ($new_type->getAtomicTypes() as $new_type_part) { - foreach ($existing_type->getAtomicTypes() as $existing_type_part) { + foreach ($existing_types as &$existing_type_part) { $matching_atomic_type = self::filterAtomicWithAnother( $existing_type_part, $new_type_part, @@ -552,9 +550,9 @@ private static function filterTypeWithAnother( } } } + $existing_type = $existing_type->setTypes($existing_types); if ($matching_atomic_types) { - $existing_type->bustCache(); return new Union($matching_atomic_types); } @@ -562,7 +560,7 @@ private static function filterTypeWithAnother( } private static function filterAtomicWithAnother( - Atomic $type_1_atomic, + Atomic &$type_1_atomic, Atomic $type_2_atomic, Codebase $codebase, bool &$any_scalar_type_match_found @@ -575,7 +573,7 @@ private static function filterAtomicWithAnother( } if ($type_1_atomic instanceof TNamedObject) { - $type_1_atomic->is_static = false; + $type_1_atomic = $type_1_atomic->setIsStatic(false); } $atomic_comparison_results = new TypeComparisonResult(); @@ -625,10 +623,7 @@ private static function filterAtomicWithAnother( && ($codebase->interfaceExists($type_1_atomic->value) || $codebase->interfaceExists($type_2_atomic->value)) ) { - $matching_atomic_type = clone $type_2_atomic; - $matching_atomic_type->extra_types[$type_1_atomic->getKey()] = $type_1_atomic; - - return $matching_atomic_type; + return $type_2_atomic->addIntersectionType($type_1_atomic); } if ($type_2_atomic instanceof TKeyedArray @@ -638,23 +633,27 @@ private static function filterAtomicWithAnother( $type_2_value = $type_2_atomic->getGenericValueType(); if (!$type_2_key->hasString()) { + $type_1_type_param = $type_1_atomic->type_param; $type_2_value = self::filterTypeWithAnother( $codebase, - $type_1_atomic->type_param, + $type_1_type_param, $type_2_value, $any_scalar_type_match_found ); + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_type_param); if ($type_2_value === null) { return null; } - $hybrid_type_part = new TKeyedArray($type_2_atomic->properties); - $hybrid_type_part->previous_key_type = Type::getInt(); - $hybrid_type_part->previous_value_type = $type_2_value; - $hybrid_type_part->is_list = true; - - return $hybrid_type_part; + return new TKeyedArray( + $type_2_atomic->properties, + null, + false, + Type::getInt(), + $type_2_value, + true + ); } } elseif ($type_1_atomic instanceof TKeyedArray && $type_2_atomic instanceof TList @@ -663,9 +662,10 @@ private static function filterAtomicWithAnother( $type_1_value = $type_1_atomic->getGenericValueType(); if (!$type_1_key->hasString()) { + $type_2_type_param = $type_2_atomic->type_param; $type_1_value = self::filterTypeWithAnother( $codebase, - $type_2_atomic->type_param, + $type_2_type_param, $type_1_value, $any_scalar_type_match_found ); @@ -674,12 +674,14 @@ private static function filterAtomicWithAnother( return null; } - $hybrid_type_part = new TKeyedArray($type_1_atomic->properties); - $hybrid_type_part->previous_key_type = Type::getInt(); - $hybrid_type_part->previous_value_type = $type_1_value; - $hybrid_type_part->is_list = true; - - return $hybrid_type_part; + return new TKeyedArray( + $type_1_atomic->properties, + null, + false, + Type::getInt(), + $type_1_value, + true + ); } } @@ -689,11 +691,7 @@ private static function filterAtomicWithAnother( && $type_2_atomic->as->hasObject() && $type_1_atomic->as->hasObject() ) { - $matching_atomic_type = clone $type_2_atomic; - - $matching_atomic_type->extra_types[$type_1_atomic->getKey()] = $type_1_atomic; - - return $matching_atomic_type; + return $type_2_atomic->addIntersectionType($type_1_atomic); } //we filter both types of standard iterables @@ -705,8 +703,9 @@ private static function filterAtomicWithAnother( || $type_1_atomic instanceof TIterable) && count($type_2_atomic->type_params) === count($type_1_atomic->type_params) ) { + $type_1_params = $type_1_atomic->type_params; foreach ($type_2_atomic->type_params as $i => $type_2_param) { - $type_1_param = $type_1_atomic->type_params[$i]; + $type_1_param = $type_1_params[$i]; $type_2_param_id = $type_2_param->getId(); @@ -721,12 +720,16 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->type_params[$i]->getId() !== $type_2_param_id) { - /** @psalm-suppress PropertyTypeCoercion */ - $type_1_atomic->type_params[$i] = $type_2_param; + if ($type_1_params[$i]->getId() !== $type_2_param_id) { + $type_1_params[$i] = $type_2_param; } } + /** @psalm-suppress ArgumentTypeCoercion */ + $type_1_atomic = $type_1_atomic->replaceTypeParams( + $type_1_params + ); + $matching_atomic_type = $type_1_atomic; $atomic_comparison_results->type_coerced = true; } @@ -750,8 +753,10 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->type_param->getId() !== $type_2_param->getId()) { - $type_1_atomic->type_param = $type_2_param; + if ($type_1_param->getId() !== $type_2_param->getId()) { + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_2_param); + } elseif ($type_1_param !== $type_1_atomic->type_param) { + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_param); } $matching_atomic_type = $type_1_atomic; @@ -764,7 +769,8 @@ private static function filterAtomicWithAnother( && $type_1_atomic instanceof TKeyedArray ) { $type_2_param = $type_2_atomic->type_params[1]; - foreach ($type_1_atomic->properties as $property_key => $type_1_param) { + $type_1_properties = $type_1_atomic->properties; + foreach ($type_1_properties as &$type_1_param) { $type_2_param = self::filterTypeWithAnother( $codebase, $type_1_param, @@ -776,12 +782,12 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->properties[$property_key]->getId() !== $type_2_param->getId()) { - $type_1_atomic->properties[$property_key] = $type_2_param; + if ($type_1_param->getId() !== $type_2_param->getId()) { + $type_1_param = $type_2_param; } } - $matching_atomic_type = $type_1_atomic; + $matching_atomic_type = $type_1_atomic->setProperties($type_1_properties); $atomic_comparison_results->type_coerced = true; } @@ -833,10 +839,10 @@ private static function refineContainedAtomicWithAnother( && $type_1_atomic instanceof TTemplateParam && $type_1_atomic->as->hasObjectType() ) { - $type_1_atomic = clone $type_1_atomic; + $type_1_as_init = $type_1_atomic->as; $type_1_as = self::filterTypeWithAnother( $codebase, - $type_1_atomic->as, + $type_1_as_init, new Union([$type_2_atomic]) ); @@ -844,9 +850,7 @@ private static function refineContainedAtomicWithAnother( return null; } - $type_1_atomic->as = $type_1_as; - - return $type_1_atomic; + return $type_1_atomic->replaceAs($type_1_as); } else { return clone $type_2_atomic; } @@ -943,6 +947,7 @@ private static function handleLiteralEquality( $can_be_equal = false; $did_remove_type = false; + $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_atomic_types as $atomic_key => $atomic_type) { if (get_class($atomic_type) === TNamedObject::class && $atomic_type->value === $fq_enum_name @@ -958,6 +963,7 @@ private static function handleLiteralEquality( $can_be_equal = true; } } + $existing_var_type = $existing_var_type->freeze(); if ($var_id && $code_location @@ -1053,18 +1059,18 @@ private static function handleLiteralEqualityWithInt( return $compatible_int_type; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1195,18 +1201,18 @@ private static function handleLiteralEqualityWithString( return $literal_asserted_type_string; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1337,18 +1343,18 @@ private static function handleLiteralEqualityWithFloat( return $literal_asserted_type; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1615,8 +1621,7 @@ private static function handleIsA( if ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index b267ffca9e4..5948512b93b 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -111,6 +111,7 @@ public static function isContainedBy( && $input_type_part instanceof TNonEmptyArray && $input_type_part->type_params[0]->isSingleIntLiteral() && $input_type_part->type_params[0]->getSingleIntLiteral()->value === 0 + && isset($input_type_part->type_params[1]) ) { //this is a special case where the only offset value of an non empty array is 0, so it's a non empty list return UnionTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 28064420305..870026ff6fa 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -63,10 +63,10 @@ public static function isContainedBy( if (($container_type_part instanceof TTemplateParam || ($container_type_part instanceof TNamedObject - && isset($container_type_part->extra_types))) + && $container_type_part->extra_types)) && ($input_type_part instanceof TTemplateParam || ($input_type_part instanceof TNamedObject - && isset($input_type_part->extra_types))) + && $input_type_part->extra_types)) ) { return ObjectComparator::isShallowlyContainedBy( $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index e89269c3e0d..d67e7087d41 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Union; use UnexpectedValueException; use function end; @@ -258,20 +259,20 @@ public static function getCallableFromAtomic( $params = []; foreach ($function_storage->params as $param) { - $param = clone $param; - if ($param->type) { - $param->type = TypeExpander::expandUnion( - $codebase, - $param->type, - null, - null, - null, - true, - true, - false, - false, - true + $param = $param->replaceType( + TypeExpander::expandUnion( + $codebase, + $param->type, + null, + null, + null, + true, + true, + false, + false, + true + ) ); } @@ -325,7 +326,7 @@ public static function getCallableFromAtomic( } } - $matching_callable = InternalCallMapHandler::getCallableFromCallMapById( + $matching_callable = clone InternalCallMapHandler::getCallableFromCallMapById( $codebase, $input_type_part->value, $args, @@ -334,6 +335,7 @@ public static function getCallableFromAtomic( $must_use = false; + /** @psalm-suppress InaccessibleProperty We just cloned this object */ $matching_callable->is_pure = $codebase->functions->isCallMapFunctionPure( $codebase, $statements_analyzer->node_data ?? null, @@ -408,7 +410,7 @@ public static function getCallableFromAtomic( $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Type\Union([$input_with_templates]), $template_result, $codebase, @@ -440,15 +442,11 @@ public static function getCallableFromAtomic( ); if ($template_result) { - $replaced_callable = clone $callable; - - TemplateInferredTypeReplacer::replace( - new Type\Union([$replaced_callable]), + $callable = TemplateInferredTypeReplacer::replace( + new Union([clone $callable]), $template_result, $codebase - ); - - $callable = $replaced_callable; + )->getSingleAtomic(); } return $callable; @@ -500,6 +498,7 @@ public static function getCallableMethodIdFromTKeyedArray( } if ($member_id) { + /** @psalm-suppress PossiblyNullArgument Psalm bug */ $codebase->analyzer->addMixedMemberName( strtolower($member_id) . '::', $calling_method_id ?: $file_name diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 6d40f771857..c6de9ca06be 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -57,6 +57,16 @@ public static function isContainedBy( $container_type_params_covariant ); + $atomic_comparison_result_type_params = null; + if ($atomic_comparison_result) { + if (!$atomic_comparison_result->replacement_atomic_type) { + $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; + } + + if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { + $atomic_comparison_result_type_params = $atomic_comparison_result->replacement_atomic_type->type_params; + } + } foreach ($input_type_params as $i => $input_param) { if (!isset($container_type_part->type_params[$i])) { break; @@ -65,16 +75,8 @@ public static function isContainedBy( $container_param = $container_type_part->type_params[$i]; if ($input_param->isNever()) { - if ($atomic_comparison_result) { - if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; - } - - if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { - /** @psalm-suppress PropertyTypeCoercion */ - $atomic_comparison_result->replacement_atomic_type->type_params[$i] - = clone $container_param; - } + if ($atomic_comparison_result_type_params !== null) { + $atomic_comparison_result_type_params[$i] = clone $container_param; } continue; @@ -136,16 +138,8 @@ public static function isContainedBy( && !$input_param->hasTemplate() ) { if ($input_param->containsAnyLiteral()) { - if ($atomic_comparison_result) { - if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; - } - - if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { - /** @psalm-suppress PropertyTypeCoercion */ - $atomic_comparison_result->replacement_atomic_type->type_params[$i] - = clone $container_param; - } + if ($atomic_comparison_result_type_params !== null) { + $atomic_comparison_result_type_params[$i] = clone $container_param; } } else { if (!($container_type_params_covariant[$i] ?? false) @@ -179,6 +173,16 @@ public static function isContainedBy( } } + if ($atomic_comparison_result + && $atomic_comparison_result->replacement_atomic_type instanceof TGenericObject + && $atomic_comparison_result_type_params + ) { + /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ + $atomic_comparison_result->replacement_atomic_type = + $atomic_comparison_result->replacement_atomic_type + ->replaceTypeParams($atomic_comparison_result_type_params); + } + if ($all_types_contain) { if ($atomic_comparison_result) { $atomic_comparison_result->to_string_cast = false; diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index 6b98ebb6c4c..96ed14f299c 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -84,6 +84,8 @@ public static function isContainedByUnion( * This method receives an array of atomics from the container and a range. * The goal is to use values in atomics in order to reduce the range. * Once the range is empty, it means that every value in range was covered by some atomics combination + * + * @psalm-suppress InaccessibleProperty $reduced_range was already cloned * @param array $container_atomic_types */ private static function reduceRangeIncrementally(array &$container_atomic_types, TIntRange $reduced_range): ?bool diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5e3aff80828..a0a15900294 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -100,8 +100,7 @@ private static function getIntersectionTypes(Atomic $type_part): array // T1 as T2 as object becomes (T1 as object) & (T2 as object) if ($as_atomic_type instanceof TTemplateParam) { $intersection_types += self::getIntersectionTypes($as_atomic_type); - $type_part = clone $type_part; - $type_part->as = $as_atomic_type->as; + $type_part = $type_part->replaceAs($as_atomic_type->as); $intersection_types[$type_part->getKey()] = $type_part; return $intersection_types; @@ -112,10 +111,8 @@ private static function getIntersectionTypes(Atomic $type_part): array return [$type_part->getKey() => $type_part]; } - $type_part = clone $type_part; - $extra_types = $type_part->extra_types; - $type_part->extra_types = null; + $type_part = $type_part->setIntersectionTypes([]); $extra_types[$type_part->getKey()] = $type_part; @@ -199,11 +196,14 @@ private static function isIntersectionShallowlyContainedBy( } if ($intersection_input_type instanceof TTemplateParam) { - $intersection_container_type = clone $intersection_container_type; - - if ($intersection_container_type instanceof TNamedObject) { + if ($intersection_container_type instanceof TNamedObject && $intersection_container_type->is_static) { // this is extra check is redundant since we're comparing to a template as type - $intersection_container_type->is_static = false; + $intersection_container_type = new TNamedObject( + $intersection_container_type->value, + false, + $intersection_container_type->definite_class, + $intersection_container_type->extra_types, + ); } return UnionTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 2d0a6531d8b..9ceabd5bd07 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -174,14 +174,15 @@ public static function isContainedBy( && $atomic_comparison_result->replacement_atomic_type ) { if (!$union_comparison_result->replacement_union_type) { - $union_comparison_result->replacement_union_type = clone $input_type; + $union_comparison_result->replacement_union_type = $input_type; } - $union_comparison_result->replacement_union_type->removeType($input_type->getKey()); - - $union_comparison_result->replacement_union_type->addType( + $replacement = $union_comparison_result->replacement_union_type->getBuilder(); + $replacement->removeType($input_type->getKey()); + $replacement->addType( $atomic_comparison_result->replacement_atomic_type ); + $union_comparison_result->replacement_union_type = $replacement->freeze(); } } @@ -321,10 +322,10 @@ public static function isContainedByInPhp( return false; } - $input_type_not_null = clone $input_type; + $input_type_not_null = $input_type->getBuilder(); $input_type_not_null->removeType('null'); - $container_type_not_null = clone $container_type; + $container_type_not_null = $container_type->getBuilder(); $container_type_not_null->removeType('null'); if ($input_type_not_null->getId() === $container_type_not_null->getId()) { diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index efdf02d80c6..dfbf026a917 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -91,18 +91,20 @@ public static function reconcile( } $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); + $existing_var_type = $existing_var_type->getBuilder(); if ($assertion_type instanceof TFalse && isset($existing_var_atomic_types['bool'])) { $existing_var_type->removeType('bool'); $existing_var_type->addType(new TTrue); } elseif ($assertion_type instanceof TTrue && isset($existing_var_atomic_types['bool'])) { + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_type->removeType('bool'); $existing_var_type->addType(new TFalse); } else { $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile( $statements_analyzer->getCodebase(), $assertion, - $existing_var_type, + $existing_var_type->freeze(), $key, $negated, $code_location, @@ -142,7 +144,7 @@ public static function reconcile( $existing_var_type->from_calculation = false; - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality @@ -158,7 +160,7 @@ public static function reconcile( $existing_var_type->addType(new TNamedObject('DateTime')); } - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality && $assertion_type instanceof TNamedObject) { @@ -251,6 +253,8 @@ public static function reconcile( } } + $existing_var_type = $existing_var_type->freeze(); + if ($assertion instanceof IsNotIdentical && ($key !== '$this' || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)) @@ -322,6 +326,7 @@ private static function handleLiteralNegatedEquality( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); $did_remove_type = false; @@ -443,6 +448,8 @@ private static function handleLiteralNegatedEquality( } } + $existing_var_type = $existing_var_type->freeze(); + if ($key && $code_location) { if ($did_match_literal_type && (!$did_remove_type || count($existing_var_atomic_types) === 1) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 62a0473fdb8..95a2bc30c04 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Type; +use AssertionError; use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Internal\Codebase\ClassConstantByWildcardResolver; @@ -74,7 +75,7 @@ use Psalm\Type\Reconciler; use Psalm\Type\Union; -use function assert; +use function array_merge; use function count; use function explode; use function get_class; @@ -473,14 +474,16 @@ public static function reconcile( if ($existing_var_type->isSingle() && $existing_var_type->hasTemplate() ) { - foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + $types = $existing_var_type->getAtomicTypes(); + foreach ($types as $k => $atomic_type) { if ($atomic_type instanceof TTemplateParam && $assertion_type) { if ($atomic_type->as->hasMixed() || $atomic_type->as->hasObject() ) { - $atomic_type->as = new Union([clone $assertion_type]); - - return $existing_var_type; + unset($types[$k]); + $atomic_type = $atomic_type->replaceAs(new Union([clone $assertion_type])); + $types[$atomic_type->getKey()] = $atomic_type; + return new Union($types); } } } @@ -504,6 +507,7 @@ private static function reconcileIsset( bool $is_equality, bool $inside_loop ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); // if key references an array offset @@ -554,7 +558,7 @@ private static function reconcileIsset( $existing_var_type->possibly_undefined_from_try = false; $existing_var_type->ignore_isset = false; - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -570,6 +574,7 @@ private static function reconcileNonEmptyCountable( bool $is_equality ): Union { $old_var_type_string = $existing_var_type->getId(); + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('array')) { $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; @@ -584,13 +589,11 @@ private static function reconcileNonEmptyCountable( $existing_var_type->removeType('array'); } else { $non_empty_array = new TNonEmptyArray( - $array_atomic_type->type_params + $array_atomic_type->type_params, + null, + $assertion instanceof HasAtLeastCount ? $assertion->count : null ); - if ($assertion instanceof HasAtLeastCount) { - $non_empty_array->min_count = $assertion->count; - } - $existing_var_type->addType($non_empty_array); } @@ -602,13 +605,11 @@ private static function reconcileNonEmptyCountable( && $array_atomic_type->count < $assertion->count) ) { $non_empty_list = new TNonEmptyList( - $array_atomic_type->type_param + $array_atomic_type->type_param, + null, + $assertion instanceof HasAtLeastCount ? $assertion->count : null ); - if ($assertion instanceof HasAtLeastCount) { - $non_empty_list->min_count = $assertion->count; - } - $did_remove_type = true; $existing_var_type->addType($non_empty_list); } @@ -633,10 +634,14 @@ private static function reconcileNonEmptyCountable( // this means a redundant condition } else { $did_remove_type = true; + $properties = $array_atomic_type->properties; for ($i = $prop_count; $i < $assertion->count; $i++) { - $array_atomic_type->properties[$i] + $properties[$i] = clone ($array_atomic_type->previous_value_type ?: Type::getMixed()); } + $array_atomic_type = $array_atomic_type->setProperties($properties); + $existing_var_type->removeType('array'); + $existing_var_type->addType($array_atomic_type); } } else { $did_remove_type = true; @@ -665,7 +670,7 @@ private static function reconcileNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -675,33 +680,32 @@ private static function reconcileExactlyCountable( Union $existing_var_type, int $count ): Union { + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('array')) { $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; if ($array_atomic_type instanceof TArray) { $non_empty_array = new TNonEmptyArray( - $array_atomic_type->type_params + $array_atomic_type->type_params, + $count ); - $non_empty_array->count = $count; - $existing_var_type->addType( $non_empty_array ); } elseif ($array_atomic_type instanceof TList) { $non_empty_list = new TNonEmptyList( - $array_atomic_type->type_param + $array_atomic_type->type_param, + $count ); - $non_empty_list->count = $count; - $existing_var_type->addType( $non_empty_list ); } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -729,45 +733,50 @@ private static function reconcileHasMethod( if ($type instanceof TNamedObject && $codebase->classOrInterfaceExists($type->value) ) { - $object_types[] = $type; - if (!$codebase->methodExists($type->value . '::' . $method_name)) { $match_found = false; - if ($type->extra_types) { - foreach ($type->extra_types as $extra_type) { - if ($extra_type instanceof TNamedObject - && $codebase->classOrInterfaceExists($extra_type->value) - && $codebase->methodExists($extra_type->value . '::' . $method_name) - ) { - $match_found = true; - } elseif ($extra_type instanceof TObjectWithProperties) { - $match_found = true; - - if (!isset($extra_type->methods[$method_name])) { - $extra_type->methods[$method_name] = 'object::' . $method_name; - $did_remove_type = true; - } + $extra_types = $type->extra_types; + foreach ($type->extra_types as $k => $extra_type) { + if ($extra_type instanceof TNamedObject + && $codebase->classOrInterfaceExists($extra_type->value) + && $codebase->methodExists($extra_type->value . '::' . $method_name) + ) { + $match_found = true; + } elseif ($extra_type instanceof TObjectWithProperties) { + $match_found = true; + + if (!isset($extra_type->methods[$method_name])) { + unset($extra_types[$k]); + $extra_type = $extra_type->setMethods(array_merge($extra_type->methods, [ + $method_name => 'object::' . $method_name + ])); + $extra_types[$extra_type->getKey()] = $extra_type; + $did_remove_type = true; } } } if (!$match_found) { - $obj = new TObjectWithProperties( + $extra_type = new TObjectWithProperties( [], [$method_name => $type->value . '::' . $method_name] ); - $type->extra_types[$obj->getKey()] = $obj; + $extra_types[$extra_type->getKey()] = $extra_type; $did_remove_type = true; } + + $type = $type->setIntersectionTypes($extra_types); } - } elseif ($type instanceof TObjectWithProperties) { $object_types[] = $type; - + } elseif ($type instanceof TObjectWithProperties) { if (!isset($type->methods[$method_name])) { - $type->methods[$method_name] = 'object::' . $method_name; + $type = $type->setMethods(array_merge($type->methods, [ + $method_name => 'object::' . $method_name + ])); $did_remove_type = true; } + $object_types[] = $type; } elseif ($type instanceof TObject || $type instanceof TMixed) { $object_types[] = new TObjectWithProperties( [], @@ -842,11 +851,10 @@ private static function reconcileString( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TString) { - $string_types[] = $type; - if (get_class($type) === TString::class) { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } + $string_types[] = $type; } elseif ($type instanceof TCallable) { $string_types[] = new TCallableString; $did_remove_type = true; @@ -858,9 +866,7 @@ private static function reconcileString( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasString() || $type->as->hasMixed() || $type->as->hasScalar()) { - $type = clone $type; - - $type->as = self::reconcileString( + $type = $type->replaceAs(self::reconcileString( $assertion, $type->as, null, @@ -869,7 +875,7 @@ private static function reconcileString( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $string_types[] = $type; } @@ -935,12 +941,12 @@ private static function reconcileInt( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TInt) { - $int_types[] = $type; - if (get_class($type) === TInt::class) { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } + $int_types[] = $type; + if ($existing_var_type->from_calculation) { $did_remove_type = true; } @@ -952,9 +958,7 @@ private static function reconcileInt( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasInt() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileInt( + $type = $type->replaceAs(self::reconcileInt( $assertion, $type->as, null, @@ -962,7 +966,7 @@ private static function reconcileInt( null, $suppressed_issues, $failed_reconciliation - ); + )); $int_types[] = $type; } @@ -1028,16 +1032,14 @@ private static function reconcileBool( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TBool) { + $type = $type->setFromDocblock(false); $bool_types[] = $type; - $type->from_docblock = false; } elseif ($type instanceof TScalar) { $bool_types[] = new TBool; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasBool() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileBool( + $type = $type->replaceAs(self::reconcileBool( $assertion, $type->as, null, @@ -1046,7 +1048,7 @@ private static function reconcileBool( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $bool_types[] = $type; } @@ -1112,9 +1114,7 @@ private static function reconcileScalar( $scalar_types[] = $type; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasScalar() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileScalar( + $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, null, @@ -1123,7 +1123,7 @@ private static function reconcileScalar( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $scalar_types[] = $type; } @@ -1177,6 +1177,7 @@ private static function reconcileNumeric( if ($existing_var_type->hasMixed()) { return Type::getNumeric(); } + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); @@ -1206,9 +1207,7 @@ private static function reconcileNumeric( $numeric_types[] = new TNumericString(); } elseif ($type instanceof TTemplateParam) { if ($type->as->hasNumeric() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileNumeric( + $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, null, @@ -1217,7 +1216,7 @@ private static function reconcileNumeric( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $numeric_types[] = $type; } @@ -1287,15 +1286,12 @@ private static function reconcileObject( } elseif ($type instanceof TTemplateParam && $type->as->isMixed() ) { - $type = clone $type; - $type->as = Type::getObject(); + $type = $type->replaceAs(Type::getObject()); $object_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasObject() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileObject( + $type = $type->replaceAs(self::reconcileObject( $assertion, $type->as, null, @@ -1304,18 +1300,20 @@ private static function reconcileObject( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $object_types[] = $type; } $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); - $object_types[] = new TGenericObject('Traversable', $clone_type->type_params); + $object_types[] = new TGenericObject( + 'Traversable', + $params + ); $did_remove_type = true; } else { @@ -1445,7 +1443,7 @@ private static function reconcileCountable( $did_remove_type = true; } elseif ($type instanceof TNamedObject || $type instanceof TIterable) { $countable = new TNamedObject('Countable'); - $type->extra_types[$countable->getKey()] = $countable; + $type = $type->addIntersectionType($countable); $iterable_types[] = $type; $did_remove_type = true; } else { @@ -1594,7 +1592,8 @@ private static function reconcileHasArrayKey( HasArrayKey $assertion ): Union { $assertion = $assertion->key; - foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + $types = $existing_var_type->getAtomicTypes(); + foreach ($types as &$atomic_type) { if ($atomic_type instanceof TKeyedArray) { $is_class_string = false; @@ -1606,16 +1605,24 @@ private static function reconcileHasArrayKey( if (isset($atomic_type->properties[$assertion])) { $atomic_type->properties[$assertion]->possibly_undefined = false; } else { - $atomic_type->properties[$assertion] = Type::getMixed(); - - if ($is_class_string) { - $atomic_type->class_strings[$assertion] = true; - } + $atomic_type = new TKeyedArray( + array_merge( + $atomic_type->properties, + [$assertion => Type::getMixed()] + ), + $is_class_string ? array_merge( + $atomic_type->class_strings ?? [], + [$assertion => true] + ) : $atomic_type->class_strings, + $atomic_type->sealed, + $atomic_type->previous_key_type, + $atomic_type->previous_value_type, + $atomic_type->is_list + ); } } } - - return $existing_var_type; + return $existing_var_type->setTypes($types); } /** @@ -1631,6 +1638,7 @@ private static function reconcileIsGreaterThan( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); //we add 1 from the assertion value because we're on a strict operator $assertion_value = $assertion->value + 1; @@ -1651,15 +1659,19 @@ private static function reconcileIsGreaterThan( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = $assertion_value; + $min_bound = $atomic_type->min_bound; + if ($min_bound === null) { + $min_bound = $assertion_value; } else { - $atomic_type->min_bound = TIntRange::getNewHighestBound( + $min_bound = TIntRange::getNewHighestBound( $assertion_value, - $atomic_type->min_bound + $min_bound ); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $min_bound, + $atomic_type->max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the type must be removed $did_remove_type = true; @@ -1720,7 +1732,7 @@ private static function reconcileIsGreaterThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1738,6 +1750,7 @@ private static function reconcileIsLessThan( ): Union { //we remove 1 from the assertion value because we're on a strict operator $assertion_value = $assertion->value - 1; + $existing_var_type = $existing_var_type->getBuilder(); $did_remove_type = false; @@ -1756,12 +1769,16 @@ private static function reconcileIsLessThan( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = $assertion_value; + $max_bound = $atomic_type->max_bound; + if ($max_bound === null) { + $max_bound = $assertion_value; } else { - $atomic_type->max_bound = min($atomic_type->max_bound, $assertion_value); + $max_bound = min($max_bound, $assertion_value); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $atomic_type->min_bound, + $max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the check is redundant } elseif ($atomic_type->isGreaterThan($assertion_value)) { @@ -1822,7 +1839,7 @@ private static function reconcileIsLessThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1863,7 +1880,7 @@ private static function reconcileTraversable( $did_remove_type = true; } elseif ($type instanceof TNamedObject) { $traversable = new TNamedObject('Traversable'); - $type->extra_types[$traversable->getKey()] = $traversable; + $type = $type->addIntersectionType($traversable); $traversable_types[] = $type; $did_remove_type = true; } else { @@ -1933,18 +1950,14 @@ private static function reconcileArray( $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); - - $array_types[] = new TArray($clone_type->type_params); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); + $array_types[] = new TArray($params); $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasArray() || $type->as->hasIterable() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileArray( + $type = $type->replaceAs(self::reconcileArray( $assertion, $type->as, null, @@ -1953,7 +1966,7 @@ private static function reconcileArray( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $array_types[] = $type; } @@ -2290,9 +2303,7 @@ private static function reconcileCallable( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasCallableType() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileCallable( + $type = $type->replaceAs(self::reconcileCallable( $assertion, $codebase, $type->as, @@ -2302,7 +2313,7 @@ private static function reconcileCallable( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); } $did_remove_type = true; @@ -2354,6 +2365,7 @@ private static function reconcileTruthyOrNonEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); //empty is used a lot to check for array offset existence, so we have to silent errors a lot @@ -2362,12 +2374,12 @@ private static function reconcileTruthyOrNonEmpty( $did_remove_type = $existing_var_type->possibly_undefined || $existing_var_type->possibly_undefined_from_try; - foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_key => $existing_var_type_part) { + foreach ($types as $existing_var_type_key => $existing_var_type_part) { //if any atomic in the union is either always falsy, we remove it. If not always truthy, we mark the check //as not redundant. if ($existing_var_type_part->isFalsy()) { $did_remove_type = true; - $existing_var_type->removeType($existing_var_type_key); + unset($types[$existing_var_type_key]); } elseif ($existing_var_type->possibly_undefined || $existing_var_type->possibly_undefined_from_try || !$existing_var_type_part->isTruthy() @@ -2376,7 +2388,7 @@ private static function reconcileTruthyOrNonEmpty( } } - if ($did_remove_type && $existing_var_type->isUnionEmpty()) { + if ($did_remove_type && !$types) { //every type was removed, this is an impossible assertion if ($code_location && $key && !$is_empty_assertion && !$recursive_check) { self::triggerIssueForImpossible( @@ -2412,74 +2424,66 @@ private static function reconcileTruthyOrNonEmpty( $failed_reconciliation = 1; - return $existing_var_type; + if (!$types) { + throw new AssertionError("We must have some types here!"); + } + return $existing_var_type->setTypes($types); } - $existing_var_type->possibly_undefined = false; - $existing_var_type->possibly_undefined_from_try = false; - - if ($existing_var_type->hasType('bool')) { - $existing_var_type->removeType('bool'); - $existing_var_type->addType(new TTrue()); + if (isset($types['bool'])) { + unset($types['bool']); + $types []= new TTrue; } - if ($existing_var_type->hasArray()) { - $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; + if (isset($types['array'])) { + $array_atomic_type = $types['array']; if ($array_atomic_type instanceof TArray && !$array_atomic_type instanceof TNonEmptyArray ) { - $existing_var_type->removeType('array'); - $existing_var_type->addType( - new TNonEmptyArray( - $array_atomic_type->type_params - ) - ); + unset($types['array']); + $types [] = new TNonEmptyArray($array_atomic_type->type_params); } elseif ($array_atomic_type instanceof TList && !$array_atomic_type instanceof TNonEmptyList ) { - $existing_var_type->removeType('array'); - $existing_var_type->addType( - new TNonEmptyList( - $array_atomic_type->type_param - ) - ); + unset($types['array']); + $types [] = new TNonEmptyList($array_atomic_type->type_param); } } - if ($existing_var_type->hasMixed()) { - $mixed_atomic_type = $existing_var_type->getAtomicTypes()['mixed']; + if (isset($types['mixed'])) { + $mixed_atomic_type = $types['mixed']; if (get_class($mixed_atomic_type) === TMixed::class) { - $existing_var_type->removeType('mixed'); - $existing_var_type->addType(new TNonEmptyMixed()); + unset($types['mixed']); + $types []= new TNonEmptyMixed(); } } - if ($existing_var_type->hasScalar()) { - $scalar_atomic_type = $existing_var_type->getAtomicTypes()['scalar']; + if (isset($types['scalar'])) { + $scalar_atomic_type = $types['scalar']; if (get_class($scalar_atomic_type) === TScalar::class) { - $existing_var_type->removeType('scalar'); - $existing_var_type->addType(new TNonEmptyScalar()); + unset($types['scalar']); + $types []= new TNonEmptyScalar(); } } - if ($existing_var_type->hasType('string')) { - $string_atomic_type = $existing_var_type->getAtomicTypes()['string']; + if (isset($types['string'])) { + $string_atomic_type = $types['string']; if (get_class($string_atomic_type) === TString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonFalsyString()); + unset($types['string']); + $types []= new TNonFalsyString(); } elseif (get_class($string_atomic_type) === TLowercaseString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonEmptyLowercaseString()); + unset($types['string']); + $types []= new TNonEmptyLowercaseString(); } elseif (get_class($string_atomic_type) === TNonspecificLiteralString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonEmptyNonspecificLiteralString()); + unset($types['string']); + $types []= new TNonEmptyNonspecificLiteralString(); } elseif (get_class($string_atomic_type) === TNonEmptyString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonFalsyString()); + unset($types['string']); + $types []= new TNonFalsyString(); } } @@ -2489,30 +2493,24 @@ private static function reconcileTruthyOrNonEmpty( if ($existing_range_types) { foreach ($existing_range_types as $int_key => $literal_type) { if ($literal_type->contains(0)) { - $existing_var_type->removeType($int_key); + unset($types[$int_key]); if ($literal_type->min_bound === null || $literal_type->min_bound <= -1) { - $existing_var_type->addType(new TIntRange($literal_type->min_bound, -1)); + $types []= new TIntRange($literal_type->min_bound, -1); } if ($literal_type->max_bound === null || $literal_type->max_bound >= 1) { - $existing_var_type->addType(new TIntRange(1, $literal_type->max_bound)); + $types []= new TIntRange(1, $literal_type->max_bound); } } } } - - if ($existing_var_type->isSingle()) { - return $existing_var_type; - } } - foreach ($existing_var_type->getAtomicTypes() as $type_key => $existing_var_atomic_type) { + foreach ($types as $type_key => $existing_var_atomic_type) { if ($existing_var_atomic_type instanceof TTemplateParam) { if (!$existing_var_atomic_type->as->isMixed()) { $template_did_fail = 0; - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::reconcileTruthyOrNonEmpty( + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs(self::reconcileTruthyOrNonEmpty( $assertion, $existing_var_atomic_type->as, $key, @@ -2521,18 +2519,29 @@ private static function reconcileTruthyOrNonEmpty( $suppressed_issues, $template_did_fail, true - ); + )); if (!$template_did_fail) { - $existing_var_type->removeType($type_key); - $existing_var_type->addType($existing_var_atomic_type); + unset($types[$type_key]); + $types []= $existing_var_atomic_type; } } } } - assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + if (!$types) { + throw new AssertionError("We must have some types here!"); + } + $new = $existing_var_type->setTypes($types); + if ($new === $existing_var_type && ($new->possibly_undefined || $new->possibly_undefined_from_try)) { + $new = clone $existing_var_type; + $new->possibly_undefined = false; + $new->possibly_undefined_from_try = false; + } else { + $new->possibly_undefined = false; + $new->possibly_undefined_from_try = false; + } + return $new; } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index ff1b3dcdfad..e744f456ad1 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -413,6 +413,7 @@ public static function reconcile( private static function reconcileCallable( Union $existing_var_type ): Union { + $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) { if ($type instanceof TLiteralString && InternalCallMapHandler::inCallMap($type->value) @@ -425,7 +426,7 @@ private static function reconcileCallable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -513,6 +514,7 @@ private static function reconcileNotNonEmptyCountable( bool $is_equality, ?int $min_count ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); @@ -570,7 +572,7 @@ private static function reconcileNotNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -587,17 +589,18 @@ private static function reconcileNull( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; - if ($existing_var_type->hasType('null')) { + if (isset($types['null'])) { $did_remove_type = true; - $existing_var_type->removeType('null'); + unset($types['null']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileNull( + $new = $type->replaceAs(self::reconcileNull( $assertion, $type->as, null, @@ -606,14 +609,17 @@ private static function reconcileNull( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); + // $did_remove_type = $did_remove_type || $new !== $type; + // TODO: This is technically wrong, but for some reason we get a + // duplicated assertion here when using template types. $did_remove_type = true; - $existing_var_type->bustCache(); + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -632,8 +638,8 @@ private static function reconcileNull( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -657,17 +663,18 @@ private static function reconcileFalse( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); - $did_remove_type = $existing_var_type->hasScalar(); + $did_remove_type = false; - if ($existing_var_type->hasType('false')) { + if (isset($types['false'])) { $did_remove_type = true; - $existing_var_type->removeType('false'); + unset($types['false']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileFalse( + $new = $type->replaceAs(self::reconcileFalse( $assertion, $type->as, null, @@ -676,14 +683,14 @@ private static function reconcileFalse( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); - $did_remove_type = true; - $existing_var_type->bustCache(); + $did_remove_type = $did_remove_type || $new !== $type; + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -702,8 +709,8 @@ private static function reconcileFalse( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -728,6 +735,7 @@ private static function reconcileFalsyOrEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = $existing_var_type->possibly_undefined @@ -786,7 +794,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } if ($existing_var_type->hasType('bool')) { @@ -872,9 +880,7 @@ private static function reconcileFalsyOrEmpty( if (!$existing_var_atomic_type->as->isMixed()) { $template_did_fail = 0; - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::reconcileFalsyOrEmpty( + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs(self::reconcileFalsyOrEmpty( $assertion, $existing_var_atomic_type->as, $key, @@ -883,7 +889,7 @@ private static function reconcileFalsyOrEmpty( $suppressed_issues, $template_did_fail, $recursive_check - ); + )); if (!$template_did_fail) { $existing_var_type->removeType($type_key); @@ -893,8 +899,9 @@ private static function reconcileFalsyOrEmpty( } } + /** @psalm-suppress RedundantCondition Psalm bug */ assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -920,9 +927,7 @@ private static function reconcileScalar( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileScalar( + $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, null, @@ -931,7 +936,7 @@ private static function reconcileScalar( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1010,9 +1015,7 @@ private static function reconcileObject( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileObject( + $type = $type->replaceAs(self::reconcileObject( $assertion, $type->as, null, @@ -1021,7 +1024,7 @@ private static function reconcileObject( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1040,11 +1043,9 @@ private static function reconcileObject( $non_object_types[] = new TCallableString(); $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); - - $non_object_types[] = new TArray($clone_type->type_params); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); + $non_object_types[] = new TArray($params); $did_remove_type = true; } elseif (!$type->isObjectType()) { @@ -1116,9 +1117,7 @@ private static function reconcileNumeric( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileNumeric( + $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, null, @@ -1127,7 +1126,7 @@ private static function reconcileNumeric( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1209,9 +1208,7 @@ private static function reconcileInt( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileInt( + $type = $type->replaceAs(self::reconcileInt( $assertion, $type->as, null, @@ -1220,7 +1217,7 @@ private static function reconcileInt( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1313,9 +1310,7 @@ private static function reconcileFloat( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileFloat( + $type = $type->replaceAs(self::reconcileFloat( $assertion, $type->as, null, @@ -1324,7 +1319,7 @@ private static function reconcileFloat( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1412,9 +1407,7 @@ private static function reconcileString( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileString( + $type = $type->replaceAs(self::reconcileString( $assertion, $type->as, null, @@ -1423,7 +1416,7 @@ private static function reconcileString( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1520,9 +1513,7 @@ private static function reconcileArray( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileArray( + $type = $type->replaceAs(self::reconcileArray( $assertion, $type->as, null, @@ -1531,7 +1522,7 @@ private static function reconcileArray( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1616,17 +1607,18 @@ private static function reconcileResource( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; - if ($existing_var_type->hasType('resource')) { + if (isset($types['resource'])) { $did_remove_type = true; - $existing_var_type->removeType('resource'); + unset($types['resource']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileResource( + $new = $type->replaceAs(self::reconcileResource( $assertion, $type->as, null, @@ -1635,14 +1627,14 @@ private static function reconcileResource( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); - $did_remove_type = true; - $existing_var_type->bustCache(); + $did_remove_type = $new !== $type; + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -1661,8 +1653,8 @@ private static function reconcileResource( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1685,6 +1677,7 @@ private static function reconcileIsLessThanOrEqualTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $assertion_value = $assertion->value; $did_remove_type = false; @@ -1705,14 +1698,17 @@ private static function reconcileIsLessThanOrEqualTo( $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = $assertion_value; + $max_bound = $assertion_value; } else { - $atomic_type->max_bound = TIntRange::getNewLowestBound( + $max_bound = TIntRange::getNewLowestBound( $assertion_value, $atomic_type->max_bound ); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $atomic_type->min_bound, + $max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the check is redundant } elseif ($atomic_type->isGreaterThan($assertion_value)) { @@ -1773,7 +1769,7 @@ private static function reconcileIsLessThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1789,6 +1785,7 @@ private static function reconcileIsGreaterThanOrEqualTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $assertion_value = $assertion->value; $did_remove_type = false; @@ -1808,12 +1805,16 @@ private static function reconcileIsGreaterThanOrEqualTo( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = $assertion_value; + $min_bound = $atomic_type->min_bound; + if ($min_bound === null) { + $min_bound = $assertion_value; } else { - $atomic_type->min_bound = max($atomic_type->min_bound, $assertion_value); + $min_bound = max($min_bound, $assertion_value); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $min_bound, + $atomic_type->max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the type must be removed $did_remove_type = true; @@ -1874,6 +1875,6 @@ private static function reconcileIsGreaterThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 6d4d753157b..7e0e76f0597 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -42,22 +42,25 @@ class TemplateInferredTypeReplacer { /** * This replaces template types in unions with the inferred types they should be + * + * @psalm-external-mutation-free */ public static function replace( Union $union, TemplateResult $template_result, ?Codebase $codebase - ): void { - $keys_to_unset = []; - + ): Union { $new_types = []; $is_mixed = false; $inferred_lower_bounds = $template_result->lower_bounds ?: []; + $types = []; + foreach ($union->getAtomicTypes() as $key => $atomic_type) { - $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $should_set = true; + $atomic_type = $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); if ($atomic_type instanceof TTemplateParam) { $template_type = self::replaceTemplateParam( @@ -68,7 +71,7 @@ public static function replace( ); if ($template_type) { - $keys_to_unset[] = $key; + $should_set = false; foreach ($template_type->getAtomicTypes() as $template_type_part) { if ($template_type_part instanceof TMixed) { @@ -113,11 +116,11 @@ public static function replace( } if ($class_template_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $class_template_type; } } elseif ($atomic_type instanceof TTemplateIndexedAccess) { - $keys_to_unset[] = $key; + $should_set = false; $template_type = null; @@ -175,7 +178,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TTemplatePropertiesOf) { @@ -186,7 +189,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TConditional @@ -199,43 +202,41 @@ public static function replace( $inferred_lower_bounds ); - $keys_to_unset[] = $key; + $should_set = false; foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) { $new_types[] = $class_template_atomic_type; } } + if ($should_set) { + $types []= $atomic_type; + } } - $union->bustCache(); - if ($is_mixed) { if (!$new_types) { throw new UnexpectedValueException('This array should be full'); } - $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $new_types, $codebase )->getAtomicTypes() - ); - - return; + )->freeze(); } - foreach ($keys_to_unset as $key) { - $union->removeType($key); + $atomic_types = array_merge($types, $new_types); + if (!$atomic_types) { + throw new UnexpectedValueException('This array should be full'); } - $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); - - $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $atomic_types, $codebase )->getAtomicTypes() - ); + )->freeze(); } /** @@ -260,34 +261,35 @@ private static function replaceTemplateParam( if ($traversed_type) { $template_type = $traversed_type; - if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = clone $atomic_type->as; - } else { - $template_type = clone $template_type; + if ($template_type->isMixed() && !$atomic_type->as->isMixed()) { + $template_type = $atomic_type->as; } if ($atomic_type->extra_types) { - foreach ($template_type->getAtomicTypes() as $template_type_key => $atomic_template_type) { + $types = []; + foreach ($template_type->getAtomicTypes() as $atomic_template_type) { if ($atomic_template_type instanceof TNamedObject || $atomic_template_type instanceof TTemplateParam || $atomic_template_type instanceof TIterable || $atomic_template_type instanceof TObjectWithProperties ) { - $atomic_template_type->extra_types = array_merge( + $types []= $atomic_template_type->setIntersectionTypes(array_merge( $atomic_type->extra_types, - $atomic_template_type->extra_types ?: [] - ); + $atomic_template_type->extra_types + )); } elseif ($atomic_template_type instanceof TObject) { $first_atomic_type = array_shift($atomic_type->extra_types); if ($atomic_type->extra_types) { - $first_atomic_type->extra_types = $atomic_type->extra_types; + $first_atomic_type = $first_atomic_type->setIntersectionTypes($atomic_type->extra_types); } - $template_type->removeType($template_type_key); - $template_type->addType($first_atomic_type); + $types []= $first_atomic_type; + } else { + $types []= $atomic_template_type; } } + $template_type = $template_type->getBuilder()->setTypes($types)->freeze(); } } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { @@ -382,8 +384,7 @@ private static function replaceTemplatePropertiesOf( } return new TPropertiesOf( - (string) $classlike_type, - clone $classlike_type, + $classlike_type, $atomic_type->visibility_filter ); } @@ -394,7 +395,7 @@ private static function replaceTemplatePropertiesOf( private static function replaceConditional( TemplateResult $template_result, Codebase $codebase, - TConditional $atomic_type, + TConditional &$atomic_type, array $inferred_lower_bounds ): Union { $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) @@ -407,16 +408,19 @@ private static function replaceConditional( $if_template_type = null; $else_template_type = null; - $atomic_type = clone $atomic_type; + $as_type = $atomic_type->as_type; + $conditional_type = $atomic_type->conditional_type; + $if_type = $atomic_type->if_type; + $else_type = $atomic_type->else_type; if ($template_type) { - self::replace( - $atomic_type->as_type, + $as_type = self::replace( + $as_type, $template_result, $codebase ); - if ($atomic_type->as_type->isNullable() && $template_type->isVoid()) { + if ($as_type->isNullable() && $template_type->isVoid()) { $template_type = Type::getNull(); } @@ -427,7 +431,7 @@ private static function replaceConditional( if (UnionTypeComparator::isContainedBy( $codebase, new Union([$candidate_atomic_type]), - $atomic_type->conditional_type, + $conditional_type, false, false, null, @@ -435,12 +439,12 @@ private static function replaceConditional( false ) && (!$candidate_atomic_type instanceof TInt - || $atomic_type->conditional_type->getId() !== 'float') + || $conditional_type->getId() !== 'float') ) { $matching_if_types[] = $candidate_atomic_type; } elseif (!UnionTypeComparator::isContainedBy( $codebase, - $atomic_type->conditional_type, + $conditional_type, new Union([$candidate_atomic_type]), false, false, @@ -459,7 +463,7 @@ private static function replaceConditional( && UnionTypeComparator::isContainedBy( $codebase, $if_candidate_type, - $atomic_type->conditional_type, + $conditional_type, false, false, null, @@ -467,7 +471,7 @@ private static function replaceConditional( false ) ) { - $if_template_type = clone $atomic_type->if_type; + $if_template_type = clone $if_type; $refined_template_result = clone $template_result; @@ -478,7 +482,7 @@ private static function replaceConditional( ) ]; - self::replace( + $if_template_type = self::replace( $if_template_type, $refined_template_result, $codebase @@ -489,7 +493,7 @@ private static function replaceConditional( && UnionTypeComparator::isContainedBy( $codebase, $else_candidate_type, - $atomic_type->as_type, + $as_type, false, false, null, @@ -497,7 +501,7 @@ private static function replaceConditional( false ) ) { - $else_template_type = clone $atomic_type->else_type; + $else_template_type = clone $else_type; $refined_template_result = clone $template_result; @@ -508,7 +512,7 @@ private static function replaceConditional( ) ]; - self::replace( + $else_template_type = self::replace( $else_template_type, $refined_template_result, $codebase @@ -517,21 +521,21 @@ private static function replaceConditional( } if (!$if_template_type && !$else_template_type) { - self::replace( - $atomic_type->if_type, + $if_type = self::replace( + $if_type, $template_result, $codebase ); - self::replace( - $atomic_type->else_type, + $else_type = self::replace( + $else_type, $template_result, $codebase ); $class_template_type = Type::combineUnionTypes( - $atomic_type->if_type, - $atomic_type->else_type, + $if_type, + $else_type, $codebase ); } else { @@ -542,6 +546,13 @@ private static function replaceConditional( ); } + $atomic_type = $atomic_type->replaceTypes( + $as_type, + $conditional_type, + $if_type, + $else_type + ); + return $class_template_type; } } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 72d56358e2a..68a61684cf0 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -54,6 +54,39 @@ */ class TemplateStandinTypeReplacer { + /** + * This method fills in the values in $template_result based on how the various atomic types + * of $union_type match up to the types inside $input_type. + */ + public static function fillTemplateResult( + Union $union_type, + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer, + ?Union $input_type, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + ?string $bound_equality_classlike = null, + int $depth = 1 + ): void { + self::replace( + $union_type, + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $bound_equality_classlike, + $depth + ); + } /** * This replaces template types in unions with standins (normally the template as type) * @@ -61,6 +94,7 @@ class TemplateStandinTypeReplacer * * This method fills in the values in $template_result based on how the various atomic types * of $union_type match up to the types inside $input_type + * */ public static function replace( Union $union_type, @@ -84,7 +118,7 @@ public static function replace( // when they're also in the union type, so those shared atomic // types will never be inferred as part of the generic type if ($input_type && !$input_type->isSingle()) { - $new_input_type = clone $input_type; + $new_input_type = $input_type->getBuilder(); foreach ($original_atomic_types as $key => $_) { if ($new_input_type->hasType($key)) { @@ -93,7 +127,7 @@ public static function replace( } if (!$new_input_type->isUnionEmpty()) { - $input_type = $new_input_type; + $input_type = $new_input_type->freeze(); } else { return $union_type; } @@ -340,9 +374,9 @@ private static function handleAtomicStandin( return [$atomic_type]; } + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, $atomic_type is not a reference */ $atomic_type = new TPropertiesOf( - (string) $classlike_type, - clone $classlike_type, + $classlike_type, $atomic_type->visibility_filter ); return [$atomic_type]; @@ -361,6 +395,7 @@ private static function handleAtomicStandin( } if (!$matching_atomic_types) { + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, $atomic_type is not a reference */ $atomic_type = $atomic_type->replaceTemplateTypesWithStandins( $template_result, $codebase, @@ -566,7 +601,7 @@ private static function findMatchingAtomicTypesForTemplate( * @return list */ private static function handleTemplateParamStandin( - TTemplateParam $atomic_type, + TTemplateParam &$atomic_type, string $key, ?Union $input_type, ?int $input_arg_offset, @@ -729,7 +764,7 @@ private static function handleTemplateParamStandin( $matching_input_keys = []; - $atomic_type->as = TypeExpander::expandUnion( + $as = TypeExpander::expandUnion( $codebase, $atomic_type->as, $calling_class, @@ -737,8 +772,8 @@ private static function handleTemplateParamStandin( null ); - $atomic_type->as = self::replace( - $atomic_type->as, + $as = self::replace( + $as, $template_result, $codebase, $statements_analyzer, @@ -752,6 +787,8 @@ private static function handleTemplateParamStandin( $depth + 1 ); + $atomic_type = $atomic_type->replaceAs($as); + if ($input_type && !$template_result->readonly && ( @@ -766,7 +803,7 @@ private static function handleTemplateParamStandin( ) ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -777,12 +814,11 @@ private static function handleTemplateParamStandin( } } } - if ($add_lower_bound) { return array_values($generic_param->getAtomicTypes()); } - $generic_param->setFromDocblock(); + $generic_param = $generic_param->setFromDocblock()->freeze(); if (isset( $template_result->lower_bounds[$param_name_key][$atomic_type->defining_class] @@ -830,16 +866,15 @@ private static function handleTemplateParamStandin( } } - foreach ($atomic_types as &$atomic_type) { - if ($atomic_type instanceof TNamedObject - || $atomic_type instanceof TTemplateParam - || $atomic_type instanceof TIterable - || $atomic_type instanceof TObjectWithProperties + foreach ($atomic_types as &$t) { + if ($t instanceof TNamedObject + || $t instanceof TTemplateParam + || $t instanceof TIterable + || $t instanceof TObjectWithProperties ) { - $atomic_type->extra_types = $extra_types; - } elseif ($atomic_type instanceof TObject && $extra_types) { - $atomic_type = reset($extra_types); - $atomic_type->extra_types = array_slice($extra_types, 1); + $t = $t->setIntersectionTypes($extra_types); + } elseif ($t instanceof TObject && $extra_types) { + $t = reset($extra_types)->setIntersectionTypes(array_slice($extra_types, 1)); } } @@ -858,7 +893,7 @@ private static function handleTemplateParamStandin( $matching_input_keys ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -869,6 +904,7 @@ private static function handleTemplateParamStandin( } } } + $generic_param = $generic_param->freeze(); $upper_bound = $template_result->upper_bounds [$param_name_key] @@ -936,13 +972,18 @@ public static function handleTemplateParamClassStandin( $atomic_types = []; + $as_type = $atomic_type->as_type; if ($input_type && !$template_result->readonly) { $valid_input_atomic_types = []; foreach ($input_type->getAtomicTypes() as $input_atomic_type) { if ($input_atomic_type instanceof TLiteralClassString) { $valid_input_atomic_types[] = new TNamedObject( - $input_atomic_type->value + $input_atomic_type->value, + false, + false, + [], + true ); } elseif ($input_atomic_type instanceof TTemplateParamClass) { $valid_input_atomic_types[] = new TTemplateParam( @@ -952,20 +993,26 @@ public static function handleTemplateParamClassStandin( : ($input_atomic_type->as === 'object' ? Type::getObject() : Type::getMixed()), - $input_atomic_type->defining_class + $input_atomic_type->defining_class, + [], + true ); } elseif ($input_atomic_type instanceof TClassString) { if ($input_atomic_type->as_type) { - $valid_input_atomic_types[] = clone $input_atomic_type->as_type; + $valid_input_atomic_types[] = $input_atomic_type->as_type->setFromDocblock(true); } elseif ($input_atomic_type->as !== 'object') { $valid_input_atomic_types[] = new TNamedObject( - $input_atomic_type->as + $input_atomic_type->as, + false, + false, + [], + true ); } else { - $valid_input_atomic_types[] = new TObject(); + $valid_input_atomic_types[] = new TObject(true); } } elseif ($input_atomic_type instanceof TDependentGetClass) { - $valid_input_atomic_types[] = new TObject(); + $valid_input_atomic_types[] = new TObject(true); } } @@ -973,16 +1020,15 @@ public static function handleTemplateParamClassStandin( if ($valid_input_atomic_types) { $generic_param = new Union($valid_input_atomic_types); - $generic_param->setFromDocblock(); } elseif ($was_single) { $generic_param = Type::getMixed(); } - if ($atomic_type->as_type) { + if ($as_type) { // sometimes templated class-strings can contain nested templates // in the as type that need to be resolved as well. $as_type_union = self::replace( - new Union([$atomic_type->as_type]), + new Union([$as_type]), $template_result, $codebase, $statements_analyzer, @@ -999,9 +1045,9 @@ public static function handleTemplateParamClassStandin( $first = $as_type_union->getSingleAtomic(); if (count($as_type_union->getAtomicTypes()) === 1 && $first instanceof TNamedObject) { - $atomic_type->as_type = $first; + $as_type = $first; } else { - $atomic_type->as_type = null; + $as_type = null; } } @@ -1046,7 +1092,7 @@ public static function handleTemplateParamClassStandin( } } - $class_string = new TClassString($atomic_type->as, $atomic_type->as_type); + $class_string = new TClassString($atomic_type->as, $as_type); if (!$atomic_types) { $atomic_types[] = $class_string; @@ -1155,6 +1201,7 @@ public static function getMostSpecificTypeFromBounds(array $lower_bounds, ?Codeb /** * @param TGenericObject|TNamedObject|TIterable $input_type_part * @param TGenericObject|TIterable $container_type_part + * @psalm-external-mutation-free * @return list */ public static function getMappedGenericTypeParams( @@ -1260,7 +1307,7 @@ public static function getMappedGenericTypeParams( $new_input_param = clone $new_input_param; - TemplateInferredTypeReplacer::replace( + $new_input_param = TemplateInferredTypeReplacer::replace( $new_input_param, new TemplateResult([], $replacement_templates), $codebase diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index c7770a32c26..2d7c74b4af5 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -81,9 +81,9 @@ class TypeCombination public $class_string_types = []; /** - * @var array|null + * @var array */ - public $extra_types; + public $extra_types = []; /** @var ?bool */ public $all_arrays_lists; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 6d25b1f40c2..b0348134cdf 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -85,10 +85,13 @@ class TypeCombiner * - and `array + array = array` * - and `array + array = array` * + * @psalm-external-mutation-free + * + * @psalm-suppress ImpurePropertyAssignment We're not actually mutating any external instance + * * @param non-empty-list $types * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar - * */ public static function combine( array $types, @@ -98,13 +101,7 @@ public static function combine( int $literal_limit = 500 ): Union { if (count($types) === 1) { - $union_type = new Union([$types[0]]); - - if ($types[0]->from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return new Union([$types[0]], $types[0]->from_docblock); } $combination = new TypeCombination(); @@ -144,23 +141,11 @@ public static function combine( && !$combination->floats ) { if (isset($combination->value_types['false'])) { - $union_type = Type::getFalse(); - - if ($from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return Type::getFalse($from_docblock); } if (isset($combination->value_types['true'])) { - $union_type = Type::getTrue(); - - if ($from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return Type::getTrue($from_docblock); } } elseif (isset($combination->value_types['void'])) { unset($combination->value_types['void']); @@ -253,10 +238,14 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { - $generic_object = new TGenericObject($generic_type, $generic_type_params); - - /** @psalm-suppress PropertyTypeCoercion */ - $generic_object->extra_types = $combination->extra_types; + /** @psalm-suppress ArgumentTypeCoercion Caused by the PropertyTypeCoercion above */ + $generic_object = new TGenericObject( + $generic_type, + $generic_type_params, + false, + false, + $combination->extra_types + ); $new_types[] = $generic_object; if ($combination->named_object_types) { @@ -268,14 +257,15 @@ public static function combine( foreach ($combination->object_type_params as $generic_type => $generic_type_params) { $generic_type = substr($generic_type, 0, (int) strpos($generic_type, '<')); - $generic_object = new TGenericObject($generic_type, $generic_type_params); - - if ($combination->object_static[$generic_type] ?? false) { - $generic_object->is_static = true; - } + /** @psalm-suppress ArgumentTypeCoercion Caused by the PropertyTypeCoercion above */ + $generic_object = new TGenericObject( + $generic_type, + $generic_type_params, + false, + $combination->object_static[$generic_type] ?? false, + $combination->extra_types + ); - /** @psalm-suppress PropertyTypeCoercion */ - $generic_object->extra_types = $combination->extra_types; $new_types[] = $generic_object; } @@ -376,6 +366,9 @@ public static function combine( return $union_type; } + /** + * @psalm-suppress ComplexMethod Unavoidably complex method + */ private static function scrapeTypeProperties( Atomic $type, TypeCombination $combination, @@ -511,7 +504,7 @@ private static function scrapeTypeProperties( ) { if ($type->extra_types) { $combination->extra_types = array_merge( - $combination->extra_types ?: [], + $combination->extra_types, $type->extra_types ); } @@ -795,11 +788,12 @@ private static function scrapeTypeProperties( $existing_template_type = $combination->value_types[$type_key]; if (!$existing_template_type->as->equals($type->as)) { - $existing_template_type->as = Type::combineUnionTypes( - clone $type->as, + $existing_template_type = $existing_template_type->replaceAs(Type::combineUnionTypes( + $type->as, $existing_template_type->as, $codebase - ); + )); + $combination->value_types[$type_key] = $existing_template_type; } return null; @@ -1180,18 +1174,28 @@ private static function scrapeIntProperties( if (isset($combination->value_types['int'])) { $current_int_type = $combination->value_types['int']; if ($current_int_type instanceof TIntRange) { + $min_bound = $current_int_type->min_bound; + $max_bound = $current_int_type->max_bound; foreach ($combination->ints as $int) { if (!$current_int_type->contains($int->value)) { - $current_int_type->min_bound = TIntRange::getNewLowestBound( - $current_int_type->min_bound, + $min_bound = TIntRange::getNewLowestBound( + $min_bound, $int->value ); - $current_int_type->max_bound = TIntRange::getNewHighestBound( - $current_int_type->max_bound, + $max_bound = TIntRange::getNewHighestBound( + $max_bound, $int->value ); } } + if ($min_bound !== $current_int_type->min_bound + || $max_bound !== $current_int_type->max_bound + ) { + $combination->value_types['int'] = new TIntRange( + $min_bound, + $max_bound + ); + } } } @@ -1214,14 +1218,16 @@ private static function scrapeIntProperties( $combination->value_types['int'] = new TInt(); } } elseif ($type instanceof TIntRange) { - $type = clone $type; + $min_bound = $type->min_bound; + $max_bound = $type->max_bound; if ($combination->ints) { foreach ($combination->ints as $int) { if (!$type->contains($int->value)) { - $type->min_bound = TIntRange::getNewLowestBound($type->min_bound, $int->value); - $type->max_bound = TIntRange::getNewHighestBound($type->max_bound, $int->value); + $min_bound = TIntRange::getNewLowestBound($min_bound, $int->value); + $max_bound = TIntRange::getNewHighestBound($max_bound, $int->value); } } + $type = new TIntRange($min_bound, $max_bound); $combination->value_types['int'] = $type; } elseif (!isset($combination->value_types['int'])) { @@ -1229,8 +1235,9 @@ private static function scrapeIntProperties( } else { $old_type = $combination->value_types['int']; if ($old_type instanceof TIntRange) { - $type->min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $type->min_bound); - $type->max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $type->max_bound); + $min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $min_bound); + $max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $max_bound); + $type = new TIntRange($min_bound, $max_bound); } else { $type = new TInt(); } @@ -1356,34 +1363,42 @@ private static function handleKeyedArrayEntries( } if ($combination->objectlike_entries) { - if ($combination->all_arrays_callable) { - $objectlike = new TCallableKeyedArray($combination->objectlike_entries); - } else { - $objectlike = new TKeyedArray($combination->objectlike_entries); - } - - if ($combination->objectlike_sealed && !$combination->array_type_params) { - $objectlike->sealed = true; - } - + $previous_key_type = null; if ($combination->objectlike_key_type) { - $objectlike->previous_key_type = $combination->objectlike_key_type; + $previous_key_type = $combination->objectlike_key_type; } elseif ($combination->array_type_params && $combination->array_type_params[0]->isArrayKey() ) { - $objectlike->previous_key_type = $combination->array_type_params[0]; + $previous_key_type = $combination->array_type_params[0]; } + $previous_value_type = null; if ($combination->objectlike_value_type) { - $objectlike->previous_value_type = $combination->objectlike_value_type; + $previous_value_type = $combination->objectlike_value_type; } elseif ($combination->array_type_params && $combination->array_type_params[1]->isMixed() ) { - $objectlike->previous_value_type = $combination->array_type_params[1]; + $previous_value_type = $combination->array_type_params[1]; } - if ($combination->all_arrays_lists) { - $objectlike->is_list = true; + if ($combination->all_arrays_callable) { + $objectlike = new TCallableKeyedArray( + $combination->objectlike_entries, + null, + $combination->objectlike_sealed && !$combination->array_type_params, + $previous_key_type, + $previous_value_type, + (bool)$combination->all_arrays_lists + ); + } else { + $objectlike = new TKeyedArray( + $combination->objectlike_entries, + null, + $combination->objectlike_sealed && !$combination->array_type_params, + $previous_key_type, + $previous_value_type, + (bool)$combination->all_arrays_lists + ); } $new_types[] = $objectlike; @@ -1488,35 +1503,37 @@ private static function getArrayTypeFromGenericParams( if ($combination->objectlike_entries && $combination->objectlike_sealed ) { - $array_type = new TKeyedArray([$generic_type_params[1]]); - $array_type->previous_key_type = Type::getInt(); - $array_type->previous_value_type = $combination->array_type_params[1]; - $array_type->is_list = true; + $array_type = new TKeyedArray( + [$generic_type_params[1]], + null, + false, + Type::getInt(), + $combination->array_type_params[1], + true + ); } else { - $array_type = new TNonEmptyList($generic_type_params[1]); - - if ($combination->array_counts && count($combination->array_counts) === 1) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->count = array_keys($combination->array_counts)[0]; - } - - if ($combination->array_min_counts) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->min_count = min(array_keys($combination->array_min_counts)); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $array_type = new TNonEmptyList( + $generic_type_params[1], + $combination->array_counts && count($combination->array_counts) === 1 + ? array_keys($combination->array_counts)[0] + : null, + $combination->array_min_counts + ? min(array_keys($combination->array_min_counts)) + : null + ); } } else { - $array_type = new TNonEmptyArray($generic_type_params); - - if ($combination->array_counts && count($combination->array_counts) === 1) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->count = array_keys($combination->array_counts)[0]; - } - - if ($combination->array_min_counts) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->min_count = min(array_keys($combination->array_min_counts)); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $array_type = new TNonEmptyArray( + $generic_type_params, + $combination->array_counts && count($combination->array_counts) === 1 + ? array_keys($combination->array_counts)[0] + : null, + $combination->array_min_counts + ? min(array_keys($combination->array_min_counts)) + : null + ); } } else { if ($combination->all_arrays_class_string_maps diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index babaf30aa77..404ed996f34 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -5,11 +5,11 @@ use Psalm\Codebase; use Psalm\Exception\CircularReferenceException; use Psalm\Exception\UnresolvableConstantException; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer; use Psalm\Internal\Type\SimpleAssertionReconciler; use Psalm\Internal\Type\SimpleNegatedAssertionReconciler; use Psalm\Internal\Type\TypeParser; use Psalm\Storage\Assertion\IsType; -use Psalm\Storage\PropertyStorage; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; @@ -58,7 +58,7 @@ class TypeExpander { /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type */ public static function expandUnion( Codebase $codebase, @@ -126,10 +126,11 @@ public static function expandUnion( } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type - * + * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param-out Atomic $return_type * @return non-empty-list * + * @psalm-suppress ConflictingReferenceConstraint, ReferenceConstraintViolation The output type is always Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( @@ -151,7 +152,8 @@ public static function expandAtomic( if ($return_type->extra_types) { $new_intersection_types = []; - foreach ($return_type->extra_types as &$extra_type) { + $extra_types = []; + foreach ($return_type->extra_types as $extra_type) { self::expandAtomic( $codebase, $extra_type, @@ -170,13 +172,13 @@ public static function expandAtomic( $new_intersection_types, $extra_type->extra_types ); - $extra_type->extra_types = []; + $extra_type = $extra_type->setIntersectionTypes([]); } + $extra_types[$extra_type->getKey()] = $extra_type; } - if ($new_intersection_types) { - $return_type->extra_types = array_merge($return_type->extra_types, $new_intersection_types); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $return_type = $return_type->setIntersectionTypes(array_merge($extra_types, $new_intersection_types)); } if ($return_type instanceof TNamedObject) { @@ -195,7 +197,7 @@ public static function expandAtomic( if ($return_type instanceof TClassString && $return_type->as_type ) { - $new_as_type = clone $return_type->as_type; + $new_as_type = $return_type->as_type; self::expandAtomic( $codebase, @@ -211,9 +213,12 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); - if ($new_as_type instanceof TNamedObject) { + if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ $return_type->as_type = $new_as_type; - $return_type->as = $return_type->as_type->value; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $return_type->as = $new_as_type->value; } } elseif ($return_type instanceof TTemplateParam) { $new_as_type = self::expandUnion( @@ -234,16 +239,21 @@ public static function expandAtomic( return array_values($new_as_type->getAtomicTypes()); } - $return_type->as = $new_as_type; + $return_type = $return_type->replaceAs($new_as_type); } if ($return_type instanceof TClassConstant) { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike( + 'self', + $self_class + ); } - - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + if (is_string($static_class_type) || $self_class) { + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } if ($evaluate_class_constants && $codebase->classOrInterfaceOrEnumExists($return_type->fq_classlike_name)) { @@ -469,9 +479,9 @@ public static function expandAtomic( || $return_type instanceof TGenericObject || $return_type instanceof TIterable ) { - foreach ($return_type->type_params as $k => $type_param) { - /** @psalm-suppress PropertyTypeCoercion */ - $return_type->type_params[$k] = self::expandUnion( + $type_params = $return_type->type_params; + foreach ($type_params as &$type_param) { + $type_param = self::expandUnion( $codebase, $type_param, $self_class, @@ -485,8 +495,11 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ + $return_type = $return_type->replaceTypeParams($type_params); } elseif ($return_type instanceof TKeyedArray) { - foreach ($return_type->properties as &$property_type) { + $properties = $return_type->properties; + foreach ($properties as &$property_type) { $property_type = self::expandUnion( $codebase, $property_type, @@ -501,8 +514,9 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + $return_type = $return_type->setProperties($properties); } elseif ($return_type instanceof TList) { - $return_type->type_param = self::expandUnion( + $return_type = $return_type->replaceTypeParam(self::expandUnion( $codebase, $return_type->type_param, $self_class, @@ -514,11 +528,12 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, - ); + )); } if ($return_type instanceof TObjectWithProperties) { - foreach ($return_type->properties as &$property_type) { + $properties = $return_type->properties; + foreach ($properties as &$property_type) { $property_type = self::expandUnion( $codebase, $property_type, @@ -533,15 +548,17 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + $return_type = $return_type->setProperties($properties); } if ($return_type instanceof TCallable || $return_type instanceof TClosure ) { - if ($return_type->params) { - foreach ($return_type->params as $param) { + $params = $return_type->params; + if ($params) { + foreach ($params as &$param) { if ($param->type) { - $param->type = self::expandUnion( + $param = $param->replaceType(self::expandUnion( $codebase, $param->type, $self_class, @@ -553,14 +570,15 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, - ); + )); } } } - if ($return_type->return_type) { - $return_type->return_type = self::expandUnion( + $sub_return_type = $return_type->return_type; + if ($sub_return_type) { + $sub_return_type = self::expandUnion( $codebase, - $return_type->return_type, + $sub_return_type, $self_class, $static_class_type, $parent_class, @@ -572,18 +590,26 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + + if ($sub_return_type !== $return_type->return_type || $params !== $return_type->params) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty We just cloned this */ + $return_type->return_type = $sub_return_type; + /** @psalm-suppress InaccessibleProperty We just cloned this */ + $return_type->params = $params; + } } return [$return_type]; } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type * @return TNamedObject|TTemplateParam */ private static function expandNamedObject( Codebase $codebase, - TNamedObject $return_type, + TNamedObject &$return_type, ?string $self_class, $static_class_type, ?string $parent_class, @@ -625,11 +651,15 @@ private static function expandNamedObject( if ($static_class_type && ($return_type_lc === 'static' || $return_type_lc === '$this')) { if (is_string($static_class_type)) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type; } else { if ($return_type instanceof TGenericObject && $static_class_type instanceof TGenericObject ) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type->value; } else { $return_type = clone $static_class_type; @@ -637,48 +667,62 @@ private static function expandNamedObject( } if (!$final && $return_type instanceof TNamedObject) { + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = true; } } elseif ($return_type->is_static && ($static_class_type instanceof TNamedObject || $static_class_type instanceof TTemplateParam) ) { - $return_type = clone $return_type; - $cloned_static = clone $static_class_type; - $extra_static = $cloned_static->extra_types ?: []; - $cloned_static->extra_types = null; + $return_type_types = $return_type->getIntersectionTypes(); + $cloned_static = $static_class_type->setIntersectionTypes([]); + $extra_static = $static_class_type->extra_types; if ($cloned_static->getKey(false) !== $return_type->getKey(false)) { - $return_type->extra_types[$static_class_type->getKey()] = clone $cloned_static; + $return_type_types[$cloned_static->getKey()] = $cloned_static; } foreach ($extra_static as $extra_static_type) { if ($extra_static_type->getKey(false) !== $return_type->getKey(false)) { - $return_type->extra_types[$extra_static_type->getKey()] = clone $extra_static_type; + $return_type_types[$extra_static_type->getKey()] = clone $extra_static_type; } } + $return_type = $return_type->setIntersectionTypes($return_type_types); } elseif ($return_type->is_static && is_string($static_class_type) && $final) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = false; } elseif ($self_class && $return_type_lc === 'self') { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $self_class; } elseif ($parent_class && $return_type_lc === 'parent') { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $parent_class; } else { - $return_type->value = $codebase->classlikes->getUnAliasedName($return_type->value); + $new_value = $codebase->classlikes->getUnAliasedName($return_type->value); + if ($return_type->value !== $new_value) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ + $return_type->value = $new_value; + } } + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, we are never assigning a TTemplateParam to $return_type */ return $return_type; } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type * * @return non-empty-list */ private static function expandConditional( Codebase $codebase, - TConditional $return_type, + TConditional &$return_type, ?string $self_class, $static_class_type, ?string $parent_class, @@ -703,8 +747,6 @@ private static function expandConditional( $throw_on_unresolvable_constant, ); - $return_type->as_type = $new_as_type; - if ($evaluate_conditional_types) { $assertion = null; @@ -825,9 +867,7 @@ private static function expandConditional( ); if (count($all_conditional_return_types) !== $number_of_types) { - $null_type = new TNull(); - $null_type->from_docblock = true; - $all_conditional_return_types[] = $null_type; + $all_conditional_return_types[] = new TNull(true); } } @@ -837,52 +877,54 @@ private static function expandConditional( $codebase ); + $return_type = $return_type->replaceTypes($new_as_type); + return array_values($combined->getAtomicTypes()); } } - $return_type->conditional_type = self::expandUnion( - $codebase, - $return_type->conditional_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $return_type->if_type = self::expandUnion( - $codebase, - $return_type->if_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $return_type->else_type = self::expandUnion( - $codebase, - $return_type->else_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, + $return_type = $return_type->replaceTypes( + $new_as_type, + self::expandUnion( + $codebase, + $return_type->conditional_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ), + self::expandUnion( + $codebase, + $return_type->if_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ), + self::expandUnion( + $codebase, + $return_type->else_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ) ); - return [$return_type]; } @@ -892,62 +934,68 @@ private static function expandConditional( */ private static function expandPropertiesOf( Codebase $codebase, - TPropertiesOf $return_type, + TPropertiesOf &$return_type, ?string $self_class, $static_class_type ): array { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike( + 'self', + $self_class + ); + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + $class_storage = null; + if ($codebase->classExists($return_type->classlike_type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($return_type->classlike_type->value); + } else { + foreach ($return_type->classlike_type->extra_types as $type) { + if ($type instanceof TNamedObject && $codebase->classExists($type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($type->value); + break; + } + } } - if (!$codebase->classExists($return_type->fq_classlike_name)) { + if (!$class_storage) { return [$return_type]; } - // Get and merge all properties from parent classes - $class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name); - $properties_types = $class_storage->properties; - foreach ($class_storage->parent_classes as $parent_class) { - if (!$codebase->classOrInterfaceExists($parent_class)) { + $properties = []; + foreach ([$class_storage->name, ...array_values($class_storage->parent_classes)] as $class) { + if (!$codebase->classExists($class)) { continue; } - $parent_class_storage = $codebase->classlike_storage_provider->get($parent_class); - $properties_types = array_merge( - $properties_types, - $parent_class_storage->properties - ); - } - - // Filter only non-static properties, and check visibility filter - $properties_types = array_filter( - $properties_types, - function (PropertyStorage $property) use ($return_type): bool { + $storage = $codebase->classlike_storage_provider->get($class); + foreach ($storage->properties as $key => $property) { + if (isset($properties[$key])) { + continue; + } if ($return_type->visibility_filter !== null && $property->visibility !== $return_type->visibility_filter ) { - return false; + continue; } - return !$property->is_static; - } - ); - - // Return property names as literal string - $properties = array_map( - function (PropertyStorage $property): ?Union { - return $property->type; - }, - $properties_types - ); - $properties = array_filter( - $properties, - function (?Union $property_type): bool { - return $property_type !== null; + if ($property->is_static || !$property->type) { + continue; + } + $type = $return_type->classlike_type instanceof TGenericObject + ? AtomicPropertyFetchAnalyzer::localizePropertyType( + $codebase, + $property->type, + $return_type->classlike_type, + $storage, + $storage + ) + : $property->type + ; + $properties[$key] = $type; } - ); + } if ($properties === []) { return [$return_type]; @@ -994,8 +1042,8 @@ private static function expandKeyOfValueOf( continue; } - if ($type_param->fq_classlike_name === 'self' && $self_class) { - $type_param->fq_classlike_name = $self_class; + if ($self_class) { + $type_param = $type_param->replaceClassLike('self', $self_class); } if ($throw_on_unresolvable_constant diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 71a2ddd1e58..e810718075d 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -96,6 +96,7 @@ use function substr; /** + * @psalm-suppress InaccessibleProperty Allowed during construction * @internal */ class TypeParser @@ -113,7 +114,8 @@ public static function parseTokens( array $type_tokens, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): Union { if (count($type_tokens) === 1) { $only_token = $type_tokens[0]; @@ -129,12 +131,18 @@ public static function parseTokens( } else { $only_token[0] = TypeTokenizer::fixScalarTerms($only_token[0], $analysis_php_version_id); - $atomic = Atomic::create($only_token[0], $analysis_php_version_id, $template_type_map, $type_aliases); - $atomic->offset_start = 0; - $atomic->offset_end = strlen($only_token[0]); - $atomic->text = isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null; + $atomic = Atomic::create( + $only_token[0], + $analysis_php_version_id, + $template_type_map, + $type_aliases, + 0, + strlen($only_token[0]), + isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null, + $from_docblock + ); - return new Union([$atomic]); + return new Union([$atomic], $from_docblock); } } @@ -145,11 +153,12 @@ public static function parseTokens( $codebase, $analysis_php_version_id, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!($parsed_type instanceof Union)) { - $parsed_type = new Union([$parsed_type]); + $parsed_type = new Union([$parsed_type], $from_docblock); } return $parsed_type; @@ -166,27 +175,47 @@ public static function getTypeFromTree( Codebase $codebase, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): TypeNode { if ($parse_tree instanceof GenericTree) { return self::getTypeFromGenericTree( $parse_tree, $codebase, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } if ($parse_tree instanceof UnionTree) { - return self::getTypeFromUnionTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromUnionTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof IntersectionTree) { - return self::getTypeFromIntersectionTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromIntersectionTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof KeyedArrayTree) { - return self::getTypeFromKeyedArrayTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromKeyedArrayTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof CallableWithReturnTypeTree) { @@ -195,7 +224,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!$callable_type instanceof TCallable && !$callable_type instanceof TClosure) { @@ -211,16 +241,26 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); - $callable_type->return_type = $return_type instanceof Union ? $return_type : new Union([$return_type]); + $callable_type->return_type = $return_type instanceof Union + ? $return_type + : new Union([$return_type], $from_docblock) + ; return $callable_type; } if ($parse_tree instanceof CallableTree) { - return self::getTypeFromCallableTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromCallableTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof EncapsulationTree) { @@ -237,7 +277,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -251,17 +292,18 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($non_nullable_type instanceof Union) { - $non_nullable_type->addType(new TNull); + $non_nullable_type = $non_nullable_type->getBuilder()->addType(new TNull($from_docblock))->freeze(); return $non_nullable_type; } return TypeCombiner::combine([ - new TNull, + new TNull($from_docblock), $non_nullable_type, ]); } @@ -273,15 +315,18 @@ public static function getTypeFromTree( } if ($parse_tree instanceof IndexedAccessTree) { - return self::getTypeFromIndexAccessTree($parse_tree, $template_type_map); + return self::getTypeFromIndexAccessTree($parse_tree, $template_type_map, $from_docblock); } if ($parse_tree instanceof TemplateAsTree) { - return new TTemplateParam( + $result = new TTemplateParam( $parse_tree->param_name, new Union([new TNamedObject($parse_tree->as)]), - 'class-string-map' + 'class-string-map', + [], + $from_docblock ); + return $result; } if ($parse_tree instanceof ConditionalTree) { @@ -302,7 +347,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $if_type = self::getTypeFromTree( @@ -310,7 +356,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $else_type = self::getTypeFromTree( @@ -318,19 +365,20 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($conditional_type instanceof Atomic) { - $conditional_type = new Union([$conditional_type]); + $conditional_type = new Union([$conditional_type], $from_docblock); } if ($if_type instanceof Atomic) { - $if_type = new Union([$if_type]); + $if_type = new Union([$if_type], $from_docblock); } if ($else_type instanceof Atomic) { - $else_type = new Union([$else_type]); + $else_type = new Union([$else_type], $from_docblock); } return new TConditional( @@ -339,7 +387,8 @@ public static function getTypeFromTree( $template_type_map[$template_param_name][$first_class], $conditional_type, $if_type, - $else_type + $else_type, + $from_docblock ); } @@ -348,7 +397,7 @@ public static function getTypeFromTree( } if ($parse_tree->value[0] === '"' || $parse_tree->value[0] === '\'') { - return new TLiteralString(substr($parse_tree->value, 1, -1)); + return new TLiteralString(substr($parse_tree->value, 1, -1), $from_docblock); } if (strpos($parse_tree->value, '::')) { @@ -360,23 +409,24 @@ public static function getTypeFromTree( return self::getGenericParamClass( $fq_classlike_name, $template_type_map[$fq_classlike_name][$first_class], - $first_class + $first_class, + $from_docblock ); } if ($const_name === 'class') { - return new TLiteralClassString($fq_classlike_name); + return new TLiteralClassString($fq_classlike_name, false, $from_docblock); } - return new TClassConstant($fq_classlike_name, $const_name); + return new TClassConstant($fq_classlike_name, $const_name, $from_docblock); } if (preg_match('/^\-?(0|[1-9][0-9]*)(\.[0-9]{1,})$/', $parse_tree->value)) { - return new TLiteralFloat((float) $parse_tree->value); + return new TLiteralFloat((float) $parse_tree->value, $from_docblock); } if (preg_match('/^\-?(0|[1-9][0-9]*)$/', $parse_tree->value)) { - return new TLiteralInt((int) $parse_tree->value); + return new TLiteralInt((int) $parse_tree->value, $from_docblock); } if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $parse_tree->value)) { @@ -385,26 +435,31 @@ public static function getTypeFromTree( $atomic_type_string = TypeTokenizer::fixScalarTerms($parse_tree->value, $analysis_php_version_id); - $atomic_type = Atomic::create($atomic_type_string, $analysis_php_version_id, $template_type_map, $type_aliases); - - $atomic_type->offset_start = $parse_tree->offset_start; - $atomic_type->offset_end = $parse_tree->offset_end; - $atomic_type->text = $parse_tree->text; - - return $atomic_type; + return Atomic::create( + $atomic_type_string, + $analysis_php_version_id, + $template_type_map, + $type_aliases, + $parse_tree->offset_start, + $parse_tree->offset_end, + $parse_tree->text, + $from_docblock + ); } private static function getGenericParamClass( string $param_name, - Union $as, - string $defining_class + Union &$as, + string $defining_class, + bool $from_docblock = false ): TTemplateParamClass { if ($as->hasMixed()) { return new TTemplateParamClass( $param_name, 'object', null, - $defining_class + $defining_class, + $from_docblock ); } @@ -420,23 +475,29 @@ private static function getGenericParamClass( $param_name, 'object', null, - $defining_class + $defining_class, + $from_docblock ); } if ($t instanceof TIterable) { $traversable = new TGenericObject( 'Traversable', - $t->type_params + $t->type_params, + false, + false, + [], + $from_docblock ); - $as->substitute(new Union([$t]), new Union([$traversable])); + $as = $as->getBuilder()->substitute(new Union([$t]), new Union([$traversable]))->freeze(); return new TTemplateParamClass( $param_name, $traversable->value, $traversable, - $defining_class + $defining_class, + $from_docblock ); } @@ -451,7 +512,8 @@ private static function getGenericParamClass( $t->param_name, $t_atomic_type->value ?? 'object', $t_atomic_type, - $t->defining_class + $t->defining_class, + $from_docblock ); } @@ -465,7 +527,8 @@ private static function getGenericParamClass( $param_name, $t->value, $t, - $defining_class + $defining_class, + $from_docblock ); } @@ -476,7 +539,7 @@ private static function getGenericParamClass( * @param non-empty-list $potential_ints * @return non-empty-list */ - public static function getComputedIntsFromMask(array $potential_ints): array + public static function getComputedIntsFromMask(array $potential_ints, bool $from_docblock = false): array { /** @var list */ $potential_values = []; @@ -499,7 +562,7 @@ public static function getComputedIntsFromMask(array $potential_ints): array $potential_values = array_unique($potential_values); return array_map( - static fn($int): TLiteralInt => new TLiteralInt($int), + static fn($int): TLiteralInt => new TLiteralInt($int, $from_docblock), array_values($potential_values) ); } @@ -515,7 +578,8 @@ private static function getTypeFromGenericTree( GenericTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock = false ) { $generic_type = $parse_tree->value; @@ -527,7 +591,8 @@ private static function getTypeFromGenericTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($generic_type === 'class-string-map' @@ -540,7 +605,7 @@ private static function getTypeFromGenericTree( } } - $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type]); + $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type], $from_docblock); } $generic_type_value = TypeTokenizer::fixScalarTerms($generic_type); @@ -550,7 +615,7 @@ private static function getTypeFromGenericTree( || $generic_type_value === 'associative-array') && count($generic_params) === 1 ) { - array_unshift($generic_params, new Union([new TArrayKey])); + array_unshift($generic_params, new Union([new TArrayKey($from_docblock)])); } elseif (count($generic_params) === 1 && in_array( $generic_type_value, @@ -558,14 +623,14 @@ private static function getTypeFromGenericTree( true ) ) { - array_unshift($generic_params, new Union([new TMixed])); + array_unshift($generic_params, new Union([new TMixed(false, $from_docblock)])); } elseif ($generic_type_value === 'Generator') { if (count($generic_params) === 1) { - array_unshift($generic_params, new Union([new TMixed])); + array_unshift($generic_params, new Union([new TMixed(false, $from_docblock)])); } for ($i = 0, $l = 4 - count($generic_params); $i < $l; ++$i) { - $generic_params[] = new Union([new TMixed]); + $generic_params[] = new Union([new TMixed(false, $from_docblock)]); } } @@ -575,49 +640,54 @@ private static function getTypeFromGenericTree( if ($generic_type_value === 'array' || $generic_type_value === 'associative-array') { if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey(); + $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { throw new TypeParseTreeException('Too many template parameters for array'); } - return new TArray($generic_params); + return new TArray($generic_params, $from_docblock); } if ($generic_type_value === 'arraylike-object') { - $traversable = new TGenericObject('Traversable', $generic_params); - $array_acccess = new TGenericObject('ArrayAccess', $generic_params); - $countable = new TNamedObject('Countable'); - - $traversable->extra_types[$array_acccess->getKey()] = $array_acccess; - $traversable->extra_types[$countable->getKey()] = $countable; - - return $traversable; + $array_acccess = new TGenericObject('ArrayAccess', $generic_params, false, false, [], $from_docblock); + $countable = new TNamedObject('Countable', false, false, [], $from_docblock); + return new TGenericObject( + 'Traversable', + $generic_params, + false, + false, + [ + $array_acccess->getKey() => $array_acccess, + $countable->getKey() => $countable + ], + $from_docblock + ); } if ($generic_type_value === 'non-empty-array') { if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey(); + $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } - return new TNonEmptyArray($generic_params); + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } if ($generic_type_value === 'iterable') { - return new TIterable($generic_params); + return new TIterable($generic_params, [], $from_docblock); } if ($generic_type_value === 'list') { - return new TList($generic_params[0]); + return new TList($generic_params[0], $from_docblock); } if ($generic_type_value === 'non-empty-list') { - return new TNonEmptyList($generic_params[0]); + return new TNonEmptyList($generic_params[0], null, null, $from_docblock); } if ($generic_type_value === 'class-string' @@ -632,7 +702,8 @@ private static function getTypeFromGenericTree( return self::getGenericParamClass( $class_name, $template_type_map[$class_name][$first_class], - $first_class + $first_class, + $from_docblock ); } @@ -646,7 +717,7 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Class string param should be a named object'); } - return new TClassString($class_name, $param_union_types[0]); + return new TClassString($class_name, $param_union_types[0], false, false, false, $from_docblock); } if ($generic_type_value === 'class-string-map') { @@ -683,7 +754,8 @@ private static function getTypeFromGenericTree( return new TClassStringMap( $template_param_name, $template_as_type, - $generic_params[1] + $generic_params[1], + $from_docblock ); } @@ -703,12 +775,19 @@ private static function getTypeFromGenericTree( $generic_type_value . '<' . $param_name . '> must be a TTemplateParam.' ); } + if ($template_param->getIntersectionTypes()) { + throw new TypeParseTreeException( + $generic_type_value . '<' . $param_name . '> must be a TTemplateParam' + . ' with no intersection types.' + ); + } return new TTemplatePropertiesOf( $param_name, $defining_class, $template_param, - TPropertiesOf::filterForTokenName($generic_type_value) + TPropertiesOf::filterForTokenName($generic_type_value), + $from_docblock ); } @@ -723,9 +802,9 @@ private static function getTypeFromGenericTree( } return new TPropertiesOf( - $param_name, $param_union_types[0], - TPropertiesOf::filterForTokenName($generic_type_value) + TPropertiesOf::filterForTokenName($generic_type_value), + $from_docblock ); } @@ -738,7 +817,8 @@ private static function getTypeFromGenericTree( return new TTemplateKeyOf( $param_name, $defining_class, - $generic_params[0] + $generic_params[0], + $from_docblock ); } @@ -748,7 +828,7 @@ private static function getTypeFromGenericTree( ); } - return new TKeyOf($generic_params[0]); + return new TKeyOf($generic_params[0], $from_docblock); } if ($generic_type_value === 'value-of') { @@ -760,7 +840,8 @@ private static function getTypeFromGenericTree( return new TTemplateValueOf( $param_name, $defining_class, - $generic_params[0] + $generic_params[0], + $from_docblock ); } @@ -798,7 +879,7 @@ private static function getTypeFromGenericTree( ); } - $atomic_type = new TLiteralInt($constant_value); + $atomic_type = new TLiteralInt($constant_value, $from_docblock); } else { throw new TypeParseTreeException( 'int-mask types must all be integer values' @@ -822,13 +903,13 @@ private static function getTypeFromGenericTree( foreach ($atomic_types as $atomic_type) { if (!$atomic_type instanceof TLiteralInt) { - return new TIntMask($atomic_types); + return new TIntMask($atomic_types, $from_docblock); } $potential_ints[] = $atomic_type->value; } - return new Union(self::getComputedIntsFromMask($potential_ints)); + return new Union(self::getComputedIntsFromMask($potential_ints, $from_docblock)); } if ($generic_type_value === 'int-mask-of') { @@ -855,7 +936,7 @@ private static function getTypeFromGenericTree( ); } - return new TIntMaskOf($param_type); + return new TIntMaskOf($param_type, $from_docblock); } if ($generic_type_value === 'int') { @@ -885,7 +966,7 @@ private static function getTypeFromGenericTree( $max_bound = $get_int_range_bound($parse_tree->children[1], $generic_params[1], TIntRange::BOUND_MAX); if ($min_bound === null && $max_bound === null) { - return new TInt(); + return new TInt($from_docblock); } if (is_int($min_bound) && is_int($max_bound) && $min_bound > $max_bound) { @@ -900,7 +981,7 @@ private static function getTypeFromGenericTree( ); } - return new TIntRange($min_bound, $max_bound); + return new TIntRange($min_bound, $max_bound, $from_docblock); } if (isset(TypeTokenizer::PSALM_RESERVED_WORDS[$generic_type_value]) @@ -910,7 +991,7 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Cannot create generic object with reserved word'); } - return new TGenericObject($generic_type_value, $generic_params); + return new TGenericObject($generic_type_value, $generic_params, false, false, [], $from_docblock); } /** @@ -922,7 +1003,8 @@ private static function getTypeFromUnionTree( UnionTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ): Union { $has_null = false; @@ -939,7 +1021,8 @@ private static function getTypeFromUnionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $has_null = true; } else { @@ -948,7 +1031,8 @@ private static function getTypeFromUnionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -964,7 +1048,7 @@ private static function getTypeFromUnionTree( } if ($has_null) { - $atomic_types[] = new TNull; + $atomic_types[] = new TNull($from_docblock); } if (!$atomic_types) { @@ -985,7 +1069,8 @@ private static function getTypeFromIntersectionTree( IntersectionTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ): Atomic { $intersection_types = []; @@ -995,7 +1080,8 @@ private static function getTypeFromIntersectionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!$atomic_type instanceof Atomic) { @@ -1059,17 +1145,25 @@ private static function getTypeFromIntersectionTree( } } - $keyed_array = new TKeyedArray($properties); - + $previous_key_type = null; + $previous_value_type = null; if ($first_type instanceof TArray) { - $keyed_array->previous_key_type = $first_type->type_params[0]; - $keyed_array->previous_value_type = $first_type->type_params[1]; + $previous_key_type = $first_type->type_params[0]; + $previous_value_type = $first_type->type_params[1]; } elseif ($last_type instanceof TArray) { - $keyed_array->previous_key_type = $last_type->type_params[0]; - $keyed_array->previous_value_type = $last_type->type_params[1]; + $previous_key_type = $last_type->type_params[0]; + $previous_value_type = $last_type->type_params[1]; } - return $keyed_array; + return new TKeyedArray( + $properties, + null, + false, + $previous_key_type ?? null, + $previous_value_type ?? null, + false, + $from_docblock + ); } $keyed_intersection_types = []; @@ -1089,7 +1183,7 @@ private static function getTypeFromIntersectionTree( $first_type = array_shift($keyed_intersection_types); if ($keyed_intersection_types) { - $first_type->extra_types = $keyed_intersection_types; + return $first_type->setIntersectionTypes($keyed_intersection_types); } } else { foreach ($intersection_types as $intersection_type) { @@ -1117,7 +1211,7 @@ private static function getTypeFromIntersectionTree( } if (!$keyed_intersection_types && $intersect_static) { - return new TNamedObject('static'); + return new TNamedObject('static', false, false, [], $from_docblock); } $first_type = array_shift($keyed_intersection_types); @@ -1129,7 +1223,7 @@ private static function getTypeFromIntersectionTree( } if ($keyed_intersection_types) { - $first_type->extra_types = $keyed_intersection_types; + return $first_type->setIntersectionTypes($keyed_intersection_types); } } @@ -1146,7 +1240,8 @@ private static function getTypeFromCallableTree( CallableTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ) { $params = []; @@ -1161,10 +1256,11 @@ private static function getTypeFromCallableTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } else { - $tree_type = new TMixed(); + $tree_type = new TMixed(false, $from_docblock); } $is_variadic = $child_tree->variadic; @@ -1179,7 +1275,8 @@ private static function getTypeFromCallableTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -1189,24 +1286,22 @@ private static function getTypeFromCallableTree( $tree_type instanceof Union ? $tree_type : new Union([$tree_type]), null, null, + null, $is_optional, false, $is_variadic ); - // type is not authoritative - $param->signature_type = null; - $params[] = $param; } $pure = strpos($parse_tree->value, 'pure-') === 0 ? true : null; if (in_array(strtolower($parse_tree->value), ['closure', '\closure', 'pure-closure'], true)) { - return new TClosure('Closure', $params, null, $pure); + return new TClosure('Closure', $params, null, $pure, [], [], $from_docblock); } - return new TCallable('callable', $params, null, $pure); + return new TCallable('callable', $params, null, $pure, $from_docblock); } /** @@ -1215,7 +1310,8 @@ private static function getTypeFromCallableTree( */ private static function getTypeFromIndexAccessTree( IndexedAccessTree $parse_tree, - array $template_type_map + array $template_type_map, + bool $from_docblock ): TTemplateIndexedAccess { if (!isset($parse_tree->children[0]) || !$parse_tree->children[0] instanceof Value) { throw new TypeParseTreeException('Unrecognised indexed access'); @@ -1258,7 +1354,8 @@ private static function getTypeFromIndexAccessTree( return new TTemplateIndexedAccess( $array_param_name, $offset_param_name, - $array_defining_class + $array_defining_class, + $from_docblock ); } @@ -1272,7 +1369,8 @@ private static function getTypeFromKeyedArrayTree( KeyedArrayTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ) { $properties = []; $class_strings = []; @@ -1290,7 +1388,8 @@ private static function getTypeFromKeyedArrayTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $property_maybe_undefined = false; $property_key = (string)$i; @@ -1300,7 +1399,8 @@ private static function getTypeFromKeyedArrayTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $property_maybe_undefined = $property_branch->possibly_undefined; if (strpos($property_branch->value, '::')) { @@ -1326,7 +1426,7 @@ private static function getTypeFromKeyedArrayTree( } if (!$property_type instanceof Union) { - $property_type = new Union([$property_type]); + $property_type = new Union([$property_type], $from_docblock); } if ($property_maybe_undefined) { @@ -1344,24 +1444,26 @@ private static function getTypeFromKeyedArrayTree( } if (!$properties) { - return new TArray([Type::getNever(), Type::getNever()]); + return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock); } if ($type === 'object') { - return new TObjectWithProperties($properties); + return new TObjectWithProperties($properties, [], [], $from_docblock); } + $class = TKeyedArray::class; if ($type === 'callable-array') { - return new TCallableKeyedArray($properties); + $class = TCallableKeyedArray::class; } - $object_like = new TKeyedArray($properties, $class_strings); - - if ($is_tuple) { - $object_like->sealed = true; - $object_like->is_list = true; - } - - return $object_like; + return new $class( + $properties, + $class_strings, + $is_tuple, + null, + null, + $is_tuple, + $from_docblock + ); } } diff --git a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php new file mode 100644 index 00000000000..c1bc1280fd4 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php @@ -0,0 +1,53 @@ +old = strtolower($old); + $this->new = $new; + } + + /** + * @psalm-suppress InaccessibleProperty Acting on clones + */ + protected function enterNode(TypeNode &$type): ?int + { + if ($type instanceof TClassConstant) { + if (strtolower($type->fq_classlike_name) === $this->old) { + $type = clone $type; + $type->fq_classlike_name = $this->new; + } + } elseif ($type instanceof TClassString) { + if ($type->as !== 'object' && strtolower($type->as) === $this->old) { + $type = clone $type; + $type->as = $this->new; + } + } elseif ($type instanceof TNamedObject || $type instanceof TLiteralClassString) { + if (strtolower($type->value) === $this->old) { + $type = clone $type; + $type->value = $this->new; + } + } + return null; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php index fb57c5c2059..472f61f93d6 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php @@ -5,15 +5,16 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use function strtolower; /** * @internal */ -class ContainsClassLikeVisitor extends NodeVisitor +class ContainsClassLikeVisitor extends ImmutableTypeVisitor { /** * @var lowercase-string @@ -26,6 +27,7 @@ class ContainsClassLikeVisitor extends NodeVisitor private $contains_classlike = false; /** + * @psalm-external-mutation-free * @param lowercase-string $fq_classlike_name */ public function __construct(string $fq_classlike_name) @@ -33,32 +35,38 @@ public function __construct(string $fq_classlike_name) $this->fq_classlike_name = $fq_classlike_name; } + /** + * @psalm-external-mutation-free + */ protected function enterNode(TypeNode $type): ?int { if ($type instanceof TNamedObject) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } if ($type instanceof TClassConstant) { if (strtolower($type->fq_classlike_name) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } if ($type instanceof TLiteralClassString) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } return null; } + /** + * @psalm-mutation-free + */ public function matches(): bool { return $this->contains_classlike; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php index 8fb36b79c2b..c340207e57d 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php @@ -8,13 +8,14 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TTrue; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; /** * @internal */ -class ContainsLiteralVisitor extends NodeVisitor +class ContainsLiteralVisitor extends ImmutableTypeVisitor { /** * @var bool @@ -30,12 +31,12 @@ protected function enterNode(TypeNode $type): ?int || $type instanceof TFalse ) { $this->contains_literal = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } if ($type instanceof TArray && $type->isEmptyArray()) { $this->contains_literal = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php new file mode 100644 index 00000000000..da0a266b0e7 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php @@ -0,0 +1,30 @@ +value === 'static' || $type->is_static)) { + $this->contains_static = true; + return TypeVisitor::STOP_TRAVERSAL; + } + return null; + } + + public function matches(): bool + { + return $this->contains_static; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php index 2f4a7b7cff0..ce7250a7150 100644 --- a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php +++ b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php @@ -4,29 +4,40 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\NodeVisitor; +use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; /** * @internal */ -class FromDocblockSetter extends NodeVisitor +class FromDocblockSetter extends TypeVisitor { + private bool $from_docblock; + public function __construct(bool $from_docblock) + { + $this->from_docblock = $from_docblock; + } /** - * @psalm-suppress MoreSpecificImplementedParamType - * - * @param Atomic|Union $type * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ - protected function enterNode(TypeNode $type): ?int + protected function enterNode(TypeNode &$type): ?int { - $type->from_docblock = true; + if (!$type instanceof Atomic && !$type instanceof Union && !$type instanceof MutableUnion) { + return null; + } + if ($type->from_docblock === $this->from_docblock) { + return null; + } + $type = clone $type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $type->from_docblock = $this->from_docblock; if ($type instanceof TTemplateParam && $type->as->isMixed() ) { - return NodeVisitor::DONT_TRAVERSE_CHILDREN; + return TypeVisitor::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php index 5806638342d..de02f7b70f5 100644 --- a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php +++ b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php @@ -6,14 +6,14 @@ use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use Psalm\Type\Union; /** * @internal */ -class TemplateTypeCollector extends NodeVisitor +class TemplateTypeCollector extends ImmutableTypeVisitor { /** * @var list diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index af0ad30c33c..dcf43591cd2 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -25,8 +25,10 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; +use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; use ReflectionProperty; @@ -40,7 +42,7 @@ /** * @internal */ -class TypeChecker extends NodeVisitor +class TypeChecker extends ImmutableTypeVisitor { /** * @var StatementsSource @@ -107,15 +109,16 @@ public function __construct( } /** - * @psalm-suppress MoreSpecificImplementedParamType - * - * @param Atomic|Union $type * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ protected function enterNode(TypeNode $type): ?int { + if (!$type instanceof Atomic && !$type instanceof Union && !$type instanceof MutableUnion) { + return null; + } + if ($type->checked) { - return NodeVisitor::DONT_TRAVERSE_CHILDREN; + return TypeVisitor::DONT_TRAVERSE_CHILDREN; } if ($type instanceof TNamedObject) { @@ -128,6 +131,7 @@ protected function enterNode(TypeNode $type): ?int $this->checkResource($type); } + /** @psalm-suppress InaccessibleProperty Doesn't affect anything else */ $type->checked = true; return null; diff --git a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php new file mode 100644 index 00000000000..658cf6c9da0 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php @@ -0,0 +1,98 @@ +> + */ + private array $extends; + private string $base_fq_class_name; + + /** + * @param array> $extends + */ + public function __construct( + array $extends, + string $base_fq_class_name + ) { + $this->extends = $extends; + $this->base_fq_class_name = $base_fq_class_name; + } + + /** + * @psalm-suppress InaccessibleProperty Acting on clones + */ + protected function enterNode(TypeNode &$type): ?int + { + if ($type instanceof TTemplateParamClass) { + if ($type->defining_class === $this->base_fq_class_name) { + if (isset($this->extends[$this->base_fq_class_name][$type->param_name])) { + $extended_param = $this->extends[$this->base_fq_class_name][$type->param_name]; + + $types = array_values($extended_param->getAtomicTypes()); + + if (count($types) === 1 && $types[0] instanceof TNamedObject) { + $type = clone $type; + $type->as_type = $types[0]; + } elseif ($type->as_type !== null) { + $type = clone $type; + $type->as_type = null; + } + } + } + } + + if ($type instanceof Union) { + $union = $type->getBuilder(); + } elseif ($type instanceof MutableUnion) { + $union = $type; + } else { + return null; + } + + foreach ($union->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TTemplateParam + && ($atomic_type->defining_class === $this->base_fq_class_name + || isset($this->extends[$atomic_type->defining_class])) + ) { + $types_to_add = Methods::getExtendedTemplatedTypes( + $atomic_type, + $this->extends + ); + + if ($types_to_add) { + $union->removeType($key); + + foreach ($types_to_add as $extra_added_type) { + $union->addType($extra_added_type); + } + } + } + } + + if ($type instanceof Union) { + $type = $union->freeze(); + } else { + $type = $union; + } + + return null; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/TypeScanner.php b/src/Psalm/Internal/TypeVisitor/TypeScanner.php index 48f6796ef28..73b2687124c 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeScanner.php +++ b/src/Psalm/Internal/TypeVisitor/TypeScanner.php @@ -7,7 +7,7 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use function strtolower; @@ -15,13 +15,16 @@ /** * @internal */ -class TypeScanner extends NodeVisitor +class TypeScanner extends ImmutableTypeVisitor { - private $scanner; + private Scanner $scanner; - private $file_storage; + private ?FileStorage $file_storage; - private $phantom_classes; + /** + * @var array + */ + private array $phantom_classes; /** * @param array $phantom_classes diff --git a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php index 1f39a9d398e..db782f4d69d 100644 --- a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php @@ -12,6 +12,8 @@ final class StringInterpreterEvent /** * Called after a statement has been checked * + * @psalm-external-mutation-free + * * @internal */ public function __construct(string $value) diff --git a/src/Psalm/Storage/Assertion.php b/src/Psalm/Storage/Assertion.php index ed7dca2542d..2fcc4324bbc 100644 --- a/src/Psalm/Storage/Assertion.php +++ b/src/Psalm/Storage/Assertion.php @@ -4,12 +4,15 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ abstract class Assertion { - /** @psalm-mutation-free */ + use ImmutableNonCloneableTrait; + abstract public function getNegation(): Assertion; - /** @psalm-mutation-free */ abstract public function isNegationOf(self $assertion): bool; abstract public function __toString(): string; @@ -19,19 +22,21 @@ public function isNegation(): bool return false; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return false; } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return null; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { + return $this; } } diff --git a/src/Psalm/Storage/Assertion/Any.php b/src/Psalm/Storage/Assertion/Any.php index 4aab3fda16a..c5d61036253 100644 --- a/src/Psalm/Storage/Assertion/Any.php +++ b/src/Psalm/Storage/Assertion/Any.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Any extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return $this; @@ -17,7 +19,6 @@ public function __toString(): string return 'mixed'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php index 43a6f0bd557..e8fc33cb2ed 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class ArrayKeyDoesNotExist extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new ArrayKeyExists(); @@ -22,7 +24,6 @@ public function __toString(): string return '!array-key-exists'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof ArrayKeyExists; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyExists.php b/src/Psalm/Storage/Assertion/ArrayKeyExists.php index 0b803cd4dd9..6ef5e84e244 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyExists.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyExists.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class ArrayKeyExists extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new ArrayKeyDoesNotExist(); @@ -17,7 +19,6 @@ public function __toString(): string return 'array-key-exists'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof ArrayKeyDoesNotExist; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php index 297e61ddb83..7913cf60164 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveAtLeastCount extends Assertion { /** @var positive-int */ @@ -15,13 +18,11 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasAtLeastCount($this->count); } - /** @psalm-mutation-free */ public function isNegation(): bool { return true; @@ -32,7 +33,6 @@ public function __toString(): string return '!has-at-least-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasAtLeastCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php index 950bf72eba6..349e1533de4 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveExactCount extends Assertion { /** @var positive-int */ @@ -20,7 +23,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasExactCount($this->count); @@ -31,7 +33,6 @@ public function __toString(): string return '!has-exact-count-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasExactCount && $assertion->count === $this->count; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php index eef48187543..64dc6fb4c95 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveMethod extends Assertion { public string $method; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasMethod($this->method); @@ -29,7 +31,6 @@ public function __toString(): string return '!method-exists-' . $this->method; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasMethod && $assertion->method === $this->method; diff --git a/src/Psalm/Storage/Assertion/Empty_.php b/src/Psalm/Storage/Assertion/Empty_.php index 91613b0f4a2..f7e14b675b7 100644 --- a/src/Psalm/Storage/Assertion/Empty_.php +++ b/src/Psalm/Storage/Assertion/Empty_.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Empty_ extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NonEmpty(); @@ -22,7 +24,6 @@ public function __toString(): string return '!non-empty'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NonEmpty; diff --git a/src/Psalm/Storage/Assertion/Falsy.php b/src/Psalm/Storage/Assertion/Falsy.php index 13c85088eeb..e555c2d16ea 100644 --- a/src/Psalm/Storage/Assertion/Falsy.php +++ b/src/Psalm/Storage/Assertion/Falsy.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Falsy extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Truthy(); @@ -22,7 +24,6 @@ public function __toString(): string return 'falsy'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Truthy; diff --git a/src/Psalm/Storage/Assertion/HasArrayKey.php b/src/Psalm/Storage/Assertion/HasArrayKey.php index 7442c4a39a7..713eda3fe1e 100644 --- a/src/Psalm/Storage/Assertion/HasArrayKey.php +++ b/src/Psalm/Storage/Assertion/HasArrayKey.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasArrayKey extends Assertion { public $key; @@ -14,7 +17,6 @@ public function __construct(string $key) $this->key = $key; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -25,7 +27,6 @@ public function __toString(): string return 'has-array-key-' . $this->key; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/HasAtLeastCount.php b/src/Psalm/Storage/Assertion/HasAtLeastCount.php index 46ec83abfeb..3a30e1d5666 100644 --- a/src/Psalm/Storage/Assertion/HasAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/HasAtLeastCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasAtLeastCount extends Assertion { /** @var positive-int */ @@ -15,7 +18,6 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveAtLeastCount($this->count); @@ -26,7 +28,6 @@ public function __toString(): string return 'has-at-least-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveAtLeastCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/HasExactCount.php b/src/Psalm/Storage/Assertion/HasExactCount.php index 36d0400c29c..b76cfc6144e 100644 --- a/src/Psalm/Storage/Assertion/HasExactCount.php +++ b/src/Psalm/Storage/Assertion/HasExactCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasExactCount extends Assertion { /** @var positive-int */ @@ -15,13 +18,11 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveExactCount($this->count); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -32,7 +33,6 @@ public function __toString(): string return '=has-exact-count-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveExactCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php index 88620c8b0f8..4bcafad2312 100644 --- a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php @@ -5,9 +5,11 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasIntOrStringArrayAccess extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -18,7 +20,6 @@ public function __toString(): string return 'has-string-or-int-array-access'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/HasMethod.php b/src/Psalm/Storage/Assertion/HasMethod.php index 94508472404..2aef8f39814 100644 --- a/src/Psalm/Storage/Assertion/HasMethod.php +++ b/src/Psalm/Storage/Assertion/HasMethod.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasMethod extends Assertion { public string $method; @@ -13,7 +16,6 @@ public function __construct(string $method) $this->method = $method; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveMethod($this->method); @@ -24,7 +26,6 @@ public function __toString(): string return 'method-exists-' . $this->method; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveMethod && $this->method === $assertion->method; diff --git a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php index d7595287857..d4aeb63c6e8 100644 --- a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php @@ -5,9 +5,11 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasStringArrayAccess extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -18,7 +20,6 @@ public function __toString(): string return 'has-string-array-access'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/InArray.php b/src/Psalm/Storage/Assertion/InArray.php index b5e1abc2ea7..ac7220da007 100644 --- a/src/Psalm/Storage/Assertion/InArray.php +++ b/src/Psalm/Storage/Assertion/InArray.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class InArray extends Assertion { public Union $type; @@ -14,7 +17,6 @@ public function __construct(Union $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NotInArray($this->type); @@ -25,7 +27,6 @@ public function __toString(): string return 'in-array-' . $this->type->getId(); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NotInArray && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsAClass.php b/src/Psalm/Storage/Assertion/IsAClass.php index 3b6c5473952..ca5409e9268 100644 --- a/src/Psalm/Storage/Assertion/IsAClass.php +++ b/src/Psalm/Storage/Assertion/IsAClass.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsAClass extends Assertion { /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ @@ -18,13 +21,11 @@ public function __construct(Atomic $type, bool $allow_string) $this->allow_string = $allow_string; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotAClass($this->type, $this->allow_string); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; @@ -35,7 +36,6 @@ public function __toString(): string return 'isa-' . ($this->allow_string ? 'string-' : '') . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotAClass diff --git a/src/Psalm/Storage/Assertion/IsClassEqual.php b/src/Psalm/Storage/Assertion/IsClassEqual.php index da5307aa7f5..a7c92f2aab2 100644 --- a/src/Psalm/Storage/Assertion/IsClassEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassEqual.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsClassEqual extends Assertion { public string $type; @@ -13,13 +16,11 @@ public function __construct(string $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsClassNotEqual($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -30,7 +31,6 @@ public function __toString(): string return '=get-class-' . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsClassNotEqual && $this->type === $assertion->type; diff --git a/src/Psalm/Storage/Assertion/IsClassNotEqual.php b/src/Psalm/Storage/Assertion/IsClassNotEqual.php index c8d05dc8b77..ae4ed1329c7 100644 --- a/src/Psalm/Storage/Assertion/IsClassNotEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassNotEqual.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsClassNotEqual extends Assertion { public string $type; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsClassEqual($this->type); @@ -29,7 +31,6 @@ public function __toString(): string return '!=get-class-' . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsClassEqual && $this->type === $assertion->type; diff --git a/src/Psalm/Storage/Assertion/IsCountable.php b/src/Psalm/Storage/Assertion/IsCountable.php index dea9295b28c..3933c4a13dd 100644 --- a/src/Psalm/Storage/Assertion/IsCountable.php +++ b/src/Psalm/Storage/Assertion/IsCountable.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsCountable extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotCountable(true); @@ -17,7 +19,6 @@ public function __toString(): string return 'countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotCountable && $assertion->is_negatable; diff --git a/src/Psalm/Storage/Assertion/IsEqualIsset.php b/src/Psalm/Storage/Assertion/IsEqualIsset.php index bded7c5ce06..8f754b375cf 100644 --- a/src/Psalm/Storage/Assertion/IsEqualIsset.php +++ b/src/Psalm/Storage/Assertion/IsEqualIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsEqualIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Any(); @@ -17,13 +19,11 @@ public function __toString(): string return '=isset'; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThan.php b/src/Psalm/Storage/Assertion/IsGreaterThan.php index 131c3fc94a9..ed58ecabc99 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThan.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThan.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsGreaterThan extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLessThanOrEqualTo($this->value); @@ -24,7 +26,6 @@ public function __toString(): string return '>' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThanOrEqualTo && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php index ce8590eafc8..20a3c05d189 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsGreaterThanOrEqualTo extends Assertion { public int $value; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLessThan($this->value); @@ -29,7 +31,6 @@ public function __toString(): string return '!<' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThan && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsIdentical.php b/src/Psalm/Storage/Assertion/IsIdentical.php index 9f6dccfae10..8730bda852d 100644 --- a/src/Psalm/Storage/Assertion/IsIdentical.php +++ b/src/Psalm/Storage/Assertion/IsIdentical.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsIdentical extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotIdentical($this->type); @@ -25,24 +27,24 @@ public function __toString(): string return '=' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIdentical && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsIsset.php b/src/Psalm/Storage/Assertion/IsIsset.php index 7b5fd06423b..219649181a1 100644 --- a/src/Psalm/Storage/Assertion/IsIsset.php +++ b/src/Psalm/Storage/Assertion/IsIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotIsset(); @@ -17,7 +19,6 @@ public function __toString(): string return 'isset'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIsset; diff --git a/src/Psalm/Storage/Assertion/IsLessThan.php b/src/Psalm/Storage/Assertion/IsLessThan.php index 0f26f5c53e1..8d78d4d4669 100644 --- a/src/Psalm/Storage/Assertion/IsLessThan.php +++ b/src/Psalm/Storage/Assertion/IsLessThan.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsLessThan extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsGreaterThanOrEqualTo($this->value); @@ -24,7 +26,6 @@ public function __toString(): string return '<' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThanOrEqualTo && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php index 36afbe8e282..2dd565915a1 100644 --- a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsLessThanOrEqualTo extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsGreaterThan($this->value); @@ -29,7 +31,6 @@ public function __toString(): string return '!>' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThan && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsLooselyEqual.php b/src/Psalm/Storage/Assertion/IsLooselyEqual.php index 35b1e6d6259..c0b23dbde7c 100644 --- a/src/Psalm/Storage/Assertion/IsLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsLooselyEqual.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsLooselyEqual extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotLooselyEqual($this->type); @@ -25,24 +27,24 @@ public function __toString(): string return '~' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotLooselyEqual && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotAClass.php b/src/Psalm/Storage/Assertion/IsNotAClass.php index 45cbd3442ef..7c542e1684c 100644 --- a/src/Psalm/Storage/Assertion/IsNotAClass.php +++ b/src/Psalm/Storage/Assertion/IsNotAClass.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotAClass extends Assertion { /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ @@ -23,7 +26,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsAClass($this->type, $this->allow_string); @@ -34,7 +36,6 @@ public function __toString(): string return 'isa-' . ($this->allow_string ? 'string-' : '') . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsAClass diff --git a/src/Psalm/Storage/Assertion/IsNotCountable.php b/src/Psalm/Storage/Assertion/IsNotCountable.php index b09a7a7dcdc..c76fe24e26e 100644 --- a/src/Psalm/Storage/Assertion/IsNotCountable.php +++ b/src/Psalm/Storage/Assertion/IsNotCountable.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsNotCountable extends Assertion { public $is_negatable; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsCountable(); @@ -29,7 +31,6 @@ public function __toString(): string return '!countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsCountable; diff --git a/src/Psalm/Storage/Assertion/IsNotIdentical.php b/src/Psalm/Storage/Assertion/IsNotIdentical.php index 2c023300a02..978ca956df6 100644 --- a/src/Psalm/Storage/Assertion/IsNotIdentical.php +++ b/src/Psalm/Storage/Assertion/IsNotIdentical.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotIdentical extends Assertion { public Atomic $type; @@ -19,13 +22,11 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsIdentical($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -36,18 +37,19 @@ public function __toString(): string return '!=' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsIdentical && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotIsset.php b/src/Psalm/Storage/Assertion/IsNotIsset.php index defa8479a32..d42486326e6 100644 --- a/src/Psalm/Storage/Assertion/IsNotIsset.php +++ b/src/Psalm/Storage/Assertion/IsNotIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsNotIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsIsset(); @@ -22,7 +24,6 @@ public function __toString(): string return '!isset'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIsset; diff --git a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php index bcf77c78514..846afb19075 100644 --- a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotLooselyEqual extends Assertion { public Atomic $type; @@ -19,13 +22,11 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLooselyEqual($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -36,18 +37,19 @@ public function __toString(): string return '!~' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLooselyEqual && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotType.php b/src/Psalm/Storage/Assertion/IsNotType.php index 321dd744e0c..24d1ee9c380 100644 --- a/src/Psalm/Storage/Assertion/IsNotType.php +++ b/src/Psalm/Storage/Assertion/IsNotType.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotType extends Assertion { public Atomic $type; @@ -19,7 +22,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsType($this->type); @@ -30,18 +32,19 @@ public function __toString(): string return '!' . $this->type->getId(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsType && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsType.php b/src/Psalm/Storage/Assertion/IsType.php index 2c822a79d40..501a5e06cca 100644 --- a/src/Psalm/Storage/Assertion/IsType.php +++ b/src/Psalm/Storage/Assertion/IsType.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsType extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotType($this->type); @@ -25,18 +27,19 @@ public function __toString(): string return $this->type->getId(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotType && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/NestedAssertions.php b/src/Psalm/Storage/Assertion/NestedAssertions.php index c7fa0a48e62..20f56ba585f 100644 --- a/src/Psalm/Storage/Assertion/NestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NestedAssertions.php @@ -8,6 +8,9 @@ use const JSON_THROW_ON_ERROR; +/** + * @psalm-immutable + */ final class NestedAssertions extends Assertion { /** @var array>> */ @@ -19,7 +22,6 @@ public function __construct(array $assertions) $this->assertions = $assertions; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NotNestedAssertions($this->assertions); @@ -30,7 +32,6 @@ public function __toString(): string return '@' . json_encode($this->assertions, JSON_THROW_ON_ERROR); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/NonEmpty.php b/src/Psalm/Storage/Assertion/NonEmpty.php index bb662d9591f..b45076d90c6 100644 --- a/src/Psalm/Storage/Assertion/NonEmpty.php +++ b/src/Psalm/Storage/Assertion/NonEmpty.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NonEmpty extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Empty_(); @@ -17,7 +19,6 @@ public function __toString(): string return 'non-empty'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Empty_; diff --git a/src/Psalm/Storage/Assertion/NonEmptyCountable.php b/src/Psalm/Storage/Assertion/NonEmptyCountable.php index c42eca24d57..8c191d22c0a 100644 --- a/src/Psalm/Storage/Assertion/NonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NonEmptyCountable.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NonEmptyCountable extends Assertion { public $is_negatable; @@ -13,13 +16,11 @@ public function __construct(bool $is_negatable) $this->is_negatable = $is_negatable; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return $this->is_negatable ? new NotNonEmptyCountable() : new Any(); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return !$this->is_negatable; @@ -30,7 +31,6 @@ public function __toString(): string return ($this->is_negatable ? '' : '=') . 'non-empty-countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $this->is_negatable && $assertion instanceof NotNonEmptyCountable; diff --git a/src/Psalm/Storage/Assertion/NotInArray.php b/src/Psalm/Storage/Assertion/NotInArray.php index 2dbbb71161b..73c352d47dd 100644 --- a/src/Psalm/Storage/Assertion/NotInArray.php +++ b/src/Psalm/Storage/Assertion/NotInArray.php @@ -5,8 +5,14 @@ use Psalm\Storage\Assertion; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class NotInArray extends Assertion { + /** + * @readonly + */ public Union $type; public function __construct(Union $type) @@ -14,7 +20,6 @@ public function __construct(Union $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new InArray($this->type); @@ -30,7 +35,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof InArray && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/NotNestedAssertions.php b/src/Psalm/Storage/Assertion/NotNestedAssertions.php index ad6f46f9ef6..7f2d33564c3 100644 --- a/src/Psalm/Storage/Assertion/NotNestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NotNestedAssertions.php @@ -8,6 +8,9 @@ use const JSON_THROW_ON_ERROR; +/** + * @psalm-immutable + */ final class NotNestedAssertions extends Assertion { /** @var array>> */ @@ -24,7 +27,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NestedAssertions($this->assertions); @@ -35,7 +37,6 @@ public function __toString(): string return '!@' . json_encode($this->assertions, JSON_THROW_ON_ERROR); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php index d4a59fbd1a6..48fc743c373 100644 --- a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NotNonEmptyCountable extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NonEmptyCountable(true); @@ -22,7 +24,6 @@ public function __toString(): string return '!non-empty-countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NonEmptyCountable && $assertion->is_negatable; diff --git a/src/Psalm/Storage/Assertion/Truthy.php b/src/Psalm/Storage/Assertion/Truthy.php index 1d92be6e158..ef853b0b3b3 100644 --- a/src/Psalm/Storage/Assertion/Truthy.php +++ b/src/Psalm/Storage/Assertion/Truthy.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Truthy extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Falsy(); @@ -17,7 +19,6 @@ public function __toString(): string return '!falsy'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Falsy; diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 4a5e0e7b2ce..f7bc9ca0662 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -4,9 +4,10 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Type\TypeNode; use Psalm\Type\Union; -final class FunctionLikeParameter implements HasAttributesInterface +final class FunctionLikeParameter implements HasAttributesInterface, TypeNode { use CustomMetadataTrait; @@ -111,23 +112,26 @@ final class FunctionLikeParameter implements HasAttributesInterface public $description; /** + * @psalm-external-mutation-free * @param Union|UnresolvedConstantComponent|null $default_type */ public function __construct( string $name, bool $by_ref, ?Union $type = null, + ?Union $signature_type = null, ?CodeLocation $location = null, ?CodeLocation $type_location = null, bool $is_optional = true, bool $is_nullable = false, bool $is_variadic = false, - $default_type = null + $default_type = null, + ?Union $out_type = null ) { $this->name = $name; $this->by_ref = $by_ref; $this->type = $type; - $this->signature_type = $type; + $this->signature_type = $signature_type; $this->is_optional = $is_optional; $this->is_nullable = $is_nullable; $this->is_variadic = $is_variadic; @@ -135,8 +139,10 @@ public function __construct( $this->type_location = $type_location; $this->signature_type_location = $type_location; $this->default_type = $default_type; + $this->out_type = $out_type; } + /** @psalm-mutation-free */ public function getId(): string { return ($this->type ? $this->type->getId() : 'mixed') @@ -144,14 +150,29 @@ public function getId(): string . ($this->is_optional ? '=' : ''); } - public function __clone() + /** @psalm-mutation-free */ + public function replaceType(Union $type): self { - if ($this->type) { - $this->type = clone $this->type; + if ($this->type === $type) { + return $this; } + $cloned = clone $this; + $cloned->type = $type; + return $cloned; + } + + /** @psalm-mutation-free */ + public function getChildNodeKeys(): array + { + $result = ['type', 'signature_type', 'out_type']; + if ($this->default_type instanceof Union) { + $result []= 'default_type'; + } + return $result; } /** + * @psalm-mutation-free * @return list */ public function getAttributeStorages(): array diff --git a/src/Psalm/Storage/ImmutableNonCloneableTrait.php b/src/Psalm/Storage/ImmutableNonCloneableTrait.php new file mode 100644 index 00000000000..7609a73a8a4 --- /dev/null +++ b/src/Psalm/Storage/ImmutableNonCloneableTrait.php @@ -0,0 +1,13 @@ +getAtomicTypes() as $atomic_type) { - $assertion = clone $assertion; - $assertion->setAtomicType($atomic_type); + $assertion = $assertion->setAtomicType($atomic_type); $assertion_rules[] = $assertion; } } else { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 3f82cb6f240..7dcfc543a5b 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -46,6 +46,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TVoid; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use UnexpectedValueException; @@ -175,6 +176,9 @@ public static function getStringFromFQCLN( return '\\' . $value; } + /** + * @psalm-pure + */ public static function getInt(bool $from_calculation = false, ?int $value = null): Union { if ($value !== null) { @@ -183,11 +187,15 @@ public static function getInt(bool $from_calculation = false, ?int $value = null $union = new Union([new TInt()]); } + /** @psalm-suppress ImpurePropertyAssignment We just created this object */ $union->from_calculation = $from_calculation; return $union; } + /** + * @psalm-pure + */ public static function getLowercaseString(): Union { $type = new TLowercaseString(); @@ -195,6 +203,9 @@ public static function getLowercaseString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyLowercaseString(): Union { $type = new TNonEmptyLowercaseString(); @@ -202,6 +213,9 @@ public static function getNonEmptyLowercaseString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyString(): Union { $type = new TNonEmptyString(); @@ -209,6 +223,9 @@ public static function getNonEmptyString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonFalsyString(): Union { $type = new TNonFalsyString(); @@ -216,6 +233,9 @@ public static function getNonFalsyString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNumeric(): Union { $type = new TNumeric; @@ -223,6 +243,9 @@ public static function getNumeric(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNumericString(): Union { $type = new TNumericString; @@ -257,6 +280,9 @@ public static function getString(?string $value = null): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getSingleLetter(): Union { $type = new TSingleLetter; @@ -264,6 +290,9 @@ public static function getSingleLetter(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getClassString(string $extends = 'object'): Union { return new Union([ @@ -276,6 +305,9 @@ public static function getClassString(string $extends = 'object'): Union ]); } + /** + * @psalm-pure + */ public static function getLiteralClassString(string $class_type, bool $definite_class = false): Union { $type = new TLiteralClassString($class_type, $definite_class); @@ -283,52 +315,73 @@ public static function getLiteralClassString(string $class_type, bool $definite_ return new Union([$type]); } - public static function getNull(): Union + /** + * @psalm-pure + */ + public static function getNull(bool $from_docblock = false): Union { - $type = new TNull; + $type = new TNull($from_docblock); return new Union([$type]); } - public static function getMixed(bool $from_loop_isset = false): Union + /** + * @psalm-pure + */ + public static function getMixed(bool $from_loop_isset = false, bool $from_docblock = false): Union { - $type = new TMixed($from_loop_isset); + $type = new TMixed($from_loop_isset, $from_docblock); return new Union([$type]); } - public static function getScalar(): Union + /** + * @psalm-pure + */ + public static function getScalar(bool $from_docblock = false): Union { - $type = new TScalar(); + $type = new TScalar($from_docblock); return new Union([$type]); } - public static function getNever(): Union + /** + * @psalm-pure + */ + public static function getNever(bool $from_docblock = false): Union { - $type = new TNever(); + $type = new TNever($from_docblock); return new Union([$type]); } - public static function getBool(): Union + /** + * @psalm-pure + */ + public static function getBool(bool $from_docblock = false): Union { - $type = new TBool; + $type = new TBool($from_docblock); return new Union([$type]); } - public static function getFloat(?float $value = null): Union + /** + * @psalm-pure + */ + public static function getFloat(?float $value = null, bool $from_docblock = false): Union { if ($value !== null) { - $type = new TLiteralFloat($value); + $type = new TLiteralFloat($value, $from_docblock); } else { - $type = new TFloat(); + $type = new TFloat($from_docblock); } return new Union([$type]); } + /** + * @psalm-pure + */ public static function getObject(): Union { $type = new TObject; @@ -336,6 +389,9 @@ public static function getObject(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getClosure(): Union { $type = new TClosure('Closure'); @@ -343,13 +399,19 @@ public static function getClosure(): Union return new Union([$type]); } - public static function getArrayKey(): Union + /** + * @psalm-pure + */ + public static function getArrayKey(bool $from_docblock = false): Union { - $type = new TArrayKey(); + $type = new TArrayKey($from_docblock); return new Union([$type]); } + /** + * @psalm-pure + */ public static function getArray(): Union { $type = new TArray( @@ -362,6 +424,9 @@ public static function getArray(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getEmptyArray(): Union { $array_type = new TArray( @@ -376,6 +441,9 @@ public static function getEmptyArray(): Union ]); } + /** + * @psalm-pure + */ public static function getList(): Union { $type = new TList(new Union([new TMixed])); @@ -383,6 +451,9 @@ public static function getList(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyList(): Union { $type = new TNonEmptyList(new Union([new TMixed])); @@ -390,33 +461,46 @@ public static function getNonEmptyList(): Union return new Union([$type]); } - public static function getVoid(): Union + /** + * @psalm-pure + */ + public static function getVoid(bool $from_docblock = false): Union { - $type = new TVoid; + $type = new TVoid($from_docblock); return new Union([$type]); } - public static function getFalse(): Union + /** + * @psalm-pure + */ + public static function getFalse(bool $from_docblock = false): Union { - $type = new TFalse; + $type = new TFalse($from_docblock); return new Union([$type]); } - public static function getTrue(): Union + /** + * @psalm-pure + */ + public static function getTrue(bool $from_docblock = false): Union { - $type = new TTrue; + $type = new TTrue($from_docblock); return new Union([$type]); } - public static function getResource(): Union + /** + * @psalm-pure + */ + public static function getResource(bool $from_docblock = false): Union { - return new Union([new TResource]); + return new Union([new TResource($from_docblock)]); } /** + * @psalm-external-mutation-free * @param non-empty-list $union_types */ public static function combineUnionTypeArray(array $union_types, ?Codebase $codebase): Union @@ -436,6 +520,9 @@ public static function combineUnionTypeArray(array $union_types, ?Codebase $code * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar * + * @psalm-external-mutation-free + * + * @psalm-suppress ImpurePropertyAssignment We're not mutating external instances */ public static function combineUnionTypes( ?Union $type_1, @@ -609,13 +696,16 @@ public static function intersectUnionTypes( if (null !== $intersection_atomic) { if (null === $combined_type) { - $combined_type = new Union([$intersection_atomic]); + $combined_type = new MutableUnion([$intersection_atomic]); } else { $combined_type->addType($intersection_atomic); } } } } + if ($combined_type) { + $combined_type = $combined_type->freeze(); + } } //if a type is contained by the other, the intersection is the narrowest type @@ -777,26 +867,24 @@ private static function intersectAtomicTypes( .' Check the preceding code for errors.' ); } - if (!$intersection_atomic->extra_types) { - $intersection_atomic->extra_types = []; - } $intersection_performed = true; - $wider_type_clone = clone $wider_type; - - $wider_type_clone->extra_types = []; + $wider_type_clone = $wider_type->setIntersectionTypes([]); - $intersection_atomic->extra_types[$wider_type_clone->getKey()] = $wider_type_clone; + $final_intersection = array_merge( + [$wider_type_clone->getKey() => $wider_type_clone], + $intersection_atomic->getIntersectionTypes() + ); $wider_type_intersection_types = $wider_type->getIntersectionTypes(); - if ($wider_type_intersection_types !== null) { - foreach ($wider_type_intersection_types as $wider_type_intersection_type) { - $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] - = clone $wider_type_intersection_type; - } + foreach ($wider_type_intersection_types as $wider_type_intersection_type) { + $final_intersection[$wider_type_intersection_type->getKey()] + = clone $wider_type_intersection_type; } + + return $intersection_atomic->setIntersectionTypes($final_intersection); } return $intersection_atomic; diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 6f93e74706a..3ec161a2b3a 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -10,6 +10,7 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; +use Psalm\Internal\TypeVisitor\ClasslikeReplacer; use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; @@ -20,7 +21,6 @@ use Psalm\Type\Atomic\TCallableList; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; -use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClassStringMap; use Psalm\Type\Atomic\TClosedResource; @@ -76,8 +76,15 @@ use function strpos; use function strtolower; +/** + * @psalm-immutable + */ abstract class Atomic implements TypeNode { + public function __construct(bool $from_docblock = false) + { + $this->from_docblock = $from_docblock; + } /** * Whether or not the type has been checked yet * @@ -108,6 +115,35 @@ abstract class Atomic implements TypeNode public $text; /** + * @return static + */ + public function setFromDocblock(bool $from_docblock): self + { + if ($from_docblock === $this->from_docblock) { + return $this; + } + $cloned = clone $this; + $cloned->from_docblock = $from_docblock; + return $cloned; + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type = $this; + /** @psalm-suppress ImpureMethodCall ClasslikeReplacer will always clone */ + (new ClasslikeReplacer( + $old, + $new + ))->traverse($type); + return $type; + } + + /** + * @psalm-suppress InaccessibleProperty Allowed during construction + * * @param int $analysis_php_version_id contains php version when the type comes from signature * @param array> $template_type_map * @param array $type_aliases @@ -116,7 +152,32 @@ public static function create( string $value, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + ?int $offset_start = null, + ?int $offset_end = null, + ?string $text = null, + bool $from_docblock = false + ): Atomic { + $result = self::createInner($value, $analysis_php_version_id, $template_type_map, $type_aliases); + $result->offset_start = $offset_start; + $result->offset_end = $offset_end; + $result->text = $text; + $result->from_docblock = $from_docblock; + return $result; + } + /** + * @psalm-suppress InaccessibleProperty Allowed during construction + * + * @param int $analysis_php_version_id contains php version when the type comes from signature + * @param array> $template_type_map + * @param array $type_aliases + */ + private static function createInner( + string $value, + ?int $analysis_php_version_id = null, + array $template_type_map = [], + array $type_aliases = [], + bool $from_docblock = false ): Atomic { switch ($value) { case 'int': @@ -178,19 +239,28 @@ public static function create( case 'array': case 'associative-array': - return new TArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'non-empty-array': - return new TNonEmptyArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TNonEmptyArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'callable-array': - return new TCallableArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TCallableArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'list': - return new TList(Type::getMixed()); + return new TList(Type::getMixed(false, $from_docblock)); case 'non-empty-list': - return new TNonEmptyList(Type::getMixed()); + return new TNonEmptyList(Type::getMixed(false, $from_docblock)); case 'non-empty-string': return new TNonEmptyString(); @@ -364,7 +434,7 @@ public function isNamedObjectType(): bool || ($this instanceof TTemplateParam && ($this->as->hasNamedObjectType() || array_filter( - $this->extra_types ?: [], + $this->extra_types, static fn($extra_type): bool => $extra_type->isNamedObjectType() ) ) @@ -522,108 +592,16 @@ public function hasArrayAccessInterface(Codebase $codebase): bool ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { return []; } - public function replaceClassLike(string $old, string $new): void - { - if ($this instanceof TNamedObject) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TNamedObject - || $this instanceof TIterable - || $this instanceof TTemplateParam - ) { - if ($this->extra_types) { - foreach ($this->extra_types as $extra_type) { - $extra_type->replaceClassLike($old, $new); - } - } - } - - if ($this instanceof TClassConstant) { - if (strtolower($this->fq_classlike_name) === $old) { - $this->fq_classlike_name = $new; - } - } - - if ($this instanceof TClassString && $this->as !== 'object') { - if (strtolower($this->as) === $old) { - $this->as = $new; - } - } - - if ($this instanceof TTemplateParam) { - $this->as->replaceClassLike($old, $new); - } - - if ($this instanceof TLiteralClassString) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TArray - || $this instanceof TGenericObject - || $this instanceof TIterable - ) { - foreach ($this->type_params as $type_param) { - $type_param->replaceClassLike($old, $new); - } - } - - if ($this instanceof TKeyedArray) { - foreach ($this->properties as $property_type) { - $property_type->replaceClassLike($old, $new); - } - } - - if ($this instanceof TClosure - || $this instanceof TCallable - ) { - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $param->type->replaceClassLike($old, $new); - } - } - } - - if ($this->return_type) { - $this->return_type->replaceClassLike($old, $new); - } - } - } - final public function __toString(): string { return $this->getId(); } - public function __clone() - { - if ($this instanceof TNamedObject - || $this instanceof TTemplateParam - || $this instanceof TIterable - || $this instanceof TObjectWithProperties - ) { - if ($this->extra_types) { - foreach ($this->extra_types as &$type) { - $type = clone $type; - } - } - } - - if ($this instanceof TTemplateParam) { - $this->as = clone $this->as; - } - } - /** * This is the true identifier for the type. It defaults to self::getKey() but can be overrided to be more precise */ @@ -670,6 +648,9 @@ abstract public function toPhpString( abstract public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool; + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -682,14 +663,19 @@ public function replaceTemplateTypesWithStandins( bool $add_lower_bound = false, int $depth = 0 ): self { + // do nothing return $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): self { // do nothing + return $this; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 8e8e1e633e8..17980ae840e 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -9,12 +9,14 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function count; use function implode; +/** + * @psalm-immutable + */ trait CallableTrait { /** @@ -41,23 +43,14 @@ public function __construct( string $value = 'callable', ?array $params = null, ?Union $return_type = null, - ?bool $is_pure = null + ?bool $is_pure = null, + bool $from_docblock = false ) { $this->value = $value; $this->params = $params; $this->return_type = $return_type; $this->is_pure = $is_pure; - } - - public function __clone() - { - if ($this->params) { - foreach ($this->params as &$param) { - $param = clone $param; - } - } - - $this->return_type = $this->return_type ? clone $this->return_type : null; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -187,7 +180,10 @@ public function getId(bool $exact = true, bool $nested = false): string . $this->value . $param_string . $return_type_string; } - public function replaceTemplateTypesWithStandins( + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -198,11 +194,15 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $callable = clone $this; + ): ?array { + $replaced = false; + $params = $this->params; + if ($params) { + foreach ($params as $offset => $param) { + if (!$param->type) { + continue; + } - if ($callable->params) { - foreach ($callable->params as $offset => $param) { $input_param_type = null; if (($input_type instanceof TClosure || $input_type instanceof TCallable) @@ -211,11 +211,7 @@ public function replaceTemplateTypesWithStandins( $input_param_type = $input_type->params[$offset]->type; } - if (!$param->type) { - continue; - } - - $param->type = TemplateStandinTypeReplacer::replace( + $new_param = $param->replaceType(TemplateStandinTypeReplacer::replace( $param->type, $template_result, $codebase, @@ -228,13 +224,16 @@ public function replaceTemplateTypesWithStandins( !$add_lower_bound, null, $depth - ); + )); + $replaced = $replaced || $new_param !== $param; + $params[$offset] = $new_param; } } - if ($callable->return_type) { - $callable->return_type = TemplateStandinTypeReplacer::replace( - $callable->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateStandinTypeReplacer::replace( + $return_type, $template_result, $codebase, $statements_analyzer, @@ -247,57 +246,60 @@ public function replaceTemplateTypesWithStandins( $replace, $add_lower_bound ); + $replaced = $replaced || $this->return_type !== $return_type; } - return $callable; + if ($replaced) { + return [$params, $return_type]; + } + return null; } - public function replaceTemplateTypesWithArgTypes( + + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - if ($this->params) { - foreach ($this->params as $param) { - if (!$param->type) { - continue; - } + ): ?array { + $replaced = false; - TemplateInferredTypeReplacer::replace( - $param->type, - $template_result, - $codebase - ); + $params = $this->params; + if ($params) { + foreach ($params as $k => $param) { + if ($param->type) { + $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( + $param->type, + $template_result, + $codebase + )); + $replaced = $replaced || $new_param !== $param; + $params[$k] = $new_param; + } } } - if ($this->return_type) { - TemplateInferredTypeReplacer::replace( - $this->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateInferredTypeReplacer::replace( + $return_type, $template_result, $codebase ); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; } + return null; } /** - * @return list + * @return list */ - public function getChildNodes(): array + protected function getCallableChildNodeKeys(): array { - $child_nodes = []; - - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $child_nodes[] = $param->type; - } - } - } - - if ($this->return_type) { - $child_nodes[] = $this->return_type; - } - - return $child_nodes; + return ['params', 'return_type']; } } diff --git a/src/Psalm/Type/Atomic/DependentType.php b/src/Psalm/Type/Atomic/DependentType.php index 4ebefe03b38..98cf7a5749c 100644 --- a/src/Psalm/Type/Atomic/DependentType.php +++ b/src/Psalm/Type/Atomic/DependentType.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ interface DependentType { public function getVarId(): string; diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index ba4df1e9873..87767a38fec 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -9,7 +9,6 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function array_map; @@ -19,8 +18,27 @@ use function strpos; use function substr; +/** + * @template TTypeParams as array + * @psalm-immutable + */ trait GenericTrait { + /** + * @param TTypeParams $type_params + * + * @return static + */ + public function replaceTypeParams(array $type_params): self + { + if ($this->type_params === $type_params) { + return $this; + } + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { $s = ''; @@ -139,22 +157,10 @@ public function toNamespacedString( '>' . $extra_types; } - public function __clone() - { - foreach ($this->type_params as &$type_param) { - $type_param = clone $type_param; - } - } - /** - * @return array + * @return TTypeParams|null */ - public function getChildNodes(): array - { - return $this->type_params; - } - - public function replaceTemplateTypesWithStandins( + protected function replaceTypeParamsTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -165,7 +171,7 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { + ): ?array { if ($input_type instanceof TList) { $input_type = new TArray([Type::getInt(), $input_type->type_param]); } @@ -185,9 +191,9 @@ public function replaceTemplateTypesWithStandins( ); } - $atomic = clone $this; + $type_params = $this->type_params; - foreach ($atomic->type_params as $offset => $type_param) { + foreach ($type_params as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TIterable @@ -208,7 +214,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_object_type_params[$offset]; } - $atomic->type_params[$offset] = TemplateStandinTypeReplacer::replace( + $type_params[$offset] = TemplateStandinTypeReplacer::replace( $type_param, $template_result, $codebase, @@ -227,31 +233,31 @@ public function replaceTemplateTypesWithStandins( ); } - return $atomic; + return $type_params === $this->type_params ? null : $type_params; } - public function replaceTemplateTypesWithArgTypes( + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->type_params as $offset => $type_param) { - TemplateInferredTypeReplacer::replace( + ): ?array { + $type_params = $this->type_params; + foreach ($type_params as $offset => $type_param) { + $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, $codebase ); if ($this instanceof TArray && $offset === 0 && $type_param->isMixed()) { - $this->type_params[0] = Type::getArrayKey(); + $type_param = Type::getArrayKey(); } - } - if ($this instanceof TGenericObject) { - $this->remapped_params = true; + $type_params[$offset] = $type_param; } - if ($this instanceof TGenericObject || $this instanceof TIterable) { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); - } + return $type_params === $this->type_params ? null : $type_params; } } diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 720379d9295..7889a0111a8 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -3,19 +3,24 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type\Atomic; use function array_map; +use function array_merge; use function implode; +/** + * @psalm-immutable + */ trait HasIntersectionTrait { /** - * @var array|null + * @var array */ - public $extra_types; + public array $extra_types = []; /** * @param array $aliased_classes @@ -49,26 +54,49 @@ private function getNamespacedIntersectionTypes( /** * @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $type + * + * @return static */ - public function addIntersectionType(Atomic $type): void + public function addIntersectionType(Atomic $type): self { - $this->extra_types[$type->getKey()] = $type; + return $this->setIntersectionTypes(array_merge( + $this->extra_types, + [$type->getKey() => $type] + )); } /** - * @return array|null + * @param array $types + * + * @return static */ - public function getIntersectionTypes(): ?array + public function setIntersectionTypes(array $types): self + { + if ($types === $this->extra_types) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $types; + return $cloned; + } + + /** + * @return array + */ + public function getIntersectionTypes(): array { return $this->extra_types; } - public function replaceIntersectionTemplateTypesWithArgTypes( + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): ?array { if (!$this->extra_types) { - return; + return null; } $new_types = []; @@ -90,11 +118,49 @@ public function replaceIntersectionTemplateTypesWithArgTypes( } } } else { - $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $extra_type = $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); $new_types[$extra_type->getKey()] = $extra_type; } } - $this->extra_types = $new_types; + return $new_types === $this->extra_types ? null : $new_types; + } + + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): ?array { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $type) { + $type = $type->replaceTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $new_types[$type->getKey()] = $type; + } + + return $new_types === $this->extra_types ? null : $new_types; } } diff --git a/src/Psalm/Type/Atomic/Scalar.php b/src/Psalm/Type/Atomic/Scalar.php index dbc2f1a4691..764b34af86a 100644 --- a/src/Psalm/Type/Atomic/Scalar.php +++ b/src/Psalm/Type/Atomic/Scalar.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ abstract class Scalar extends Atomic { public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index 1a29e4d27a4..a6da68a042c 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -4,6 +4,7 @@ /** * Denotes an anonymous class (i.e. `new class{}`) with potential methods + * @psalm-immutable */ final class TAnonymousClassInstance extends TNamedObject { @@ -14,10 +15,15 @@ final class TAnonymousClassInstance extends TNamedObject /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, ?string $extends = null) - { - parent::__construct($value, $is_static); + public function __construct( + string $value, + bool $is_static = false, + ?string $extends = null, + array $extra_types = [] + ) { + parent::__construct($value, $is_static, false, $extra_types); $this->extends = $extends; } diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 88041e209a8..0b51e220fbf 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -10,15 +13,19 @@ /** * Denotes a simple array of the form `array`. It expects an array with two elements, both union types. + * @psalm-immutable */ class TArray extends Atomic { + /** + * @use GenericTrait + */ use GenericTrait; /** * @var array{Union, Union} */ - public $type_params; + public array $type_params; /** * @var string @@ -30,9 +37,10 @@ class TArray extends Atomic * * @param array{Union, Union} $type_params */ - public function __construct(array $type_params) + public function __construct(array $type_params, bool $from_docblock = false) { $this->type_params = $type_params; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -96,4 +104,61 @@ public function isEmptyArray(): bool { return $this->type_params[1]->isNever(); } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $type_params = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + public function getChildNodeKeys(): array + { + return ['type_params']; + } } diff --git a/src/Psalm/Type/Atomic/TArrayKey.php b/src/Psalm/Type/Atomic/TArrayKey.php index bf317292958..9938d6a7082 100644 --- a/src/Psalm/Type/Atomic/TArrayKey.php +++ b/src/Psalm/Type/Atomic/TArrayKey.php @@ -4,6 +4,7 @@ /** * Denotes the `array-key` type, used for something that could be the offset of an `array`. + * @psalm-immutable */ class TArrayKey extends Scalar { diff --git a/src/Psalm/Type/Atomic/TBool.php b/src/Psalm/Type/Atomic/TBool.php index ec7cbc06258..2312152afbd 100644 --- a/src/Psalm/Type/Atomic/TBool.php +++ b/src/Psalm/Type/Atomic/TBool.php @@ -4,6 +4,7 @@ /** * Denotes the `bool` type where the exact value is unknown. + * @psalm-immutable */ class TBool extends Scalar { diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index c0dc900af9e..18c62ed283e 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -2,10 +2,14 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; /** * Denotes the `callable` type. Can result from an `is_callable` check. + * @psalm-immutable */ final class TCallable extends Atomic { @@ -32,4 +36,63 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return $this->params === null && $this->return_type === null; } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + + public function getChildNodeKeys(): array + { + return $this->getCallableChildNodeKeys(); + } } diff --git a/src/Psalm/Type/Atomic/TCallableArray.php b/src/Psalm/Type/Atomic/TCallableArray.php index 4dba9674653..332dfb1118c 100644 --- a/src/Psalm/Type/Atomic/TCallableArray.php +++ b/src/Psalm/Type/Atomic/TCallableArray.php @@ -4,6 +4,7 @@ /** * Denotes an array that is _also_ `callable`. + * @psalm-immutable */ final class TCallableArray extends TNonEmptyArray { diff --git a/src/Psalm/Type/Atomic/TCallableKeyedArray.php b/src/Psalm/Type/Atomic/TCallableKeyedArray.php index 91c89e0600e..21a774e454e 100644 --- a/src/Psalm/Type/Atomic/TCallableKeyedArray.php +++ b/src/Psalm/Type/Atomic/TCallableKeyedArray.php @@ -4,6 +4,7 @@ /** * Denotes an object-like array that is _also_ `callable`. + * @psalm-immutable */ final class TCallableKeyedArray extends TKeyedArray { diff --git a/src/Psalm/Type/Atomic/TCallableList.php b/src/Psalm/Type/Atomic/TCallableList.php index 1429d36ed0e..67c490e173a 100644 --- a/src/Psalm/Type/Atomic/TCallableList.php +++ b/src/Psalm/Type/Atomic/TCallableList.php @@ -4,6 +4,7 @@ /** * Denotes a list that is _also_ `callable`. + * @psalm-immutable */ final class TCallableList extends TNonEmptyList { diff --git a/src/Psalm/Type/Atomic/TCallableObject.php b/src/Psalm/Type/Atomic/TCallableObject.php index 2bb7e3b2791..9114a313de2 100644 --- a/src/Psalm/Type/Atomic/TCallableObject.php +++ b/src/Psalm/Type/Atomic/TCallableObject.php @@ -4,6 +4,7 @@ /** * Denotes an object that is also `callable` (i.e. it has `__invoke` defined). + * @psalm-immutable */ final class TCallableObject extends TObject { diff --git a/src/Psalm/Type/Atomic/TCallableString.php b/src/Psalm/Type/Atomic/TCallableString.php index 1f52a5da8a5..c95456a0ede 100644 --- a/src/Psalm/Type/Atomic/TCallableString.php +++ b/src/Psalm/Type/Atomic/TCallableString.php @@ -4,6 +4,7 @@ /** * Denotes the `callable-string` type, used to represent an unknown string that is also `callable`. + * @psalm-immutable */ final class TCallableString extends TNonFalsyString { diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 79bd6497246..dea31daa0c9 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -7,6 +7,7 @@ /** * Denotes a class constant whose value might not yet be known. + * @psalm-immutable */ final class TClassConstant extends Atomic { @@ -16,10 +17,11 @@ final class TClassConstant extends Atomic /** @var string */ public $const_name; - public function __construct(string $fq_classlike_name, string $const_name) + public function __construct(string $fq_classlike_name, string $const_name, bool $from_docblock = false) { $this->fq_classlike_name = $fq_classlike_name; $this->const_name = $const_name; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index 1124c9e6a78..b8eec740027 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -20,6 +20,7 @@ /** * Denotes the `class-string` type, used to describe a string representing a valid PHP class. * The parent type from which the classes descend may or may not be specified in the constructor. + * @psalm-immutable */ class TClassString extends TString { @@ -28,10 +29,7 @@ class TClassString extends TString */ public $as; - /** - * @var ?TNamedObject - */ - public $as_type; + public ?TNamedObject $as_type; /** @var bool */ public $is_loaded = false; @@ -42,12 +40,21 @@ class TClassString extends TString /** @var bool */ public $is_enum = false; - public function __construct(string $as = 'object', ?TNamedObject $as_type = null) - { + public function __construct( + string $as = 'object', + ?TNamedObject $as_type = null, + bool $is_loaded = false, + bool $is_interface = false, + bool $is_enum = false, + bool $from_docblock = false + ) { $this->as = $as; $this->as_type = $as_type; + $this->is_loaded = $is_loaded; + $this->is_interface = $is_interface; + $this->is_enum = $is_enum; + $this->from_docblock = $from_docblock; } - public function getKey(bool $include_extra = true): string { if ($this->is_interface) { @@ -128,11 +135,14 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->as_type ? [$this->as_type] : []; + return $this->as_type ? ['as_type'] : []; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -144,11 +154,9 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $class_string = clone $this; - - if (!$class_string->as_type) { - return $class_string; + ): self { + if (!$this->as_type) { + return $this; } if ($input_type instanceof TLiteralClassString) { @@ -160,7 +168,7 @@ public function replaceTemplateTypesWithStandins( } $as_type = TemplateStandinTypeReplacer::replace( - new Union([$class_string->as_type]), + new Union([$this->as_type]), $template_result, $codebase, $statements_analyzer, @@ -176,15 +184,19 @@ public function replaceTemplateTypesWithStandins( $as_type_types = array_values($as_type->getAtomicTypes()); - $class_string->as_type = count($as_type_types) === 1 + $as_type = count($as_type_types) === 1 && $as_type_types[0] instanceof TNamedObject ? $as_type_types[0] : null; - if (!$class_string->as_type) { - $class_string->as = 'object'; + if ($this->as_type === $as_type) { + return $this; } - - return $class_string; + $cloned = clone $this; + $cloned->as_type = $as_type; + if (!$cloned->as_type) { + $cloned->as = 'object'; + } + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index aadcad0b7ce..fe3e1c3e39c 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -16,6 +16,7 @@ /** * Represents an array where the type of each value * is a function of its string key value + * @psalm-immutable */ final class TClassStringMap extends Atomic { @@ -24,10 +25,7 @@ final class TClassStringMap extends Atomic */ public $param_name; - /** - * @var ?TNamedObject - */ - public $as_type; + public ?TNamedObject $as_type; /** * @var Union @@ -37,11 +35,16 @@ final class TClassStringMap extends Atomic /** * Constructs a new instance of a list */ - public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param) - { - $this->value_param = $value_param; + public function __construct( + string $param_name, + ?TNamedObject $as_type, + Union $value_param, + bool $from_docblock = false + ) { $this->param_name = $param_name; $this->as_type = $as_type; + $this->value_param = $value_param; + $this->from_docblock = $from_docblock; } public function getId(bool $exact = true, bool $nested = false): string @@ -56,11 +59,6 @@ public function getId(bool $exact = true, bool $nested = false): string . '>'; } - public function __clone() - { - $this->value_param = clone $this->value_param; - } - /** * @param array $aliased_classes * @@ -117,6 +115,10 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @psalm-suppress InaccessibleProperty We're only acting on cloned instances + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -128,10 +130,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $map = clone $this; + ): self { + $cloned = null; - foreach ([Type::getString(), $map->value_param] as $offset => $type_param) { + foreach ([Type::getString(), $this->value_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -170,28 +172,40 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $map->value_param = $value_param; + if ($offset === 1 && ($cloned || $this->value_param !== $value_param)) { + $cloned ??= clone $this; + $cloned->value_param = $value_param; } } - return $map; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $value_param = TemplateInferredTypeReplacer::replace( $this->value_param, $template_result, $codebase ); + if ($value_param === $this->value_param) { + return $this; + } + return new static( + $this->param_name, + $this->as_type, + $value_param + ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->value_param]; + return ['value_param']; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TClosedResource.php b/src/Psalm/Type/Atomic/TClosedResource.php index 5d4828fbf25..3d867d73818 100644 --- a/src/Psalm/Type/Atomic/TClosedResource.php +++ b/src/Psalm/Type/Atomic/TClosedResource.php @@ -6,6 +6,7 @@ /** * Denotes the `resource` type that has been closed (e.g. a file handle through `fclose()`). + * @psalm-immutable */ final class TClosedResource extends Atomic { diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index fda5f17726e..7b0345e9eb3 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -2,8 +2,18 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\FunctionLikeParameter; +use Psalm\Type\Atomic; +use Psalm\Type\Union; + +use function array_merge; + /** * Represents a closure where we know the return type and params + * @psalm-immutable */ final class TClosure extends TNamedObject { @@ -12,8 +22,110 @@ final class TClosure extends TNamedObject /** @var array */ public $byref_uses = []; + /** + * @param list $params + * @param array $byref_uses + * @param array $extra_types + */ + public function __construct( + string $value = 'callable', + ?array $params = null, + ?Union $return_type = null, + ?bool $is_pure = null, + array $byref_uses = [], + array $extra_types = [], + bool $from_docblock = false + ) { + $this->value = $value; + $this->params = $params; + $this->return_type = $return_type; + $this->is_pure = $is_pure; + $this->byref_uses = $byref_uses; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes( + TemplateResult $template_result, + ?Codebase $codebase + ): self { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + public function getChildNodeKeys(): array + { + return array_merge(parent::getChildNodeKeys(), $this->getCallableChildNodeKeys()); + } } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 070f7ac2d71..b1c2d6b9794 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -10,6 +10,7 @@ /** * Internal representation of a conditional return type in phpdoc. For example ($param1 is int ? int : string) + * @psalm-immutable */ final class TConditional extends Atomic { @@ -49,7 +50,8 @@ public function __construct( Union $as_type, Union $conditional_type, Union $if_type, - Union $else_type + Union $else_type, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; @@ -57,14 +59,33 @@ public function __construct( $this->conditional_type = $conditional_type; $this->if_type = $if_type; $this->else_type = $else_type; + $this->from_docblock = $from_docblock; } - public function __clone() - { - $this->conditional_type = clone $this->conditional_type; - $this->if_type = clone $this->if_type; - $this->else_type = clone $this->else_type; - $this->as_type = clone $this->as_type; + public function replaceTypes( + ?Union $as_type, + ?Union $conditional_type = null, + ?Union $if_type = null, + ?Union $else_type = null + ): self { + $as_type ??= $this->as_type; + $conditional_type ??= $this->conditional_type; + $if_type ??= $this->if_type; + $else_type ??= $this->else_type; + + if ($as_type === $this->as_type + && $conditional_type === $this->conditional_type + && $if_type === $this->if_type + && $else_type === $this->else_type + ) { + return $this; + } + $cloned = clone $this; + $cloned->as_type = $as_type; + $cloned->conditional_type = $conditional_type; + $cloned->if_type = $if_type; + $cloned->else_type = $else_type; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -114,9 +135,9 @@ public function toNamespacedString( return ''; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->conditional_type, $this->if_type, $this->else_type]; + return ['conditional_type', 'if_type', 'else_type']; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -124,14 +145,28 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $conditional = TemplateInferredTypeReplacer::replace( $this->conditional_type, $template_result, $codebase ); + if ($conditional === $this->conditional_type) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $this->as_type, + $conditional, + $this->if_type, + $this->else_type + ); } } diff --git a/src/Psalm/Type/Atomic/TDependentGetClass.php b/src/Psalm/Type/Atomic/TDependentGetClass.php index b1aacc2ceb0..bde7432c18b 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -2,11 +2,11 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; use Psalm\Type\Union; /** * Represents a string whose value is a fully-qualified class found by get_class($var) + * @psalm-immutable */ final class TDependentGetClass extends TString implements DependentType { @@ -51,7 +51,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TClassString { return new TClassString(); } diff --git a/src/Psalm/Type/Atomic/TDependentGetDebugType.php b/src/Psalm/Type/Atomic/TDependentGetDebugType.php index aa51ceb6f84..fd56a78cbc9 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -2,10 +2,9 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a string whose value is that of a type found by get_debug_type($var) + * @psalm-immutable */ final class TDependentGetDebugType extends TString implements DependentType { @@ -34,7 +33,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TString { return new TString(); } diff --git a/src/Psalm/Type/Atomic/TDependentGetType.php b/src/Psalm/Type/Atomic/TDependentGetType.php index 6d53ef62bbd..44181f9d573 100644 --- a/src/Psalm/Type/Atomic/TDependentGetType.php +++ b/src/Psalm/Type/Atomic/TDependentGetType.php @@ -4,6 +4,7 @@ /** * Represents a string whose value is that of a type found by gettype($var) + * @psalm-immutable */ final class TDependentGetType extends TString { diff --git a/src/Psalm/Type/Atomic/TDependentListKey.php b/src/Psalm/Type/Atomic/TDependentListKey.php index 22f2e1c95bc..c18109e6ae7 100644 --- a/src/Psalm/Type/Atomic/TDependentListKey.php +++ b/src/Psalm/Type/Atomic/TDependentListKey.php @@ -2,10 +2,9 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a list key created from foreach ($list as $key => $value) + * @psalm-immutable */ final class TDependentListKey extends TInt implements DependentType { @@ -39,7 +38,7 @@ public function getAssertionString(): string return 'int'; } - public function getReplacement(): Atomic + public function getReplacement(): TInt { return new TInt(); } diff --git a/src/Psalm/Type/Atomic/TEmptyMixed.php b/src/Psalm/Type/Atomic/TEmptyMixed.php index adbe375cfbd..fb4b5c297b6 100644 --- a/src/Psalm/Type/Atomic/TEmptyMixed.php +++ b/src/Psalm/Type/Atomic/TEmptyMixed.php @@ -5,6 +5,7 @@ /** * Denotes the `mixed` type, but empty. * Generated for `$x` inside the `if` statement `if (!$x) {...}` when `$x` is `mixed` outside. + * @psalm-immutable */ final class TEmptyMixed extends TMixed { diff --git a/src/Psalm/Type/Atomic/TEmptyNumeric.php b/src/Psalm/Type/Atomic/TEmptyNumeric.php index 7199f6821e2..1a3ce1f67c8 100644 --- a/src/Psalm/Type/Atomic/TEmptyNumeric.php +++ b/src/Psalm/Type/Atomic/TEmptyNumeric.php @@ -4,6 +4,7 @@ /** * Denotes the `numeric` type that's also empty (which can also result from an `is_numeric` and `empty` check). + * @psalm-immutable */ final class TEmptyNumeric extends TNumeric { diff --git a/src/Psalm/Type/Atomic/TEmptyScalar.php b/src/Psalm/Type/Atomic/TEmptyScalar.php index 036d28bef1d..cb2543810d9 100644 --- a/src/Psalm/Type/Atomic/TEmptyScalar.php +++ b/src/Psalm/Type/Atomic/TEmptyScalar.php @@ -4,6 +4,7 @@ /** * Denotes a `scalar` type that is also empty. + * @psalm-immutable */ final class TEmptyScalar extends TScalar { diff --git a/src/Psalm/Type/Atomic/TEnumCase.php b/src/Psalm/Type/Atomic/TEnumCase.php index 5522714948e..614a59c1d5d 100644 --- a/src/Psalm/Type/Atomic/TEnumCase.php +++ b/src/Psalm/Type/Atomic/TEnumCase.php @@ -4,6 +4,7 @@ /** * Denotes an enum with a specific value + * @psalm-immutable */ final class TEnumCase extends TNamedObject { diff --git a/src/Psalm/Type/Atomic/TFalse.php b/src/Psalm/Type/Atomic/TFalse.php index dbcb4ec8a6b..2ccd395d002 100644 --- a/src/Psalm/Type/Atomic/TFalse.php +++ b/src/Psalm/Type/Atomic/TFalse.php @@ -4,6 +4,7 @@ /** * Denotes the `false` value type + * @psalm-immutable */ final class TFalse extends TBool { diff --git a/src/Psalm/Type/Atomic/TFloat.php b/src/Psalm/Type/Atomic/TFloat.php index f30592dd8fd..d856df6e2cc 100644 --- a/src/Psalm/Type/Atomic/TFloat.php +++ b/src/Psalm/Type/Atomic/TFloat.php @@ -4,6 +4,7 @@ /** * Denotes the `float` type, where the exact value is unknown. + * @psalm-immutable */ class TFloat extends Scalar { diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 44f885e0757..7e78bd1324e 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -13,15 +16,19 @@ /** * Denotes an object type that has generic parameters e.g. `ArrayObject` + * @psalm-immutable */ final class TGenericObject extends TNamedObject { + /** + * @use GenericTrait> + */ use GenericTrait; /** * @var non-empty-list */ - public $type_params; + public array $type_params; /** @var bool if the parameters have been remapped to another class */ public $remapped_params = false; @@ -29,15 +36,26 @@ final class TGenericObject extends TNamedObject /** * @param string $value the name of the object * @param non-empty-list $type_params + * @param array $extra_types */ - public function __construct(string $value, array $type_params) - { + public function __construct( + string $value, + array $type_params, + bool $remapped_params = false, + bool $is_static = false, + array $extra_types = [], + bool $from_docblock = false + ) { if ($value[0] === '\\') { $value = substr($value, 1); } $this->value = $value; $this->type_params = $type_params; + $this->remapped_params = $remapped_params; + $this->is_static = $is_static; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -103,8 +121,84 @@ public function getAssertionString(): string return $this->value; } - public function getChildNodes(): array + public function getChildNodeKeys(): array + { + return array_merge(parent::getChildNodeKeys(), ['type_params']); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $this->value, + $types ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self { - return array_merge($this->type_params, $this->extra_types ?? []); + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $this->value, + $type_params ?? $this->type_params, + true, + $this->is_static, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TInt.php b/src/Psalm/Type/Atomic/TInt.php index 95f6506509a..fd7ad05cb7d 100644 --- a/src/Psalm/Type/Atomic/TInt.php +++ b/src/Psalm/Type/Atomic/TInt.php @@ -4,6 +4,7 @@ /** * Denotes the `int` type, where the exact value is unknown. + * @psalm-immutable */ class TInt extends Scalar { diff --git a/src/Psalm/Type/Atomic/TIntMask.php b/src/Psalm/Type/Atomic/TIntMask.php index 4d3dca8340a..a77babf1062 100644 --- a/src/Psalm/Type/Atomic/TIntMask.php +++ b/src/Psalm/Type/Atomic/TIntMask.php @@ -7,6 +7,7 @@ /** * Represents the type that is the result of a bitmask combination of its parameters. * `int-mask<1, 2, 4>` corresponds to `0|1|2|3|4|5|6|7` + * @psalm-immutable */ final class TIntMask extends TInt { @@ -14,9 +15,10 @@ final class TIntMask extends TInt public $values; /** @param non-empty-array $values */ - public function __construct(array $values) + public function __construct(array $values, bool $from_docblock = false) { $this->values = $values; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index 3e210e409e8..b1fd8c987f0 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -8,6 +8,7 @@ * Represents the type that is the result of a bitmask combination of its parameters. * This is the same concept as TIntMask but TIntMaskOf is used with a reference to constants in code * `int-mask-of` will corresponds to `0|1|2|3|4|5|6|7` if there are three constant 1, 2 and 4 + * @psalm-immutable */ final class TIntMaskOf extends TInt { @@ -17,9 +18,10 @@ final class TIntMaskOf extends TInt /** * @param TClassConstant|TKeyOf|TValueOf $value */ - public function __construct(Atomic $value) + public function __construct(Atomic $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -45,6 +47,11 @@ public function toNamespacedString( . '>'; } + public function getChildNodeKeys(): array + { + return ['value']; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; diff --git a/src/Psalm/Type/Atomic/TIntRange.php b/src/Psalm/Type/Atomic/TIntRange.php index eec4e671f65..57fe237f520 100644 --- a/src/Psalm/Type/Atomic/TIntRange.php +++ b/src/Psalm/Type/Atomic/TIntRange.php @@ -7,6 +7,7 @@ /** * Denotes an interval of integers between two bounds + * @psalm-immutable */ final class TIntRange extends TInt { @@ -22,10 +23,11 @@ final class TIntRange extends TInt */ public $max_bound; - public function __construct(?int $min_bound, ?int $max_bound) + public function __construct(?int $min_bound, ?int $max_bound, bool $from_docblock = false) { $this->min_bound = $min_bound; $this->max_bound = $max_bound; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index ed8462a5924..91f75bc9fb8 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -2,27 +2,33 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; use function count; use function implode; use function substr; /** * denotes the `iterable` type(which can also result from an `is_iterable` check). + * @psalm-immutable */ final class TIterable extends Atomic { use HasIntersectionTrait; + /** + * @use GenericTrait + */ use GenericTrait; /** * @var array{Union, Union} */ - public $type_params; + public array $type_params; /** * @var string @@ -35,16 +41,19 @@ final class TIterable extends Atomic public $has_docblock_params = false; /** - * @param list $type_params + * @param array{Union, Union}|array $type_params + * @param array $extra_types */ - public function __construct(array $type_params = []) + public function __construct(array $type_params = [], array $extra_types = [], bool $from_docblock = false) { - if (count($type_params) === 2) { + if (isset($type_params[0], $type_params[1])) { $this->has_docblock_params = true; $this->type_params = $type_params; } else { $this->type_params = [Type::getMixed(), Type::getMixed()]; } + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -113,8 +122,75 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } - public function getChildNodes(): array + public function getChildNodeKeys(): array + { + return ['type_params', 'extra_types']; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self { - return array_merge($this->type_params, $this->extra_types ?? []); + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $types ?? $this->type_params, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TKeyOf.php b/src/Psalm/Type/Atomic/TKeyOf.php index 027a0e04fb5..b8a4b33f854 100644 --- a/src/Psalm/Type/Atomic/TKeyOf.php +++ b/src/Psalm/Type/Atomic/TKeyOf.php @@ -10,15 +10,17 @@ /** * Represents an offset of an array. + * @psalm-immutable */ final class TKeyOf extends TArrayKey { /** @var Union */ public $type; - public function __construct(Union $type) + public function __construct(Union $type, bool $from_docblock = false) { $this->type = $type; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b92926ffb1b..99e14a64deb 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -31,6 +31,7 @@ /** * Represents an 'object-like array' - an array with known keys. + * @psalm-immutable */ class TKeyedArray extends Atomic { @@ -77,10 +78,37 @@ class TKeyedArray extends Atomic * @param non-empty-array $properties * @param array $class_strings */ - public function __construct(array $properties, ?array $class_strings = null) - { + public function __construct( + array $properties, + ?array $class_strings = null, + bool $sealed = false, + ?Union $previous_key_type = null, + ?Union $previous_value_type = null, + bool $is_list = false, + bool $from_docblock = false + ) { $this->properties = $properties; $this->class_strings = $class_strings; + $this->sealed = $sealed; + $this->previous_key_type = $previous_key_type; + $this->previous_value_type = $previous_value_type; + $this->is_list = $is_list; + $this->from_docblock = $from_docblock; + } + + /** + * @param non-empty-array $properties + * + * @return static + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; } public function getId(bool $exact = true, bool $nested = false): string @@ -213,6 +241,9 @@ public function getGenericValueType(): Union return $value_type; } + /** + * @return TArray|TNonEmptyArray + */ public function getGenericArrayType(bool $allow_non_empty = true): TArray { $key_types = []; @@ -263,19 +294,15 @@ public function isNonEmpty(): bool return false; } - public function __clone() - { - foreach ($this->properties as &$property) { - $property = clone $property; - } - } - public function getKey(bool $include_extra = true): string { /** @var string */ return static::KEY; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -287,10 +314,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = $this->properties; - foreach ($this->properties as $offset => $property) { + foreach ($properties as $offset => $property) { $input_type_param = null; if ($input_type instanceof TKeyedArray @@ -299,7 +326,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -315,25 +342,40 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + ): self { + $properties = $this->properties; + foreach ($properties as $offset => $property) { + $properties[$offset] = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + if ($properties !== $this->properties) { + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + return $this; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->properties; + return ['properties']; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 44d6578ee41..78beb798b34 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -18,6 +18,8 @@ * - its keys are integers * - they start at 0 * - they are consecutive and go upwards (no negative int) + * + * @psalm-immutable */ class TList extends Atomic { @@ -32,19 +34,28 @@ class TList extends Atomic /** * Constructs a new instance of a list */ - public function __construct(Union $type_param) + public function __construct(Union $type_param, bool $from_docblock = false) { $this->type_param = $type_param; + $this->from_docblock = $from_docblock; } - public function getId(bool $exact = true, bool $nested = false): string + /** + * @return static + */ + public function replaceTypeParam(Union $type_param): self { - return static::KEY . '<' . $this->type_param->getId($exact) . '>'; + if ($type_param === $this->type_param) { + return $this; + } + $cloned = clone $this; + $cloned->type_param = $type_param; + return $cloned; } - public function __clone() + public function getId(bool $exact = true, bool $nested = false): string { - $this->type_param = clone $this->type_param; + return static::KEY . '<' . $this->type_param->getId($exact) . '>'; } /** @@ -100,6 +111,10 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @psalm-suppress InaccessibleProperty We're only acting on cloned instances + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -111,10 +126,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $list = clone $this; + ): self { + $cloned = null; - foreach ([Type::getInt(), $list->type_param] as $offset => $type_param) { + foreach ([Type::getInt(), $this->type_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -153,23 +168,27 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $list->type_param = $type_param; + if ($offset === 1 && ($cloned || $this->type_param !== $type_param)) { + $cloned ??= clone $this; + $cloned->type_param = $type_param; } } - return $list; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + return $this->replaceTypeParam(TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase - ); + )); } public function equals(Atomic $other_type, bool $ensure_source_equality): bool @@ -194,8 +213,8 @@ public function getAssertionString(): string return $this->getId(); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->type_param]; + return ['type_param']; } } diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index 6339a5757b3..cc17a77d61e 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -10,6 +10,7 @@ /** * Denotes a specific class string, generated by expressions like `A::class`. + * @psalm-immutable */ final class TLiteralClassString extends TLiteralString { @@ -19,9 +20,9 @@ final class TLiteralClassString extends TLiteralString */ public $definite_class = false; - public function __construct(string $value, bool $definite_class = false) + public function __construct(string $value, bool $definite_class = false, bool $from_docblock = false) { - parent::__construct($value); + parent::__construct($value, $from_docblock); $this->definite_class = $definite_class; } diff --git a/src/Psalm/Type/Atomic/TLiteralFloat.php b/src/Psalm/Type/Atomic/TLiteralFloat.php index 4e11468304f..991625ec328 100644 --- a/src/Psalm/Type/Atomic/TLiteralFloat.php +++ b/src/Psalm/Type/Atomic/TLiteralFloat.php @@ -4,15 +4,17 @@ /** * Denotes a floating point value where the exact numeric value is known. + * @psalm-immutable */ final class TLiteralFloat extends TFloat { /** @var float */ public $value; - public function __construct(float $value) + public function __construct(float $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLiteralInt.php b/src/Psalm/Type/Atomic/TLiteralInt.php index 390606ebb1b..6a506061d3f 100644 --- a/src/Psalm/Type/Atomic/TLiteralInt.php +++ b/src/Psalm/Type/Atomic/TLiteralInt.php @@ -4,15 +4,17 @@ /** * Denotes an integer value where the exact numeric value is known. + * @psalm-immutable */ final class TLiteralInt extends TInt { /** @var int */ public $value; - public function __construct(int $value) + public function __construct(int $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLiteralString.php b/src/Psalm/Type/Atomic/TLiteralString.php index 71ae7f73551..9f000b90aa9 100644 --- a/src/Psalm/Type/Atomic/TLiteralString.php +++ b/src/Psalm/Type/Atomic/TLiteralString.php @@ -8,15 +8,17 @@ /** * Denotes a string whose value is known. + * @psalm-immutable */ class TLiteralString extends TString { /** @var string */ public $value; - public function __construct(string $value) + public function __construct(string $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLowercaseString.php b/src/Psalm/Type/Atomic/TLowercaseString.php index a9eecb9f362..b65ac5e9dcb 100644 --- a/src/Psalm/Type/Atomic/TLowercaseString.php +++ b/src/Psalm/Type/Atomic/TLowercaseString.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class TLowercaseString extends TString { public function getId(bool $exact = true, bool $nested = false): string diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index 0c029645a47..d122a432b16 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -6,15 +6,18 @@ /** * Denotes the `mixed` type, used when you don’t know the type of an expression. + * + * @psalm-immutable */ class TMixed extends Atomic { /** @var bool */ public $from_loop_isset = false; - public function __construct(bool $from_loop_isset = false) + public function __construct(bool $from_loop_isset = false, bool $from_docblock = false) { $this->from_loop_isset = $from_loop_isset; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 88643acd73f..0323952391f 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -3,6 +3,7 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; @@ -14,6 +15,7 @@ /** * Denotes an object type where the type of the object is known e.g. `Exception`, `Throwable`, `Foo\Bar` + * @psalm-immutable */ class TNamedObject extends Atomic { @@ -37,9 +39,15 @@ class TNamedObject extends Atomic /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, bool $definite_class = false) - { + public function __construct( + string $value, + bool $is_static = false, + bool $definite_class = false, + array $extra_types = [], + bool $from_docblock = false + ) { if ($value[0] === '\\') { $value = substr($value, 1); } @@ -47,6 +55,18 @@ public function __construct(string $value, bool $is_static = false, bool $defini $this->value = $value; $this->is_static = $is_static; $this->definite_class = $definite_class; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + public function setIsStatic(bool $is_static): self + { + if ($this->is_static === $is_static) { + return $this; + } + $cloned = clone $this; + $cloned->is_static = $is_static; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -134,15 +154,58 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return ($this->value !== 'static' && $this->is_static === false) || $analysis_php_version_id >= 8_00_00; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } - public function getChildNodes(): array + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($intersection) { + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; + } + return $this; + } + public function getChildNodeKeys(): array { - return $this->extra_types ?? []; + return ['extra_types']; } } diff --git a/src/Psalm/Type/Atomic/TNever.php b/src/Psalm/Type/Atomic/TNever.php index b10bfc10cf6..9c468445de7 100644 --- a/src/Psalm/Type/Atomic/TNever.php +++ b/src/Psalm/Type/Atomic/TNever.php @@ -7,6 +7,7 @@ /** * Denotes the `no-return`/`never-return` type for functions that never return, either throwing an exception or * terminating (like the builtin `exit()`). + * @psalm-immutable */ final class TNever extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index d81b5dfe9c8..2c2fba076c8 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -2,9 +2,12 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Denotes array known to be non-empty of the form `non-empty-array`. * It expects an array with two elements, both union types. + * @psalm-immutable */ class TNonEmptyArray extends TArray { @@ -22,4 +25,38 @@ class TNonEmptyArray extends TArray * @var string */ public $value = 'non-empty-array'; + + /** + * @param array{Union, Union} $type_params + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct( + array $type_params, + ?int $count = null, + ?int $min_count = null, + string $value = 'non-empty-array', + bool $from_docblock = false + ) { + $this->type_params = $type_params; + $this->count = $count; + $this->min_count = $min_count; + $this->value = $value; + $this->from_docblock = $from_docblock; + } + + /** + * @param positive-int|null $count + * + * @return static + */ + public function setCount(?int $count): self + { + if ($count === $this->count) { + return $this; + } + $cloned = clone $this; + $cloned->count = $count; + return $cloned; + } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index 9e6892ba855..c56bd0b5672 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -2,8 +2,11 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Represents a non-empty list + * @psalm-immutable */ class TNonEmptyList extends TList { @@ -20,6 +23,39 @@ class TNonEmptyList extends TList /** @var non-empty-lowercase-string */ public const KEY = 'non-empty-list'; + /** + * Constructs a new instance of a list + * + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct( + Union $type_param, + ?int $count = null, + ?int $min_count = null, + bool $from_docblock = false + ) { + $this->type_param = $type_param; + $this->count = $count; + $this->min_count = $min_count; + $this->from_docblock = $from_docblock; + } + + /** + * @param positive-int|null $count + * + * @return static + */ + public function setCount(?int $count): self + { + if ($count === $this->count) { + return $this; + } + $cloned = clone $this; + $cloned->count = $count; + return $cloned; + } + public function getAssertionString(): string { return 'non-empty-list'; diff --git a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php index 52812e75783..ab39bc3d497 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php @@ -4,6 +4,7 @@ /** * Denotes a non-empty-string where every character is lowercased. (which can also result from a `strtolower` call). + * @psalm-immutable */ final class TNonEmptyLowercaseString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TNonEmptyMixed.php b/src/Psalm/Type/Atomic/TNonEmptyMixed.php index 1b96eb47f88..b7c829e079c 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyMixed.php +++ b/src/Psalm/Type/Atomic/TNonEmptyMixed.php @@ -5,6 +5,7 @@ /** * Denotes the `mixed` type, but not empty. * Generated for `$x` inside the `if` statement `if ($x) {...}` when `$x` is `mixed` outside. + * @psalm-immutable */ final class TNonEmptyMixed extends TMixed { diff --git a/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php b/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php index 4802a72a261..6cfd2526f34 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-string` type, where the exact value is unknown but * we know that the string is not from user input + * @psalm-immutable */ final class TNonEmptyNonspecificLiteralString extends TNonspecificLiteralString { diff --git a/src/Psalm/Type/Atomic/TNonEmptyScalar.php b/src/Psalm/Type/Atomic/TNonEmptyScalar.php index ec3d56d6fb7..7de002334cd 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyScalar.php +++ b/src/Psalm/Type/Atomic/TNonEmptyScalar.php @@ -4,6 +4,7 @@ /** * Denotes a `scalar` type that is also non-empty. + * @psalm-immutable */ final class TNonEmptyScalar extends TScalar { diff --git a/src/Psalm/Type/Atomic/TNonEmptyString.php b/src/Psalm/Type/Atomic/TNonEmptyString.php index 2a1dbdcf8c4..5aeedb303ba 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyString.php @@ -4,6 +4,7 @@ /** * Denotes a string, that is also non-empty (every string except '') + * @psalm-immutable */ class TNonEmptyString extends TString { diff --git a/src/Psalm/Type/Atomic/TNonFalsyString.php b/src/Psalm/Type/Atomic/TNonFalsyString.php index 467951c1848..e352d1d9f71 100644 --- a/src/Psalm/Type/Atomic/TNonFalsyString.php +++ b/src/Psalm/Type/Atomic/TNonFalsyString.php @@ -4,6 +4,7 @@ /** * Denotes a string, that is also non-falsy (every string except '' and '0') + * @psalm-immutable */ class TNonFalsyString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php index 08c2f4d7af8..fbc5e71f24f 100644 --- a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-int` type, where the exact value is unknown but * we know that the int is not from user input + * @psalm-immutable */ final class TNonspecificLiteralInt extends TInt { diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralString.php b/src/Psalm/Type/Atomic/TNonspecificLiteralString.php index b946f0cc944..b6c73cc20ee 100644 --- a/src/Psalm/Type/Atomic/TNonspecificLiteralString.php +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralString.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-string` type, where the exact value is unknown but * we know that the string is not from user input + * @psalm-immutable */ class TNonspecificLiteralString extends TString { diff --git a/src/Psalm/Type/Atomic/TNull.php b/src/Psalm/Type/Atomic/TNull.php index 959a53d0e4a..685509d9678 100644 --- a/src/Psalm/Type/Atomic/TNull.php +++ b/src/Psalm/Type/Atomic/TNull.php @@ -6,6 +6,7 @@ /** * Denotes the `null` type + * @psalm-immutable */ final class TNull extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNumeric.php b/src/Psalm/Type/Atomic/TNumeric.php index 2a0c70acf7f..942a1df0e2c 100644 --- a/src/Psalm/Type/Atomic/TNumeric.php +++ b/src/Psalm/Type/Atomic/TNumeric.php @@ -4,6 +4,7 @@ /** * Denotes the `numeric` type (which can also result from an `is_numeric` check). + * @psalm-immutable */ class TNumeric extends Scalar { diff --git a/src/Psalm/Type/Atomic/TNumericString.php b/src/Psalm/Type/Atomic/TNumericString.php index 8f2109ec0d0..ed1040a3cb4 100644 --- a/src/Psalm/Type/Atomic/TNumericString.php +++ b/src/Psalm/Type/Atomic/TNumericString.php @@ -4,6 +4,7 @@ /** * Denotes a string that's also a numeric value e.g. `"5"`. It can result from `is_string($s) && is_numeric($s)`. + * @psalm-immutable */ final class TNumericString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TObject.php b/src/Psalm/Type/Atomic/TObject.php index 1725c136869..bffa66a5413 100644 --- a/src/Psalm/Type/Atomic/TObject.php +++ b/src/Psalm/Type/Atomic/TObject.php @@ -6,6 +6,7 @@ /** * Denotes the `object` type + * @psalm-immutable */ class TObject extends Atomic { diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 81d3fc76f66..96f1e2d805e 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -12,13 +12,12 @@ use function array_keys; use function array_map; -use function array_merge; -use function array_values; use function count; use function implode; /** * Denotes an object with specified member variables e.g. `object{foo:int, bar:string}`. + * @psalm-immutable */ final class TObjectWithProperties extends TObject { @@ -39,11 +38,44 @@ final class TObjectWithProperties extends TObject * * @param array $properties * @param array $methods + * @param array $extra_types */ - public function __construct(array $properties, array $methods = []) - { + public function __construct( + array $properties, + array $methods = [], + array $extra_types = [], + bool $from_docblock = false + ) { $this->properties = $properties; $this->methods = $methods; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + /** + * @param array $properties + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + + /** + * @param array $methods + */ + public function setMethods(array $methods): self + { + if ($methods === $this->methods) { + return $this; + } + $cloned = clone $this; + $cloned->methods = $methods; + return $cloned; } public function getId(bool $exact = true, bool $nested = false): string @@ -58,6 +90,7 @@ public function getId(bool $exact = true, bool $nested = false): string ', ', array_map( /** + * @psalm-pure * @param string|int $name */ static fn($name, Union $type): string => $name . ($type->possibly_undefined ? '?' : '') . ':' @@ -70,6 +103,9 @@ public function getId(bool $exact = true, bool $nested = false): string $methods_string = implode( ', ', array_map( + /** + * @psalm-pure + */ static fn(string $name): string => $name . '()', array_keys($this->methods) ) @@ -100,6 +136,7 @@ public function toNamespacedString( ', ', array_map( /** + * @psalm-pure * @param string|int $name */ static fn($name, Union $type): string => @@ -136,13 +173,6 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function __clone() - { - foreach ($this->properties as &$property) { - $property = clone $property; - } - } - public function equals(Atomic $other_type, bool $ensure_source_equality): bool { if (!$other_type instanceof self) { @@ -170,6 +200,9 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -181,8 +214,8 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = []; foreach ($this->properties as $offset => $property) { $input_type_param = null; @@ -193,7 +226,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -209,25 +242,56 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static($properties, $this->methods, $intersection ?? $this->extra_types); } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + ): self { + $properties = $this->properties; + foreach ($properties as $offset => $property) { + $properties[$offset] = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static( + $properties, + $this->methods, + $intersection ?? $this->extra_types + ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_merge($this->properties, $this->extra_types !== null ? array_values($this->extra_types) : []); + return ['properties', 'extra_types']; } public function getAssertionString(): string diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index b3e009c6c92..ffee4f946c5 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -9,8 +9,10 @@ * their apropriate types as values. * * @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of' + * + * @psalm-immutable */ -class TPropertiesOf extends Atomic +final class TPropertiesOf extends Atomic { // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are @@ -19,14 +21,7 @@ class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - /** - * @var string - */ - public $fq_classlike_name; - /** - * @var TNamedObject - */ - public $classlike_type; + public TNamedObject $classlike_type; /** * @var self::VISIBILITY_*|null */ @@ -45,6 +40,19 @@ public static function tokenNames(): array ]; } + /** + * @param self::VISIBILITY_*|null $visibility_filter + */ + public function __construct( + TNamedObject $classlike_type, + ?int $visibility_filter, + bool $from_docblock = false + ) { + $this->classlike_type = $classlike_type; + $this->visibility_filter = $visibility_filter; + $this->from_docblock = $from_docblock; + } + /** * @param TokenName $tokenName * @return self::VISIBILITY_*|null @@ -64,6 +72,7 @@ public static function filterForTokenName(string $token_name): ?int } /** + * @psalm-pure * @return TokenName */ public static function tokenNameForFilter(?int $visibility_filter): string @@ -80,17 +89,9 @@ public static function tokenNameForFilter(?int $visibility_filter): string } } - /** - * @param self::VISIBILITY_*|null $visibility_filter - */ - public function __construct( - string $fq_classlike_name, - TNamedObject $classlike_type, - ?int $visibility_filter - ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->classlike_type = $classlike_type; - $this->visibility_filter = $visibility_filter; + public function getChildNodeKeys(): array + { + return ['classlike_type']; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TResource.php b/src/Psalm/Type/Atomic/TResource.php index e7ca229412c..bb7c8f3a95d 100644 --- a/src/Psalm/Type/Atomic/TResource.php +++ b/src/Psalm/Type/Atomic/TResource.php @@ -6,6 +6,7 @@ /** * Denotes the `resource` type (e.g. a file handle). + * @psalm-immutable */ final class TResource extends Atomic { diff --git a/src/Psalm/Type/Atomic/TScalar.php b/src/Psalm/Type/Atomic/TScalar.php index 5fc10c5b0a8..de32876a374 100644 --- a/src/Psalm/Type/Atomic/TScalar.php +++ b/src/Psalm/Type/Atomic/TScalar.php @@ -5,6 +5,7 @@ /** * Denotes the `scalar` super type (which can also result from an `is_scalar` check). * This type encompasses `float`, `int`, `bool` and `string`. + * @psalm-immutable */ class TScalar extends Scalar { diff --git a/src/Psalm/Type/Atomic/TSingleLetter.php b/src/Psalm/Type/Atomic/TSingleLetter.php index 80613302479..23e8a354b64 100644 --- a/src/Psalm/Type/Atomic/TSingleLetter.php +++ b/src/Psalm/Type/Atomic/TSingleLetter.php @@ -4,6 +4,7 @@ /** * Denotes a string that has a length of 1 + * @psalm-immutable */ final class TSingleLetter extends TString { diff --git a/src/Psalm/Type/Atomic/TString.php b/src/Psalm/Type/Atomic/TString.php index ee08f110f43..2cfd5f51474 100644 --- a/src/Psalm/Type/Atomic/TString.php +++ b/src/Psalm/Type/Atomic/TString.php @@ -4,6 +4,7 @@ /** * Denotes the `string` type, where the exact value is unknown. + * @psalm-immutable */ class TString extends Scalar { diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index ac3edfd4717..0ef28969bab 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class TTemplateIndexedAccess extends Atomic { /** @@ -24,11 +27,13 @@ final class TTemplateIndexedAccess extends Atomic public function __construct( string $array_param_name, string $offset_param_name, - string $defining_class + string $defining_class, + bool $from_docblock = false ) { $this->array_param_name = $array_param_name; $this->offset_param_name = $offset_param_name; $this->defining_class = $defining_class; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index bb2f5f947e0..9f667a225ca 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TKeyOf when the type of the array is a template + * @psalm-immutable */ final class TTemplateKeyOf extends Atomic { @@ -31,11 +32,13 @@ final class TTemplateKeyOf extends Atomic public function __construct( string $param_name, string $defining_class, - Union $as + Union $as, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -81,14 +84,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index b34d9e4d49f..775f52993e2 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -12,6 +12,7 @@ /** * denotes a template parameter that has been previously specified in a `@template` tag. + * @psalm-immutable */ final class TTemplateParam extends Atomic { @@ -32,11 +33,34 @@ final class TTemplateParam extends Atomic */ public $defining_class; - public function __construct(string $param_name, Union $extends, string $defining_class) - { + /** + * @param array $extra_types + */ + public function __construct( + string $param_name, + Union $extends, + string $defining_class, + array $extra_types = [], + bool $from_docblock = false + ) { $this->param_name = $param_name; $this->as = $extends; $this->defining_class = $defining_class; + $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + /** + * @return static + */ + public function replaceAs(Union $as): self + { + if ($as === $this->as) { + return $this; + } + $cloned = clone $this; + $cloned->as = $as; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -113,9 +137,9 @@ public function toNamespacedString( return $this->param_name . $intersection_types; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->as]; + return ['as', 'extra_types']; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -123,10 +147,19 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TTemplateParamClass.php b/src/Psalm/Type/Atomic/TTemplateParamClass.php index a64aadef247..62544ab6217 100644 --- a/src/Psalm/Type/Atomic/TTemplateParamClass.php +++ b/src/Psalm/Type/Atomic/TTemplateParamClass.php @@ -4,6 +4,7 @@ /** * Denotes a `class-string` corresponding to a template parameter previously specified in a `@template` tag. + * @psalm-immutable */ final class TTemplateParamClass extends TClassString { @@ -21,12 +22,14 @@ public function __construct( string $param_name, string $as, ?TNamedObject $as_type, - string $defining_class + string $defining_class, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->as = $as; $this->as_type = $as_type; $this->defining_class = $defining_class; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5a1ac50e5b8..59fbb539153 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TPropertiesOf when the type of the array is a template + * @psalm-immutable */ final class TTemplatePropertiesOf extends Atomic { @@ -37,12 +38,14 @@ public function __construct( string $param_name, string $defining_class, TTemplateParam $as, - ?int $visibility_filter + ?int $visibility_filter, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; $this->visibility_filter = $visibility_filter; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -76,14 +79,30 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( - new Union([$this->as]), - $template_result, - $codebase + ): self { + $param = new TTemplateParam( + $this->as->param_name, + TemplateInferredTypeReplacer::replace( + new Union([$this->as]), + $template_result, + $codebase, + ), + $this->as->defining_class + ); + if ($param->as === $this->as->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $param, + $this->visibility_filter ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index d00cb597484..bcae2992e87 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TValueOf when the type of the array or enum is a template + * @psalm-immutable */ final class TTemplateValueOf extends Atomic { @@ -31,11 +32,13 @@ final class TTemplateValueOf extends Atomic public function __construct( string $param_name, string $defining_class, - Union $as + Union $as, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -81,14 +84,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTraitString.php b/src/Psalm/Type/Atomic/TTraitString.php index bb4cf93b684..eb0f4b44593 100644 --- a/src/Psalm/Type/Atomic/TTraitString.php +++ b/src/Psalm/Type/Atomic/TTraitString.php @@ -4,6 +4,7 @@ /** * Denotes the `trait-string` type, used to describe a string representing a valid PHP trait. + * @psalm-immutable */ final class TTraitString extends TString { diff --git a/src/Psalm/Type/Atomic/TTrue.php b/src/Psalm/Type/Atomic/TTrue.php index 64545045e17..fa0293dd01d 100644 --- a/src/Psalm/Type/Atomic/TTrue.php +++ b/src/Psalm/Type/Atomic/TTrue.php @@ -4,6 +4,7 @@ /** * Denotes the `true` value type + * @psalm-immutable */ final class TTrue extends TBool { diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 8e70ff71951..8174ba7ac23 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -7,6 +7,9 @@ use function array_map; use function implode; +/** + * @psalm-immutable + */ final class TTypeAlias extends Atomic { /** @@ -20,10 +23,28 @@ final class TTypeAlias extends Atomic /** @var string */ public $alias_name; - public function __construct(string $declaring_fq_classlike_name, string $alias_name) + /** + * @param array|null $extra_types + */ + public function __construct(string $declaring_fq_classlike_name, string $alias_name, ?array $extra_types = null) { $this->declaring_fq_classlike_name = $declaring_fq_classlike_name; $this->alias_name = $alias_name; + $this->extra_types = $extra_types; + } + /** + * @param array|null $extra_types + */ + public function setIntersectionTypes(?array $extra_types): self + { + if ($extra_types === $this->extra_types) { + return $this; + } + return new self( + $this->declaring_fq_classlike_name, + $this->alias_name, + $extra_types + ); } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 7cabc8ffad2..486021b8257 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -15,15 +15,17 @@ /** * Represents a value of an array or enum. + * @psalm-immutable */ final class TValueOf extends Atomic { /** @var Union */ public $type; - public function __construct(Union $type) + public function __construct(Union $type, bool $from_docblock = false) { $this->type = $type; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TVoid.php b/src/Psalm/Type/Atomic/TVoid.php index f885fe03370..dfd0657c8d7 100644 --- a/src/Psalm/Type/Atomic/TVoid.php +++ b/src/Psalm/Type/Atomic/TVoid.php @@ -6,6 +6,7 @@ /** * Denotes the `void` type, normally just used to annotate a function/method that returns nothing + * @psalm-immutable */ final class TVoid extends Atomic { diff --git a/src/Psalm/Type/NodeVisitor.php b/src/Psalm/Type/ImmutableTypeVisitor.php similarity index 50% rename from src/Psalm/Type/NodeVisitor.php rename to src/Psalm/Type/ImmutableTypeVisitor.php index d33325a5db0..7578277f2c7 100644 --- a/src/Psalm/Type/NodeVisitor.php +++ b/src/Psalm/Type/ImmutableTypeVisitor.php @@ -2,7 +2,9 @@ namespace Psalm\Type; -abstract class NodeVisitor +use function is_array; + +abstract class ImmutableTypeVisitor { public const STOP_TRAVERSAL = 1; public const DONT_TRAVERSE_CHILDREN = 2; @@ -27,8 +29,22 @@ public function traverse(TypeNode $node): bool return false; } - foreach ($node->getChildNodes() as $child_node) { - if ($this->traverse($child_node) === false) { + foreach ($node->getChildNodeKeys() as $key) { + if ($node instanceof Union || $node instanceof MutableUnion) { + $child_node = $node->getAtomicTypes(); + } else { + /** @var TypeNode|non-empty-array|null */ + $child_node = $node->{$key}; + } + if ($child_node === null) { + continue; + } + if (is_array($child_node)) { + $visitor_result = $this->traverseArray($child_node); + } else { + $visitor_result = $this->traverse($child_node); + } + if ($visitor_result === false) { return false; } } @@ -37,14 +53,15 @@ public function traverse(TypeNode $node): bool } /** - * @param array $nodes + * @param non-empty-array $nodes */ - public function traverseArray(array $nodes): void + public function traverseArray(array $nodes): bool { foreach ($nodes as $node) { if ($this->traverse($node) === false) { - return; + return false; } } + return true; } } diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php new file mode 100644 index 00000000000..a187a3be98e --- /dev/null +++ b/src/Psalm/Type/MutableUnion.php @@ -0,0 +1,500 @@ + + */ + private $types; + + /** + * Whether the type originated in a docblock + * + * @var bool + */ + public $from_docblock = false; + + /** + * Whether the type originated from integer calculation + * + * @var bool + */ + public $from_calculation = false; + + /** + * Whether the type originated from a property + * + * This helps turn isset($foo->bar) into a different sort of issue + * + * @var bool + */ + public $from_property = false; + + /** + * Whether the type originated from *static* property + * + * Unlike non-static properties, static properties have no prescribed place + * like __construct() to be initialized in + * + * @var bool + */ + public $from_static_property = false; + + /** + * Whether the property that this type has been derived from has been initialized in a constructor + * + * @var bool + */ + public $initialized = true; + + /** + * Which class the type was initialised in + * + * @var ?string + */ + public $initialized_class; + + /** + * Whether or not the type has been checked yet + * + * @var bool + */ + public $checked = false; + + /** + * @var bool + */ + public $failed_reconciliation = false; + + /** + * Whether or not to ignore issues with possibly-null values + * + * @var bool + */ + public $ignore_nullable_issues = false; + + /** + * Whether or not to ignore issues with possibly-false values + * + * @var bool + */ + public $ignore_falsable_issues = false; + + /** + * Whether or not to ignore issues with isset on this type + * + * @var bool + */ + public $ignore_isset = false; + + /** + * Whether or not this variable is possibly undefined + * + * @var bool + */ + public $possibly_undefined = false; + + /** + * Whether or not this variable is possibly undefined + * + * @var bool + */ + public $possibly_undefined_from_try = false; + + /** + * Whether or not this union had a template, since replaced + * + * @var bool + */ + public $had_template = false; + + /** + * Whether or not this union comes from a template "as" default + * + * @var bool + */ + public $from_template_default = false; + + /** + * @var array + */ + private $literal_string_types = []; + + /** + * @var array + */ + private $typed_class_strings = []; + + /** + * @var array + */ + private $literal_int_types = []; + + /** + * @var array + */ + private $literal_float_types = []; + + /** + * True if the type was passed or returned by reference, or if the type refers to an object's + * property or an item in an array. Note that this is not true for locally created references + * that don't refer to properties or array items (see Context::$references_in_scope). + * + * @var bool + */ + public $by_ref = false; + + /** + * @var bool + */ + public $reference_free = false; + + /** + * @var bool + */ + public $allow_mutations = true; + + /** + * @var bool + */ + public $has_mutations = true; + + /** + * This is a cache of getId on non-exact mode + * @var null|string + */ + private $id; + + /** + * This is a cache of getId on exact mode + * @var null|string + */ + private $exact_id; + + + /** + * @var array + */ + public $parent_nodes = []; + + /** + * @var bool + */ + public $different = false; + + /** + * @psalm-external-mutation-free + * @param non-empty-array $types + */ + public function setTypes(array $types): self + { + $this->literal_float_types = []; + $this->literal_int_types = []; + $this->literal_string_types = []; + $this->typed_class_strings = []; + + $from_docblock = false; + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + $this->from_docblock = $from_docblock; + + return $this; + } + + /** + * @psalm-external-mutation-free + */ + public function addType(Atomic $type): self + { + $this->types[$type->getKey()] = $type; + + if ($type instanceof TLiteralString) { + $this->literal_string_types[$type->getKey()] = $type; + } elseif ($type instanceof TLiteralInt) { + $this->literal_int_types[$type->getKey()] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$type->getKey()] = $type; + } elseif ($type instanceof TString && $this->literal_string_types) { + foreach ($this->literal_string_types as $key => $_) { + unset($this->literal_string_types[$key], $this->types[$key]); + } + if (!$type instanceof TClassString + || (!$type->as_type && !$type instanceof TTemplateParamClass) + ) { + foreach ($this->typed_class_strings as $key => $_) { + unset($this->typed_class_strings[$key], $this->types[$key]); + } + } + } elseif ($type instanceof TInt && $this->literal_int_types) { + //we remove any literal that is already included in a wider type + $int_type_in_range = TIntRange::convertToIntRange($type); + foreach ($this->literal_int_types as $key => $literal_int_type) { + if ($int_type_in_range->contains($literal_int_type->value)) { + unset($this->literal_int_types[$key], $this->types[$key]); + } + } + } elseif ($type instanceof TFloat && $this->literal_float_types) { + foreach ($this->literal_float_types as $key => $_) { + unset($this->literal_float_types[$key], $this->types[$key]); + } + } + + $this->bustCache(); + + return $this; + } + + /** + * @psalm-external-mutation-free + */ + public function removeType(string $type_string): bool + { + if (isset($this->types[$type_string])) { + unset($this->types[$type_string]); + + if (strpos($type_string, '(')) { + unset( + $this->literal_string_types[$type_string], + $this->literal_int_types[$type_string], + $this->literal_float_types[$type_string] + ); + } + + $this->bustCache(); + + return true; + } + + if ($type_string === 'string') { + if ($this->literal_string_types) { + foreach ($this->literal_string_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_string_types = []; + } + + if ($this->typed_class_strings) { + foreach ($this->typed_class_strings as $typed_class_key => $_) { + unset($this->types[$typed_class_key]); + } + $this->typed_class_strings = []; + } + + unset($this->types['class-string'], $this->types['trait-string']); + } elseif ($type_string === 'int' && $this->literal_int_types) { + foreach ($this->literal_int_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_int_types = []; + } elseif ($type_string === 'float' && $this->literal_float_types) { + foreach ($this->literal_float_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_float_types = []; + } + + return false; + } + + public function setFromDocblock(bool $fromDocblock = true): self + { + $this->from_docblock = $fromDocblock; + + (new FromDocblockSetter($fromDocblock))->traverseArray($this->types); + + return $this; + } + + /** + * @psalm-external-mutation-free + */ + public function bustCache(): void + { + $this->id = null; + $this->exact_id = null; + } + + /** + * @psalm-external-mutation-free + * @param Union|MutableUnion $old_type + * @param Union|MutableUnion|null $new_type + */ + public function substitute($old_type, $new_type = null): self + { + if ($this->hasMixed() && !$this->isEmptyMixed()) { + return $this; + } + $old_type = $old_type->getBuilder(); + if ($new_type) { + $new_type = $new_type->getBuilder(); + } + + if ($new_type && $new_type->ignore_nullable_issues) { + $this->ignore_nullable_issues = true; + } + + if ($new_type && $new_type->ignore_falsable_issues) { + $this->ignore_falsable_issues = true; + } + + foreach ($old_type->types as $old_type_part) { + $had = isset($this->types[$old_type_part->getKey()]); + $this->removeType($old_type_part->getKey()); + if (!$had) { + if ($old_type_part instanceof TFalse + && isset($this->types['bool']) + && !isset($this->types['true']) + ) { + $this->removeType('bool'); + $this->types['true'] = new TTrue; + } elseif ($old_type_part instanceof TTrue + && isset($this->types['bool']) + && !isset($this->types['false']) + ) { + $this->removeType('bool'); + $this->types['false'] = new TFalse; + } elseif (isset($this->types['iterable'])) { + if ($old_type_part instanceof TNamedObject + && $old_type_part->value === 'Traversable' + && !isset($this->types['array']) + ) { + $this->removeType('iterable'); + $this->types['array'] = new TArray([Type::getArrayKey(), Type::getMixed()]); + } + + if ($old_type_part instanceof TArray + && !isset($this->types['traversable']) + ) { + $this->removeType('iterable'); + $this->types['traversable'] = new TNamedObject('Traversable'); + } + } elseif (isset($this->types['array-key'])) { + if ($old_type_part instanceof TString + && !isset($this->types['int']) + ) { + $this->removeType('array-key'); + $this->types['int'] = new TInt(); + } + + if ($old_type_part instanceof TInt + && !isset($this->types['string']) + ) { + $this->removeType('array-key'); + $this->types['string'] = new TString(); + } + } + } + } + + if ($new_type) { + foreach ($new_type->types as $key => $new_type_part) { + if (!isset($this->types[$key]) + || ($new_type_part instanceof Scalar + && get_class($new_type_part) === get_class($this->types[$key])) + ) { + $this->types[$key] = $new_type_part; + } else { + $this->types[$key] = TypeCombiner::combine([$new_type_part, $this->types[$key]])->getSingleAtomic(); + } + } + } elseif (count($this->types) === 0) { + $this->types['mixed'] = new TMixed(); + } + + $this->bustCache(); + + return $this; + } + + /** + * @psalm-mutation-free + */ + public function getBuilder(): self + { + return $this; + } + + /** + * @psalm-mutation-free + */ + public function freeze(): Union + { + $union = new Union($this->getAtomicTypes()); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; + } + if ($key === 'id') { + continue; + } + if ($key === 'exact_id') { + continue; + } + if ($key === 'literal_string_types') { + continue; + } + if ($key === 'typed_class_strings') { + continue; + } + if ($key === 'literal_int_types') { + continue; + } + if ($key === 'literal_float_types') { + continue; + } + /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ + $union->{$key} = $value; + } + return $union; + } +} diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 19b706bcb62..c1585ac75ca 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -50,7 +50,6 @@ use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; -use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use ReflectionProperty; @@ -283,7 +282,7 @@ public static function reconcileKeyedTypes( ); if ($result_type_candidate->isUnionEmpty()) { - $result_type_candidate->addType(new TNever); + $result_type_candidate = $result_type_candidate->getBuilder()->addType(new TNever)->freeze(); } $orred_type = Type::combineUnionTypes( @@ -715,7 +714,9 @@ private static function getValueForKey( if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { + $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate = $new_base_type_candidate->freeze(); } $new_base_type_candidate->possibly_undefined = true; @@ -729,7 +730,10 @@ private static function getValueForKey( if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { - $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate = $new_base_type_candidate + ->getBuilder() + ->addType(new TNull) + ->freeze(); } $new_base_type_candidate->possibly_undefined = true; @@ -963,11 +967,11 @@ private static function getPropertyType( } /** + * @param Union|MutableUnion $existing_var_type * @param string[] $suppressed_issues - * */ protected static function triggerIssueForImpossible( - Union $existing_var_type, + $existing_var_type, string $old_var_type_string, string $key, Assertion $assertion, @@ -1132,13 +1136,11 @@ private static function adjustTKeyedArrayType( [ $array_key_offset => clone $result_type, ], - null + null, + false, + $previous_key_type->isNever() ? null : $previous_key_type, + $previous_value_type ); - - if (!$previous_key_type->isNever()) { - $base_atomic_type->previous_key_type = $previous_key_type; - } - $base_atomic_type->previous_value_type = $previous_value_type; } elseif ($base_atomic_type instanceof TList) { $previous_key_type = Type::getInt(); $previous_value_type = clone $base_atomic_type->type_param; @@ -1147,21 +1149,21 @@ private static function adjustTKeyedArrayType( [ $array_key_offset => clone $result_type, ], - null + null, + false, + $previous_key_type, + $previous_value_type, + true ); - - $base_atomic_type->is_list = true; - - $base_atomic_type->previous_key_type = $previous_key_type; - $base_atomic_type->previous_value_type = $previous_value_type; } elseif ($base_atomic_type instanceof TClassStringMap) { // do nothing } else { - $base_atomic_type = clone $base_atomic_type; - $base_atomic_type->properties[$array_key_offset] = clone $result_type; + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = clone $result_type; + $base_atomic_type = $base_atomic_type->setProperties($properties); } - $new_base_type->addType($base_atomic_type); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; @@ -1181,24 +1183,34 @@ private static function adjustTKeyedArrayType( } } - protected static function refineArrayKey(Union $key_type): void + protected static function refineArrayKey(Union $key_type): Union { - foreach ($key_type->getAtomicTypes() as $key => $cat) { + return self::refineArrayKeyInner($key_type) ?? $key_type; + } + private static function refineArrayKeyInner(Union $key_type): ?Union + { + $refined = false; + $types = []; + foreach ($key_type->getAtomicTypes() as $cat) { if ($cat instanceof TTemplateParam) { - self::refineArrayKey($cat->as); - $key_type->bustCache(); - } elseif ($cat instanceof TScalar || $cat instanceof TMixed) { - $key_type->removeType($key); - $key_type->addType(new TArrayKey()); - } elseif (!$cat instanceof TString && !$cat instanceof TInt) { - $key_type->removeType($key); - $key_type->addType(new TArrayKey()); + $as = self::refineArrayKeyInner($cat->as); + if ($as) { + $refined = true; + $types []= $cat->replaceAs($as); + } else { + $types []= $cat; + } + } elseif ($cat instanceof TArrayKey || $cat instanceof TString || $cat instanceof TInt) { + $types []= $cat; + } else { + $refined = true; + $types []= new TArrayKey; } } - if ($key_type->isUnionEmpty()) { - // this should ideally prompt some sort of error - $key_type->addType(new TArrayKey()); + if ($refined) { + return $key_type->getBuilder()->setTypes($types)->freeze(); } + return null; } } diff --git a/src/Psalm/Type/TypeNode.php b/src/Psalm/Type/TypeNode.php index 6bdc054c28c..f8d8cf4a506 100644 --- a/src/Psalm/Type/TypeNode.php +++ b/src/Psalm/Type/TypeNode.php @@ -5,7 +5,7 @@ interface TypeNode { /** - * @return array + * @return list */ - public function getChildNodes(): array; + public function getChildNodeKeys(): array; } diff --git a/src/Psalm/Type/TypeVisitor.php b/src/Psalm/Type/TypeVisitor.php new file mode 100644 index 00000000000..c4e98bcf517 --- /dev/null +++ b/src/Psalm/Type/TypeVisitor.php @@ -0,0 +1,104 @@ +enterNode($node); + + if ($visitor_result === self::DONT_TRAVERSE_CHILDREN) { + return true; + } + + if ($visitor_result === self::STOP_TRAVERSAL) { + return false; + } + + $cloned = $node !== $old; + foreach ($node->getChildNodeKeys() as $key) { + if ($node instanceof Union || $node instanceof MutableUnion) { + $child_node = $node->getAtomicTypes(); + } else { + /** @var TypeNode|non-empty-array|null */ + $child_node = $node->{$key}; + } + if ($child_node === null) { + continue; + } + $orig = $child_node; + if (is_array($child_node)) { + $visitor_result = $this->traverseArray($child_node); + } else { + $visitor_result = $this->traverse($child_node); + } + if ($child_node !== $orig) { + if ($node instanceof Union) { + /** @var non-empty-array $child_node */ + $node = $node->getBuilder()->setTypes($child_node)->freeze(); + } elseif ($node instanceof MutableUnion) { + // This mutates in-place + /** @var non-empty-array $child_node */ + $node->setTypes($child_node); + } else { + if (!$cloned) { + $cloned = true; + $node = clone $node; + } + if ($key === 'extra_types' && is_array($child_node)) { + $new = []; + /** @var Union */ + foreach ($child_node as $value) { + $new[$value->getKey()] = $value; + } + $child_node = $new; + } + $node->{$key} = $child_node; + } + } + if ($visitor_result === false) { + return false; + } + } + + return true; + } + + /** + * @template T as array + * @param T $nodes + * @param-out T $nodes + */ + public function traverseArray(array &$nodes): bool + { + foreach ($nodes as &$node) { + if ($this->traverse($node) === false) { + return false; + } + } + return true; + } +} diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 9d004d386ce..cdcfc5d9ed0 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -2,60 +2,22 @@ namespace Psalm\Type; -use InvalidArgumentException; -use Psalm\CodeLocation; -use Psalm\Codebase; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\Type\TypeCombiner; -use Psalm\Internal\TypeVisitor\ContainsClassLikeVisitor; -use Psalm\Internal\TypeVisitor\ContainsLiteralVisitor; use Psalm\Internal\TypeVisitor\FromDocblockSetter; -use Psalm\Internal\TypeVisitor\TemplateTypeCollector; -use Psalm\Internal\TypeVisitor\TypeChecker; -use Psalm\Internal\TypeVisitor\TypeScanner; -use Psalm\StatementsSource; -use Psalm\Storage\FileStorage; -use Psalm\Type; -use Psalm\Type\Atomic\Scalar; -use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClassString; -use Psalm\Type\Atomic\TClassStringMap; -use Psalm\Type\Atomic\TClosure; -use Psalm\Type\Atomic\TConditional; -use Psalm\Type\Atomic\TEmptyMixed; -use Psalm\Type\Atomic\TFalse; -use Psalm\Type\Atomic\TFloat; -use Psalm\Type\Atomic\TInt; -use Psalm\Type\Atomic\TIntRange; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\Atomic\TLowercaseString; -use Psalm\Type\Atomic\TMixed; -use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\Atomic\TNonEmptyLowercaseString; -use Psalm\Type\Atomic\TNonspecificLiteralInt; -use Psalm\Type\Atomic\TNonspecificLiteralString; -use Psalm\Type\Atomic\TString; -use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\Atomic\TTrue; +use Stringable; -use function array_filter; -use function array_unique; -use function count; -use function get_class; -use function implode; -use function ksort; -use function reset; -use function sort; -use function strpos; +use function get_object_vars; -final class Union implements TypeNode +final class Union implements TypeNode, Stringable { + use UnionTrait; + /** + * @psalm-readonly * @var non-empty-array */ private $types; @@ -236,1423 +198,59 @@ final class Union implements TypeNode public $different = false; /** - * Constructs an Union instance - * - * @param non-empty-array $types + * @psalm-mutation-free + * @param non-empty-array $types */ - public function __construct(array $types) + public function setTypes(array $types): self { - $from_docblock = false; - - $keyed_types = []; - - foreach ($types as $type) { - $key = $type->getKey(); - $keyed_types[$key] = $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - - $from_docblock = $from_docblock || $type->from_docblock; + if ($types === $this->types) { + return $this; } - - $this->types = $keyed_types; - - $this->from_docblock = $from_docblock; - } - - /** - * @param non-empty-array $types - */ - public function replaceTypes(array $types): void - { - $this->types = $types; + return $this->getBuilder()->setTypes($types)->freeze(); } /** * @psalm-mutation-free - * @return non-empty-array */ - public function getAtomicTypes(): array - { - return $this->types; - } - - public function addType(Atomic $type): void - { - $this->types[$type->getKey()] = $type; - - if ($type instanceof TLiteralString) { - $this->literal_string_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralInt) { - $this->literal_int_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$type->getKey()] = $type; - } elseif ($type instanceof TString && $this->literal_string_types) { - foreach ($this->literal_string_types as $key => $_) { - unset($this->literal_string_types[$key], $this->types[$key]); - } - if (!$type instanceof TClassString - || (!$type->as_type && !$type instanceof TTemplateParamClass) - ) { - foreach ($this->typed_class_strings as $key => $_) { - unset($this->typed_class_strings[$key], $this->types[$key]); - } - } - } elseif ($type instanceof TInt && $this->literal_int_types) { - //we remove any literal that is already included in a wider type - $int_type_in_range = TIntRange::convertToIntRange($type); - foreach ($this->literal_int_types as $key => $literal_int_type) { - if ($int_type_in_range->contains($literal_int_type->value)) { - unset($this->literal_int_types[$key], $this->types[$key]); - } - } - } elseif ($type instanceof TFloat && $this->literal_float_types) { - foreach ($this->literal_float_types as $key => $_) { - unset($this->literal_float_types[$key], $this->types[$key]); - } - } - - $this->bustCache(); - } - - public function __clone() - { - $this->literal_string_types = []; - $this->literal_int_types = []; - $this->literal_float_types = []; - $this->typed_class_strings = []; - - foreach ($this->types as $key => &$type) { - $type = clone $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - } - } - - public function __toString(): string - { - $types = []; - - $printed_int = false; - $printed_float = false; - $printed_string = false; - - foreach ($this->types as $type) { - if ($type instanceof TLiteralFloat) { - if ($printed_float) { - continue; - } - - $printed_float = true; - } elseif ($type instanceof TLiteralString) { - if ($printed_string) { - continue; - } - - $printed_string = true; - } elseif ($type instanceof TLiteralInt) { - if ($printed_int) { - continue; - } - - $printed_int = true; - } - - $types[] = $type->getId(false); - } - - sort($types); - return implode('|', $types); - } - - public function getKey(): string - { - $types = []; - - $printed_int = false; - $printed_float = false; - $printed_string = false; - - foreach ($this->types as $type) { - if ($type instanceof TLiteralFloat) { - if ($printed_float) { - continue; - } - - $types[] = 'float'; - $printed_float = true; - } elseif ($type instanceof TLiteralString) { - if ($printed_string) { - continue; - } - - $types[] = 'string'; - $printed_string = true; - } elseif ($type instanceof TLiteralInt) { - if ($printed_int) { - continue; - } - - $types[] = 'int'; - $printed_int = true; - } else { - $types[] = $type->getKey(); - } - } - - sort($types); - return implode('|', $types); - } - - public function getId(bool $exact = true): string + public function getBuilder(): MutableUnion { - if ($exact && $this->exact_id) { - return $this->exact_id; - } elseif (!$exact && $this->id) { - return $this->id; - } - - $types = []; - foreach ($this->types as $type) { - $types[] = $type->getId($exact); - } - $types = array_unique($types); - sort($types); - - if (count($types) > 1) { - foreach ($types as $i => $type) { - if (strpos($type, ' as ') && strpos($type, '(') === false) { - $types[$i] = '(' . $type . ')'; - } - } - } - - $id = implode('|', $types); - - if ($exact) { - $this->exact_id = $id; - } else { - $this->id = $id; - } - - return $id; - } - - /** - * @param array $aliased_classes - * - */ - public function toNamespacedString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - bool $use_phpdoc_format - ): string { - $other_types = []; - - $literal_ints = []; - $literal_strings = []; - - $has_non_literal_int = false; - $has_non_literal_string = false; - - foreach ($this->types as $type) { - $type_string = $type->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format); - if ($type instanceof TLiteralInt) { - $literal_ints[] = $type_string; - } elseif ($type instanceof TLiteralString) { - $literal_strings[] = $type_string; - } else { - if (get_class($type) === TString::class) { - $has_non_literal_string = true; - } elseif (get_class($type) === TInt::class) { - $has_non_literal_int = true; - } - $other_types[] = $type_string; - } - } - - if (count($literal_ints) <= 3 && !$has_non_literal_int) { - $other_types = [...$other_types, ...$literal_ints]; - } else { - $other_types[] = 'int'; - } - - if (count($literal_strings) <= 3 && !$has_non_literal_string) { - $other_types = [...$other_types, ...$literal_strings]; - } else { - $other_types[] = 'string'; - } - - sort($other_types); - return implode('|', array_unique($other_types)); - } - - /** - * @param array $aliased_classes - */ - public function toPhpString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - int $analysis_php_version_id - ): ?string { - if (!$this->isSingleAndMaybeNullable()) { - if ($analysis_php_version_id < 8_00_00) { - return null; - } - } elseif ($analysis_php_version_id < 7_00_00 - || (isset($this->types['null']) && $analysis_php_version_id < 7_01_00) - ) { - return null; - } - - $types = $this->types; - - $nullable = false; - - if (isset($types['null']) && count($types) > 1) { - unset($types['null']); - - $nullable = true; - } - - $falsable = false; - - if (isset($types['false']) && count($types) > 1) { - unset($types['false']); - - $falsable = true; - } - - $php_types = []; - - foreach ($types as $atomic_type) { - $php_type = $atomic_type->toPhpString( - $namespace, - $aliased_classes, - $this_class, - $analysis_php_version_id - ); - - if (!$php_type) { - return null; - } - - $php_types[] = $php_type; - } - - if ($falsable) { - if ($nullable) { - $php_types['null'] = 'null'; + $union = new MutableUnion($this->getAtomicTypes()); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; } - $php_types['false'] = 'false'; - ksort($php_types); - return implode('|', array_unique($php_types)); - } - - if ($analysis_php_version_id < 8_00_00) { - return ($nullable ? '?' : '') . implode('|', array_unique($php_types)); - } - if ($nullable) { - $php_types['null'] = 'null'; - } - return implode('|', array_unique($php_types)); - } - - public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool - { - if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { - return false; - } - - $types = $this->types; - - if (isset($types['null'])) { - if (count($types) > 1) { - unset($types['null']); - } else { - return false; + if ($key === 'id') { + continue; } - } - - return !array_filter( - $types, - static fn($atomic_type): bool => !$atomic_type->canBeFullyExpressedInPhp($analysis_php_version_id) - ); - } - - public function removeType(string $type_string): bool - { - if (isset($this->types[$type_string])) { - unset($this->types[$type_string]); - - if (strpos($type_string, '(')) { - unset( - $this->literal_string_types[$type_string], - $this->literal_int_types[$type_string], - $this->literal_float_types[$type_string] - ); + if ($key === 'exact_id') { + continue; } - - $this->bustCache(); - - return true; - } - - if ($type_string === 'string') { - if ($this->literal_string_types) { - foreach ($this->literal_string_types as $literal_key => $_) { - unset($this->types[$literal_key]); - } - $this->literal_string_types = []; + if ($key === 'literal_string_types') { + continue; } - - if ($this->typed_class_strings) { - foreach ($this->typed_class_strings as $typed_class_key => $_) { - unset($this->types[$typed_class_key]); - } - $this->typed_class_strings = []; + if ($key === 'typed_class_strings') { + continue; } - - unset($this->types['class-string'], $this->types['trait-string']); - } elseif ($type_string === 'int' && $this->literal_int_types) { - foreach ($this->literal_int_types as $literal_key => $_) { - unset($this->types[$literal_key]); + if ($key === 'literal_int_types') { + continue; } - $this->literal_int_types = []; - } elseif ($type_string === 'float' && $this->literal_float_types) { - foreach ($this->literal_float_types as $literal_key => $_) { - unset($this->types[$literal_key]); + if ($key === 'literal_float_types') { + continue; } - $this->literal_float_types = []; + /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ + $union->{$key} = $value; } - - return false; - } - - public function bustCache(): void - { - $this->id = null; - $this->exact_id = null; - } - - public function hasType(string $type_string): bool - { - return isset($this->types[$type_string]); - } - - public function hasArray(): bool - { - return isset($this->types['array']); - } - - public function hasIterable(): bool - { - return isset($this->types['iterable']); - } - - public function hasList(): bool - { - return isset($this->types['array']) && $this->types['array'] instanceof TList; - } - - public function hasClassStringMap(): bool - { - return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; - } - - public function isTemplatedClassString(): bool - { - return $this->isSingle() - && count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TTemplateParamClass - ) - ) === 1; - } - - public function hasArrayAccessInterface(Codebase $codebase): bool - { - return (bool)array_filter( - $this->types, - static fn($type): bool => $type->hasArrayAccessInterface($codebase) - ); - } - - public function hasCallableType(): bool - { - return $this->getCallableTypes() || $this->getClosureTypes(); - } - - /** - * @return array - */ - public function getCallableTypes(): array - { - return array_filter( - $this->types, - static fn($type): bool => $type instanceof TCallable - ); + return $union; } /** - * @return array + * @psalm-mutation-free */ - public function getClosureTypes(): array - { - return array_filter( - $this->types, - static fn($type): bool => $type instanceof TClosure - ); - } - - public function hasObject(): bool - { - return isset($this->types['object']); - } - - public function hasObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isObjectType()) { - return true; - } - } - - return false; - } - - public function isObjectType(): bool - { - foreach ($this->types as $type) { - if (!$type->isObjectType()) { - return false; - } - } - - return true; - } - - public function hasNamedObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isNamedObjectType()) { - return true; - } - } - - return false; - } - - public function isStaticObject(): bool - { - foreach ($this->types as $type) { - if (!$type instanceof TNamedObject - || !$type->is_static - ) { - return false; - } - } - - return true; - } - - public function hasStaticObject(): bool - { - foreach ($this->types as $type) { - if ($type instanceof TNamedObject - && $type->is_static - ) { - return true; - } - } - - return false; - } - - public function isNullable(): bool - { - if (isset($this->types['null'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isNullable()) { - return true; - } - } - - return false; - } - - public function isFalsable(): bool - { - if (isset($this->types['false'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isFalsable()) { - return true; - } - } - - return false; - } - - public function hasBool(): bool - { - return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); - } - - public function hasString(): bool - { - return isset($this->types['string']) - || isset($this->types['class-string']) - || isset($this->types['trait-string']) - || isset($this->types['numeric-string']) - || isset($this->types['callable-string']) - || isset($this->types['array-key']) - || $this->literal_string_types - || $this->typed_class_strings; - } - - public function hasLowercaseString(): bool - { - return isset($this->types['string']) - && ($this->types['string'] instanceof TLowercaseString - || $this->types['string'] instanceof TNonEmptyLowercaseString); - } - - public function hasLiteralClassString(): bool - { - return count($this->typed_class_strings) > 0; - } - - public function hasInt(): bool - { - return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types - || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); - } - - public function hasArrayKey(): bool - { - return isset($this->types['array-key']); - } - - public function hasFloat(): bool - { - return isset($this->types['float']) || $this->literal_float_types; - } - - public function hasScalar(): bool - { - return isset($this->types['scalar']); - } - - public function hasNumeric(): bool - { - return isset($this->types['numeric']); - } - - public function hasScalarType(): bool - { - return isset($this->types['int']) - || isset($this->types['float']) - || isset($this->types['string']) - || isset($this->types['class-string']) - || isset($this->types['trait-string']) - || isset($this->types['bool']) - || isset($this->types['false']) - || isset($this->types['true']) - || isset($this->types['numeric']) - || isset($this->types['numeric-string']) - || $this->literal_int_types - || $this->literal_float_types - || $this->literal_string_types - || $this->typed_class_strings; - } - - public function hasTemplate(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TTemplateParam - || ($type instanceof TNamedObject - && $type->extra_types - && array_filter( - $type->extra_types, - static fn($t): bool => $t instanceof TTemplateParam - ) - ) - ); - } - - public function hasConditional(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TConditional - ); - } - - public function hasTemplateOrStatic(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TTemplateParam - || ($type instanceof TNamedObject - && ($type->is_static - || ($type->extra_types - && array_filter( - $type->extra_types, - static fn($t): bool => $t instanceof TTemplateParam - ) - ) - ) - ) - ); - } - - public function hasMixed(): bool - { - return isset($this->types['mixed']); - } - - public function isMixed(): bool - { - return isset($this->types['mixed']) && count($this->types) === 1; - } - - public function isEmptyMixed(): bool - { - return isset($this->types['mixed']) - && $this->types['mixed'] instanceof TEmptyMixed - && count($this->types) === 1; - } - - public function isVanillaMixed(): bool - { - return isset($this->types['mixed']) - && get_class($this->types['mixed']) === TMixed::class - && !$this->types['mixed']->from_loop_isset - && count($this->types) === 1; - } - - public function isArrayKey(): bool - { - return isset($this->types['array-key']) && count($this->types) === 1; - } - - public function isNull(): bool - { - return count($this->types) === 1 && isset($this->types['null']); - } - - public function isFalse(): bool - { - return count($this->types) === 1 && isset($this->types['false']); - } - - public function isAlwaysFalsy(): bool - { - foreach ($this->getAtomicTypes() as $atomic_type) { - if (!$atomic_type->isFalsy()) { - return false; - } - } - - return true; - } - - public function isTrue(): bool - { - return count($this->types) === 1 && isset($this->types['true']); - } - - public function isAlwaysTruthy(): bool - { - if ($this->possibly_undefined || $this->possibly_undefined_from_try) { - return false; - } - - foreach ($this->getAtomicTypes() as $atomic_type) { - if (!$atomic_type->isTruthy()) { - return false; - } - } - - return true; - } - - public function isVoid(): bool - { - return isset($this->types['void']) && count($this->types) === 1; - } - - public function isNever(): bool - { - return isset($this->types['never']) && count($this->types) === 1; - } - - public function isGenerator(): bool - { - return count($this->types) === 1 - && (($single_type = reset($this->types)) instanceof TNamedObject) - && ($single_type->value === 'Generator'); - } - - public function substitute(Union $old_type, ?Union $new_type = null): void - { - if ($this->hasMixed() && !$this->isEmptyMixed()) { - return; - } - - if ($new_type && $new_type->ignore_nullable_issues) { - $this->ignore_nullable_issues = true; - } - - if ($new_type && $new_type->ignore_falsable_issues) { - $this->ignore_falsable_issues = true; - } - - foreach ($old_type->types as $old_type_part) { - if (!$this->removeType($old_type_part->getKey())) { - if ($old_type_part instanceof TFalse - && isset($this->types['bool']) - && !isset($this->types['true']) - ) { - $this->removeType('bool'); - $this->types['true'] = new TTrue; - } elseif ($old_type_part instanceof TTrue - && isset($this->types['bool']) - && !isset($this->types['false']) - ) { - $this->removeType('bool'); - $this->types['false'] = new TFalse; - } elseif (isset($this->types['iterable'])) { - if ($old_type_part instanceof TNamedObject - && $old_type_part->value === 'Traversable' - && !isset($this->types['array']) - ) { - $this->removeType('iterable'); - $this->types['array'] = new TArray([Type::getArrayKey(), Type::getMixed()]); - } - - if ($old_type_part instanceof TArray - && !isset($this->types['traversable']) - ) { - $this->removeType('iterable'); - $this->types['traversable'] = new TNamedObject('Traversable'); - } - } elseif (isset($this->types['array-key'])) { - if ($old_type_part instanceof TString - && !isset($this->types['int']) - ) { - $this->removeType('array-key'); - $this->types['int'] = new TInt(); - } - - if ($old_type_part instanceof TInt - && !isset($this->types['string']) - ) { - $this->removeType('array-key'); - $this->types['string'] = new TString(); - } - } - } - } - - if ($new_type) { - foreach ($new_type->types as $key => $new_type_part) { - if (!isset($this->types[$key]) - || ($new_type_part instanceof Scalar - && get_class($new_type_part) === get_class($this->types[$key])) - ) { - $this->types[$key] = $new_type_part; - } else { - $this->types[$key] = TypeCombiner::combine([$new_type_part, $this->types[$key]])->getSingleAtomic(); - } - } - } elseif (count($this->types) === 0) { - $this->types['mixed'] = new TMixed(); - } - - $this->bustCache(); - } - - public function isSingle(): bool - { - $type_count = count($this->types); - - $int_literal_count = count($this->literal_int_types); - $string_literal_count = count($this->literal_string_types); - $float_literal_count = count($this->literal_float_types); - - if (($int_literal_count && $string_literal_count) - || ($int_literal_count && $float_literal_count) - || ($string_literal_count && $float_literal_count) - ) { - return false; - } - - if ($int_literal_count || $string_literal_count || $float_literal_count) { - $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; - } - - return $type_count === 1; - } - - public function isSingleAndMaybeNullable(): bool - { - $is_nullable = isset($this->types['null']); - - $type_count = count($this->types); - - if ($type_count === 1 && $is_nullable) { - return false; - } - - $int_literal_count = count($this->literal_int_types); - $string_literal_count = count($this->literal_string_types); - $float_literal_count = count($this->literal_float_types); - - if (($int_literal_count && $string_literal_count) - || ($int_literal_count && $float_literal_count) - || ($string_literal_count && $float_literal_count) - ) { - return false; - } - - if ($int_literal_count || $string_literal_count || $float_literal_count) { - $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; - } - - return ($type_count - (int) $is_nullable) === 1; - } - - /** - * @return bool true if this is an int - */ - public function isInt(bool $check_templates = false): bool - { - return count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TInt - || ($check_templates - && $type instanceof TTemplateParam - && $type->as->isInt() - ) - ) - ) === count($this->types); - } - - /** - * @return bool true if this is a float - */ - public function isFloat(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['float']) || $this->literal_float_types; - } - - /** - * @return bool true if this is a string - */ - public function isString(bool $check_templates = false): bool - { - return count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TString - || ($check_templates - && $type instanceof TTemplateParam - && $type->as->isString() - ) - ) - ) === count($this->types); - } - - /** - * @return bool true if this is a boolean - */ - public function isBool(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['bool']); - } - - /** - * @return bool true if this is an array - */ - public function isArray(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['array']); - } - - /** - * @return bool true if this is a string literal with only one possible value - */ - public function isSingleStringLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_string_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleStringLiteral is false - * - * @return TLiteralString the only string literal represented by this union type - */ - public function getSingleStringLiteral(): TLiteralString - { - if (count($this->types) !== 1 || count($this->literal_string_types) !== 1) { - throw new InvalidArgumentException('Not a string literal'); - } - - return reset($this->literal_string_types); - } - - public function allStringLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString) { - return false; - } - } - - return true; - } - - public function allIntLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralInt) { - return false; - } - } - - return true; - } - - /** - * @psalm-suppress PossiblyUnusedMethod Public API - */ - public function allFloatLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralFloat) { - return false; - } - } - - return true; - } - - /** - * @psalm-assert-if-true array< - * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue - * > $this->getAtomicTypes() - */ - public function allSpecificLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString - && !$atomic_key_type instanceof TLiteralInt - && !$atomic_key_type instanceof TLiteralFloat - && !$atomic_key_type instanceof TFalse - && !$atomic_key_type instanceof TTrue - ) { - return false; - } - } - - return true; - } - - /** - * @psalm-assert-if-true array< - * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue - * > $this->getAtomicTypes() - */ - public function allLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString - && !$atomic_key_type instanceof TLiteralInt - && !$atomic_key_type instanceof TLiteralFloat - && !$atomic_key_type instanceof TNonspecificLiteralString - && !$atomic_key_type instanceof TNonspecificLiteralInt - && !$atomic_key_type instanceof TFalse - && !$atomic_key_type instanceof TTrue - ) { - return false; - } - } - - return true; - } - - public function hasLiteralValue(): bool - { - return $this->literal_int_types - || $this->literal_string_types - || $this->literal_float_types - || isset($this->types['false']) - || isset($this->types['true']); - } - - public function isSingleLiteral(): bool - { - return count($this->types) === 1 - && count($this->literal_int_types) - + count($this->literal_string_types) - + count($this->literal_float_types) === 1 - ; - } - - /** - * @return TLiteralInt|TLiteralString|TLiteralFloat - */ - public function getSingleLiteral() - { - if (!$this->isSingleLiteral()) { - throw new InvalidArgumentException("Not a single literal"); - } - - return ($literal = reset($this->literal_int_types)) !== false - ? $literal - : (($literal = reset($this->literal_string_types)) !== false - ? $literal - : reset($this->literal_float_types)) - ; - } - - public function hasLiteralString(): bool - { - return count($this->literal_string_types) > 0; - } - - public function hasLiteralInt(): bool - { - return count($this->literal_int_types) > 0; - } - - /** - * @return bool true if this is a int literal with only one possible value - */ - public function isSingleIntLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_int_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleIntLiteral is false - * - * @return TLiteralInt the only int literal represented by this union type - */ - public function getSingleIntLiteral(): TLiteralInt - { - if (count($this->types) !== 1 || count($this->literal_int_types) !== 1) { - throw new InvalidArgumentException('Not an int literal'); - } - - return reset($this->literal_int_types); - } - - /** - * @param array $suppressed_issues - * @param array $phantom_classes - * - */ - public function check( - StatementsSource $source, - CodeLocation $code_location, - array $suppressed_issues, - array $phantom_classes = [], - bool $inferred = true, - bool $inherited = false, - bool $prevent_template_covariance = false, - ?string $calling_method_id = null - ): bool { - if ($this->checked) { - return true; - } - - $checker = new TypeChecker( - $source, - $code_location, - $suppressed_issues, - $phantom_classes, - $inferred, - $inherited, - $prevent_template_covariance, - $calling_method_id - ); - - $checker->traverseArray($this->types); - - $this->checked = true; - - return !$checker->hasErrors(); - } - - /** - * @param array $phantom_classes - * - */ - public function queueClassLikesForScanning( - Codebase $codebase, - ?FileStorage $file_storage = null, - array $phantom_classes = [] - ): void { - $scanner_visitor = new TypeScanner( - $codebase->scanner, - $file_storage, - $phantom_classes - ); - - $scanner_visitor->traverseArray($this->types); - } - - /** - * @param lowercase-string $fq_class_like_name - */ - public function containsClassLike(string $fq_class_like_name): bool - { - $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); - - $classlike_visitor->traverseArray($this->types); - - return $classlike_visitor->matches(); - } - - public function containsAnyLiteral(): bool - { - $literal_visitor = new ContainsLiteralVisitor(); - - $literal_visitor->traverseArray($this->types); - - return $literal_visitor->matches(); - } - - /** - * @return list - */ - public function getTemplateTypes(): array - { - $template_type_collector = new TemplateTypeCollector(); - - $template_type_collector->traverseArray($this->types); - - return $template_type_collector->getTemplateTypes(); - } - - public function setFromDocblock(): void - { - $this->from_docblock = true; - - (new FromDocblockSetter())->traverseArray($this->types); - } - - public function replaceClassLike(string $old, string $new): void - { - foreach ($this->types as $key => $atomic_type) { - $atomic_type->replaceClassLike($old, $new); - - $this->removeType($key); - $this->addType($atomic_type); - } - } - - public function equals(Union $other_type, bool $ensure_source_equality = true): bool - { - if ($other_type === $this) { - return true; - } - - if ($other_type->id && $this->id && $other_type->id !== $this->id) { - return false; - } - - if ($other_type->exact_id && $this->exact_id && $other_type->exact_id !== $this->exact_id) { - return false; - } - - if ($this->possibly_undefined !== $other_type->possibly_undefined) { - return false; - } - - if ($this->had_template !== $other_type->had_template) { - return false; - } - - if ($this->possibly_undefined_from_try !== $other_type->possibly_undefined_from_try) { - return false; - } - - if ($this->from_calculation !== $other_type->from_calculation) { - return false; - } - - if ($this->initialized !== $other_type->initialized) { - return false; - } - - if ($ensure_source_equality && $this->from_docblock !== $other_type->from_docblock) { - return false; - } - - if (count($this->types) !== count($other_type->types)) { - return false; - } - - if ($this->parent_nodes !== $other_type->parent_nodes) { - return false; - } - - if ($this->different || $other_type->different) { - return false; - } - - $other_atomic_types = $other_type->types; - - foreach ($this->types as $key => $atomic_type) { - if (!isset($other_atomic_types[$key])) { - return false; - } - - if (!$atomic_type->equals($other_atomic_types[$key], $ensure_source_equality)) { - return false; - } - } - - return true; - } - - /** - * @return array - */ - public function getLiteralStrings(): array - { - return $this->literal_string_types; - } - - /** - * @return array - */ - public function getLiteralInts(): array - { - return $this->literal_int_types; - } - - /** - * @return array - */ - public function getRangeInts(): array - { - $ranges = []; - foreach ($this->getAtomicTypes() as $atomic) { - if ($atomic instanceof TIntRange) { - $ranges[$atomic->getKey()] = $atomic; - } - } - - return $ranges; - } - - /** - * @return array - */ - public function getLiteralFloats(): array - { - return $this->literal_float_types; - } - - /** - * @return array - */ - public function getChildNodes(): array - { - return $this->types; - } - - /** - * @return bool true if this is a float literal with only one possible value - */ - public function isSingleFloatLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_float_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleFloatLiteral is false - * - * @return TLiteralFloat the only float literal represented by this union type - */ - public function getSingleFloatLiteral(): TLiteralFloat - { - if (count($this->types) !== 1 || count($this->literal_float_types) !== 1) { - throw new InvalidArgumentException('Not a float literal'); - } - - return reset($this->literal_float_types); - } - - public function hasLiteralFloat(): bool - { - return count($this->literal_float_types) > 0; - } - - public function getSingleAtomic(): Atomic - { - return reset($this->types); - } - - public function isEmptyArray(): bool - { - return count($this->types) === 1 - && isset($this->types['array']) - && $this->types['array'] instanceof TArray - && $this->types['array']->isEmptyArray(); - } - - public function isUnionEmpty(): bool + public function setFromDocblock(bool $fromDocblock = true): self { - return $this->types === []; + $cloned = clone $this; + /** @psalm-suppress ImpureMethodCall Acting on clone */ + (new FromDocblockSetter($fromDocblock))->traverse($cloned); + return $cloned; } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php new file mode 100644 index 00000000000..ce7ff78965f --- /dev/null +++ b/src/Psalm/Type/UnionTrait.php @@ -0,0 +1,1469 @@ + $types + */ + public function __construct(array $types, bool $from_docblock = false) + { + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + + $this->from_docblock = $from_docblock; + } + + /** + * @psalm-mutation-free + * @return non-empty-array + */ + public function getAtomicTypes(): array + { + return $this->types; + } + + /** + * @psalm-mutation-free + */ + public function __toString(): string + { + $types = []; + + $printed_int = false; + $printed_float = false; + $printed_string = false; + + foreach ($this->types as $type) { + if ($type instanceof TLiteralFloat) { + if ($printed_float) { + continue; + } + + $printed_float = true; + } elseif ($type instanceof TLiteralString) { + if ($printed_string) { + continue; + } + + $printed_string = true; + } elseif ($type instanceof TLiteralInt) { + if ($printed_int) { + continue; + } + + $printed_int = true; + } + + $types[] = $type->getId(false); + } + + sort($types); + return implode('|', $types); + } + + /** + * @psalm-mutation-free + */ + public function getKey(): string + { + $types = []; + + $printed_int = false; + $printed_float = false; + $printed_string = false; + + foreach ($this->types as $type) { + if ($type instanceof TLiteralFloat) { + if ($printed_float) { + continue; + } + + $types[] = 'float'; + $printed_float = true; + } elseif ($type instanceof TLiteralString) { + if ($printed_string) { + continue; + } + + $types[] = 'string'; + $printed_string = true; + } elseif ($type instanceof TLiteralInt) { + if ($printed_int) { + continue; + } + + $types[] = 'int'; + $printed_int = true; + } else { + $types[] = $type->getKey(); + } + } + + sort($types); + return implode('|', $types); + } + + /** + * @psalm-mutation-free + */ + public function getId(bool $exact = true): string + { + if ($exact && $this->exact_id) { + return $this->exact_id; + } elseif (!$exact && $this->id) { + return $this->id; + } + + $types = []; + foreach ($this->types as $type) { + $types[] = $type->getId($exact); + } + $types = array_unique($types); + sort($types); + + if (count($types) > 1) { + foreach ($types as $i => $type) { + if (strpos($type, ' as ') && strpos($type, '(') === false) { + $types[$i] = '(' . $type . ')'; + } + } + } + + $id = implode('|', $types); + + if ($exact) { + /** @psalm-suppress ImpurePropertyAssignment Cache */ + $this->exact_id = $id; + } else { + /** @psalm-suppress ImpurePropertyAssignment Cache */ + $this->id = $id; + } + + return $id; + } + + /** + * @param array $aliased_classes + * @psalm-mutation-free + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + $other_types = []; + + $literal_ints = []; + $literal_strings = []; + + $has_non_literal_int = false; + $has_non_literal_string = false; + + foreach ($this->types as $type) { + $type_string = $type->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format); + if ($type instanceof TLiteralInt) { + $literal_ints[] = $type_string; + } elseif ($type instanceof TLiteralString) { + $literal_strings[] = $type_string; + } else { + if (get_class($type) === TString::class) { + $has_non_literal_string = true; + } elseif (get_class($type) === TInt::class) { + $has_non_literal_int = true; + } + $other_types[] = $type_string; + } + } + + if (count($literal_ints) <= 3 && !$has_non_literal_int) { + $other_types = array_merge($other_types, $literal_ints); + } else { + $other_types[] = 'int'; + } + + if (count($literal_strings) <= 3 && !$has_non_literal_string) { + $other_types = array_merge($other_types, $literal_strings); + } else { + $other_types[] = 'string'; + } + + sort($other_types); + return implode('|', array_unique($other_types)); + } + + /** + * @psalm-mutation-free + * @param array $aliased_classes + */ + public function toPhpString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + int $analysis_php_version_id + ): ?string { + if (!$this->isSingleAndMaybeNullable()) { + if ($analysis_php_version_id < 8_00_00) { + return null; + } + } elseif ($analysis_php_version_id < 7_00_00 + || (isset($this->types['null']) && $analysis_php_version_id < 7_01_00) + ) { + return null; + } + + $types = $this->types; + + $nullable = false; + + if (isset($types['null']) && count($types) > 1) { + unset($types['null']); + + $nullable = true; + } + + $falsable = false; + + if (isset($types['false']) && count($types) > 1) { + unset($types['false']); + + $falsable = true; + } + + $php_types = []; + + foreach ($types as $atomic_type) { + $php_type = $atomic_type->toPhpString( + $namespace, + $aliased_classes, + $this_class, + $analysis_php_version_id + ); + + if (!$php_type) { + return null; + } + + $php_types[] = $php_type; + } + + if ($falsable) { + if ($nullable) { + $php_types['null'] = 'null'; + } + $php_types['false'] = 'false'; + ksort($php_types); + return implode('|', array_unique($php_types)); + } + + if ($analysis_php_version_id < 8_00_00) { + return ($nullable ? '?' : '') . implode('|', array_unique($php_types)); + } + if ($nullable) { + $php_types['null'] = 'null'; + } + return implode('|', array_unique($php_types)); + } + + /** + * @psalm-mutation-free + */ + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool + { + if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { + return false; + } + + $types = $this->types; + + if (isset($types['null'])) { + if (count($types) > 1) { + unset($types['null']); + } else { + return false; + } + } + + return !array_filter( + $types, + static fn($atomic_type): bool => !$atomic_type->canBeFullyExpressedInPhp($analysis_php_version_id) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasType(string $type_string): bool + { + return isset($this->types[$type_string]); + } + + /** + * @psalm-mutation-free + */ + public function hasArray(): bool + { + return isset($this->types['array']); + } + + /** + * @psalm-mutation-free + */ + public function hasIterable(): bool + { + return isset($this->types['iterable']); + } + + /** + * @psalm-mutation-free + */ + public function hasList(): bool + { + return isset($this->types['array']) && $this->types['array'] instanceof TList; + } + + /** + * @psalm-mutation-free + */ + public function hasClassStringMap(): bool + { + return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; + } + + /** + * @psalm-mutation-free + */ + public function isTemplatedClassString(): bool + { + return $this->isSingle() + && count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TTemplateParamClass + ) + ) === 1; + } + + /** + * @psalm-mutation-free + */ + public function hasArrayAccessInterface(Codebase $codebase): bool + { + return (bool)array_filter( + $this->types, + static fn($type): bool => $type->hasArrayAccessInterface($codebase) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasCallableType(): bool + { + return $this->getCallableTypes() || $this->getClosureTypes(); + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getCallableTypes(): array + { + return array_filter( + $this->types, + static fn($type): bool => $type instanceof TCallable + ); + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getClosureTypes(): array + { + return array_filter( + $this->types, + static fn($type): bool => $type instanceof TClosure + ); + } + + /** + * @psalm-mutation-free + */ + public function hasObject(): bool + { + return isset($this->types['object']); + } + + /** + * @psalm-mutation-free + */ + public function hasObjectType(): bool + { + foreach ($this->types as $type) { + if ($type->isObjectType()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isObjectType(): bool + { + foreach ($this->types as $type) { + if (!$type->isObjectType()) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function hasNamedObjectType(): bool + { + foreach ($this->types as $type) { + if ($type->isNamedObjectType()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isStaticObject(): bool + { + foreach ($this->types as $type) { + if (!$type instanceof TNamedObject + || !$type->is_static + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function hasStaticObject(): bool + { + foreach ($this->types as $type) { + if ($type instanceof TNamedObject + && $type->is_static + ) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isNullable(): bool + { + if (isset($this->types['null'])) { + return true; + } + + foreach ($this->types as $type) { + if ($type instanceof TTemplateParam && $type->as->isNullable()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function isFalsable(): bool + { + if (isset($this->types['false'])) { + return true; + } + + foreach ($this->types as $type) { + if ($type instanceof TTemplateParam && $type->as->isFalsable()) { + return true; + } + } + + return false; + } + + /** + * @psalm-mutation-free + */ + public function hasBool(): bool + { + return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); + } + + /** + * @psalm-mutation-free + */ + public function hasString(): bool + { + return isset($this->types['string']) + || isset($this->types['class-string']) + || isset($this->types['trait-string']) + || isset($this->types['numeric-string']) + || isset($this->types['callable-string']) + || isset($this->types['array-key']) + || $this->literal_string_types + || $this->typed_class_strings; + } + + /** + * @psalm-mutation-free + */ + public function hasLowercaseString(): bool + { + return isset($this->types['string']) + && ($this->types['string'] instanceof TLowercaseString + || $this->types['string'] instanceof TNonEmptyLowercaseString); + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralClassString(): bool + { + return count($this->typed_class_strings) > 0; + } + + /** + * @psalm-mutation-free + */ + public function hasInt(): bool + { + return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types + || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); + } + + /** + * @psalm-mutation-free + */ + public function hasArrayKey(): bool + { + return isset($this->types['array-key']); + } + + /** + * @psalm-mutation-free + */ + public function hasFloat(): bool + { + return isset($this->types['float']) || $this->literal_float_types; + } + + /** + * @psalm-mutation-free + */ + public function hasScalar(): bool + { + return isset($this->types['scalar']); + } + + /** + * @psalm-mutation-free + */ + public function hasNumeric(): bool + { + return isset($this->types['numeric']); + } + + /** + * @psalm-mutation-free + */ + public function hasScalarType(): bool + { + return isset($this->types['int']) + || isset($this->types['float']) + || isset($this->types['string']) + || isset($this->types['class-string']) + || isset($this->types['trait-string']) + || isset($this->types['bool']) + || isset($this->types['false']) + || isset($this->types['true']) + || isset($this->types['numeric']) + || isset($this->types['numeric-string']) + || $this->literal_int_types + || $this->literal_float_types + || $this->literal_string_types + || $this->typed_class_strings; + } + + /** + * @psalm-mutation-free + */ + public function hasTemplate(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TTemplateParam + || ($type instanceof TNamedObject + && $type->extra_types + && array_filter( + $type->extra_types, + static fn($t): bool => $t instanceof TTemplateParam + ) + ) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasConditional(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TConditional + ); + } + + /** + * @psalm-mutation-free + */ + public function hasTemplateOrStatic(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TTemplateParam + || ($type instanceof TNamedObject + && ($type->is_static + || ($type->extra_types + && array_filter( + $type->extra_types, + static fn($t): bool => $t instanceof TTemplateParam + ) + ) + ) + ) + ); + } + + /** + * @psalm-mutation-free + */ + public function hasMixed(): bool + { + return isset($this->types['mixed']); + } + + /** + * @psalm-mutation-free + */ + public function isMixed(): bool + { + return isset($this->types['mixed']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isEmptyMixed(): bool + { + return isset($this->types['mixed']) + && $this->types['mixed'] instanceof TEmptyMixed + && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isVanillaMixed(): bool + { + return isset($this->types['mixed']) + && get_class($this->types['mixed']) === TMixed::class + && !$this->types['mixed']->from_loop_isset + && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isArrayKey(): bool + { + return isset($this->types['array-key']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isNull(): bool + { + return count($this->types) === 1 && isset($this->types['null']); + } + + /** + * @psalm-mutation-free + */ + public function isFalse(): bool + { + return count($this->types) === 1 && isset($this->types['false']); + } + + /** + * @psalm-mutation-free + */ + public function isAlwaysFalsy(): bool + { + foreach ($this->getAtomicTypes() as $atomic_type) { + if (!$atomic_type->isFalsy()) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function isTrue(): bool + { + return count($this->types) === 1 && isset($this->types['true']); + } + + /** + * @psalm-mutation-free + */ + public function isAlwaysTruthy(): bool + { + if ($this->possibly_undefined || $this->possibly_undefined_from_try) { + return false; + } + + foreach ($this->getAtomicTypes() as $atomic_type) { + if (!$atomic_type->isTruthy()) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function isVoid(): bool + { + return isset($this->types['void']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isNever(): bool + { + return isset($this->types['never']) && count($this->types) === 1; + } + + /** + * @psalm-mutation-free + */ + public function isGenerator(): bool + { + return count($this->types) === 1 + && (($single_type = reset($this->types)) instanceof TNamedObject) + && ($single_type->value === 'Generator'); + } + + /** + * @psalm-mutation-free + */ + public function isSingle(): bool + { + $type_count = count($this->types); + + $int_literal_count = count($this->literal_int_types); + $string_literal_count = count($this->literal_string_types); + $float_literal_count = count($this->literal_float_types); + + if (($int_literal_count && $string_literal_count) + || ($int_literal_count && $float_literal_count) + || ($string_literal_count && $float_literal_count) + ) { + return false; + } + + if ($int_literal_count || $string_literal_count || $float_literal_count) { + $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; + } + + return $type_count === 1; + } + + /** + * @psalm-mutation-free + */ + public function isSingleAndMaybeNullable(): bool + { + $is_nullable = isset($this->types['null']); + + $type_count = count($this->types); + + if ($type_count === 1 && $is_nullable) { + return false; + } + + $int_literal_count = count($this->literal_int_types); + $string_literal_count = count($this->literal_string_types); + $float_literal_count = count($this->literal_float_types); + + if (($int_literal_count && $string_literal_count) + || ($int_literal_count && $float_literal_count) + || ($string_literal_count && $float_literal_count) + ) { + return false; + } + + if ($int_literal_count || $string_literal_count || $float_literal_count) { + $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; + } + + return ($type_count - (int) $is_nullable) === 1; + } + + /** + * @psalm-mutation-free + * @return bool true if this is an int + */ + public function isInt(bool $check_templates = false): bool + { + return count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TInt + || ($check_templates + && $type instanceof TTemplateParam + && $type->as->isInt() + ) + ) + ) === count($this->types); + } + + /** + * @psalm-mutation-free + * @return bool true if this is a float + */ + public function isFloat(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['float']) || $this->literal_float_types; + } + + /** + * @psalm-mutation-free + * @return bool true if this is a string + */ + public function isString(bool $check_templates = false): bool + { + return count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TString + || ($check_templates + && $type instanceof TTemplateParam + && $type->as->isString() + ) + ) + ) === count($this->types); + } + + /** + * @psalm-mutation-free + * @return bool true if this is a boolean + */ + public function isBool(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['bool']); + } + + /** + * @psalm-mutation-free + * @return bool true if this is an array + */ + public function isArray(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['array']); + } + + /** + * @psalm-mutation-free + * @return bool true if this is a string literal with only one possible value + */ + public function isSingleStringLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_string_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleStringLiteral is false + * @psalm-mutation-free + * @return TLiteralString the only string literal represented by this union type + */ + public function getSingleStringLiteral(): TLiteralString + { + if (count($this->types) !== 1 || count($this->literal_string_types) !== 1) { + throw new InvalidArgumentException('Not a string literal'); + } + + return reset($this->literal_string_types); + } + + /** + * @psalm-mutation-free + */ + public function allStringLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function allIntLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralInt) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function allFloatLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralFloat) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allSpecificLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TNonspecificLiteralString + && !$atomic_key_type instanceof TNonspecificLiteralInt + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralValue(): bool + { + return $this->literal_int_types + || $this->literal_string_types + || $this->literal_float_types + || isset($this->types['false']) + || isset($this->types['true']); + } + + /** + * @psalm-mutation-free + */ + public function isSingleLiteral(): bool + { + return count($this->types) === 1 + && count($this->literal_int_types) + + count($this->literal_string_types) + + count($this->literal_float_types) === 1 + ; + } + + /** + * @psalm-mutation-free + * @return TLiteralInt|TLiteralString|TLiteralFloat + */ + public function getSingleLiteral() + { + if (!$this->isSingleLiteral()) { + throw new InvalidArgumentException("Not a single literal"); + } + + return ($literal = reset($this->literal_int_types)) !== false + ? $literal + : (($literal = reset($this->literal_string_types)) !== false + ? $literal + : reset($this->literal_float_types)) + ; + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralString(): bool + { + return count($this->literal_string_types) > 0; + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralInt(): bool + { + return count($this->literal_int_types) > 0; + } + + /** + * @psalm-mutation-free + * @return bool true if this is a int literal with only one possible value + */ + public function isSingleIntLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_int_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleIntLiteral is false + * @psalm-mutation-free + * @return TLiteralInt the only int literal represented by this union type + */ + public function getSingleIntLiteral(): TLiteralInt + { + if (count($this->types) !== 1 || count($this->literal_int_types) !== 1) { + throw new InvalidArgumentException('Not an int literal'); + } + + return reset($this->literal_int_types); + } + + /** + * @param array $suppressed_issues + * @param array $phantom_classes + */ + public function check( + StatementsSource $source, + CodeLocation $code_location, + array $suppressed_issues, + array $phantom_classes = [], + bool $inferred = true, + bool $inherited = false, + bool $prevent_template_covariance = false, + ?string $calling_method_id = null + ): bool { + if ($this->checked) { + return true; + } + + $checker = new TypeChecker( + $source, + $code_location, + $suppressed_issues, + $phantom_classes, + $inferred, + $inherited, + $prevent_template_covariance, + $calling_method_id + ); + + $checker->traverseArray($this->types); + + $this->checked = true; + + return !$checker->hasErrors(); + } + + /** + * @param array $phantom_classes + */ + public function queueClassLikesForScanning( + Codebase $codebase, + ?FileStorage $file_storage = null, + array $phantom_classes = [] + ): void { + $scanner_visitor = new TypeScanner( + $codebase->scanner, + $file_storage, + $phantom_classes + ); + + $scanner_visitor->traverseArray($this->types); + } + + /** + * @param lowercase-string $fq_class_like_name + * @psalm-mutation-free + */ + public function containsClassLike(string $fq_class_like_name): bool + { + $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); + + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ + $classlike_visitor->traverseArray($this->types); + + return $classlike_visitor->matches(); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type = $this; + (new ClasslikeReplacer( + $old, + $new + ))->traverse($type); + return $type; + } + + /** @psalm-mutation-free */ + public function containsAnyLiteral(): bool + { + $literal_visitor = new ContainsLiteralVisitor(); + + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ + $literal_visitor->traverseArray($this->types); + + return $literal_visitor->matches(); + } + + /** + * @psalm-mutation-free + * @return list + */ + public function getTemplateTypes(): array + { + $template_type_collector = new TemplateTypeCollector(); + + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ + $template_type_collector->traverseArray($this->types); + + return $template_type_collector->getTemplateTypes(); + } + + /** + * @psalm-mutation-free + */ + public function equals(self $other_type, bool $ensure_source_equality = true): bool + { + if ($other_type === $this) { + return true; + } + + if ($other_type->id && $this->id && $other_type->id !== $this->id) { + return false; + } + + if ($other_type->exact_id && $this->exact_id && $other_type->exact_id !== $this->exact_id) { + return false; + } + + if ($this->possibly_undefined !== $other_type->possibly_undefined) { + return false; + } + + if ($this->had_template !== $other_type->had_template) { + return false; + } + + if ($this->possibly_undefined_from_try !== $other_type->possibly_undefined_from_try) { + return false; + } + + if ($this->from_calculation !== $other_type->from_calculation) { + return false; + } + + if ($this->initialized !== $other_type->initialized) { + return false; + } + + if ($ensure_source_equality && $this->from_docblock !== $other_type->from_docblock) { + return false; + } + + if (count($this->types) !== count($other_type->types)) { + return false; + } + + if ($this->parent_nodes !== $other_type->parent_nodes) { + return false; + } + + if ($this->different || $other_type->different) { + return false; + } + + $other_atomic_types = $other_type->types; + + foreach ($this->types as $key => $atomic_type) { + if (!isset($other_atomic_types[$key])) { + return false; + } + + if (!$atomic_type->equals($other_atomic_types[$key], $ensure_source_equality)) { + return false; + } + } + + return true; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getLiteralStrings(): array + { + return $this->literal_string_types; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getLiteralInts(): array + { + return $this->literal_int_types; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getRangeInts(): array + { + $ranges = []; + foreach ($this->getAtomicTypes() as $atomic) { + if ($atomic instanceof TIntRange) { + $ranges[$atomic->getKey()] = $atomic; + } + } + + return $ranges; + } + + /** + * @psalm-mutation-free + * @return array + */ + public function getLiteralFloats(): array + { + return $this->literal_float_types; + } + + /** + * @psalm-mutation-free + * @return list + */ + public function getChildNodeKeys(): array + { + return ['types']; + } + + /** + * @psalm-mutation-free + * @return bool true if this is a float literal with only one possible value + */ + public function isSingleFloatLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_float_types) === 1; + } + + /** + * @psalm-mutation-free + * @throws InvalidArgumentException if isSingleFloatLiteral is false + * + * @return TLiteralFloat the only float literal represented by this union type + */ + public function getSingleFloatLiteral(): TLiteralFloat + { + if (count($this->types) !== 1 || count($this->literal_float_types) !== 1) { + throw new InvalidArgumentException('Not a float literal'); + } + + return reset($this->literal_float_types); + } + + /** + * @psalm-mutation-free + */ + public function hasLiteralFloat(): bool + { + return count($this->literal_float_types) > 0; + } + + /** + * @psalm-mutation-free + */ + public function getSingleAtomic(): Atomic + { + return reset($this->types); + } + + /** + * @psalm-mutation-free + */ + public function isEmptyArray(): bool + { + return count($this->types) === 1 + && isset($this->types['array']) + && $this->types['array'] instanceof TArray + && $this->types['array']->isEmptyArray(); + } + + /** + * @psalm-mutation-free + */ + public function isUnionEmpty(): bool + { + return $this->types === []; + } +} diff --git a/tests/AlgebraTest.php b/tests/AlgebraTest.php index 6571701c436..a4b7eb8085e 100644 --- a/tests/AlgebraTest.php +++ b/tests/AlgebraTest.php @@ -83,7 +83,7 @@ public function testNegateFormulaWithUnreconcilableTerm(): void $a1 = new IsType(new TInt()); $formula = [ new Clause(['$a' => [(string)$a1 => $a1]], 1, 1), - new Clause(['$b' => [(string)$a1 => clone $a1]], 1, 2, false, false), + new Clause(['$b' => [(string)$a1 => $a1]], 1, 2, false, false), ]; $negated_formula = Algebra::negateFormula($formula); diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 76003499ced..fdf1f43914d 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -564,11 +564,20 @@ function f(array $p): void ], 'unsetTKeyedArrayOffset' => [ 'code' => ' "value"]; - unset($x["a"]); - $x[] = 5; - takesInt($x[0]);', + $x1 = ["a" => "value"]; + unset($x1["a"]); + + $x2 = ["a" => "value", "b" => "value"]; + unset($x2["a"]); + + $x3 = ["a" => "value", "b" => "value"]; + $k = "a"; + unset($x3[$k]);', + 'assertions' => [ + '$x1===' => 'array', + '$x2===' => "array{b: 'value'}", + '$x3===' => "array{b: 'value'}", + ] ], 'domNodeListAccessible' => [ 'code' => ' [ '$_arr1===' => 'non-empty-array<1, 5>', - '$_arr2===' => 'non-empty-array<1, 5>', + '$_arr2===' => 'array{1: 5}', ] ], 'accessArrayWithSingleStringLiteralOffset' => [ diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index d3b938f8833..49ab9eeb1e8 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -55,7 +55,8 @@ public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $e $custom_array_map_storage->params = [ ...array_map( function (TCallable $expected, int $offset) { - $param = new FunctionLikeParameter('fn' . $offset, false, new Union([$expected])); + $t = new Union([$expected]); + $param = new FunctionLikeParameter('fn' . $offset, false, $t, $t); $param->is_optional = false; return $param; @@ -71,10 +72,15 @@ function (TCallable $expected, int $offset) { private static function createLastArrayMapParam(Union $input_array_type): FunctionLikeParameter { - $last_array_map_param = new FunctionLikeParameter('input', false, $input_array_type); - $last_array_map_param->is_optional = false; - - return $last_array_map_param; + return new FunctionLikeParameter( + 'input', + false, + $input_array_type, + $input_array_type, + null, + null, + false + ); } /** @@ -106,13 +112,13 @@ private static function createExpectedCallable( DynamicTemplateProvider $template_provider, int $return_template_offset = 0 ): TCallable { - $expected_callable = new TCallable('callable'); - $expected_callable->params = [new FunctionLikeParameter('a', false, $input_type)]; - $expected_callable->return_type = new Union([ - $template_provider->createTemplate('T' . $return_template_offset) - ]); - - return $expected_callable; + return new TCallable( + 'callable', + [new FunctionLikeParameter('a', false, $input_type, $input_type)], + new Union([ + $template_provider->createTemplate('T' . $return_template_offset) + ]) + ); } /** diff --git a/tests/Config/Plugin/Hook/FooMethodProvider.php b/tests/Config/Plugin/Hook/FooMethodProvider.php index 222a51ed537..b5d150e0326 100644 --- a/tests/Config/Plugin/Hook/FooMethodProvider.php +++ b/tests/Config/Plugin/Hook/FooMethodProvider.php @@ -43,7 +43,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array { $method_name_lowercase = $event->getMethodNameLowercase(); if ($method_name_lowercase === 'magicmethod' || $method_name_lowercase === 'magicmethod2') { - return [new FunctionLikeParameter('first', false, Type::getString())]; + return [new FunctionLikeParameter('first', false, Type::getString(), Type::getString())]; } return null; diff --git a/tests/Config/Plugin/Hook/MagicFunctionProvider.php b/tests/Config/Plugin/Hook/MagicFunctionProvider.php index b9ec9ae1d6c..9ffa3706ccf 100644 --- a/tests/Config/Plugin/Hook/MagicFunctionProvider.php +++ b/tests/Config/Plugin/Hook/MagicFunctionProvider.php @@ -36,7 +36,7 @@ public static function doesFunctionExist(FunctionExistenceProviderEvent $event): */ public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array { - return [new FunctionLikeParameter('first', false, Type::getString())]; + return [new FunctionLikeParameter('first', false, Type::getString(), Type::getString())]; } public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 890b920c155..8dd2a40cb0b 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -437,6 +437,9 @@ public function testIssuesIndex(): void } $issues_index_contents = file($issues_index, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES); + if ($issues_index_contents === false) { + throw new UnexpectedValueException("Issues index returned false"); + } array_shift($issues_index_contents); // Remove title $issues_index_list = array_map(function (string $issues_line) { diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 16eb66e9a3a..6181f6611ec 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -755,10 +755,7 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa } } - /** - * - * @psalm-suppress UndefinedMethod - */ + /** @psalm-suppress UndefinedMethod */ public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void { if (version_compare(PHP_VERSION, '8.1.0', '>=')) { diff --git a/tests/PropertiesOfTest.php b/tests/PropertiesOfTest.php index 9f5d3668552..77d6c248d59 100644 --- a/tests/PropertiesOfTest.php +++ b/tests/PropertiesOfTest.php @@ -16,6 +16,44 @@ class PropertiesOfTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'propertiesOfIntersection' => [ + 'code' => ' + */ + function test1($a) {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test2() {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test3() {} + + /** @var i $i */ + assert($i instanceof b); + $result1 = test1($i); + $result2 = test2(); + $result3 = test3(); + ', + 'assertions' => [ + '$result1===' => 'array{a: int}', + '$result2===' => 'array{a: int}', + '$result3===' => 'array{a: int}', + ] + ], 'publicPropertiesOf' => [ 'code' => ' [ + 'code' => 'test($container); + + if ($container->expr) { + if (random_int(0, 1)) { + self::test( + $container, + ); + } + return $container->expr; + } + return 0; + } + + private static function test( + a $_, + ): void { + } + }' + ], 'noCrashTemplateInsideGenerator' => [ 'code' => 'setCallback(function() { return "b";});', - 'error_message' => 'InvalidScalarArgument', + 'error_message' => 'InvalidArgument', ], 'preventBoundsMismatchDifferentContainers' => [ 'code' => ' [ + 'code' => ' + */ + final class AObject extends MyObject {} + /** + * @extends MyMapper + */ + final class AMapper extends MyMapper {} + + + /** + * @extends MyObject + */ + final class BObject extends MyObject {} + /** + * @extends MyMapper + */ + final class BMapper extends MyMapper {} + + + /** + * Get source, asserting class type + * + * @template T as MyObject + * + * @param class-string $class + * @param AObject|BObject $source + * + * @return T + */ + function getSourceAssertType(string $class, MyObject $source): MyObject { + if (!$source instanceof $class) { + throw new RuntimeException("Invalid class!"); + } + return $source; + }' + ] ]; } diff --git a/tests/Template/PropertiesOfTemplateTest.php b/tests/Template/PropertiesOfTemplateTest.php index 35f16c8e06a..3ec94ab7563 100644 --- a/tests/Template/PropertiesOfTemplateTest.php +++ b/tests/Template/PropertiesOfTemplateTest.php @@ -51,6 +51,37 @@ class A { $cConcat = $c . "foo"; ', ], + 'propertiesOfTemplateParamWithTemplate' => [ + 'code' => ' + */ + function asArray($obj) { + /** @var properties-of */ + $properties = []; + return $properties; + } + + /** @template T */ + class A { + /** @var bool */ + private $b = true; + /** @var string */ + protected $c = "c"; + + /** @param T $a */ + public function __construct(public $a) {} + } + + $obj = new A(42); + $objAsArray = asArray($obj); + ', + 'assertions' => [ + '$objAsArray===' => 'array{a: 42, b: bool, c: string}' + ] + ], 'privatePropertiesPicksPrivate' => [ 'code' => 'from_docblock = true; $converted_types[] = $converted_type; } diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 3e8602751bc..8ab726ddeef 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -1960,7 +1960,10 @@ function foo(array &$arr): void { ], 'byRefDeeplyNestedArrayParam' => [ 'code' => '> $arr */ + /** + * @param non-empty-list> $arr + * @param-out non-empty-list> $arr + */ function foo(array &$arr): void { $b = 5; $arr[0][0] = $b; @@ -1968,7 +1971,10 @@ function foo(array &$arr): void { ], 'nestedReferencesToByRefParam' => [ 'code' => '> $arr */ + /** + * @param non-empty-list> $arr + * @param-out non-empty-list> $arr + */ function foo(array &$arr): void { $a = &$arr[0]; $b = &$a[0];