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..09f1f3a3587 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -112,6 +112,9 @@ + + verifyType + $non_existent_method_ids[0] $parts[1] @@ -289,6 +292,14 @@ $cs[0] + + + $callable + + + TCallable|TClosure|null + + $combination->array_type_params[1] @@ -311,31 +322,6 @@ array_keys($template_type_map[$template_param_name])[0] - - - VirtualClass - - - - - VirtualFunction - - - - - VirtualInterface - - - - - VirtualTrait - - - - - VirtualConst - - array_keys($template_type_map[$value])[0] @@ -345,6 +331,26 @@ $this->type_params[1] + + replaceTypeParams + replaceTypeParams + replaceTypeParams + + + + + replaceAs + + + + + $allow_mutations + $failed_reconciliation + $from_template_default + $has_mutations + $initialized_class + $reference_free + @@ -352,6 +358,17 @@ $type[0][0] + + + $ignore_isset + + + + + allFloatLiterals + allFloatLiterals + + $subNodes['expr'] diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 286cbf72ee4..8b894dad1d8 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -485,7 +485,10 @@ 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) + ->freeze(); if ($new_type && $new_type->from_docblock) { $existing_type->setFromDocblock(); @@ -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 ff9bed24448..f8148f1c183 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -758,7 +758,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, @@ -791,12 +791,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 @@ -1294,7 +1294,11 @@ 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] = + $this->inferred_property_types[$property_name] + ->getBuilder() + ->addType(new TNull()) + ->freeze(); $this->inferred_property_types[$property_name]->setFromDocblock(); } } @@ -1543,7 +1547,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/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 29fc51a3080..b45ef64e430 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -992,8 +992,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( @@ -1836,7 +1837,7 @@ private function getFunctionInformation( 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, @@ -1844,9 +1845,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); } } 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/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..a3c55b1b1c0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -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 d49901cc710..c1ba0f7539b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -66,6 +66,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; @@ -803,10 +804,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)) { @@ -819,14 +817,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)) { @@ -960,15 +956,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, @@ -977,9 +973,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; @@ -992,7 +992,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) { @@ -1012,7 +1012,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) === '()'; @@ -1081,7 +1081,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) { @@ -1094,15 +1094,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, @@ -1111,9 +1111,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; @@ -1126,7 +1130,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( @@ -1136,7 +1140,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) === '()'; @@ -1188,7 +1192,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); @@ -1198,7 +1202,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( @@ -1243,10 +1247,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)]; } @@ -3337,7 +3341,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 )) @@ -3525,8 +3529,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..e0bd6b08401 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,7 +297,7 @@ private static function updateTypeWithKeyValues( $has_matching_objectlike_property = false; $has_matching_string = false; - $child_stmt_type = clone $child_stmt_type; + $child_stmt_type = $child_stmt_type->getBuilder(); foreach ($child_stmt_type->getAtomicTypes() as $type) { if ($type instanceof TTemplateParam) { @@ -351,7 +353,7 @@ private static function updateTypeWithKeyValues( } } - $child_stmt_type->bustCache(); + $child_stmt_type = $child_stmt_type->freeze(); if (!$has_matching_objectlike_property && !$has_matching_string) { if (count($key_values) === 1) { @@ -511,9 +513,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 +557,7 @@ private static function updateArrayAssignmentChildType( ] ); - TemplateInferredTypeReplacer::replace( + $value_type = TemplateInferredTypeReplacer::replace( $value_type, $template_result, $codebase @@ -742,17 +742,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 +890,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/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c787b2443c9..c185eb579d5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -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/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index a4f3924df52..9539eceaf7f 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, @@ -212,8 +215,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, @@ -225,8 +227,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(); $has_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..8774fbf5227 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -44,6 +44,7 @@ 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) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index e87585606f5..34cf5152e7e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -834,6 +834,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 +855,7 @@ public static function verifyType( $input_type->addType($candidate_callable); } } + $input_type = $input_type->freeze(); } $union_comparison_results = new TypeComparisonResult(); @@ -1384,9 +1386,10 @@ 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()) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 68b90dc676d..533cda89821 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, @@ -601,7 +599,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 @@ -769,7 +767,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; } @@ -1234,7 +1232,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 +1257,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 +1384,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 +1614,7 @@ private static function getProvisionalTemplateResultForFunctionLike( $calling_class_storage->final ?? false ); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $fleshed_out_param_type, $template_result, $codebase, @@ -1794,7 +1794,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 a34f9e6d030..88b6df7a3a6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -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, @@ -508,7 +516,7 @@ 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_type = $context->vars_in_scope[$var_id]->getBuilder(); $array_atomic_types = $array_type->getAtomicTypes(); @@ -574,6 +582,7 @@ public static function handleByRefArrayAdjustment( } } + $array_type = $array_type->freeze(); $context->removeDescendents($var_id, $array_type); $context->vars_in_scope[$var_id] = $array_type; } @@ -582,13 +591,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 +732,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 +761,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, @@ -861,7 +869,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/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index ed207aded41..e5be6f5244e 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 @@ -501,8 +502,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 +612,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..9d6401c66c0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -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..db15ccafa41 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -400,7 +400,7 @@ public static function analyze( ) { $keys_to_remove = []; - $class_type = clone $class_type; + $class_type = $class_type->getBuilder(); foreach ($class_type->getAtomicTypes() as $key => $type) { if (!$type instanceof TNamedObject) { @@ -418,7 +418,7 @@ public static function analyze( $context->removeVarFromConflictingClauses($lhs_var_id, null, $statements_analyzer); - $context->vars_in_scope[$lhs_var_id] = $class_type; + $context->vars_in_scope[$lhs_var_id] = $class_type->freeze(); } return true; 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 619868af958..50946b5a501 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -469,13 +469,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( @@ -720,7 +720,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) { @@ -730,7 +730,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 f7e0d7e09aa..55445238f9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -567,7 +567,7 @@ private static function getMethodReturnType( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index ade1205332c..8640158444e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -756,9 +756,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 +770,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])) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 48795d26f34..1e7624f9feb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -78,7 +78,7 @@ public static function analyze( } if ($maybe_type->hasBool()) { - $casted_type = clone $maybe_type; + $casted_type = $maybe_type->getBuilder(); if (isset($casted_type->getAtomicTypes()['bool'])) { $casted_type->addType(new TLiteralInt(0)); $casted_type->addType(new TLiteralInt(1)); @@ -95,7 +95,7 @@ public static function analyze( $casted_type->removeType('false'); if ($casted_type->isInt()) { - $valid_int_type = $casted_type; + $valid_int_type = $casted_type->freeze(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 57bfbdce1f4..27e89050bc4 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,17 @@ 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_original, + Union &$offset_type_original, bool $in_assignment, ?string $extended_var_id, Context $context, PhpParser\Node\Expr $assign_value = null, Union $replacement_type = null ): Union { + $array_type = $array_type_original->getBuilder(); + $offset_type = $offset_type_original->getBuilder(); + $codebase = $statements_analyzer->getCodebase(); $has_array_access = false; @@ -847,6 +853,9 @@ public static function getArrayAccessTypeGivenOffset( } } + $array_type_original = $array_type->freeze(); + $offset_type_original = $offset_type->freeze(); + if ($array_access_type === null) { // shouldn’t happen, but don’t crash return Type::getMixed(); @@ -864,7 +873,7 @@ public static function getArrayAccessTypeGivenOffset( } 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 +921,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 +970,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 +996,7 @@ public static function replaceOffsetTypeWithInts(Union $offset_type): Union } } - return $offset_type; + return $offset_type->freeze(); } /** @@ -1085,11 +1084,11 @@ private static function handleArrayAccessOnArray( bool $in_assignment, Atomic &$type, array &$key_values, - Union $array_type, + MutableUnion $array_type, string $type_string, PhpParser\Node\Expr\ArrayDimFetch $stmt, ?Union $replacement_type, - Union &$offset_type, + MutableUnion $offset_type, Atomic $original_type, Codebase $codebase, ?string $extended_var_id, @@ -1143,7 +1142,7 @@ private static function handleArrayAccessOnArray( } } - $offset_type = self::replaceOffsetTypeWithInts($offset_type); + $offset_type = self::replaceOffsetTypeWithInts($offset_type->freeze())->getBuilder(); if ($type instanceof TList && (($in_assignment && $stmt->dim) @@ -1226,10 +1225,10 @@ private static function handleArrayAccessOnTArray( Codebase $codebase, Context $context, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, + MutableUnion $array_type, ?string $extended_var_id, TArray $type, - Union $offset_type, + MutableUnion $offset_type, bool $in_assignment, array &$expected_offset_types, ?Union &$array_access_type, @@ -1241,7 +1240,7 @@ private static function handleArrayAccessOnTArray( if ($type->isEmptyArray()) { $type->type_params[0] = $offset_type->isMixed() ? Type::getArrayKey() - : $offset_type; + : $offset_type->freeze(); } } elseif (!$type->isEmptyArray()) { $expected_offset_type = $type->type_params[0]->hasMixed() @@ -1278,7 +1277,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 +1335,7 @@ private static function handleArrayAccessOnTArray( if (UnionTypeComparator::canExpressionTypesBeIdentical( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type )) { $has_valid_offset = true; @@ -1378,7 +1377,7 @@ private static function handleArrayAccessOnTArray( private static function handleArrayAccessOnClassStringMap( Codebase $codebase, TClassStringMap $type, - Union $offset_type, + MutableUnion $offset_type, ?Union $replacement_type, ?Union &$array_access_type ): void { @@ -1440,7 +1439,7 @@ private static function handleArrayAccessOnClassStringMap( $expected_value_param_get = clone $type->value_param; - TemplateInferredTypeReplacer::replace( + $expected_value_param_get = TemplateInferredTypeReplacer::replace( $expected_value_param_get, $template_result_get, $codebase @@ -1449,7 +1448,7 @@ private static function handleArrayAccessOnClassStringMap( if ($replacement_type) { $expected_value_param_set = clone $type->value_param; - TemplateInferredTypeReplacer::replace( + $replacement_type = TemplateInferredTypeReplacer::replace( $replacement_type, $template_result_set, $codebase @@ -1483,11 +1482,11 @@ 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, + MutableUnion $array_type, array &$expected_offset_types, string $type_string, bool &$has_valid_offset @@ -1589,7 +1588,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 +1599,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, $key_type, - $offset_type, + $offset_type->freeze(), true, $offset_type->ignore_falsable_issues ); @@ -1620,7 +1619,7 @@ 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; @@ -1682,7 +1681,7 @@ private static function handleArrayAccessOnList( Codebase $codebase, PhpParser\Node\Expr\ArrayDimFetch $stmt, TList $type, - Union $offset_type, + MutableUnion $offset_type, ?string $extended_var_id, array $key_values, Context $context, @@ -1897,7 +1896,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 +1959,7 @@ private static function handleArrayAccessOnString( if (!UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $valid_offset_type, true )) { @@ -1981,7 +1980,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..6136d708155 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -743,7 +743,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/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index cb0c56810f5..1549caeb5ce 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -218,7 +218,7 @@ public static function infer( return null; } - $invalidTypes = clone $stmt_expr_type; + $invalidTypes = $stmt_expr_type->getBuilder(); $invalidTypes->removeType('string'); $invalidTypes->removeType('int'); $invalidTypes->removeType('float'); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index 4847e04599b..54692df7dde 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -177,7 +177,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/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 2c9a1644902..45347e0dc13 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -58,7 +58,7 @@ public static function analyze( ); if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) { - $root_type = clone $context->vars_in_scope[$root_var_id]; + $root_type = $context->vars_in_scope[$root_var_id]->getBuilder(); foreach ($root_type->getAtomicTypes() as $atomic_root_type) { if ($atomic_root_type instanceof TKeyedArray) { @@ -126,7 +126,7 @@ public static function analyze( } } - $context->vars_in_scope[$root_var_id] = $root_type; + $context->vars_in_scope[$root_var_id] = $root_type->freeze(); $context->removeVarFromConflictingClauses( $root_var_id, diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 9a78716c0dc..d7f760e396b 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1460,9 +1460,9 @@ 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 = $type->getBuilder(); - $type->replaceClassLike($old_fq_class_name, $new_fq_class_name); + $type = $type->replaceClassLike($old_fq_class_name, $new_fq_class_name)->freeze(); $bounds = $type_location->getSelectionBounds(); @@ -1500,9 +1500,9 @@ public function handleDocblockTypeInMigration( $destination_class = $codebase->classes_to_move[$fq_class_name_lc]; if ($type->containsClassLike($fq_class_name_lc)) { - $type = clone $type; + $type = $type->getBuilder(); - $type->replaceClassLike($fq_class_name_lc, $destination_class); + $type = $type->replaceClassLike($fq_class_name_lc, $destination_class)->freeze(); } $this->airliftClassDefinedDocblockType( diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index ba56971366e..262101a74f1 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -494,7 +494,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,7 +520,7 @@ public static function localizeType( return $type; } - $type = clone $type; + $type = $type->getBuilder(); foreach ($type->getAtomicTypes() as $key => $atomic_type) { if ($atomic_type instanceof TTemplateParam @@ -618,9 +618,7 @@ public static function localizeType( } } - $type->bustCache(); - - return $type; + return $type->freeze(); } /** @@ -889,12 +887,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/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 2a99fff148d..9987fe6d70f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1629,7 +1629,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(); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 084a6ce1bb7..d79bac47b01 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -846,7 +846,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(); @@ -888,7 +888,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; @@ -1010,7 +1010,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(); } } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 5583040e62b..9f7c130bda5 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -839,7 +839,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(); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 9265d3c8b15..b359c60ec2e 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -169,7 +169,7 @@ public static function resolve( } 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..5cc17218c32 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -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/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/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/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/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 3778bbb5a45..e62b04d849f 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -402,14 +402,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; } } @@ -943,6 +941,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 +957,7 @@ private static function handleLiteralEquality( $can_be_equal = true; } } + $existing_var_type = $existing_var_type->freeze(); if ($var_id && $code_location @@ -1615,8 +1615,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/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..d3a93bd60eb 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; @@ -408,7 +409,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 +441,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; diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5e3aff80828..a2423abf3b3 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -115,7 +115,7 @@ private static function getIntersectionTypes(Atomic $type_part): array $type_part = clone $type_part; $extra_types = $type_part->extra_types; - $type_part->extra_types = null; + $type_part->extra_types = []; $extra_types[$type_part->getKey()] = $type_part; 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..0ca73f5c665 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -504,6 +504,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 +555,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 +571,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']; @@ -665,7 +667,7 @@ private static function reconcileNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -675,6 +677,7 @@ 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']; @@ -701,7 +704,7 @@ private static function reconcileExactlyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1177,6 +1180,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(); @@ -1631,6 +1635,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; @@ -1720,7 +1725,7 @@ private static function reconcileIsGreaterThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1738,6 +1743,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; @@ -1822,7 +1828,7 @@ private static function reconcileIsLessThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -2354,6 +2360,7 @@ private static function reconcileTruthyOrNonEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $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 @@ -2412,7 +2419,7 @@ private static function reconcileTruthyOrNonEmpty( $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } $existing_var_type->possibly_undefined = false; @@ -2501,7 +2508,7 @@ private static function reconcileTruthyOrNonEmpty( } if ($existing_var_type->isSingle()) { - return $existing_var_type; + return $existing_var_type->freeze(); } } @@ -2532,7 +2539,7 @@ private static function reconcileTruthyOrNonEmpty( } assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index ff1b3dcdfad..e08262cea9b 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,6 +589,7 @@ private static function reconcileNull( int &$failed_reconciliation, bool $is_equality ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; @@ -633,7 +636,7 @@ private static function reconcileNull( } if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + return $existing_var_type->freeze(); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -660,6 +663,7 @@ private static function reconcileFalse( $old_var_type_string = $existing_var_type->getId(); $did_remove_type = $existing_var_type->hasScalar(); + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('false')) { $did_remove_type = true; $existing_var_type->removeType('false'); @@ -703,7 +707,7 @@ private static function reconcileFalse( } if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + return $existing_var_type->freeze(); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -728,6 +732,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 +791,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } if ($existing_var_type->hasType('bool')) { @@ -894,7 +899,7 @@ private static function reconcileFalsyOrEmpty( } assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1621,7 +1626,9 @@ private static function reconcileResource( if ($existing_var_type->hasType('resource')) { $did_remove_type = true; + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_type->removeType('resource'); + $existing_var_type = $existing_var_type->freeze(); } foreach ($existing_var_type->getAtomicTypes() as $type) { @@ -1685,6 +1692,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; @@ -1773,7 +1781,7 @@ private static function reconcileIsLessThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1789,6 +1797,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; @@ -1874,6 +1883,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..d293baecdd6 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -47,17 +47,18 @@ 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 +69,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 +114,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 +176,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 +187,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 +200,42 @@ 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; } } - } - $union->bustCache(); + if ($should_set) { + $types []= $atomic_type; + } + } 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 +260,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,7 +383,6 @@ private static function replaceTemplatePropertiesOf( } return new TPropertiesOf( - (string) $classlike_type, clone $classlike_type, $atomic_type->visibility_filter ); @@ -410,7 +410,7 @@ private static function replaceConditional( $atomic_type = clone $atomic_type; if ($template_type) { - self::replace( + $atomic_type->as_type = self::replace( $atomic_type->as_type, $template_result, $codebase @@ -478,7 +478,7 @@ private static function replaceConditional( ) ]; - self::replace( + $if_template_type = self::replace( $if_template_type, $refined_template_result, $codebase @@ -508,7 +508,7 @@ private static function replaceConditional( ) ]; - self::replace( + $else_template_type = self::replace( $else_template_type, $refined_template_result, $codebase @@ -517,13 +517,13 @@ private static function replaceConditional( } if (!$if_template_type && !$else_template_type) { - self::replace( + $atomic_type->if_type = self::replace( $atomic_type->if_type, $template_result, $codebase ); - self::replace( + $atomic_type->else_type = self::replace( $atomic_type->else_type, $template_result, $codebase diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 5b3341d5570..8ed8450b820 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) * @@ -84,7 +117,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 +126,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; } @@ -341,7 +374,6 @@ private static function handleAtomicStandin( } $atomic_type = new TPropertiesOf( - (string) $classlike_type, clone $classlike_type, $atomic_type->visibility_filter ); @@ -766,7 +798,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,6 +809,7 @@ private static function handleTemplateParamStandin( } } } + $generic_param = $generic_param->freeze(); if ($add_lower_bound) { return array_values($generic_param->getAtomicTypes()); @@ -858,7 +891,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 +902,7 @@ private static function handleTemplateParamStandin( } } } + $generic_param = $generic_param->freeze(); $upper_bound = $template_result->upper_bounds [$param_name_key] @@ -1258,7 +1292,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..b8e3f18ca0f 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -511,7 +511,7 @@ private static function scrapeTypeProperties( ) { if ($type->extra_types) { $combination->extra_types = array_merge( - $combination->extra_types ?: [], + $combination->extra_types, $type->extra_types ); } diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index babaf30aa77..5d831be2cef 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; @@ -130,6 +130,7 @@ public static function expandUnion( * * @return non-empty-list * + * @psalm-suppress ConflictingReferenceConstraint Ultimately, the output type is always an Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( @@ -644,9 +645,8 @@ private static function expandNamedObject( || $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; + $extra_static = $static_class_type->extra_types; + $cloned_static = $static_class_type->setIntersectionTypes([]); if ($cloned_static->getKey(false) !== $return_type->getKey(false)) { $return_type->extra_types[$static_class_type->getKey()] = clone $cloned_static; @@ -892,62 +892,65 @@ 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]; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 71a2ddd1e58..711375ba150 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -255,7 +255,7 @@ public static function getTypeFromTree( ); if ($non_nullable_type instanceof Union) { - $non_nullable_type->addType(new TNull); + $non_nullable_type = $non_nullable_type->getBuilder()->addType(new TNull)->freeze(); return $non_nullable_type; } @@ -396,7 +396,7 @@ public static function getTypeFromTree( private static function getGenericParamClass( string $param_name, - Union $as, + Union &$as, string $defining_class ): TTemplateParamClass { if ($as->hasMixed()) { @@ -430,7 +430,7 @@ private static function getGenericParamClass( $t->type_params ); - $as->substitute(new Union([$t]), new Union([$traversable])); + $as = $as->getBuilder()->substitute(new Union([$t]), new Union([$traversable]))->freeze(); return new TTemplateParamClass( $param_name, @@ -703,6 +703,12 @@ 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, @@ -723,7 +729,6 @@ private static function getTypeFromGenericTree( } return new TPropertiesOf( - $param_name, $param_union_types[0], TPropertiesOf::filterForTokenName($generic_type_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..f333dbeba93 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -144,6 +144,16 @@ public function getId(): string . ($this->is_optional ? '=' : ''); } + public function replaceType(Union $type): self + { + if ($this->type === $type) { + return $this; + } + $cloned = clone $this; + $cloned->type = $type; + return $cloned; + } + public function __clone() { if ($this->type) { 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 8508caa2c15..2e9670fd6af 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -45,6 +45,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; @@ -601,13 +602,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 @@ -783,11 +787,9 @@ private static function intersectAtomicTypes( $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) { + $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] + = clone $wider_type_intersection_type; } } diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 126ac673269..b95b789f0cb 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -20,7 +20,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; @@ -113,6 +112,26 @@ abstract class Atomic implements TypeNode * @param array $type_aliases */ public static function create( + string $value, + ?int $analysis_php_version_id = null, + array $template_type_map = [], + array $type_aliases = [], + ?int $offset_start = null, + ?int $offset_end = null, + ?string $text = null + ): 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; + return $result; + } + /** + * @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 = [], @@ -354,7 +373,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() ) ) @@ -517,77 +536,12 @@ public function getChildNodes(): array return []; } - public function replaceClassLike(string $old, string $new): void + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self { - 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); - } - } + return $this; } final public function __toString(): string @@ -660,6 +614,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, @@ -672,14 +629,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..328c4a01ce2 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -187,7 +187,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 +201,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 +218,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 +231,16 @@ public function replaceTemplateTypesWithStandins( !$add_lower_bound, null, $depth - ); + )); + $replaced = $replaced || $new_param !== $param; + $param = $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,42 +253,88 @@ 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 &$param) { + if ($param->type) { + $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( + $param->type, + $template_result, + $codebase + )); + $replaced = $replaced || $new_param !== $param; + $param = $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 array{list|null, Union|null}|null + */ + protected function replaceCallableClassLike(string $old, string $new): ?array + { + $replaced = false; + + $params = $this->params; + if ($params) { + foreach ($params as &$param) { + if ($param->type) { + $new_param = $param->replaceType($param->type->replaceClassLike($old, $new)); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; + } + } + } + + $return_type = $this->return_type; + if ($return_type) { + $return_type = $return_type->replaceClassLike($old, $new); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; } + return null; } /** * @return list */ - public function getChildNodes(): array + protected function getCallableChildNodes(): array { $child_nodes = []; diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index ba4df1e9873..d92861d1355 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,31 @@ use function strpos; use function substr; +/** + * @template TTypeParams as array + */ trait GenericTrait { + /** + * @var TTypeParams + */ + public array $type_params; + + /** + * @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 = ''; @@ -147,14 +169,9 @@ public function __clone() } /** - * @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 +182,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 +202,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 +225,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 +244,41 @@ 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; - } + return $type_params === $this->type_params ? null : $type_params; + } - if ($this instanceof TGenericObject || $this instanceof TIterable) { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsClassLike(string $old, string $new): ?array + { + $type_params = $this->type_params; + foreach ($type_params as &$type_param) { + $type_param = $type_param->replaceClassLike($old, $new); } + 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..3f19aa9b9bd 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -3,19 +3,21 @@ 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; trait HasIntersectionTrait { /** - * @var array|null + * @var array */ - public $extra_types; + public array $extra_types = []; /** * @param array $aliased_classes @@ -49,26 +51,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 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 + 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 +115,65 @@ 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; + } + + /** + * @return array|null + */ + protected function replaceIntersectionClassLike(string $old, string $new): ?array + { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $extra_type) { + $extra_type = $extra_type->replaceClassLike($old, $new); + $new_types[$extra_type->getKey()] = $extra_type; + } + return $new_types === $this->extra_types ? null : $new_types; } } diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index 1a29e4d27a4..1d77db1e5ef 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -14,10 +14,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..208ab12da29 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; @@ -13,12 +16,10 @@ */ class TArray extends Atomic { - use GenericTrait; - /** - * @var array{Union, Union} + * @use GenericTrait */ - public $type_params; + use GenericTrait; /** * @var string @@ -96,4 +97,75 @@ public function isEmptyArray(): bool { return $this->type_params[1]->isNever(); } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike($old, $new); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @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 getChildNodes(): array + { + return $this->type_params; + } } diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index c0dc900af9e..8f4420bd1b6 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.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; /** @@ -32,4 +35,79 @@ 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 + ); + } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->replaceCallableClassLike($old, $new); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + + public function getChildNodes(): array + { + return $this->getCallableChildNodes(); + } } diff --git a/src/Psalm/Type/Atomic/TCallableString.php b/src/Psalm/Type/Atomic/TCallableString.php index 1f52a5da8a5..a3dc8036333 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`. + * */ final class TCallableString extends TNonFalsyString { diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 79bd6497246..d85492f69a9 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -5,6 +5,8 @@ use Psalm\Type; use Psalm\Type\Atomic; +use function strtolower; + /** * Denotes a class constant whose value might not yet be known. */ @@ -22,6 +24,20 @@ public function __construct(string $fq_classlike_name, string $const_name) $this->const_name = $const_name; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if (strtolower($this->fq_classlike_name) === $old) { + return new TClassConstant( + $new, + $this->const_name + ); + } + return $this; + } + public function getKey(bool $include_extra = true): string { return 'class-constant(' . $this->fq_classlike_name . '::' . $this->const_name . ')'; diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index 1124c9e6a78..e040bf4a54e 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -42,12 +42,31 @@ 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 + ) { $this->as = $as; $this->as_type = $as_type; + $this->is_loaded = $is_loaded; + $this->is_interface = $is_interface; + $this->is_enum = $is_enum; + } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if ($this->as !== 'object' && strtolower($this->as) === $old) { + $cloned = clone $this; + $cloned->as = $new; + return $cloned; + } + return $this; } - public function getKey(bool $include_extra = true): string { if ($this->is_interface) { @@ -133,6 +152,9 @@ public function getChildNodes(): array return $this->as_type ? [$this->as_type] : []; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -144,11 +166,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 +180,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 +196,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..63e4faa96a1 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -39,9 +39,9 @@ final class TClassStringMap extends Atomic */ public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param) { - $this->value_param = $value_param; $this->param_name = $param_name; $this->as_type = $as_type; + $this->value_param = $value_param; } public function getId(bool $exact = true, bool $nested = false): string @@ -117,6 +117,9 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -128,10 +131,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,23 +173,35 @@ 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 diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index fda5f17726e..f21afe32c09 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -2,6 +2,16 @@ 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; +use function strtolower; + /** * Represents a closure where we know the return type and params */ @@ -12,8 +22,129 @@ 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 = [] + ) { + $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; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->replaceCallableClassLike($old, $new); + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + strtolower($this->value) === $old ? $new : $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 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 getChildNodes(): array + { + return array_merge(parent::getChildNodes(), $this->getCallableChildNodes()); + } } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 070f7ac2d71..e9caef03ddc 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -124,14 +124,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..c1b1b91fd41 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -2,7 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; use Psalm\Type\Union; /** @@ -51,7 +50,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..e732cd3dbbe 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -2,8 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a string whose value is that of a type found by get_debug_type($var) */ @@ -34,7 +32,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/TDependentListKey.php b/src/Psalm/Type/Atomic/TDependentListKey.php index 22f2e1c95bc..076217fe8c0 100644 --- a/src/Psalm/Type/Atomic/TDependentListKey.php +++ b/src/Psalm/Type/Atomic/TDependentListKey.php @@ -2,8 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a list key created from foreach ($list as $key => $value) */ @@ -39,7 +37,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/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 44f885e0757..ea0d00d343f 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; @@ -9,6 +12,7 @@ use function count; use function implode; use function strrpos; +use function strtolower; use function substr; /** @@ -16,12 +20,10 @@ */ final class TGenericObject extends TNamedObject { - use GenericTrait; - /** - * @var non-empty-list + * @use GenericTrait> */ - public $type_params; + use GenericTrait; /** @var bool if the parameters have been remapped to another class */ public $remapped_params = false; @@ -29,15 +31,24 @@ 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 = [] + ) { 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; } public function getKey(bool $include_extra = true): string @@ -105,6 +116,101 @@ public function getAssertionString(): string public function getChildNodes(): array { - return array_merge($this->type_params, $this->extra_types ?? []); + return array_merge(parent::getChildNodes(), $this->type_params); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike($old, $new); + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + strtolower($this->value) === $old ? $new : $this->value, + $type_params ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $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( + $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 + { + $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/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index ed8462a5924..eca66a1b89f 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -2,11 +2,15 @@ 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 array_values; use function count; use function implode; use function substr; @@ -17,12 +21,10 @@ final class TIterable extends Atomic { use HasIntersectionTrait; - use GenericTrait; - /** - * @var array{Union, Union} + * @use GenericTrait */ - public $type_params; + use GenericTrait; /** * @var string @@ -35,16 +37,18 @@ 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 = []) { - 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; } public function getKey(bool $include_extra = true): string @@ -115,6 +119,94 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool public function getChildNodes(): array { - return array_merge($this->type_params, $this->extra_types ?? []); + return array_merge($this->type_params, array_values($this->extra_types)); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike( + $old, + $new + ); + $intersection = $this->replaceIntersectionClassLike( + $old, + $new + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $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/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b92926ffb1b..0235fba89b3 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -77,10 +77,47 @@ 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 + ) { $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; + } + + /** + * @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; + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $properties = $this->properties; + foreach ($properties as &$property_type) { + $property_type = $property_type->replaceClassLike($old, $new); + } + return $this->setProperties($properties); } public function getId(bool $exact = true, bool $nested = false): string @@ -276,6 +313,9 @@ public function getKey(bool $include_extra = true): string return static::KEY; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -287,10 +327,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 +339,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $property = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -315,20 +355,35 @@ 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 &$property) { + $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + if ($properties !== $this->properties) { + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + return $this; } public function getChildNodes(): array diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 44d6578ee41..63c334cf6a1 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -18,6 +18,7 @@ * - its keys are integers * - they start at 0 * - they are consecutive and go upwards (no negative int) + * */ class TList extends Atomic { @@ -37,6 +38,19 @@ public function __construct(Union $type_param) $this->type_param = $type_param; } + /** + * @return static + */ + public function replaceTypeParam(Union $type_param): self + { + if ($type_param === $this->type_param) { + return $this; + } + $cloned = clone $this; + $cloned->type_param = $type_param; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { return static::KEY . '<' . $this->type_param->getId($exact) . '>'; @@ -100,6 +114,9 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -111,10 +128,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 +170,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 diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index 6339a5757b3..735d45ba3ac 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -61,6 +61,17 @@ public function getAssertionString(): string return $this->getKey(); } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if (strtolower($this->value) === $old) { + return new static($new, $this->definite_class); + } + return $this; + } + /** * @param array $aliased_classes */ diff --git a/src/Psalm/Type/Atomic/TLowercaseString.php b/src/Psalm/Type/Atomic/TLowercaseString.php index a9eecb9f362..92afd724299 100644 --- a/src/Psalm/Type/Atomic/TLowercaseString.php +++ b/src/Psalm/Type/Atomic/TLowercaseString.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +/** + */ 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..1fb883003b1 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -6,6 +6,7 @@ /** * Denotes the `mixed` type, used when you don’t know the type of an expression. + * */ class TMixed extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 88643acd73f..77707a699ab 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -3,13 +3,16 @@ 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 function array_map; +use function array_values; use function implode; use function strrpos; +use function strtolower; use function substr; /** @@ -37,9 +40,14 @@ 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 = [] + ) { if ($value[0] === '\\') { $value = substr($value, 1); } @@ -47,6 +55,7 @@ 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; } public function getKey(bool $include_extra = true): string @@ -134,15 +143,75 @@ 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 replaceClassLike(string $old, string $new): self + { + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$intersection && strtolower($this->value) !== $old) { + return $this; + } + $cloned = clone $this; + if (strtolower($cloned->value) === $old) { + $cloned->value = $new; + } + $cloned->extra_types = $intersection ?? $this->extra_types; + return $cloned; + } + + /** + * @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; } + /** + * @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 getChildNodes(): array { - return $this->extra_types ?? []; + return array_values($this->extra_types); } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index d81b5dfe9c8..ed24494a74d 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -2,6 +2,8 @@ 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. @@ -22,4 +24,21 @@ 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' + ) { + $this->type_params = $type_params; + $this->count = $count; + $this->min_count = $min_count; + $this->value = $value; + } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index 9e6892ba855..3a73f9790f2 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Represents a non-empty list */ @@ -20,6 +22,19 @@ 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) + { + $this->type_param = $type_param; + $this->count = $count; + $this->min_count = $min_count; + } + public function getAssertionString(): string { return 'non-empty-list'; diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 81d3fc76f66..032029bec6f 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -39,11 +39,13 @@ 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 = []) { $this->properties = $properties; $this->methods = $methods; + $this->extra_types = $extra_types; } public function getId(bool $exact = true, bool $nested = false): string @@ -170,6 +172,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 +186,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 +198,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 +214,75 @@ 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 replaceClassLike(string $old, string $new): self + { + $properties = $this->properties; + foreach ($properties as &$property) { + $property = $property->replaceClassLike($old, $new); + } + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$intersection && $properties === $this->properties) { + 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 ($this->properties as &$property) { + $property = 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 { - return array_merge($this->properties, $this->extra_types !== null ? array_values($this->extra_types) : []); + return array_merge($this->properties, array_values($this->extra_types)); } public function getAssertionString(): string diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index b3e009c6c92..7cbfbdee376 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -9,8 +9,9 @@ * their apropriate types as values. * * @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of' + * */ -class TPropertiesOf extends Atomic +final class TPropertiesOf extends Atomic { // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are @@ -19,10 +20,6 @@ class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - /** - * @var string - */ - public $fq_classlike_name; /** * @var TNamedObject */ @@ -45,6 +42,17 @@ public static function tokenNames(): array ]; } + /** + * @param self::VISIBILITY_*|null $visibility_filter + */ + public function __construct( + TNamedObject $classlike_type, + ?int $visibility_filter + ) { + $this->classlike_type = $classlike_type; + $this->visibility_filter = $visibility_filter; + } + /** * @param TokenName $tokenName * @return self::VISIBILITY_*|null @@ -81,16 +89,18 @@ public static function tokenNameForFilter(?int $visibility_filter): string } /** - * @param self::VISIBILITY_*|null $visibility_filter + * @return static */ - 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 replaceClassLike(string $old, string $new): self + { + $replaced = $this->classlike_type->replaceClassLike($old, $new); + if ($replaced === $this->classlike_type) { + return $this; + } + return new static( + $replaced, + $this->visibility_filter + ); } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index ac3edfd4717..f408ba22b94 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,8 @@ use Psalm\Type\Atomic; +/** + */ final class TTemplateIndexedAccess extends Atomic { /** diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index bb2f5f947e0..26ef36b955b 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -81,14 +81,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..eca94f16c16 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -8,6 +8,8 @@ use Psalm\Type\Union; use function array_map; +use function array_merge; +use function array_values; use function implode; /** @@ -32,11 +34,31 @@ 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 = []) { $this->param_name = $param_name; $this->as = $extends; $this->defining_class = $defining_class; + $this->extra_types = $extra_types; + } + + /** + * @return static + */ + public function replaceAs(Union $as): self + { + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $as, + $this->defining_class, + $this->extra_types + ); } public function getKey(bool $include_extra = true): string @@ -115,7 +137,7 @@ public function toNamespacedString( public function getChildNodes(): array { - return [$this->as]; + return array_merge([$this->as], array_values($this->extra_types)); } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -123,10 +145,40 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $intersection = $this->replaceIntersectionClassLike($old, $new); + $replaced = $this->as->replaceClassLike($old, $new); + if (!$intersection && $replaced === $this->as) { + return $this; + } + return new static( + $this->param_name, + $replaced, + $this->defining_class, + $intersection ?? $this->extra_types + ); + } + + /** + * @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; + } + return new static( + $this->param_name, + $this->as, + $this->defining_class, + $intersection + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5a1ac50e5b8..8caf397ee0a 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -76,14 +76,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..adcb287d559 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -81,14 +81,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/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 8e70ff71951..58853597c99 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -7,6 +7,8 @@ use function array_map; use function implode; +/** + */ final class TTypeAlias extends Atomic { /** diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php new file mode 100644 index 00000000000..85df70b29da --- /dev/null +++ b/src/Psalm/Type/MutableUnion.php @@ -0,0 +1,485 @@ + + */ + 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; + + /** + * @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; + } + + 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; + } + + 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 bustCache(): void + { + $this->id = null; + $this->exact_id = null; + } + + /** + * @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; + } + + + public function replaceClassLike(string $old, string $new): self + { + foreach ($this->types as $key => $atomic_type) { + $atomic_type = $atomic_type->replaceClassLike($old, $new); + + $this->removeType($key); + $this->addType($atomic_type); + } + return $this; + } + + public function getBuilder(): self + { + return $this; + } + + 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; + } + $union->{$key} = $value; + } + return $union; + } +} diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 19b706bcb62..7fd5fb055d2 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -283,7 +283,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 +715,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 +731,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 +968,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, @@ -1161,7 +1166,7 @@ private static function adjustTKeyedArrayType( $base_atomic_type->properties[$array_key_offset] = clone $result_type; } - $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,8 +1186,9 @@ private static function adjustTKeyedArrayType( } } - protected static function refineArrayKey(Union $key_type): void + protected static function refineArrayKey(Union &$key_type): void { + $key_type = $key_type->getBuilder(); foreach ($key_type->getAtomicTypes() as $key => $cat) { if ($cat instanceof TTemplateParam) { self::refineArrayKey($cat->as); @@ -1200,5 +1206,6 @@ protected static function refineArrayKey(Union $key_type): void // this should ideally prompt some sort of error $key_type->addType(new TArrayKey()); } + $key_type = $key_type->freeze(); } } diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 9d004d386ce..61efdd1cc9e 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -2,60 +2,21 @@ 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; @@ -235,1424 +196,46 @@ final class Union implements TypeNode */ public $different = false; - /** - * Constructs an Union instance - * - * @param non-empty-array $types - */ - public function __construct(array $types) - { - $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; - } - - /** - * @param non-empty-array $types - */ - public function replaceTypes(array $types): void - { - $this->types = $types; - } - - /** - * @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'; - } - $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; - } + foreach ($this->getAtomicTypes() as $type) { + $types []= clone $type; } - - 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] - ); - } - - $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 = []; + $union = new MutableUnion($types); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + 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]); - } - $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 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 array - */ - 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; + if ($key === 'id') { + continue; } - } - - return false; - } - - public function isObjectType(): bool - { - foreach ($this->types as $type) { - if (!$type->isObjectType()) { - return false; + if ($key === 'exact_id') { + continue; } - } - - return true; - } - - public function hasNamedObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isNamedObjectType()) { - return true; + if ($key === 'literal_string_types') { + continue; } - } - - return false; - } - - public function isStaticObject(): bool - { - foreach ($this->types as $type) { - if (!$type instanceof TNamedObject - || !$type->is_static - ) { - return false; + if ($key === 'typed_class_strings') { + continue; } - } - - return true; - } - - public function hasStaticObject(): bool - { - foreach ($this->types as $type) { - if ($type instanceof TNamedObject - && $type->is_static - ) { - return true; + if ($key === 'literal_int_types') { + continue; } - } - - 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; + if ($key === 'literal_float_types') { + continue; } + $union->{$key} = $value; } - - return false; + return $union; } - public function isFalsable(): bool + public function replaceClassLike(string $old, string $new): self { - if (isset($this->types['false'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isFalsable()) { - return true; - } + $types = $this->types; + foreach ($types as &$atomic_type) { + $atomic_type = $atomic_type->replaceClassLike($old, $new); } - - 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 - { - return $this->types === []; + return $types === $this->types ? $this : $this->getBuilder()->setTypes($types)->freeze(); } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php new file mode 100644 index 00000000000..cbf55c95dd7 --- /dev/null +++ b/src/Psalm/Type/UnionTrait.php @@ -0,0 +1,1287 @@ + $types + */ + public function __construct(array $types) + { + $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; + } + + 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; + } + } + } + + /** + * @psalm-mutation-free + * @return non-empty-array + */ + public function getAtomicTypes(): array + { + return $this->types; + } + + 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 bustCache(): void + { + $this->id = null; + $this->exact_id = null; + } + + 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) { + $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 = 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)); + } + + /** + * @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)); + } + + 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) + ); + } + + 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 array + */ + 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 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; + } + + 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 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; + } + + /** + * @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 + { + 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..f1d3e16bb63 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -1098,7 +1098,7 @@ function foo(?array $arr, string $s) : void { $_arr2[$index] = 5;', 'assertions' => [ '$_arr1===' => 'non-empty-array<1, 5>', - '$_arr2===' => 'non-empty-array<1, 5>', + '$_arr2===' => 'array{1: 5}', ] ], 'accessArrayWithSingleStringLiteralOffset' => [ diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e69385648bc..b7f0d053399 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -754,10 +754,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' => ' + */ + 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' => '