diff --git a/.github/workflows/bcc.yml b/.github/workflows/bcc.yml index 8bc58be9b..5cc8c3194 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 a060c7ed3..3d4eb36dc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /tests/fixtures/symlinktest/* .idea/ +.vscode/ diff --git a/UPGRADING.md b/UPGRADING.md index 9f2ad3f0d..6e176c69e 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 95396607e..e140ba1a0 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] @@ -346,12 +332,36 @@ $this->type_params[1] + + + + + + $allow_mutations + $failed_reconciliation + $from_template_default + $has_mutations + $initialized_class + $reference_free + + $type[0] $type[0][0] + + + $ignore_isset + + + + + allFloatLiterals + allFloatLiterals + + $subNodes['expr'] diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 286cbf72e..8b894dad1 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -485,7 +485,10 @@ final class Context 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 @@ final class Context $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 ff9bed244..d585d3985 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -791,12 +791,12 @@ class ClassAnalyzer extends ClassLikeAnalyzer $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 @@ class ClassAnalyzer extends ClassLikeAnalyzer ); } 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 @@ class ClassAnalyzer extends ClassLikeAnalyzer } 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 29fc51a30..735dc7fef 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -992,8 +992,9 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer 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( @@ -1844,9 +1845,9 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer $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 584762453..f6bfe8db4 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -362,7 +362,7 @@ class MethodComparator $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 @@ class MethodComparator } } - 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 @@ class MethodComparator 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 @@ class MethodComparator $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/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index f33c0d82a..a3c55b1b1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -225,10 +225,10 @@ class ArrayAnalyzer } 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/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index c78f72ef4..e0bd6b084 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 @@ class ArrayAssignmentAnalyzer $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 @@ class ArrayAssignmentAnalyzer $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 @@ class ArrayAssignmentAnalyzer } } - $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 @@ class ArrayAssignmentAnalyzer } } - $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 @@ class ArrayAssignmentAnalyzer ] ); - TemplateInferredTypeReplacer::replace( + $value_type = TemplateInferredTypeReplacer::replace( $value_type, $template_result, $codebase @@ -742,17 +742,21 @@ class ArrayAssignmentAnalyzer $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 @@ class ArrayAssignmentAnalyzer ); } + $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 c787b2443..c185eb579 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -1561,7 +1561,10 @@ class AssignmentAnalyzer 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 a4f3924df..9539eceaf 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\TNonEmptyNonspecificLiteralString; 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 @@ class ConcatAnalyzer } 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 @@ class ConcatAnalyzer } } - $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 @@ class ConcatAnalyzer $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 878dc11ec..8774fbf52 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -44,6 +44,7 @@ class BitwiseNotAnalyzer $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 e87585606..34cf5152e 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 @@ class ArgumentAnalyzer 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 @@ class ArgumentAnalyzer $input_type->addType($candidate_callable); } } + $input_type = $input_type->freeze(); } $union_comparison_results = new TypeComparisonResult(); @@ -1384,9 +1386,10 @@ class ArgumentAnalyzer $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 68b90dc67..424c748f3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -494,7 +494,7 @@ class ArgumentsAnalyzer // 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 @@ -601,7 +601,7 @@ class ArgumentsAnalyzer $context->calling_method_id ?: $context->calling_function_id ); - TemplateInferredTypeReplacer::replace( + $replaced_type = TemplateInferredTypeReplacer::replace( $replaced_type, $replace_template_result, $codebase @@ -1234,7 +1234,7 @@ class ArgumentsAnalyzer ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_type = TemplateInferredTypeReplacer::replace( $original_by_ref_type, $template_result, $codebase @@ -1259,7 +1259,7 @@ class ArgumentsAnalyzer ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_out_type = TemplateInferredTypeReplacer::replace( $original_by_ref_out_type, $template_result, $codebase @@ -1386,16 +1386,18 @@ class ArgumentsAnalyzer $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(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index a34f9e6d0..f3852df51 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -266,7 +266,7 @@ class ArrayFunctionArgumentsAnalyzer 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 +285,7 @@ class ArrayFunctionArgumentsAnalyzer $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 +509,7 @@ class ArrayFunctionArgumentsAnalyzer $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 +575,7 @@ class ArrayFunctionArgumentsAnalyzer } } + $array_type = $array_type->freeze(); $context->removeDescendents($var_id, $array_type); $context->vars_in_scope[$var_id] = $array_type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index ed207aded..e5be6f524 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\TNamedObject; 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 @@ class FunctionCallReturnTypeFetcher null ); - TemplateInferredTypeReplacer::replace( + $return_type = TemplateInferredTypeReplacer::replace( $return_type, $template_result, $codebase @@ -501,8 +502,10 @@ class FunctionCallReturnTypeFetcher 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 @@ class FunctionCallReturnTypeFetcher $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/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index b16fadae0..72c0c7cbd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -300,9 +300,11 @@ class ExistingAtomicMethodCallAnalyzer extends CallAnalyzer 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 2ed17b4bc..d62ee2d69 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 @@ class MethodCallReturnTypeFetcher 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 086a8d255..ac2346d95 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 @@ class MissingMethodCallHandler $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 @@ class MissingMethodCallHandler $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 a755115db..db15ccafa 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 @@ class MethodCallAnalyzer extends CallAnalyzer ) { $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 @@ class MethodCallAnalyzer extends CallAnalyzer $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 772ad322c..e1754c01c 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 @@ class StaticCallAnalyzer extends CallAnalyzer 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 619868af9..50946b5a5 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 @@ class AtomicStaticCallAnalyzer $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 @@ class AtomicStaticCallAnalyzer 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 @@ class AtomicStaticCallAnalyzer } } - $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 f7e0d7e09..55445238f 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 @@ class ExistingAtomicStaticCallAnalyzer 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 ade120533..b39e5579c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -756,9 +756,8 @@ class CallAnalyzer $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 ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 48795d26f..1e7624f9f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -78,7 +78,7 @@ class CastAnalyzer } 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 @@ class CastAnalyzer $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 57bfbdce1..27e89050b 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\TTemplateKeyOf; 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 @@ class ArrayFetchAnalyzer && !$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 @@ class ArrayFetchAnalyzer $new_offset_type->removeType($offset_key); } } + + $new_offset_type = $new_offset_type->freeze(); } } } @@ -456,14 +459,17 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer } } + $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 @@ class ArrayFetchAnalyzer } 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 @@ class ArrayFetchAnalyzer } 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 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer } } - return $offset_type; + return $offset_type->freeze(); } /** @@ -1085,11 +1084,11 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer } } - $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 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer } 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 @@ class ArrayFetchAnalyzer if (UnionTypeComparator::canExpressionTypesBeIdentical( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type )) { $has_valid_offset = true; @@ -1378,7 +1377,7 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer $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 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer ?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 @@ class ArrayFetchAnalyzer $is_contained = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $key_type, true, $offset_type->ignore_falsable_issues, @@ -1600,7 +1599,7 @@ class ArrayFetchAnalyzer $is_contained = UnionTypeComparator::isContainedBy( $codebase, $key_type, - $offset_type, + $offset_type->freeze(), true, $offset_type->ignore_falsable_issues ); @@ -1620,7 +1619,7 @@ class ArrayFetchAnalyzer $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 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer 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 @@ class ArrayFetchAnalyzer if (!UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $valid_offset_type, true )) { @@ -1981,7 +1980,7 @@ class ArrayFetchAnalyzer * @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 d9c83bdc4..6136d7081 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 @@ class AtomicPropertyFetchAnalyzer } } - 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 195bccced..2e25c2cd7 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 @@ class InstancePropertyFetchAnalyzer $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 @@ class InstancePropertyFetchAnalyzer $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 cb0c56810..1549caeb5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -218,7 +218,7 @@ class SimpleTypeInferer 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 4847e0459..54692df7d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -177,7 +177,7 @@ class YieldAnalyzer } 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 c063431a3..2cf90031d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -284,10 +284,8 @@ class ReturnAnalyzer 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 2c9a16449..45347e0dc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -58,7 +58,7 @@ class UnsetAnalyzer ); 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 @@ class UnsetAnalyzer } } - $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 9a78716c0..d7f760e39 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1460,9 +1460,9 @@ class ClassLikes 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 @@ class ClassLikes $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 ba5697136..da7f59b61 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -494,7 +494,7 @@ class Methods 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 @@ class Methods return $type; } - $type = clone $type; + $type = $type->getBuilder(); foreach ($type->getAtomicTypes() as $key => $atomic_type) { if ($atomic_type instanceof TTemplateParam @@ -618,9 +618,7 @@ class Methods } } - $type->bustCache(); - - return $type; + return $type->freeze(); } /** diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 2a99fff14..9987fe6d7 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1629,7 +1629,7 @@ class ClassLikeNodeScanner 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 084a6ce1b..d79bac47b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -846,7 +846,7 @@ class FunctionLikeDocblockScanner && !$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 @@ class FunctionLikeDocblockScanner } 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 @@ class FunctionLikeDocblockScanner $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 5583040e6..9f7c130bd 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -839,7 +839,7 @@ class FunctionLikeNodeScanner ); 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 9265d3c8b..b359c60ec 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -169,7 +169,7 @@ class TypeHintResolver } 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 be8e069e7..5cc17218c 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -153,11 +153,11 @@ class ArrayFilterReturnTypeProvider implements FunctionReturnTypeProviderInterfa } 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 5ac91d639..f89b9012e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -86,7 +86,7 @@ class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTypeProv 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 0ea580949..64266953d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -85,7 +85,7 @@ class ArrayPopReturnTypeProvider implements FunctionReturnTypeProviderInterface } 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 e24f5a3e5..afa872b72 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -104,7 +104,7 @@ class FilterVarReturnTypeProvider implements FunctionReturnTypeProviderInterface $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 @@ class FilterVarReturnTypeProvider implements FunctionReturnTypeProviderInterface 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 00f633bd8..2a3d18e80 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\Event\FunctionReturnTypeProviderEvent; 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 @@ class FirstArgStringReturnTypeProvider implements FunctionReturnTypeProviderInte 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 6061ddf0d..2dfa36816 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\Event\FunctionReturnTypeProviderEvent; 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 @@ class StrReplaceReturnTypeProvider implements FunctionReturnTypeProviderInterfac $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 14bb1707b..a77ec7ec6 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 3778bbb5a..ee5c5667f 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -943,6 +943,7 @@ class AssertionReconciler extends Reconciler $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 +959,7 @@ class AssertionReconciler extends Reconciler $can_be_equal = true; } } + $existing_var_type = $existing_var_type->freeze(); if ($var_id && $code_location diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index e89269c3e..2670ff04d 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\TList; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Union; use UnexpectedValueException; use function end; @@ -440,15 +441,11 @@ class CallableTypeComparator ); 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/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 2d0a6531d..9ceabd5bd 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -174,14 +174,15 @@ class UnionTypeComparator && $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 @@ class UnionTypeComparator 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 efdf02d80..dfbf026a9 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -91,18 +91,20 @@ class NegatedAssertionReconciler extends Reconciler } $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 @@ class NegatedAssertionReconciler extends Reconciler $existing_var_type->from_calculation = false; - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality @@ -158,7 +160,7 @@ class NegatedAssertionReconciler extends Reconciler $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 @@ class NegatedAssertionReconciler extends Reconciler } } + $existing_var_type = $existing_var_type->freeze(); + if ($assertion instanceof IsNotIdentical && ($key !== '$this' || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)) @@ -322,6 +326,7 @@ class NegatedAssertionReconciler extends Reconciler ?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 @@ class NegatedAssertionReconciler extends Reconciler } } + $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 62a0473fd..0ca73f5c6 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -504,6 +504,7 @@ class SimpleAssertionReconciler extends Reconciler 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 @@ class SimpleAssertionReconciler extends Reconciler $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 @@ class SimpleAssertionReconciler extends Reconciler 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 @@ class SimpleAssertionReconciler extends Reconciler } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -675,6 +677,7 @@ class SimpleAssertionReconciler extends Reconciler 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 @@ class SimpleAssertionReconciler extends Reconciler } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1177,6 +1180,7 @@ class SimpleAssertionReconciler extends Reconciler 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 @@ class SimpleAssertionReconciler extends Reconciler ?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 @@ class SimpleAssertionReconciler extends Reconciler $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1738,6 +1743,7 @@ class SimpleAssertionReconciler extends Reconciler ): 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 @@ class SimpleAssertionReconciler extends Reconciler $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -2354,6 +2360,7 @@ class SimpleAssertionReconciler extends Reconciler 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 @@ class SimpleAssertionReconciler extends Reconciler $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } $existing_var_type->possibly_undefined = false; @@ -2501,7 +2508,7 @@ class SimpleAssertionReconciler extends Reconciler } if ($existing_var_type->isSingle()) { - return $existing_var_type; + return $existing_var_type->freeze(); } } @@ -2532,7 +2539,7 @@ class SimpleAssertionReconciler extends Reconciler } 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 ff1b3dcdf..e08262cea 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -413,6 +413,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler 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 @@ class SimpleNegatedAssertionReconciler extends Reconciler } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -513,6 +514,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler 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 @@ class SimpleNegatedAssertionReconciler extends Reconciler } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -587,6 +589,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler 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 @@ class SimpleNegatedAssertionReconciler extends Reconciler } if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + return $existing_var_type->freeze(); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -660,6 +663,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler $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 @@ class SimpleNegatedAssertionReconciler extends Reconciler } if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + return $existing_var_type->freeze(); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -728,6 +732,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler 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 @@ class SimpleNegatedAssertionReconciler extends Reconciler $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } if ($existing_var_type->hasType('bool')) { @@ -894,7 +899,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler } assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1621,7 +1626,9 @@ class SimpleNegatedAssertionReconciler extends Reconciler 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 @@ class SimpleNegatedAssertionReconciler extends Reconciler ?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 @@ class SimpleNegatedAssertionReconciler extends Reconciler $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1789,6 +1797,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler ?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 @@ class SimpleNegatedAssertionReconciler extends Reconciler $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 6d4d75315..0b0664f98 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -47,7 +47,7 @@ class TemplateInferredTypeReplacer Union $union, TemplateResult $template_result, ?Codebase $codebase - ): void { + ): Union { $keys_to_unset = []; $new_types = []; @@ -56,6 +56,7 @@ class TemplateInferredTypeReplacer $inferred_lower_bounds = $template_result->lower_bounds ?: []; + $union = $union->getBuilder(); foreach ($union->getAtomicTypes() as $key => $atomic_type) { $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); @@ -214,14 +215,12 @@ class TemplateInferredTypeReplacer throw new UnexpectedValueException('This array should be full'); } - $union->replaceTypes( + return $union->replaceTypes( TypeCombiner::combine( $new_types, $codebase )->getAtomicTypes() - ); - - return; + )->freeze(); } foreach ($keys_to_unset as $key) { @@ -230,12 +229,12 @@ class TemplateInferredTypeReplacer $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); - $union->replaceTypes( + return $union->replaceTypes( TypeCombiner::combine( $atomic_types, $codebase )->getAtomicTypes() - ); + )->freeze(); } /** @@ -261,9 +260,9 @@ class TemplateInferredTypeReplacer $template_type = $traversed_type; if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = clone $atomic_type->as; + $template_type = $atomic_type->as->getBuilder(); } else { - $template_type = clone $template_type; + $template_type = $template_type->getBuilder(); } if ($atomic_type->extra_types) { @@ -289,6 +288,7 @@ class TemplateInferredTypeReplacer } } } + $template_type = $template_type->freeze(); } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { foreach ($template_type_map as $template_class => $_) { @@ -410,7 +410,7 @@ class TemplateInferredTypeReplacer $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 @@ class TemplateInferredTypeReplacer ) ]; - self::replace( + $if_template_type = self::replace( $if_template_type, $refined_template_result, $codebase @@ -508,7 +508,7 @@ class TemplateInferredTypeReplacer ) ]; - self::replace( + $else_template_type = self::replace( $else_template_type, $refined_template_result, $codebase @@ -517,13 +517,13 @@ class TemplateInferredTypeReplacer } 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 5b3341d55..32c3afaa7 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -84,7 +84,7 @@ class TemplateStandinTypeReplacer // 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 +93,7 @@ class TemplateStandinTypeReplacer } if (!$new_input_type->isUnionEmpty()) { - $input_type = $new_input_type; + $input_type = $new_input_type->freeze(); } else { return $union_type; } @@ -766,7 +766,7 @@ class TemplateStandinTypeReplacer ) ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -777,6 +777,7 @@ class TemplateStandinTypeReplacer } } } + $generic_param = $generic_param->freeze(); if ($add_lower_bound) { return array_values($generic_param->getAtomicTypes()); @@ -858,7 +859,7 @@ class TemplateStandinTypeReplacer $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 +870,7 @@ class TemplateStandinTypeReplacer } } } + $generic_param = $generic_param->freeze(); $upper_bound = $template_result->upper_bounds [$param_name_key] @@ -1258,7 +1260,7 @@ class TemplateStandinTypeReplacer $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/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 71a2ddd1e..a77c77093 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -255,7 +255,7 @@ class TypeParser ); 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 @@ class TypeParser private static function getGenericParamClass( string $param_name, - Union $as, + Union &$as, string $defining_class ): TTemplateParamClass { if ($as->hasMixed()) { @@ -430,7 +430,7 @@ class TypeParser $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, diff --git a/src/Psalm/Storage/Possibilities.php b/src/Psalm/Storage/Possibilities.php index c52688180..eb885c9e6 100644 --- a/src/Psalm/Storage/Possibilities.php +++ b/src/Psalm/Storage/Possibilities.php @@ -45,7 +45,7 @@ final class Possibilities if ($assertion_type) { $union = new Union([clone $assertion_type]); - TemplateInferredTypeReplacer::replace( + $union = TemplateInferredTypeReplacer::replace( $union, $template_result, $codebase diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 8508caa2c..42db8658e 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -45,6 +45,7 @@ use Psalm\Type\Atomic\TString; 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 @@ abstract class Type 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 diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 126ac6732..f76cd62e0 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -549,7 +549,7 @@ abstract class Atomic implements TypeNode } if ($this instanceof TTemplateParam) { - $this->as->replaceClassLike($old, $new); + $this->as = $this->as->getBuilder()->replaceClassLike($old, $new)->freeze(); } if ($this instanceof TLiteralClassString) { @@ -562,14 +562,14 @@ abstract class Atomic implements TypeNode || $this instanceof TGenericObject || $this instanceof TIterable ) { - foreach ($this->type_params as $type_param) { - $type_param->replaceClassLike($old, $new); + foreach ($this->type_params as &$type_param) { + $type_param = $type_param->getBuilder()->replaceClassLike($old, $new)->freeze(); } } if ($this instanceof TKeyedArray) { - foreach ($this->properties as $property_type) { - $property_type->replaceClassLike($old, $new); + foreach ($this->properties as &$property_type) { + $property_type = $property_type->getBuilder()->replaceClassLike($old, $new)->freeze(); } } @@ -579,13 +579,13 @@ abstract class Atomic implements TypeNode if ($this->params) { foreach ($this->params as $param) { if ($param->type) { - $param->type->replaceClassLike($old, $new); + $param->type = $param->type->getBuilder()->replaceClassLike($old, $new)->freeze(); } } } if ($this->return_type) { - $this->return_type->replaceClassLike($old, $new); + $this->return_type = $this->return_type->getBuilder()->replaceClassLike($old, $new)->freeze(); } } } diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 8e8e1e633..072c21957 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -262,7 +262,7 @@ trait CallableTrait continue; } - TemplateInferredTypeReplacer::replace( + $param->type = TemplateInferredTypeReplacer::replace( $param->type, $template_result, $codebase @@ -271,7 +271,7 @@ trait CallableTrait } if ($this->return_type) { - TemplateInferredTypeReplacer::replace( + $this->return_type = TemplateInferredTypeReplacer::replace( $this->return_type, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index ba4df1e98..78578107a 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -234,8 +234,8 @@ trait GenericTrait TemplateResult $template_result, ?Codebase $codebase ): void { - foreach ($this->type_params as $offset => $type_param) { - TemplateInferredTypeReplacer::replace( + foreach ($this->type_params as $offset => &$type_param) { + $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index aadcad0b7..79dea6b49 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -182,7 +182,7 @@ final class TClassStringMap extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->value_param = TemplateInferredTypeReplacer::replace( $this->value_param, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 070f7ac2d..9b23b9664 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -128,7 +128,7 @@ final class TConditional extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->conditional_type = TemplateInferredTypeReplacer::replace( $this->conditional_type, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b92926ffb..45bb4eac0 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -322,8 +322,8 @@ class TKeyedArray extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + foreach ($this->properties as &$property) { + $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 44d6578ee..4b3302856 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -165,7 +165,7 @@ class TList extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->type_param = TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 81d3fc76f..0338bd8fc 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -216,8 +216,8 @@ final class TObjectWithProperties extends TObject TemplateResult $template_result, ?Codebase $codebase ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + foreach ($this->properties as &$property) { + $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index bb2f5f947..754063833 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -85,7 +85,7 @@ final class TTemplateKeyOf extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5a1ac50e5..5dd0f0d52 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -80,10 +80,10 @@ final class TTemplatePropertiesOf extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->as = TemplateInferredTypeReplacer::replace( new Union([$this->as]), $template_result, $codebase - ); + )->getSingleAtomic(); } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index d00cb5974..23fe4d071 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -85,7 +85,7 @@ final class TTemplateValueOf extends Atomic TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php new file mode 100644 index 000000000..9703ea3b1 --- /dev/null +++ b/src/Psalm/Type/MutableUnion.php @@ -0,0 +1,455 @@ + + */ + 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 replaceTypes(array $types): self + { + $this->types = $types; + 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->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 19b706bcb..7fd5fb055 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -283,7 +283,7 @@ class Reconciler ); 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 @@ class Reconciler 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 @@ class Reconciler 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 @@ class Reconciler } /** + * @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 @@ class Reconciler $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 @@ class Reconciler } } - 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 @@ class Reconciler // 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 9d004d386..146ce582a 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,37 @@ 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 + public function getBuilder(): MutableUnion { $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; + foreach ($this->getAtomicTypes() as $type) { + $types []= clone $type; + } + $union = new MutableUnion($types); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; } - - $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(); + if ($key === 'id') { + continue; } - } - - sort($types); - return implode('|', $types); - } - - 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 . ')'; - } + if ($key === 'exact_id') { + continue; } - } - - $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 ($key === 'literal_string_types') { + continue; } - } - - 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; + if ($key === 'typed_class_strings') { + continue; } - } 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; + if ($key === 'literal_int_types') { + continue; } - - $php_types[] = $php_type; - } - - if ($falsable) { - if ($nullable) { - $php_types['null'] = 'null'; + if ($key === 'literal_float_types') { + continue; } - $php_types['false'] = 'false'; - ksort($php_types); - return implode('|', array_unique($php_types)); + $union->{$key} = $value; } - - 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 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; - } - - 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 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 $union; } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php new file mode 100644 index 000000000..cbf55c95d --- /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/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 76003499c..f1d3e16bb 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -1098,7 +1098,7 @@ class ArrayAccessTest extends TestCase $_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/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 6dc1a02a1..0068e7cc0 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -4457,7 +4457,7 @@ class ClassTemplateTest extends TestCase } $a = new A(function() { return "a";}); $a->setCallback(function() { return "b";});', - 'error_message' => 'InvalidScalarArgument', + 'error_message' => 'InvalidArgument', ], 'preventBoundsMismatchDifferentContainers' => [ 'code' => '