1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Immutable Unions

This commit is contained in:
Daniil Gentili 2022-10-03 10:45:36 +02:00
parent 028ac7f540
commit 3abd0b961f
74 changed files with 2135 additions and 1726 deletions

View File

@ -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 }}

1
.gitignore vendored
View File

@ -12,3 +12,4 @@
/tests/fixtures/symlinktest/*
.idea/
.vscode/

View File

@ -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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@f7fc28bf5c23969ef1d9c8fd26f020ade0a2db9e">
<files psalm-version="dev-master@d7e897b0eb65539a8b769a911efdf49925dd4e7d">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$comment_block-&gt;tags['variablesfrom'][0]</code>
@ -112,6 +112,9 @@
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php">
<ComplexMethod occurrences="1">
<code>verifyType</code>
</ComplexMethod>
<PossiblyUndefinedIntArrayOffset occurrences="3">
<code>$non_existent_method_ids[0]</code>
<code>$parts[1]</code>
@ -289,6 +292,14 @@
<code>$cs[0]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php">
<LessSpecificReturnStatement occurrences="1">
<code>$callable</code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType occurrences="1">
<code>TCallable|TClosure|null</code>
</MoreSpecificReturnType>
</file>
<file src="src/Psalm/Internal/Type/TypeCombiner.php">
<PossiblyUndefinedIntArrayOffset occurrences="6">
<code>$combination-&gt;array_type_params[1]</code>
@ -311,31 +322,6 @@
<code>array_keys($template_type_map[$template_param_name])[0]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Node/Stmt/VirtualClass.php">
<PropertyNotSetInConstructor occurrences="1">
<code>VirtualClass</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Psalm/Node/Stmt/VirtualFunction.php">
<PropertyNotSetInConstructor occurrences="1">
<code>VirtualFunction</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Psalm/Node/Stmt/VirtualInterface.php">
<PropertyNotSetInConstructor occurrences="1">
<code>VirtualInterface</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Psalm/Node/Stmt/VirtualTrait.php">
<PropertyNotSetInConstructor occurrences="1">
<code>VirtualTrait</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Psalm/Node/VirtualConst.php">
<PropertyNotSetInConstructor occurrences="1">
<code>VirtualConst</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Psalm/Type/Atomic.php">
<PossiblyUndefinedIntArrayOffset occurrences="1">
<code>array_keys($template_type_map[$value])[0]</code>
@ -346,12 +332,36 @@
<code>$this-&gt;type_params[1]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Type/Atomic/TTemplatePropertiesOf.php">
<PropertyTypeCoercion occurrences="1"/>
</file>
<file src="src/Psalm/Type/MutableUnion.php">
<PossiblyUnusedProperty occurrences="6">
<code>$allow_mutations</code>
<code>$failed_reconciliation</code>
<code>$from_template_default</code>
<code>$has_mutations</code>
<code>$initialized_class</code>
<code>$reference_free</code>
</PossiblyUnusedProperty>
</file>
<file src="src/Psalm/Type/Reconciler.php">
<PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$type[0]</code>
<code>$type[0][0]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Type/Union.php">
<PossiblyUnusedProperty occurrences="1">
<code>$ignore_isset</code>
</PossiblyUnusedProperty>
</file>
<file src="src/Psalm/Type/UnionTrait.php">
<PossiblyUnusedMethod occurrences="2">
<code>allFloatLiterals</code>
<code>allFloatLiterals</code>
</PossiblyUnusedMethod>
</file>
<file src="vendor/nikic/php-parser/lib/PhpParser/Node/Expr/ArrowFunction.php">
<PossiblyUndefinedStringArrayOffset occurrences="1">
<code>$subNodes['expr']</code>

View File

@ -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();
}
}
}

View File

@ -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()) {

View File

@ -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);
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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,

View File

@ -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) {

View File

@ -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()) {

View File

@ -494,7 +494,7 @@ class ArgumentsAnalyzer
// The map function expects callable(A):B as second param
// We know that previous arg type is list<int> 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();
}
}

View File

@ -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;
}

View File

@ -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
);

View File

@ -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(

View File

@ -618,7 +618,7 @@ class MethodCallReturnTypeFetcher
null
);
TemplateInferredTypeReplacer::replace(
$return_type_candidate = TemplateInferredTypeReplacer::replace(
$return_type_candidate,
$template_result,
$codebase

View File

@ -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

View File

@ -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;

View File

@ -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
);

View File

@ -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;

View File

@ -567,7 +567,7 @@ class ExistingAtomicStaticCallAnalyzer
null
);
TemplateInferredTypeReplacer::replace(
$return_type_candidate = TemplateInferredTypeReplacer::replace(
$return_type_candidate,
$template_result,
$codebase

View File

@ -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
);

View File

@ -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();
}
}

View File

@ -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) {
// shouldnt happen, but dont 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 {

View File

@ -743,7 +743,7 @@ class AtomicPropertyFetchAnalyzer
}
}
TemplateInferredTypeReplacer::replace(
$class_property_type = TemplateInferredTypeReplacer::replace(
$class_property_type,
new TemplateResult([], $template_types),
$codebase

View File

@ -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);
}
}
}

View File

@ -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');

View File

@ -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);

View File

@ -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
);

View File

@ -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,

View File

@ -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(

View File

@ -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();
}
/**

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}

View File

@ -169,7 +169,7 @@ class TypeHintResolver
}
if ($is_nullable) {
$type->addType(new TNull);
$type = $type->getBuilder()->addType(new TNull)->freeze();
}
return $type;

View File

@ -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()) {

View File

@ -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();

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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;

View File

@ -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()) {

View File

@ -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)

View File

@ -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();
}
/**

View File

@ -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();
}
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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();
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
}
}

View File

@ -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

View File

@ -0,0 +1,455 @@
<?php
namespace Psalm\Type;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\Type\TypeCombiner;
use Psalm\Type;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTrue;
use Stringable;
use function count;
use function get_class;
use function get_object_vars;
use function strpos;
final class MutableUnion implements TypeNode, Stringable
{
use UnionTrait;
/**
* @var non-empty-array<string, Atomic>
*/
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<string, TLiteralString>
*/
private $literal_string_types = [];
/**
* @var array<string, TClassString>
*/
private $typed_class_strings = [];
/**
* @var array<string, TLiteralInt>
*/
private $literal_int_types = [];
/**
* @var array<string, TLiteralFloat>
*/
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<string, DataFlowNode>
*/
public $parent_nodes = [];
/**
* @var bool
*/
public $different = false;
/**
* @param non-empty-array<string, Atomic> $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;
}
}

View File

@ -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();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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' => [

View File

@ -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' => '<?php