From d0be59e16ef6e55f0349c60eab272947e21a2c11 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 4 Nov 2022 19:04:23 +0100 Subject: [PATCH] Immutable unions (#8627) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Update --- composer.json | 2 +- examples/plugins/ClassUnqualifier.php | 1 + psalm-baseline.xml | 68 +++++-- src/Psalm/Aliases.php | 2 + src/Psalm/CodeLocation.php | 34 +++- .../CodeLocation/DocblockTypeLocation.php | 3 + src/Psalm/CodeLocation/ParseErrorLocation.php | 5 +- src/Psalm/CodeLocation/Raw.php | 1 + src/Psalm/Codebase.php | 7 +- src/Psalm/Context.php | 24 +-- src/Psalm/ErrorBaseline.php | 1 + src/Psalm/FileSource.php | 15 ++ src/Psalm/Internal/Analyzer/CanAlias.php | 3 + src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 43 +++-- .../Internal/Analyzer/ClassLikeAnalyzer.php | 11 +- .../Internal/Analyzer/ClosureAnalyzer.php | 23 ++- .../Internal/Analyzer/DataFlowNodeData.php | 4 + src/Psalm/Internal/Analyzer/FileAnalyzer.php | 22 +++ .../FunctionLike/ReturnTypeAnalyzer.php | 2 + .../FunctionLike/ReturnTypeCollector.php | 4 +- .../Analyzer/FunctionLikeAnalyzer.php | 181 +++++++++++------- .../Internal/Analyzer/MethodAnalyzer.php | 1 + .../Internal/Analyzer/MethodComparator.php | 2 - .../Internal/Analyzer/ProjectAnalyzer.php | 3 + .../Internal/Analyzer/SourceAnalyzer.php | 19 +- .../Statements/Block/ForeachAnalyzer.php | 20 +- .../Statements/Block/IfElse/IfAnalyzer.php | 2 +- .../Statements/Block/IfElseAnalyzer.php | 12 +- .../Statements/Block/LoopAnalyzer.php | 35 ++-- .../Statements/Block/SwitchCaseAnalyzer.php | 12 +- .../Analyzer/Statements/Block/TryAnalyzer.php | 101 +++++----- .../Analyzer/Statements/BreakAnalyzer.php | 6 +- .../Analyzer/Statements/ContinueAnalyzer.php | 5 +- .../Statements/Expression/ArrayAnalyzer.php | 20 +- .../Statements/Expression/AssertionFinder.php | 6 +- .../Assignment/ArrayAssignmentAnalyzer.php | 112 ++++++----- .../InstancePropertyAssignmentAnalyzer.php | 21 +- .../StaticPropertyAssignmentAnalyzer.php | 2 - .../Expression/AssignmentAnalyzer.php | 105 +++++----- .../BinaryOp/ArithmeticOpAnalyzer.php | 20 +- .../Expression/BinaryOp/CoalesceAnalyzer.php | 13 +- .../Expression/BinaryOp/OrAnalyzer.php | 4 +- .../Expression/BinaryOpAnalyzer.php | 9 +- .../Expression/BitwiseNotAnalyzer.php | 5 +- .../Expression/BooleanNotAnalyzer.php | 17 +- .../Expression/Call/ArgumentAnalyzer.php | 41 ++-- .../Expression/Call/ArgumentsAnalyzer.php | 33 ++-- .../Call/ArrayFunctionArgumentsAnalyzer.php | 9 +- .../Expression/Call/FunctionCallAnalyzer.php | 6 +- .../Call/FunctionCallReturnTypeFetcher.php | 45 ++--- .../Call/Method/AtomicMethodCallAnalyzer.php | 4 +- .../ExistingAtomicMethodCallAnalyzer.php | 8 +- .../Method/MethodCallReturnTypeFetcher.php | 29 +-- .../Call/Method/MissingMethodCallHandler.php | 4 +- .../Expression/Call/MethodCallAnalyzer.php | 9 +- .../Call/NamedFunctionCallHandler.php | 8 +- .../Expression/Call/NewAnalyzer.php | 20 +- .../Expression/Call/StaticCallAnalyzer.php | 10 +- .../StaticMethod/AtomicStaticCallAnalyzer.php | 6 +- .../ExistingAtomicStaticCallAnalyzer.php | 7 +- .../Statements/Expression/CallAnalyzer.php | 4 +- .../Statements/Expression/CastAnalyzer.php | 20 +- .../Expression/ClassConstAnalyzer.php | 6 +- .../Statements/Expression/CloneAnalyzer.php | 11 +- .../Expression/EncapsulatedStringAnalyzer.php | 37 ++-- .../Expression/Fetch/ArrayFetchAnalyzer.php | 77 ++++---- .../Fetch/AtomicPropertyFetchAnalyzer.php | 30 +-- .../Expression/Fetch/ConstFetchAnalyzer.php | 2 +- .../Fetch/InstancePropertyFetchAnalyzer.php | 12 +- .../Fetch/StaticPropertyFetchAnalyzer.php | 48 ++--- .../Fetch/VariableFetchAnalyzer.php | 148 +++++++------- .../Expression/SimpleTypeInferer.php | 8 +- .../Statements/Expression/TernaryAnalyzer.php | 2 +- .../Expression/UnaryPlusMinusAnalyzer.php | 9 +- .../Statements/Expression/YieldAnalyzer.php | 16 +- .../Expression/YieldFromAnalyzer.php | 4 +- .../Analyzer/Statements/GlobalAnalyzer.php | 8 +- .../Analyzer/Statements/ReturnAnalyzer.php | 13 +- .../Analyzer/Statements/StaticAnalyzer.php | 3 +- .../Analyzer/Statements/ThrowAnalyzer.php | 5 +- .../Analyzer/Statements/UnsetAnalyzer.php | 7 +- .../Internal/Analyzer/StatementsAnalyzer.php | 5 +- src/Psalm/Internal/Analyzer/TraitAnalyzer.php | 4 + src/Psalm/Internal/Clause.php | 2 + src/Psalm/Internal/Codebase/ClassLikes.php | 7 +- src/Psalm/Internal/Codebase/Methods.php | 33 ++-- src/Psalm/Internal/Codebase/Populator.php | 3 +- src/Psalm/Internal/Codebase/Reflection.php | 3 + src/Psalm/Internal/Codebase/Scanner.php | 1 + src/Psalm/Internal/DataFlow/Path.php | 4 + src/Psalm/Internal/Diff/DiffElem.php | 4 + .../FileManipulation/CodeMigration.php | 3 + .../FunctionDocblockManipulator.php | 2 + .../Internal/Fork/ForkProcessDoneMessage.php | 3 + .../Internal/Fork/ForkProcessErrorMessage.php | 3 + .../Internal/Fork/ForkTaskDoneMessage.php | 4 + src/Psalm/Internal/MethodIdentifier.php | 3 + .../PhpVisitor/ConditionCloningVisitor.php | 2 +- .../Reflector/ClassLikeNodeScanner.php | 100 +++++----- .../Reflector/ExpressionScanner.php | 2 + .../Reflector/FunctionLikeDocblockScanner.php | 40 +++- .../Reflector/FunctionLikeNodeScanner.php | 3 +- .../Internal/PhpVisitor/ReflectorVisitor.php | 6 + .../PhpVisitor/SimpleNameResolver.php | 4 + .../PhpVisitor/TypeMappingVisitor.php | 2 +- .../PhpVisitor/YieldTypeCollector.php | 4 +- .../Provider/ClassLikeStorageProvider.php | 3 + .../Internal/Provider/FakeFileProvider.php | 1 + src/Psalm/Internal/Provider/FileProvider.php | 6 + .../DomDocumentPropertyTypeProvider.php | 8 +- .../ArrayColumnReturnTypeProvider.php | 2 +- .../ArrayFillReturnTypeProvider.php | 2 +- .../ArrayFilterReturnTypeProvider.php | 10 +- .../ArrayMapReturnTypeProvider.php | 8 +- .../ArrayMergeReturnTypeProvider.php | 8 +- ...rayPointerAdjustmentReturnTypeProvider.php | 11 +- .../ArrayPopReturnTypeProvider.php | 10 +- .../ArrayRandReturnTypeProvider.php | 2 +- .../ArrayReduceReturnTypeProvider.php | 8 +- .../ArrayReverseReturnTypeProvider.php | 6 +- .../ArraySliceReturnTypeProvider.php | 9 +- .../ArraySpliceReturnTypeProvider.php | 8 +- .../ArrayUniqueReturnTypeProvider.php | 4 +- .../ReturnTypeProvider/DomNodeAppendChild.php | 2 +- .../ExplodeReturnTypeProvider.php | 7 +- .../FilterVarReturnTypeProvider.php | 2 +- .../FirstArgStringReturnTypeProvider.php | 7 +- .../InArrayReturnTypeProvider.php | 1 + .../MktimeReturnTypeProvider.php | 10 +- .../ParseUrlReturnTypeProvider.php | 116 ++++++----- .../StrReplaceReturnTypeProvider.php | 8 +- .../StrTrReturnTypeProvider.php | 2 +- src/Psalm/Internal/Scanner/FileScanner.php | 5 + .../Internal/Scanner/PhpStormMetaScanner.php | 12 +- .../UnresolvedConstant/UnresolvedBinaryOp.php | 3 + .../UnresolvedConstant/UnresolvedConcatOp.php | 3 + .../UnresolvedDivisionOp.php | 3 + .../UnresolvedMultiplicationOp.php | 3 + .../UnresolvedSubtractionOp.php | 3 + .../UnresolvedConstant/UnresolvedTernary.php | 3 + .../Scanner/UnresolvedConstantComponent.php | 3 + .../Internal/Type/AssertionReconciler.php | 72 +++---- .../Type/Comparator/ArrayTypeComparator.php | 15 +- .../Comparator/CallableTypeComparator.php | 15 +- .../Type/Comparator/GenericTypeComparator.php | 8 +- .../Comparator/IntegerRangeComparator.php | 8 +- .../Type/NegatedAssertionReconciler.php | 14 +- .../Type/SimpleAssertionReconciler.php | 33 ++-- .../Type/SimpleNegatedAssertionReconciler.php | 75 ++++---- .../Type/TemplateInferredTypeReplacer.php | 22 +-- .../Type/TemplateStandinTypeReplacer.php | 56 +++--- .../Type/TypeAlias/InlineTypeAlias.php | 3 + .../Type/TypeAlias/LinkableTypeAlias.php | 3 + src/Psalm/Internal/Type/TypeCombiner.php | 45 +++-- src/Psalm/Internal/Type/TypeExpander.php | 106 +++++----- src/Psalm/Internal/Type/TypeParser.php | 21 +- .../TypeVisitor/ClasslikeReplacer.php | 27 +-- .../TypeVisitor/ContainsClassLikeVisitor.php | 9 +- .../TypeVisitor/ContainsLiteralVisitor.php | 7 +- .../TypeVisitor/ContainsStaticVisitor.php | 5 +- .../TypeVisitor/FromDocblockSetter.php | 16 +- .../TypeVisitor/TemplateTypeCollector.php | 4 +- .../Internal/TypeVisitor/TypeChecker.php | 5 +- .../Internal/TypeVisitor/TypeLocalizer.php | 19 +- .../Internal/TypeVisitor/TypeScanner.php | 4 +- src/Psalm/Storage/AttributeArg.php | 4 + src/Psalm/Storage/AttributeStorage.php | 4 + src/Psalm/Storage/ClassConstantStorage.php | 78 ++++---- src/Psalm/Storage/ClassLikeStorage.php | 2 +- src/Psalm/Storage/FunctionLikeParameter.php | 54 +++++- src/Psalm/Storage/Possibilities.php | 2 +- src/Psalm/Type.php | 82 ++++---- src/Psalm/Type/Atomic.php | 82 +++++++- src/Psalm/Type/Atomic/CallableTrait.php | 28 ++- src/Psalm/Type/Atomic/GenericTrait.php | 2 +- .../Type/Atomic/HasIntersectionTrait.php | 4 +- src/Psalm/Type/Atomic/TArray.php | 2 +- src/Psalm/Type/Atomic/TCallable.php | 2 +- src/Psalm/Type/Atomic/TClassString.php | 15 +- src/Psalm/Type/Atomic/TClassStringMap.php | 6 +- src/Psalm/Type/Atomic/TClosure.php | 2 +- src/Psalm/Type/Atomic/TConditional.php | 4 +- src/Psalm/Type/Atomic/TGenericObject.php | 2 +- src/Psalm/Type/Atomic/TIntMaskOf.php | 2 +- src/Psalm/Type/Atomic/TIterable.php | 2 +- src/Psalm/Type/Atomic/TKeyedArray.php | 27 ++- src/Psalm/Type/Atomic/TList.php | 10 +- src/Psalm/Type/Atomic/TLiteralString.php | 14 ++ src/Psalm/Type/Atomic/TNamedObject.php | 41 +++- .../Type/Atomic/TObjectWithProperties.php | 2 +- src/Psalm/Type/Atomic/TPropertiesOf.php | 2 +- src/Psalm/Type/Atomic/TTemplateParam.php | 2 +- src/Psalm/Type/ImmutableTypeVisitor.php | 67 ------- src/Psalm/Type/MutableTypeVisitor.php | 55 ++++++ src/Psalm/Type/MutableUnion.php | 46 ++--- src/Psalm/Type/Reconciler.php | 101 +++++----- src/Psalm/Type/TypeNode.php | 8 +- src/Psalm/Type/TypeVisitor.php | 87 ++------- src/Psalm/Type/Union.php | 172 ++++++++++++++--- src/Psalm/Type/UnionTrait.php | 64 +++++-- .../TypeReconciliation/ArrayKeyExistsTest.php | 49 +++++ tests/UnusedVariableTest.php | 10 + 202 files changed, 2388 insertions(+), 1603 deletions(-) delete mode 100644 src/Psalm/Type/ImmutableTypeVisitor.php create mode 100644 src/Psalm/Type/MutableTypeVisitor.php diff --git a/composer.json b/composer.json index b53d3f184a8..28c25c4210b 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,7 @@ "phpspec/prophecy": "^1.15", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psalm/plugin-phpunit": "^0.16", + "psalm/plugin-phpunit": "^0.18", "slevomat/coding-standard": "^7.0", "phpstan/phpdoc-parser": "1.6.4", "squizlabs/php_codesniffer": "^3.6", diff --git a/examples/plugins/ClassUnqualifier.php b/examples/plugins/ClassUnqualifier.php index bc84b0e21ac..c0926ddc55a 100644 --- a/examples/plugins/ClassUnqualifier.php +++ b/examples/plugins/ClassUnqualifier.php @@ -37,6 +37,7 @@ public static function afterClassLikeExistenceCheck( $type_token[0] = $aliases[strtolower($fq_class_name)]; } } + unset($type_token); $new_candidate_type = implode( '', diff --git a/psalm-baseline.xml b/psalm-baseline.xml index a1dcc3a49a3..52f9aea8240 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -46,6 +46,10 @@ + + if (AtomicTypeComparator::isContainedBy( + if (AtomicTypeComparator::isContainedBy( + $iterator_atomic_type->type_params[1] @@ -150,6 +154,18 @@ $callable_arg->items[1] + + + $stmt_type + $stmt_type + $stmt_type + + + + + $stmt_type + + $invalid_fetch_types[0] @@ -293,16 +309,10 @@ - - get - get - get + getClassTemplateTypes has - - $candidate_param_type->from_template_default - @@ -326,8 +336,21 @@ array_keys($template_type_map[$template_param_name])[0] + + + CustomMetadataTrait + + + + + traverse + traverse + traverse + traverse + + - + classExtendsOrImplements classExtendsOrImplements classExtendsOrImplements @@ -338,10 +361,15 @@ interfaceExtends interfaceExtends interfaceExtends + traverse + traverse array_keys($template_type_map[$value])[0] + + $value + @@ -400,10 +428,8 @@ replace replace - + $key_type->possibly_undefined - $value_type->possibly_undefined - $value_type->possibly_undefined @@ -420,10 +446,6 @@ replace replace - - $type->possibly_undefined - $type->possibly_undefined - @@ -457,12 +479,28 @@ $type[0][0] + + + $node + + + + + visit + + $ignore_isset + + traverse + traverse + traverseArray + traverseArray + allFloatLiterals allFloatLiterals diff --git a/src/Psalm/Aliases.php b/src/Psalm/Aliases.php index ab271df3ee8..e112f956b88 100644 --- a/src/Psalm/Aliases.php +++ b/src/Psalm/Aliases.php @@ -54,6 +54,8 @@ final class Aliases * @param array $functions_flipped * @param array $constants_flipped * @internal + * + * @psalm-mutation-free */ public function __construct( ?string $namespace = null, diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 82c6bdad744..664827f94ac 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -7,6 +7,7 @@ use PhpParser; use Psalm\Internal\Analyzer\CommentAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Storage\ImmutableNonCloneableTrait; use UnexpectedValueException; use function explode; @@ -25,8 +26,13 @@ use const PREG_OFFSET_CAPTURE; +/** + * @psalm-immutable + */ class CodeLocation { + use ImmutableNonCloneableTrait; + /** @var string */ public $file_path; @@ -85,7 +91,7 @@ class CodeLocation private $docblock_start_line_number; /** @var int|null */ - private $docblock_line_number; + protected $docblock_line_number; /** @var null|int */ private $regex_type; @@ -111,9 +117,12 @@ public function __construct( ?CodeLocation $previous_location = null, bool $single_line = false, ?int $regex_type = null, - ?string $selected_text = null + ?string $selected_text = null, + ?int $comment_line = null ) { + /** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */ $this->file_start = (int)$stmt->getAttribute('startFilePos'); + /** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */ $this->file_end = (int)$stmt->getAttribute('endFilePos'); $this->raw_file_start = $this->file_start; $this->raw_file_end = $this->file_end; @@ -124,6 +133,7 @@ public function __construct( $this->previous_location = $previous_location; $this->text = $selected_text; + /** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */ $doc_comment = $stmt->getDocComment(); $this->docblock_start = $doc_comment ? $doc_comment->getStartFilePos() : null; @@ -131,14 +141,30 @@ public function __construct( $this->preview_start = $this->docblock_start ?: $this->file_start; + /** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */ $this->raw_line_number = $stmt->getLine(); + + $this->docblock_line_number = $comment_line; } - public function setCommentLine(int $line): void + /** + * @psalm-suppress PossiblyUnusedMethod Part of public API + * @return static + */ + public function setCommentLine(?int $line): self { - $this->docblock_line_number = $line; + if ($line === $this->docblock_line_number) { + return $this; + } + $cloned = clone $this; + $cloned->docblock_line_number = $line; + return $cloned; } + /** + * @psalm-external-mutation-free + * @psalm-suppress InaccessibleProperty Mainly used for caching + */ private function calculateRealLocation(): void { if ($this->have_recalculated) { diff --git a/src/Psalm/CodeLocation/DocblockTypeLocation.php b/src/Psalm/CodeLocation/DocblockTypeLocation.php index 224b740aac6..f38c79f01ce 100644 --- a/src/Psalm/CodeLocation/DocblockTypeLocation.php +++ b/src/Psalm/CodeLocation/DocblockTypeLocation.php @@ -5,6 +5,7 @@ use Psalm\CodeLocation; use Psalm\FileSource; +/** @psalm-immutable */ class DocblockTypeLocation extends CodeLocation { public function __construct( @@ -26,5 +27,7 @@ public function __construct( $this->single_line = false; $this->preview_start = $this->file_start; + + $this->docblock_line_number = $line_number; } } diff --git a/src/Psalm/CodeLocation/ParseErrorLocation.php b/src/Psalm/CodeLocation/ParseErrorLocation.php index eedae0bb194..bafe08545fc 100644 --- a/src/Psalm/CodeLocation/ParseErrorLocation.php +++ b/src/Psalm/CodeLocation/ParseErrorLocation.php @@ -8,6 +8,7 @@ use function substr; use function substr_count; +/** @psalm-immutable */ class ParseErrorLocation extends CodeLocation { public function __construct( @@ -16,9 +17,9 @@ public function __construct( string $file_path, string $file_name ) { - /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ + /** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */ $this->file_start = (int)$error->getAttributes()['startFilePos']; - /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ + /** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */ $this->file_end = (int)$error->getAttributes()['endFilePos']; $this->raw_file_start = $this->file_start; $this->raw_file_end = $this->file_end; diff --git a/src/Psalm/CodeLocation/Raw.php b/src/Psalm/CodeLocation/Raw.php index f75a5d38f4c..e3569932372 100644 --- a/src/Psalm/CodeLocation/Raw.php +++ b/src/Psalm/CodeLocation/Raw.php @@ -7,6 +7,7 @@ use function substr; use function substr_count; +/** @psalm-immutable */ class Raw extends CodeLocation { public function __construct( diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 55a17ddfc1c..bc93e434df5 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -507,6 +507,7 @@ public function scanFiles(int $threads = 1): void } } + /** @psalm-mutation-free */ public function getFileContents(string $file_path): string { return $this->file_provider->getContents($file_path); @@ -1951,9 +1952,9 @@ public function addTaintSource( string $taint_id, array $taints = TaintKindGroup::ALL_INPUT, ?CodeLocation $code_location = null - ): void { + ): Union { if (!$this->taint_flow_graph) { - return; + return $expr_type; } $source = new TaintSource( @@ -1966,7 +1967,7 @@ public function addTaintSource( $this->taint_flow_graph->addSource($source); - $expr_type->parent_nodes[$source->id] = $source; + return $expr_type->addParentNodes([$source->id => $source]); } /** diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 0afbe3de4e2..fc9d283c864 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -432,17 +432,6 @@ public function __destruct() $this->case_scope = null; } - public function __clone() - { - foreach ($this->clauses as &$clause) { - $clause = clone $clause; - } - - foreach ($this->constants as &$constant) { - $constant = clone $constant; - } - } - /** * Updates the parent context, looking at the changes within a block and then applying those changes, where * necessary, to the parent context @@ -471,15 +460,13 @@ public function update( if (!$existing_type) { if ($new_type) { - $this->vars_in_scope[$var_id] = clone $new_type; + $this->vars_in_scope[$var_id] = $new_type; $updated_vars[$var_id] = true; } continue; } - $existing_type = clone $existing_type; - // if the type changed within the block of statements, process the replacement // also never allow ourselves to remove all types from a union if ((!$new_type || !$old_type->equals($new_type)) @@ -543,7 +530,12 @@ public function getRedefinedVars(array $new_vars_in_scope, bool $include_new_var $new_type = $new_vars_in_scope[$var_id]; - if (!$this_type->equals($new_type)) { + if (!$this_type->equals( + $new_type, + true, + !($this_type->propagate_parent_nodes || $new_type->propagate_parent_nodes) + ) + ) { $redefined_vars[$var_id] = $this_type; } } @@ -720,7 +712,7 @@ public static function filterClauses( $result_type = AssertionReconciler::reconcile( $assertion, - clone $new_type, + $new_type, null, $statements_analyzer, false, diff --git a/src/Psalm/ErrorBaseline.php b/src/Psalm/ErrorBaseline.php index 296a5eff0e6..84a804cdfe4 100644 --- a/src/Psalm/ErrorBaseline.php +++ b/src/Psalm/ErrorBaseline.php @@ -230,6 +230,7 @@ static function (array $carry, IssueData $issue): array { foreach ($groupedIssues as &$issues) { ksort($issues); } + unset($issues); return $groupedIssues; } diff --git a/src/Psalm/FileSource.php b/src/Psalm/FileSource.php index 9d1c5ff943f..fd672862365 100644 --- a/src/Psalm/FileSource.php +++ b/src/Psalm/FileSource.php @@ -4,13 +4,28 @@ interface FileSource { + /** + * @psalm-mutation-free + */ public function getFileName(): string; + /** + * @psalm-mutation-free + */ public function getFilePath(): string; + /** + * @psalm-mutation-free + */ public function getRootFileName(): string; + /** + * @psalm-mutation-free + */ public function getRootFilePath(): string; + /** + * @psalm-mutation-free + */ public function getAliases(): Aliases; } diff --git a/src/Psalm/Internal/Analyzer/CanAlias.php b/src/Psalm/Internal/Analyzer/CanAlias.php index 8f2ca0ee483..d97e0db4cc9 100644 --- a/src/Psalm/Internal/Analyzer/CanAlias.php +++ b/src/Psalm/Internal/Analyzer/CanAlias.php @@ -135,6 +135,7 @@ public function visitGroupUse(PhpParser\Node\Stmt\GroupUse $stmt): void } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlipped(): array @@ -143,6 +144,7 @@ public function getAliasedClassesFlipped(): array } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlippedReplaceable(): array @@ -150,6 +152,7 @@ public function getAliasedClassesFlippedReplaceable(): array return $this->aliased_classes_flipped_replaceable; } + /** @psalm-mutation-free */ public function getAliases(): Aliases { return new Aliases( diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index d3eaf7107a4..a7a32ad833b 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -73,9 +73,11 @@ use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic\TGenericObject; +use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Atomic\TVoid; use Psalm\Type\Union; use UnexpectedValueException; @@ -305,6 +307,7 @@ public function analyze( null ); + /** @psalm-suppress UnusedMethodCall This call actually has the side effect of creating issues */ $union->check( $this, new CodeLocation( @@ -320,6 +323,7 @@ public function analyze( if ($storage->template_extended_params) { foreach ($storage->template_extended_params as $type_map) { foreach ($type_map as $atomic_type) { + /** @psalm-suppress UnusedMethodCall This call actually has the side effect of creating issues */ $atomic_type->check( $this, new CodeLocation( @@ -748,11 +752,9 @@ public static function addContextProperties( continue; } - $property_type = clone $property_storage->type; + $property_type = $property_storage->type; - $guide_property_type = $guide_property_storage->type === null - ? Type::getMixed() - : clone $guide_property_storage->type; + $guide_property_type = $guide_property_storage->type ?? Type::getMixed(); // Set upper bounds for all templates $lower_bounds = []; @@ -831,24 +833,28 @@ public static function addContextProperties( } if ($property_storage->type) { - $property_type = clone $property_storage->type; + $property_type = $property_storage->type; if (!$property_type->isMixed() && !$property_storage->is_promoted && !$property_storage->has_default && !($property_type->isNullable() && $property_type->from_docblock) ) { - $property_type->initialized = false; - $property_type->from_property = true; - $property_type->from_static_property = $property_storage->is_static === true; + $property_type = $property_type->setProperties([ + 'initialized' => false, + 'from_property' => true, + 'from_static_property' => $property_storage->is_static === true + ]); } } else { - $property_type = Type::getMixed(); - if (!$property_storage->has_default && !$property_storage->is_promoted) { - $property_type->initialized = false; - $property_type->from_property = true; - $property_type->from_static_property = $property_storage->is_static === true; + $property_type = new Union([new TMixed()], [ + 'initialized' => false, + 'from_property' => true, + 'from_static_property' => $property_storage->is_static === true + ]); + } else { + $property_type = Type::getMixed(); } } @@ -886,7 +892,7 @@ public static function addContextProperties( $type_params = []; foreach ($class_template_params as $type_map) { - $type_params[] = clone array_values($type_map)[0]; + $type_params[] = array_values($type_map)[0]; } $this_object_type = new TGenericObject($this_object_type->value, $type_params); @@ -924,6 +930,7 @@ public static function addContextProperties( } } + /** @psalm-suppress UnusedMethodCall This call actually has the side effect of creating issues */ $fleshed_out_type->check( $statements_source, $property_type_location, @@ -1249,8 +1256,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { [, $property_name] = explode('::$', $property_id); if (!isset($method_context->vars_in_scope['$this->' . $property_name])) { - $end_type = Type::getVoid(); - $end_type->initialized = false; + $end_type = new Union([new TVoid()], ['initialized' => false]); } else { $end_type = $method_context->vars_in_scope['$this->' . $property_name]; } @@ -1776,10 +1782,11 @@ private function analyzeClassMethod( $method_context = clone $class_context; foreach ($method_context->vars_in_scope as $context_var_id => $context_type) { - $method_context->vars_in_scope[$context_var_id] = clone $context_type; + $method_context->vars_in_scope[$context_var_id] = $context_type; if ($context_type->from_property && $stmt->name->name !== '__construct') { - $method_context->vars_in_scope[$context_var_id]->initialized = true; + $method_context->vars_in_scope[$context_var_id] = + $method_context->vars_in_scope[$context_var_id]->setProperties(['initialized' => true]); } } diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index a01207ad2eb..56eed4212eb 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -433,6 +433,7 @@ public static function getFQCLNFromNameObject( } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlipped(): array @@ -445,6 +446,7 @@ public function getAliasedClassesFlipped(): array } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlippedReplaceable(): array @@ -456,17 +458,20 @@ public function getAliasedClassesFlippedReplaceable(): array return []; } + /** @psalm-mutation-free */ public function getFQCLN(): string { return $this->fq_class_name; } + /** @psalm-mutation-free */ public function getClassName(): ?string { return $this->class->name->name ?? null; } /** + * @psalm-mutation-free * @return array>|null */ public function getTemplateTypeMap(): ?array @@ -474,11 +479,13 @@ public function getTemplateTypeMap(): ?array return $this->storage->template_types; } + /** @psalm-mutation-free */ public function getParentFQCLN(): ?string { return $this->parent_fq_class_name; } + /** @psalm-mutation-free */ public function isStatic(): bool { return false; @@ -765,15 +772,13 @@ protected function checkTemplateParams( } if (!$template_type->isMixed()) { - $template_type_copy = clone $template_type; - $template_result = new TemplateResult( $previous_extended ?: [], [] ); $template_type_copy = TemplateStandinTypeReplacer::replace( - $template_type_copy, + $template_type, $template_result, $codebase, null, diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 59d1165c014..7d6fae0fc85 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -13,6 +13,7 @@ use Psalm\Issue\UndefinedVariable; use Psalm\IssueBuffer; use Psalm\Type; +use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; @@ -45,6 +46,8 @@ public function __construct(PhpParser\Node\FunctionLike $function, SourceAnalyze parent::__construct($function, $source, $storage); } + + /** @psalm-mutation-free */ public function getTemplateTypeMap(): ?array { return $this->source->getTemplateTypeMap(); @@ -90,7 +93,7 @@ public static function analyzeExpression( ) ) { /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ - $use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this']; + $use_context->vars_in_scope['$this'] = $context->vars_in_scope['$this']; } elseif ($context->self) { $this_atomic = new TNamedObject($context->self, true); @@ -100,7 +103,7 @@ public static function analyzeExpression( foreach ($context->vars_in_scope as $var => $type) { if (strpos($var, '$this->') === 0) { - $use_context->vars_in_scope[$var] = clone $type; + $use_context->vars_in_scope[$var] = $type; } } @@ -133,7 +136,7 @@ public static function analyzeExpression( // insert the ref into the current context if passed by ref, as whatever we're passing // the closure to could execute it straight away. if ($use->byRef && !$context->hasVariable($use_var_id)) { - $context->vars_in_scope[$use_var_id] = Type::getMixed(); + $context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]); } if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph @@ -152,11 +155,12 @@ public static function analyzeExpression( $use_context->vars_in_scope[$use_var_id] = $context->hasVariable($use_var_id) && !$use->byRef - ? clone $context->vars_in_scope[$use_var_id] + ? $context->vars_in_scope[$use_var_id] : Type::getMixed(); if ($use->byRef) { - $use_context->vars_in_scope[$use_var_id]->by_ref = true; + $use_context->vars_in_scope[$use_var_id] = + $use_context->vars_in_scope[$use_var_id]->setProperties(['by_ref' => true]); $use_context->references_to_external_scope[$use_var_id] = true; } @@ -164,7 +168,7 @@ public static function analyzeExpression( foreach ($context->vars_in_scope as $var_id => $type) { if (preg_match('/^\$' . $use->var->name . '[\[\-]/', $var_id)) { - $use_context->vars_in_scope[$var_id] = clone $type; + $use_context->vars_in_scope[$var_id] = $type; $use_context->vars_possibly_in_scope[$var_id] = true; } } @@ -179,7 +183,7 @@ public static function analyzeExpression( foreach ($short_closure_visitor->getUsedVariables() as $use_var_id => $_) { if ($context->hasVariable($use_var_id)) { - $use_context->vars_in_scope[$use_var_id] = clone $context->vars_in_scope[$use_var_id]; + $use_context->vars_in_scope[$use_var_id] = $context->vars_in_scope[$use_var_id]; if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { $parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes; @@ -325,8 +329,9 @@ public static function analyzeClosureUses( continue; } } elseif ($use->byRef) { - $new_type = Type::getMixed(); - $new_type->parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes; + $new_type = new Union([new TMixed()], [ + 'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes + ]); $context->remove($use_var_id); diff --git a/src/Psalm/Internal/Analyzer/DataFlowNodeData.php b/src/Psalm/Internal/Analyzer/DataFlowNodeData.php index 6973c69f219..46c97f99957 100644 --- a/src/Psalm/Internal/Analyzer/DataFlowNodeData.php +++ b/src/Psalm/Internal/Analyzer/DataFlowNodeData.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Analyzer; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,6 +11,8 @@ */ class DataFlowNodeData { + use ImmutableNonCloneableTrait; + /** * @var int */ diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index 0d57d4c24e1..9e8074847fe 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -465,12 +465,14 @@ public function getFunctionLikeAnalyzer(MethodIdentifier $method_id): ?MethodAna return $class_analyzer_to_examine->getFunctionLikeAnalyzer($method_name); } + /** @psalm-mutation-free */ public function getNamespace(): ?string { return null; } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlipped(?string $namespace_name = null): array @@ -483,6 +485,7 @@ public function getAliasedClassesFlipped(?string $namespace_name = null): array } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlippedReplaceable(?string $namespace_name = null): array @@ -508,21 +511,25 @@ public static function clearCache(): void InternalCallMapHandler::clearCache(); } + /** @psalm-mutation-free */ public function getFileName(): string { return $this->file_name; } + /** @psalm-mutation-free */ public function getFilePath(): string { return $this->file_path; } + /** @psalm-mutation-free */ public function getRootFileName(): string { return $this->root_file_name ?: $this->file_name; } + /** @psalm-mutation-free */ public function getRootFilePath(): string { return $this->root_file_path ?: $this->file_path; @@ -544,17 +551,20 @@ public function addParentFilePath(string $file_path): void $this->parent_file_paths[$file_path] = true; } + /** @psalm-mutation-free */ public function hasParentFilePath(string $file_path): bool { return $this->file_path === $file_path || isset($this->parent_file_paths[$file_path]); } + /** @psalm-mutation-free */ public function hasAlreadyRequiredFilePath(string $file_path): bool { return isset($this->required_file_paths[$file_path]); } /** + * @psalm-mutation-free * @return list */ public function getRequiredFilePaths(): array @@ -563,6 +573,7 @@ public function getRequiredFilePaths(): array } /** + * @psalm-mutation-free * @return list */ public function getParentFilePaths(): array @@ -570,12 +581,14 @@ public function getParentFilePaths(): array return array_keys($this->parent_file_paths); } + /** @psalm-mutation-free */ public function getRequireNesting(): int { return count($this->parent_file_paths); } /** + * @psalm-mutation-free * @return array */ public function getSuppressedIssues(): array @@ -607,22 +620,26 @@ public function removeSuppressedIssues(array $new_issues): void $this->suppressed_issues = array_diff_key($this->suppressed_issues, $new_issues); } + /** @psalm-mutation-free */ public function getFQCLN(): ?string { return null; } + /** @psalm-mutation-free */ public function getParentFQCLN(): ?string { return null; } + /** @psalm-mutation-free */ public function getClassName(): ?string { return null; } /** + * @psalm-mutation-free * @return array>|null */ public function getTemplateTypeMap(): ?array @@ -630,6 +647,7 @@ public function getTemplateTypeMap(): ?array return null; } + /** @psalm-mutation-free */ public function isStatic(): bool { return false; @@ -651,16 +669,19 @@ public function getProjectAnalyzer(): ProjectAnalyzer return $this->project_analyzer; } + /** @psalm-mutation-free */ public function getCodebase(): Codebase { return $this->codebase; } + /** @psalm-mutation-free */ public function getFirstStatementOffset(): int { return $this->first_statement_offset; } + /** @psalm-mutation-free */ public function getNodeTypeProvider(): NodeTypeProvider { if (!$this->node_data) { @@ -670,6 +691,7 @@ public function getNodeTypeProvider(): NodeTypeProvider return $this->node_data; } + /** @psalm-mutation-free */ public function getReturnType(): ?Union { return $this->return_type; diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 7e18ff9a250..ba279ff711a 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -73,6 +73,7 @@ class ReturnTypeAnalyzer * @return false|null * * @psalm-suppress PossiblyUnusedReturnValue unused but seems important + * @psalm-suppress ComplexMethod Unavoidably complex method */ public static function verifyReturnType( FunctionLike $function, @@ -784,6 +785,7 @@ public static function checkReturnType( $parent_class ); + /** @psalm-suppress UnusedMethodCall This call actually has the side effect of creating issues */ $fleshed_out_return_type->check( $function_like_analyzer, $storage->return_type_location, diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index d84d226640f..294088cbcbb 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -270,8 +270,8 @@ private static function processYieldTypes( if ($type instanceof TArray) { [$key_type_param, $value_type_param] = $type->type_params; - $key_type = Type::combineUnionTypes(clone $key_type_param, $key_type); - $value_type = Type::combineUnionTypes(clone $value_type_param, $value_type); + $key_type = Type::combineUnionTypes($key_type_param, $key_type); + $value_type = Type::combineUnionTypes($value_type_param, $value_type); } elseif ($type instanceof TIterable || $type instanceof TNamedObject ) { diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 8b51864ea13..5d4f4ca2b08 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -55,6 +55,7 @@ use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -192,7 +193,7 @@ public function analyze( if ($global_context) { foreach ($global_context->constants as $const_name => $var_type) { if (!$context->hasVariable($const_name)) { - $context->vars_in_scope[$const_name] = clone $var_type; + $context->vars_in_scope[$const_name] = $var_type; } } } @@ -268,7 +269,10 @@ public function analyze( $statements_analyzer->data_flow_graph->addNode($use_assignment); - $context->vars_in_scope[$use_var_id]->parent_nodes += [$use_assignment->id => $use_assignment]; + $context->vars_in_scope[$use_var_id] = + $context->vars_in_scope[$use_var_id]->addParentNodes( + [$use_assignment->id => $use_assignment] + ); } if ($use->byRef) { @@ -623,8 +627,9 @@ public function analyze( /** * @var TClosure */ - $closure_atomic = clone $function_type->getSingleAtomic(); + $closure_atomic = $function_type->getSingleAtomic(); + $new_closure_return_type = $closure_atomic->return_type; if (($storage->return_type === $storage->signature_return_type) && (!$storage->return_type || $storage->return_type->hasMixed() @@ -634,14 +639,25 @@ public function analyze( $storage->return_type )) ) { - /** @psalm-suppress InaccessibleProperty Acting on clone */ - $closure_atomic->return_type = $closure_return_type; + $new_closure_return_type = $closure_return_type; } - /** @psalm-suppress InaccessibleProperty Acting on clone */ - $closure_atomic->is_pure = !$this->inferred_impure; - - $statements_analyzer->node_data->setType($this->function, new Union([$closure_atomic])); + $new_closure_is_pure = !$this->inferred_impure; + + $statements_analyzer->node_data->setType( + $this->function, + new Union([ + new TClosure( + $closure_atomic->value, + $closure_atomic->params, + $new_closure_return_type, + $new_closure_is_pure, + $closure_atomic->byref_uses, + $closure_atomic->extra_types, + $closure_atomic->from_docblock, + ) + ]) + ); } } @@ -1039,8 +1055,48 @@ private function processParams( ); } + + $parent_nodes = []; + if ($statements_analyzer->data_flow_graph + && $function_param->location + ) { + //don't add to taint flow graph if the type can't transmit taints + if (!$statements_analyzer->data_flow_graph instanceof TaintFlowGraph + || $function_param->type === null + || !$function_param->type->isSingle() + || (!$function_param->type->isInt() + && !$function_param->type->isFloat() + && !$function_param->type->isBool()) + ) { + $param_assignment = DataFlowNode::getForAssignment( + $function_param_id, + $function_param->location + ); + + $statements_analyzer->data_flow_graph->addNode($param_assignment); + + if ($cased_method_id) { + $type_source = DataFlowNode::getForMethodArgument( + $cased_method_id, + $cased_method_id, + $offset, + $function_param->location, + null + ); + + $statements_analyzer->data_flow_graph->addPath($type_source, $param_assignment, 'param'); + } + + if ($storage->variadic) { + $this->param_nodes += [$param_assignment->id => $param_assignment]; + } + + $parent_nodes = [$param_assignment->id => $param_assignment]; + } + } + if ($function_param->type) { - $param_type = clone $function_param->type; + $param_type = $function_param->type; try { $param_type = TypeExpander::expandUnion( @@ -1084,8 +1140,13 @@ private function processParams( $check_stmts = false; } } + + $param_type = $param_type->addParentNodes($parent_nodes); } else { - $param_type = Type::getMixed(); + $param_type = new Union([new TMixed()], [ + 'by_ref' => $function_param->by_ref, + 'parent_nodes' => $parent_nodes + ]); } $var_type = $param_type; @@ -1094,57 +1155,26 @@ private function processParams( if ($storage->allow_named_arg_calls) { $var_type = new Union([ new TArray([Type::getArrayKey(), $param_type]), + ], [ + 'by_ref' => $function_param->by_ref, + 'parent_nodes' => $parent_nodes ]); } else { $var_type = new Union([ new TList($param_type), + ], [ + 'by_ref' => $function_param->by_ref, + 'parent_nodes' => $parent_nodes ]); } } - if ($statements_analyzer->data_flow_graph - && $function_param->location - ) { - //don't add to taint flow graph if the type can't transmit taints - if (!$statements_analyzer->data_flow_graph instanceof TaintFlowGraph - || $function_param->type === null - || !$function_param->type->isSingle() - || (!$function_param->type->isInt() - && !$function_param->type->isFloat() - && !$function_param->type->isBool()) - ) { - $param_assignment = DataFlowNode::getForAssignment( - $function_param_id, - $function_param->location - ); - - $statements_analyzer->data_flow_graph->addNode($param_assignment); - - if ($cased_method_id) { - $type_source = DataFlowNode::getForMethodArgument( - $cased_method_id, - $cased_method_id, - $offset, - $function_param->location, - null - ); - - $statements_analyzer->data_flow_graph->addPath($type_source, $param_assignment, 'param'); - } - - if ($storage->variadic) { - $this->param_nodes += [$param_assignment->id => $param_assignment]; - } - - $var_type->parent_nodes += [$param_assignment->id => $param_assignment]; - } - } - $context->vars_in_scope[$function_param_id] = $var_type; $context->vars_possibly_in_scope[$function_param_id] = true; if ($function_param->by_ref) { - $context->vars_in_scope[$function_param_id]->by_ref = true; + $context->vars_in_scope[$function_param_id] = + $context->vars_in_scope[$function_param_id]->setProperties(['by_ref' => true]); $context->references_to_external_scope[$function_param_id] = true; } @@ -1239,8 +1269,7 @@ private function processParams( } if ($has_template_types) { - $substituted_type = clone $param_type; - if ($substituted_type->check( + if ($param_type->check( $this->source, $function_param->type_location, $this->suppressed_issues, @@ -1665,6 +1694,7 @@ public function getId(): string } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlipped(): array @@ -1680,6 +1710,7 @@ public function getAliasedClassesFlipped(): array } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlippedReplaceable(): array @@ -1695,6 +1726,7 @@ public function getAliasedClassesFlippedReplaceable(): array } /** + * @psalm-mutation-free * @return array>|null */ public function getTemplateTypeMap(): ?array @@ -1861,6 +1893,26 @@ private function getFunctionInformation( ); } + $props = []; + if ($storage->external_mutation_free + && !$storage->mutation_free_inferred + ) { + $props = ['reference_free' => true]; + if ($this->function->name->name !== '__construct') { + $props['allow_mutations'] = false; + } + } + + if ($codebase->taint_flow_graph + && $storage->specialize_call + && $storage->location + ) { + $new_parent_node = DataFlowNode::getForAssignment('$this in ' . $method_id, $storage->location); + + $codebase->taint_flow_graph->addNode($new_parent_node); + $props['parent_nodes'] = [$new_parent_node->id => $new_parent_node]; + } + if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); @@ -1878,29 +1930,10 @@ private function getFunctionInformation( } } - $context->vars_in_scope['$this'] = $this->storage->if_this_is_type; + $context->vars_in_scope['$this'] = $this->storage->if_this_is_type + ->setProperties($props); } else { - $context->vars_in_scope['$this'] = new Union([$this_object_type]); - } - - if ($codebase->taint_flow_graph - && $storage->specialize_call - && $storage->location - ) { - $new_parent_node = DataFlowNode::getForAssignment('$this in ' . $method_id, $storage->location); - - $codebase->taint_flow_graph->addNode($new_parent_node); - $context->vars_in_scope['$this']->parent_nodes += [$new_parent_node->id => $new_parent_node]; - } - - if ($storage->external_mutation_free - && !$storage->mutation_free_inferred - ) { - $context->vars_in_scope['$this']->reference_free = true; - - if ($this->function->name->name !== '__construct') { - $context->vars_in_scope['$this']->allow_mutations = false; - } + $context->vars_in_scope['$this'] = new Union([$this_object_type], $props); } $context->vars_possibly_in_scope['$this'] = true; diff --git a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php index a3b02c1435a..9ade34375ff 100644 --- a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php @@ -27,6 +27,7 @@ */ class MethodAnalyzer extends FunctionLikeAnalyzer { + /** @psalm-external-mutation-free */ public function __construct( PhpParser\Node\Stmt\ClassMethod $function, SourceAnalyzer $source, diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index f6bfe8db4b9..6a0394256fb 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -1080,8 +1080,6 @@ private static function transformTemplates( } if ($new_bases) { - $mapped_type = clone $mapped_type; - foreach ($new_bases as $new_base_class_name) { self::transformTemplates( $template_extended_params, diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index a59f2111f6d..2e1bc2867aa 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -563,11 +563,14 @@ static function (): void { } } + /** @psalm-mutation-free */ public static function getInstance(): ProjectAnalyzer { + /** @psalm-suppress ImpureStaticProperty */ return self::$instance; } + /** @psalm-mutation-free */ public function canReportIssues(string $file_path): bool { return isset($this->project_files[$file_path]); diff --git a/src/Psalm/Internal/Analyzer/SourceAnalyzer.php b/src/Psalm/Internal/Analyzer/SourceAnalyzer.php index 8be2d28a7ce..506ee7b5cde 100644 --- a/src/Psalm/Internal/Analyzer/SourceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/SourceAnalyzer.php @@ -23,12 +23,14 @@ public function __destruct() unset($this->source); } + /** @psalm-mutation-free */ public function getAliases(): Aliases { return $this->source->getAliases(); } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlipped(): array @@ -37,6 +39,7 @@ public function getAliasedClassesFlipped(): array } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlippedReplaceable(): array @@ -44,36 +47,43 @@ public function getAliasedClassesFlippedReplaceable(): array return $this->source->getAliasedClassesFlippedReplaceable(); } + /** @psalm-mutation-free */ public function getFQCLN(): ?string { return $this->source->getFQCLN(); } + /** @psalm-mutation-free */ public function getClassName(): ?string { return $this->source->getClassName(); } + /** @psalm-mutation-free */ public function getParentFQCLN(): ?string { return $this->source->getParentFQCLN(); } + /** @psalm-mutation-free */ public function getFileName(): string { return $this->source->getFileName(); } + /** @psalm-mutation-free */ public function getFilePath(): string { return $this->source->getFilePath(); } + /** @psalm-mutation-free */ public function getRootFileName(): string { return $this->source->getRootFileName(); } + /** @psalm-mutation-free */ public function getRootFilePath(): string { return $this->source->getRootFilePath(); @@ -84,16 +94,19 @@ public function setRootFilePath(string $file_path, string $file_name): void $this->source->setRootFilePath($file_path, $file_name); } + /** @psalm-mutation-free */ public function hasParentFilePath(string $file_path): bool { return $this->source->hasParentFilePath($file_path); } + /** @psalm-mutation-free */ public function hasAlreadyRequiredFilePath(string $file_path): bool { return $this->source->hasAlreadyRequiredFilePath($file_path); } + /** @psalm-mutation-free */ public function getRequireNesting(): int { return $this->source->getRequireNesting(); @@ -109,7 +122,7 @@ public function getSource(): StatementsSource /** * Get a list of suppressed issues - * + *@psalm-mutation-free * @return array */ public function getSuppressedIssues(): array @@ -133,11 +146,13 @@ public function removeSuppressedIssues(array $new_issues): void $this->source->removeSuppressedIssues($new_issues); } + /** @psalm-mutation-free */ public function getNamespace(): ?string { return $this->source->getNamespace(); } + /** @psalm-mutation-free */ public function isStatic(): bool { return $this->source->isStatic(); @@ -168,6 +183,7 @@ public function getFileAnalyzer(): FileAnalyzer } /** + * @psalm-mutation-free * @return array>|null */ public function getTemplateTypeMap(): ?array @@ -175,6 +191,7 @@ public function getTemplateTypeMap(): ?array return $this->source->getTemplateTypeMap(); } + /** @psalm-mutation-free */ public function getNodeTypeProvider(): NodeTypeProvider { return $this->source->getNodeTypeProvider(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 6939ec53888..092e06cfa18 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -212,6 +212,7 @@ public static function analyze( } if (isset($context->vars_in_scope[$var_comment->var_id])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $comment_type->parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes; } @@ -266,7 +267,7 @@ public static function analyze( $foreach_context = clone $context; foreach ($foreach_context->vars_in_scope as $context_var_id => $context_type) { - $foreach_context->vars_in_scope[$context_var_id] = clone $context_type; + $foreach_context->vars_in_scope[$context_var_id] = $context_type; } $foreach_context->inside_loop = true; @@ -291,10 +292,10 @@ public static function analyze( ); } - $value_type ??= Type::getMixed(); - - if ($stmt->byRef) { - $value_type->by_ref = true; + if ($value_type !== null) { + $value_type = $value_type->setProperties(['by_ref' => $stmt->byRef]); + } else { + $value_type = new Union([new TMixed()], ['by_ref' => $stmt->byRef]); } if ($stmt->byRef @@ -342,7 +343,9 @@ public static function analyze( if (isset($foreach_context->vars_in_scope[$var_comment->var_id])) { $existing_var_type = $foreach_context->vars_in_scope[$var_comment->var_id]; + /** @psalm-suppress InaccessibleProperty We just created this type */ $comment_type->parent_nodes = $existing_var_type->parent_nodes; + /** @psalm-suppress InaccessibleProperty We just created this type */ $comment_type->by_ref = $existing_var_type->by_ref; } @@ -386,6 +389,9 @@ public static function analyze( /** * @param PhpParser\Node\Stmt\Foreach_|PhpParser\Node\Expr\YieldFrom $stmt + * + * @psalm-suppress ComplexMethod + * * @return false|null */ public static function checkIteratorType( @@ -491,7 +497,7 @@ public static function checkIteratorType( $always_non_empty_array = false; } - $value_type = Type::combineUnionTypes($value_type, clone $iterator_atomic_type->type_params[1]); + $value_type = Type::combineUnionTypes($value_type, $iterator_atomic_type->type_params[1]); $key_type_part = $iterator_atomic_type->type_params[0]; @@ -756,7 +762,7 @@ public static function handleIterable( ) { $value_type = Type::combineUnionTypes( $value_type, - new Union([clone $iterator_atomic_type]) + new Union([$iterator_atomic_type]) ); $key_type = Type::combineUnionTypes( diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php index 5d6cdb28e91..d1b0e1f0fa5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php @@ -394,7 +394,7 @@ public static function addConditionallyAssignedVarsToContext( foreach ($assigned_in_conditional_var_ids as $var_id => $_) { if (isset($post_leaving_if_context->vars_in_scope[$var_id])) { - $post_if_context->vars_in_scope[$var_id] = clone $post_leaving_if_context->vars_in_scope[$var_id]; + $post_if_context->vars_in_scope[$var_id] = $post_leaving_if_context->vars_in_scope[$var_id]; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php index fd7751def33..b89c8dc54a7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php @@ -311,7 +311,7 @@ public static function analyze( ) { $context->clauses = $else_context->clauses; foreach ($else_context->vars_in_scope as $var_id => $type) { - $context->vars_in_scope[$var_id] = clone $type; + $context->vars_in_scope[$var_id] = $type; } foreach ($pre_assignment_else_redefined_vars as $var_id => $reconciled_type) { @@ -376,15 +376,18 @@ public static function analyze( ); if ($if_scope->new_vars) { - foreach ($if_scope->new_vars as $var_id => $type) { + foreach ($if_scope->new_vars as $var_id => &$type) { if (isset($context->vars_possibly_in_scope[$var_id]) && $statements_analyzer->data_flow_graph ) { - $type->parent_nodes += $statements_analyzer->getParentNodesForPossiblyUndefinedVariable($var_id); + $type = $type->addParentNodes( + $statements_analyzer->getParentNodesForPossiblyUndefinedVariable($var_id) + ); } $context->vars_in_scope[$var_id] = $type; } + unset($type); } if ($if_scope->redefined_vars) { @@ -429,7 +432,8 @@ public static function analyze( $context->vars_in_scope[$var_id] = $combined_type; } else { - $context->vars_in_scope[$var_id]->parent_nodes += $type->parent_nodes; + $context->vars_in_scope[$var_id] = + $context->vars_in_scope[$var_id]->addParentNodes($type->parent_nodes); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php index d3f428692c8..c8dce61b121 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php @@ -111,7 +111,7 @@ public static function analyze( $continue_context = clone $loop_context; foreach ($continue_context->vars_in_scope as $context_var_id => $context_type) { - $continue_context->vars_in_scope[$context_var_id] = clone $context_type; + $continue_context->vars_in_scope[$context_var_id] = $context_type; } $continue_context->loop_scope = $loop_scope; @@ -174,10 +174,6 @@ public static function analyze( $continue_context = clone $loop_context; - foreach ($continue_context->vars_in_scope as $context_var_id => $context_type) { - $continue_context->vars_in_scope[$context_var_id] = clone $context_type; - } - $continue_context->loop_scope = $loop_scope; $continue_context->protected_var_ids = $loop_scope->protected_var_ids; @@ -321,7 +317,7 @@ public static function analyze( ) ) { if (isset($pre_loop_context->vars_in_scope[$var_id])) { - $continue_context->vars_in_scope[$var_id] = clone $pre_loop_context->vars_in_scope[$var_id]; + $continue_context->vars_in_scope[$var_id] = $pre_loop_context->vars_in_scope[$var_id]; } else { $continue_context->removePossibleReference($var_id); } @@ -412,8 +408,11 @@ public static function analyze( $loop_parent_context->removeVarFromConflictingClauses($var_id); } else { - $loop_parent_context->vars_in_scope[$var_id]->parent_nodes - += $loop_context->vars_in_scope[$var_id]->parent_nodes; + $loop_parent_context->vars_in_scope[$var_id] + = $loop_parent_context->vars_in_scope[$var_id]->addParentNodes( + $loop_context->vars_in_scope[$var_id]->parent_nodes + ) + ; } } @@ -425,8 +424,11 @@ public static function analyze( } if ($continue_context->vars_in_scope[$var_id]->hasMixed()) { - $continue_context->vars_in_scope[$var_id]->parent_nodes - += $loop_parent_context->vars_in_scope[$var_id]->parent_nodes; + $continue_context->vars_in_scope[$var_id] + = $continue_context->vars_in_scope[$var_id]->addParentNodes( + $loop_parent_context->vars_in_scope[$var_id]->parent_nodes + ) + ; $loop_parent_context->vars_in_scope[$var_id] = $continue_context->vars_in_scope[$var_id]; @@ -443,10 +445,12 @@ public static function analyze( $loop_parent_context->removeVarFromConflictingClauses($var_id); } else { - $loop_parent_context->vars_in_scope[$var_id]->parent_nodes = array_merge( - $loop_parent_context->vars_in_scope[$var_id]->parent_nodes, - $continue_context->vars_in_scope[$var_id]->parent_nodes - ); + $loop_parent_context->vars_in_scope[$var_id] = + $loop_parent_context->vars_in_scope[$var_id]->setParentNodes(array_merge( + $loop_parent_context->vars_in_scope[$var_id]->parent_nodes, + $continue_context->vars_in_scope[$var_id]->parent_nodes + )) + ; } } } @@ -548,7 +552,8 @@ private static function updateLoopScopeContexts( $type ); } else { - $continue_context->vars_in_scope[$var]->parent_nodes += $type->parent_nodes; + $continue_context->vars_in_scope[$var] = + $continue_context->vars_in_scope[$var]->addParentNodes($type->parent_nodes); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php index cb8991c68d4..0831552fddc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php @@ -502,7 +502,7 @@ public static function analyze( foreach ($case_scope->break_vars as $var_id => $type) { if (isset($context->vars_in_scope[$var_id])) { $switch_scope->possibly_redefined_vars[$var_id] = Type::combineUnionTypes( - clone $type, + $type, $switch_scope->possibly_redefined_vars[$var_id] ?? null ); } @@ -516,7 +516,7 @@ public static function analyze( unset($switch_scope->new_vars_in_scope[$var_id]); } else { $switch_scope->new_vars_in_scope[$var_id] = Type::combineUnionTypes( - clone $case_scope->break_vars[$var_id], + $case_scope->break_vars[$var_id], $type ); } @@ -530,7 +530,7 @@ public static function analyze( foreach ($switch_scope->redefined_vars as $var_id => $type) { if (isset($case_scope->break_vars[$var_id])) { $switch_scope->redefined_vars[$var_id] = Type::combineUnionTypes( - clone $case_scope->break_vars[$var_id], + $case_scope->break_vars[$var_id], $type ); } else { @@ -595,7 +595,7 @@ private static function handleNonReturningCase( } else { foreach ($case_redefined_vars as $var_id => $type) { $switch_scope->possibly_redefined_vars[$var_id] = Type::combineUnionTypes( - clone $type, + $type, $switch_scope->possibly_redefined_vars[$var_id] ?? null ); } @@ -610,7 +610,7 @@ private static function handleNonReturningCase( } else { $switch_scope->redefined_vars[$var_id] = Type::combineUnionTypes( $type, - clone $case_redefined_vars[$var_id] + $case_redefined_vars[$var_id] ); } } @@ -630,7 +630,7 @@ private static function handleNonReturningCase( unset($switch_scope->new_vars_in_scope[$new_var]); } else { $switch_scope->new_vars_in_scope[$new_var] = - Type::combineUnionTypes(clone $case_context->vars_in_scope[$new_var], $type); + Type::combineUnionTypes($case_context->vars_in_scope[$new_var], $type); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 68fc8398a9b..7543989fd8c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -86,16 +86,6 @@ public static function analyze( } $context->inside_try = $was_inside_try; - if ($try_context->finally_scope) { - foreach ($context->vars_in_scope as $var_id => $type) { - $try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( - $try_context->finally_scope->vars_in_scope[$var_id] ?? null, - $type, - $statements_analyzer->getCodebase() - ); - } - } - $context->has_returned = false; $try_block_control_actions = ScopeAnalyzer::getControlActions( @@ -114,10 +104,9 @@ public static function analyze( foreach ($context->vars_in_scope as $var_id => $type) { if (!isset($try_context->vars_in_scope[$var_id])) { - $try_context->vars_in_scope[$var_id] = clone $type; + $try_context->vars_in_scope[$var_id] = $type; - $context->vars_in_scope[$var_id]->possibly_undefined = true; - $context->vars_in_scope[$var_id]->possibly_undefined_from_try = true; + $context->vars_in_scope[$var_id] = $type->setPossiblyUndefined(true, true); } else { $try_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $try_context->vars_in_scope[$var_id], @@ -126,6 +115,16 @@ public static function analyze( } } + if ($try_context->finally_scope) { + foreach ($context->vars_in_scope as $var_id => $type) { + $try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( + $try_context->finally_scope->vars_in_scope[$var_id] ?? null, + $type, + $statements_analyzer->getCodebase() + ); + } + } + $try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope; $try_context->possibly_thrown_exceptions = $context->possibly_thrown_exceptions; @@ -164,9 +163,10 @@ public static function analyze( foreach ($catch_context->vars_in_scope as $var_id => $type) { if (!isset($old_context->vars_in_scope[$var_id])) { - $type = clone $type; - $type->possibly_undefined_from_try = true; - $catch_context->vars_in_scope[$var_id] = $type; + $catch_context->vars_in_scope[$var_id] = $type->setPossiblyUndefined( + $catch_context->vars_in_scope[$var_id]->possibly_undefined, + true + ); } else { $catch_context->vars_in_scope[$var_id] = Type::combineUnionTypes( $type, @@ -266,17 +266,17 @@ public static function analyze( $catch_context->vars_in_scope[$catch_var_id] = new Union( array_map( static function (string $fq_catch_class) use ($codebase): TNamedObject { - $catch_class_type = new TNamedObject($fq_catch_class); - - if (version_compare(PHP_VERSION, '7.0.0dev', '>=') - && strtolower($fq_catch_class) !== 'throwable' - && $codebase->interfaceExists($fq_catch_class) - && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') - ) { - return $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); - } - - return $catch_class_type; + return new TNamedObject( + $fq_catch_class, + false, + false, + version_compare(PHP_VERSION, '7.0.0dev', '>=') + && strtolower($fq_catch_class) !== 'throwable' + && $codebase->interfaceExists($fq_catch_class) + && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') + ? ['Throwable' => new TNamedObject('Throwable')] + : [] + ); }, $fq_catch_classes ) @@ -310,9 +310,11 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { if ($statements_analyzer->data_flow_graph) { $catch_var_node = DataFlowNode::getForAssignment($catch_var_id, $location); - $catch_context->vars_in_scope[$catch_var_id]->parent_nodes = [ - $catch_var_node->id => $catch_var_node - ]; + $catch_context->vars_in_scope[$catch_var_id] = + $catch_context->vars_in_scope[$catch_var_id]->addParentNodes([ + $catch_var_node->id => $catch_var_node + ]) + ; if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { $statements_analyzer->data_flow_graph->addPath( @@ -395,7 +397,7 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { } if ($try_context->finally_scope) { - foreach ($catch_context->vars_in_scope as $var_id => $type) { + foreach ($catch_context->vars_in_scope as $var_id => &$type) { if (isset($try_context->finally_scope->vars_in_scope[$var_id])) { if ($try_context->finally_scope->vars_in_scope[$var_id] !== $type) { $try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( @@ -405,11 +407,13 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { ); } } else { - $try_context->finally_scope->vars_in_scope[$var_id] = $type; - $type->possibly_undefined = true; - $type->possibly_undefined_from_try = true; + $try_context->finally_scope->vars_in_scope[$var_id] = $type->setPossiblyUndefined( + true, + true + ); } } + unset($type); } } @@ -439,20 +443,22 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { if (isset($context->vars_in_scope[$var_id]) && isset($finally_context->vars_in_scope[$var_id]) ) { - if ($context->vars_in_scope[$var_id]->possibly_undefined - && $context->vars_in_scope[$var_id]->possibly_undefined_from_try - ) { - $context->vars_in_scope[$var_id]->possibly_undefined = false; - $context->vars_in_scope[$var_id]->possibly_undefined_from_try = false; - } + $possibly_undefined = $context->vars_in_scope[$var_id]->possibly_undefined + && $context->vars_in_scope[$var_id]->possibly_undefined_from_try; $context->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->vars_in_scope[$var_id], $finally_context->vars_in_scope[$var_id], $codebase ); + if ($possibly_undefined) { + /** @psalm-suppress InaccessibleProperty We just created this type */ + $context->vars_in_scope[$var_id]->possibly_undefined = false; + /** @psalm-suppress InaccessibleProperty We just created this type */ + $context->vars_in_scope[$var_id]->possibly_undefined_from_try = false; + } } elseif (isset($finally_context->vars_in_scope[$var_id])) { - $context->vars_in_scope[$var_id] = clone $finally_context->vars_in_scope[$var_id]; + $context->vars_in_scope[$var_id] = $finally_context->vars_in_scope[$var_id]; } } } @@ -460,14 +466,13 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { foreach ($definitely_newly_assigned_var_ids as $var_id => $_) { if (isset($context->vars_in_scope[$var_id])) { - $new_type = clone $context->vars_in_scope[$var_id]; - - if ($new_type->possibly_undefined_from_try) { - $new_type->possibly_undefined = false; - $new_type->possibly_undefined_from_try = false; + if ($context->vars_in_scope[$var_id]->possibly_undefined_from_try) { + $context->vars_in_scope[$var_id] = + $context->vars_in_scope[$var_id]->setPossiblyUndefined( + false, + false + ); } - - $context->vars_in_scope[$var_id] = $new_type; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php index 82691aaa459..1b220288fc5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php @@ -57,7 +57,7 @@ public static function analyze( } if ($context->finally_scope) { - foreach ($context->vars_in_scope as $var_id => $type) { + foreach ($context->vars_in_scope as $var_id => &$type) { if (isset($context->finally_scope->vars_in_scope[$var_id])) { $context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->finally_scope->vars_in_scope[$var_id], @@ -65,11 +65,11 @@ public static function analyze( $statements_analyzer->getCodebase() ); } else { + $type = $type->setPossiblyUndefined(true, true); $context->finally_scope->vars_in_scope[$var_id] = $type; - $type->possibly_undefined = true; - $type->possibly_undefined_from_try = true; } } + unset($type); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php index 60cb5527e45..7466d720e4e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php @@ -78,7 +78,7 @@ public static function analyze( } if ($context->finally_scope) { - foreach ($context->vars_in_scope as $var_id => $type) { + foreach ($context->vars_in_scope as $var_id => &$type) { if (isset($context->finally_scope->vars_in_scope[$var_id])) { $context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->finally_scope->vars_in_scope[$var_id], @@ -86,9 +86,8 @@ public static function analyze( $statements_analyzer->getCodebase() ); } else { + $type = $type->setPossiblyUndefined(true, true); $context->finally_scope->vars_in_scope[$var_id] = $type; - $type->possibly_undefined = true; - $type->possibly_undefined_from_try = true; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 52eb2c41d08..8c5d7c2d895 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -129,11 +129,9 @@ public static function analyze( $array_creation_info->all_list ); - $stmt_type = new Union([$atomic_type]); - - if ($array_creation_info->parent_taint_nodes) { - $stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes; - } + $stmt_type = new Union([$atomic_type], [ + 'parent_nodes' => $array_creation_info->parent_taint_nodes + ]); $statements_analyzer->node_data->setType($stmt, $stmt_type); @@ -155,12 +153,10 @@ public static function analyze( $stmt_type = new Union([ $array_type, + ], [ + 'parent_nodes' => $array_creation_info->parent_taint_nodes ]); - if ($array_creation_info->parent_taint_nodes) { - $stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes; - } - $statements_analyzer->node_data->setType($stmt, $stmt_type); return true; @@ -240,12 +236,10 @@ public static function analyze( $stmt_type = new Union([ $array_type, + ], [ + 'parent_nodes' => $array_creation_info->parent_taint_nodes ]); - if ($array_creation_info->parent_taint_nodes) { - $stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes; - } - $statements_analyzer->node_data->setType($stmt, $stmt_type); return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 670fae607b4..f17b6cc5496 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -3697,11 +3697,7 @@ private static function getArrayKeyExistsAssertions( } } - $key_type = $atomic_type->getGenericKeyType(); - - if ($key_possibly_undefined) { - $key_type->possibly_undefined = true; - } + $key_type = $atomic_type->getGenericKeyType($key_possibly_undefined); } else { $key_type = $atomic_type->type_params[0]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index fecc1cda373..fe4c759ecd0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -41,6 +41,7 @@ use function array_reverse; use function array_shift; use function array_slice; +use function assert; use function count; use function end; use function implode; @@ -187,11 +188,11 @@ public static function updateArrayType( if (count($string_literals) + count($int_literals) === count($all_atomic_types)) { foreach ($string_literals as $string_literal) { - $key_values[] = clone $string_literal; + $key_values[] = $string_literal; } foreach ($int_literals as $int_literal) { - $key_values[] = clone $int_literal; + $key_values[] = $int_literal; } } } @@ -315,7 +316,7 @@ private static function updateTypeWithKeyValues( if (isset($properties[$key_value->value])) { $has_matching_objectlike_property = true; - $properties[$key_value->value] = clone $current_type; + $properties[$key_value->value] = $current_type; } } $type = $type->setProperties($properties); @@ -349,8 +350,8 @@ private static function updateTypeWithKeyValues( $has_matching_objectlike_property = true; $changed = true; - $type = $type->replaceTypeParam(Type::combineUnionTypes( - clone $current_type, + $type = $type->setTypeParam(Type::combineUnionTypes( + $current_type, $type->type_param, $codebase, true, @@ -371,7 +372,7 @@ private static function updateTypeWithKeyValues( $key_value = $key_values[0]; $object_like = new TKeyedArray( - [$key_value->value => clone $current_type], + [$key_value->value => $current_type], $key_value instanceof TLiteralClassString ? [$key_value->value => true] : null, @@ -387,7 +388,7 @@ private static function updateTypeWithKeyValues( $array_assignment_type = new Union([ new TNonEmptyArray([ new Union($array_assignment_literals), - clone $current_type + $current_type ]) ]); } @@ -410,7 +411,7 @@ private static function updateTypeWithKeyValues( private static function taintArrayAssignment( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\ArrayDimFetch $expr, - Union $stmt_type, + Union &$stmt_type, Union $child_stmt_type, ?string $var_var_id, array $key_values @@ -430,7 +431,7 @@ private static function taintArrayAssignment( $old_parent_nodes = $stmt_type->parent_nodes; - $stmt_type->parent_nodes = [$parent_node->id => $parent_node]; + $stmt_type = $stmt_type->setParentNodes([$parent_node->id => $parent_node]); foreach ($old_parent_nodes as $old_parent_node) { $statements_analyzer->data_flow_graph->addPath( @@ -622,11 +623,11 @@ private static function updateArrayAssignmentChildType( if ($atomic_root_types['array']->is_list && $array_atomic_type instanceof TList ) { - $array_atomic_type = clone $atomic_root_types['array']; + $array_atomic_type = $atomic_root_types['array']; - $new_child_type = new Union([$array_atomic_type]); - - $new_child_type->parent_nodes = $root_type->parent_nodes; + $new_child_type = new Union([$array_atomic_type], [ + 'parent_nodes' => $root_type->parent_nodes + ]); } } elseif ($array_atomic_type instanceof TList) { $array_atomic_type = new TNonEmptyList( @@ -736,24 +737,22 @@ private static function analyzeNestedArrayAssignment( $full_var_id = false; } - if (!($child_stmt_var_type = $statements_analyzer->node_data->getType($child_stmt->var))) { + if (!($array_type = $statements_analyzer->node_data->getType($child_stmt->var))) { return; } - if ($child_stmt_var_type->isNever()) { - $child_stmt_var_type = Type::getEmptyArray(); - $statements_analyzer->node_data->setType($child_stmt->var, $child_stmt_var_type); + if ($array_type->isNever()) { + $array_type = Type::getEmptyArray(); + $statements_analyzer->node_data->setType($child_stmt->var, $array_type); } $extended_var_id = $root_var_id . implode('', $var_id_additions); if ($parent_var_id && isset($context->vars_in_scope[$parent_var_id])) { - $child_stmt_var_type = clone $context->vars_in_scope[$parent_var_id]; - $statements_analyzer->node_data->setType($child_stmt->var, $child_stmt_var_type); + $array_type = $context->vars_in_scope[$parent_var_id]; + $statements_analyzer->node_data->setType($child_stmt->var, $array_type); } - $array_type = clone $child_stmt_var_type; - $is_last = $i === count($child_stmts) - 1; $child_stmt_dim_type_or_int = $child_stmt_dim_type ?? Type::getInt(); @@ -769,7 +768,10 @@ private static function analyzeNestedArrayAssignment( !$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->dim, + $child_stmt_dim_type_or_int + ); } $statements_analyzer->node_data->setType( @@ -777,20 +779,6 @@ private static function analyzeNestedArrayAssignment( $child_stmt_type ); - $statements_analyzer->node_data->setType($child_stmt->var, $array_type); - - if ($root_var_id) { - if (!$parent_var_id) { - $rooted_parent_id = $root_var_id; - $root_type = $array_type; - } else { - $rooted_parent_id = $parent_var_id; - } - - $context->vars_in_scope[$rooted_parent_id] = $array_type; - $context->possibly_assigned_var_ids[$rooted_parent_id] = true; - } - if ($is_last) { // we need this slight hack as the type we're putting it has to be // different from the type we're getting out @@ -817,6 +805,20 @@ private static function analyzeNestedArrayAssignment( } } + $statements_analyzer->node_data->setType($child_stmt->var, $array_type); + + if ($root_var_id) { + if (!$parent_var_id) { + $rooted_parent_id = $root_var_id; + $root_type = $array_type; + } else { + $rooted_parent_id = $parent_var_id; + } + + $context->vars_in_scope[$rooted_parent_id] = $array_type; + $context->possibly_assigned_var_ids[$rooted_parent_id] = true; + } + $current_type = $child_stmt_type; $current_dim = $child_stmt->dim; @@ -854,7 +856,7 @@ private static function analyzeNestedArrayAssignment( $offset_already_existed = true; } - $context->vars_in_scope[$extended_var_id] = clone $assignment_type; + $context->vars_in_scope[$extended_var_id] = $assignment_type; $context->possibly_assigned_var_ids[$extended_var_id] = true; } @@ -903,12 +905,12 @@ private static function analyzeNestedArrayAssignment( true ); } - - $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 ($new_child_type->hasNull() || $new_child_type->possibly_undefined) { + $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; $statements_analyzer->node_data->setType($child_stmt, $new_child_type); @@ -924,19 +926,35 @@ private static function analyzeNestedArrayAssignment( if ($root_var_id) { $extended_var_id = $root_var_id . implode('', $var_id_additions); $parent_array_var_id = $root_var_id . implode('', array_slice($var_id_additions, 0, -1)); - $context->vars_in_scope[$extended_var_id] = clone $child_stmt_type; + $context->vars_in_scope[$extended_var_id] = $child_stmt_type; $context->possibly_assigned_var_ids[$extended_var_id] = true; } if ($statements_analyzer->data_flow_graph) { + $t_orig = $statements_analyzer->node_data->getType($child_stmt->var); + $array_type = $t_orig ?? Type::getMixed(); self::taintArrayAssignment( $statements_analyzer, $child_stmt, - $statements_analyzer->node_data->getType($child_stmt->var) ?? Type::getMixed(), + $array_type, $new_child_type, $parent_array_var_id, $child_stmt->dim ? self::getDimKeyValues($statements_analyzer, $child_stmt->dim) : [], ); + if ($t_orig) { + $statements_analyzer->node_data->setType($child_stmt->var, $array_type); + } + if ($root_var_id) { + if ($parent_array_var_id === $root_var_id) { + $rooted_parent_id = $root_var_id; + $root_type = $array_type; + } else { + assert($parent_array_var_id !== null); + $rooted_parent_id = $parent_array_var_id; + } + + $context->vars_in_scope[$rooted_parent_id] = $array_type; + } } } } @@ -965,11 +983,11 @@ private static function getDimKeyValues( if (count($string_literals) + count($int_literals) === count($all_atomic_types)) { foreach ($string_literals as $string_literal) { - $key_values[] = clone $string_literal; + $key_values[] = $string_literal; } foreach ($int_literals as $int_literal) { - $key_values[] = clone $int_literal; + $key_values[] = $int_literal; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index ea9bf51f9ff..f5c2000bac8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -453,7 +453,7 @@ private static function taintProperty( PhpParser\Node\Expr\PropertyFetch $stmt, string $property_id, ClassLikeStorage $class_storage, - Union $assignment_value_type, + Union &$assignment_value_type, Context $context ): void { if (!$statements_analyzer->data_flow_graph) { @@ -481,7 +481,8 @@ private static function taintProperty( if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { - $context->vars_in_scope[$var_id]->parent_nodes = []; + $context->vars_in_scope[$var_id] = + $context->vars_in_scope[$var_id]->setParentNodes([]); return; } @@ -523,7 +524,9 @@ private static function taintProperty( } } - $stmt_var_type = clone $context->vars_in_scope[$var_id]; + $stmt_var_type = $context->vars_in_scope[$var_id]->setParentNodes( + [$var_node->id => $var_node] + ); if ($context->vars_in_scope[$var_id]->parent_nodes) { foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) { @@ -531,15 +534,13 @@ private static function taintProperty( } } - $stmt_var_type->parent_nodes = [$var_node->id => $var_node]; - $context->vars_in_scope[$var_id] = $stmt_var_type; } } else { if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { - $assignment_value_type->parent_nodes = []; + $assignment_value_type = $assignment_value_type->setParentNodes([]); return; } @@ -863,7 +864,7 @@ private static function analyzeRegularAssignment( if ($context->collect_initializations && $lhs_var_id === '$this' ) { - $context_type->initialized_class = $context->self; + $context_type = $context_type->setProperties(['initialized_class' => $context->self]); } // because we don't want to be assigning for property declarations @@ -1055,7 +1056,7 @@ private static function analyzeAtomicAssignment( if (isset($class_storage->pseudo_property_set_types['$' . $prop_name])) { $class_property_type = TypeExpander::expandUnion( $codebase, - clone $class_storage->pseudo_property_set_types['$' . $prop_name], + $class_storage->pseudo_property_set_types['$' . $prop_name], $fq_class_name, $fq_class_name, $class_storage->parent_class @@ -1362,7 +1363,7 @@ private static function analyzeAtomicAssignment( $class_property_type = TypeExpander::expandUnion( $codebase, - clone $class_property_type, + $class_property_type, $fq_class_name, $lhs_type_part, $declaring_class_storage->parent_class, @@ -1489,7 +1490,7 @@ public static function getExpandedPropertyType( return null; } - $property_type = clone $property_storage->type; + $property_type = $property_storage->type; $fleshed_out_type = !$property_type->isMixed() ? TypeExpander::expandUnion( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php index e43f70e09bd..8d303408d6a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/StaticPropertyAssignmentAnalyzer.php @@ -228,8 +228,6 @@ public static function analyze( $source_analyzer->inferred_property_types[$prop_name_name] ?? null ); } - } else { - $class_property_type = clone $class_property_type; } if ($assignment_value_type->hasMixed()) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index da243bb3e9b..9155c2b1082 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -198,12 +198,10 @@ public static function analyze( $var_id, $comment_type, $comment_type_location, - $not_ignored_docblock_var_ids + $not_ignored_docblock_var_ids, + $var_id === $var_comment->var_id + && $assign_value_type && $comment_type && $assign_value_type->by_ref ); - - if ($var_id === $var_comment->var_id && $assign_value_type && $comment_type) { - $comment_type->by_ref = $assign_value_type->by_ref; - } } } @@ -294,18 +292,18 @@ public static function analyze( $parent_nodes = $temp_assign_value_type->parent_nodes ?? []; - $assign_value_type = $comment_type; - $assign_value_type->parent_nodes = $parent_nodes; + $assign_value_type = $comment_type->setParentNodes($parent_nodes); } elseif (!$assign_value_type) { if ($assign_value) { $assign_value_type = $statements_analyzer->node_data->getType($assign_value); } if ($assign_value_type) { - $assign_value_type = clone $assign_value_type; - $assign_value_type->from_property = false; - $assign_value_type->from_static_property = false; - $assign_value_type->ignore_isset = false; + $assign_value_type = $assign_value_type->setProperties([ + 'from_property' => false, + 'from_static_property' => false, + 'ignore_isset' => false, + ]); } else { $assign_value_type = Type::getMixed(); } @@ -323,7 +321,7 @@ public static function analyze( $assignment_node = new DataFlowNode('unknown-origin', 'unknown origin', null); } - $assign_value_type->parent_nodes = [ + $parent_nodes = [ $assignment_node->id => $assignment_node ]; @@ -331,9 +329,11 @@ public static function analyze( // Copy previous assignment's parent nodes inside a try. Since an exception could be thrown at any // point this is a workaround to ensure that use of a variable also uses all previous assignments. if (isset($context->vars_in_scope[$extended_var_id])) { - $assign_value_type->parent_nodes += $context->vars_in_scope[$extended_var_id]->parent_nodes; + $parent_nodes += $context->vars_in_scope[$extended_var_id]->parent_nodes; } } + + $assign_value_type = $assign_value_type->setParentNodes($parent_nodes); } if ($extended_var_id && isset($context->vars_in_scope[$extended_var_id])) { @@ -352,7 +352,7 @@ public static function analyze( $statements_analyzer->getSource()->inferred_has_mutation = true; } - $assign_value_type->by_ref = true; + $assign_value_type = $assign_value_type->setByRef(true); } // removes dependent vars from $context @@ -549,12 +549,10 @@ public static function analyze( $data_flow_graph = $statements_analyzer->data_flow_graph; if ($context->vars_in_scope[$var_id]->parent_nodes) { - $context->vars_in_scope[$var_id] = clone $context->vars_in_scope[$var_id]; - if ($data_flow_graph instanceof TaintFlowGraph && in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { - $context->vars_in_scope[$var_id]->parent_nodes = []; + $context->vars_in_scope[$var_id] = $context->vars_in_scope[$var_id]->setParentNodes([]); } else { $var_location = new CodeLocation($statements_analyzer->getSource(), $assign_var); @@ -592,8 +590,9 @@ public static function analyze( ); } - $assign_value_type = clone $assign_value_type; - $assign_value_type->parent_nodes = [$new_parent_node->id => $new_parent_node]; + $assign_value_type = $assign_value_type->setParentNodes( + [$new_parent_node->id => $new_parent_node] + ); } } } @@ -699,7 +698,8 @@ public static function assignTypeFromVarDocblock( ?string $var_id = null, ?Union &$comment_type = null, ?DocblockTypeLocation &$comment_type_location = null, - array $not_ignored_docblock_var_ids = [] + array $not_ignored_docblock_var_ids = [], + bool $by_ref = false ): void { if (!$var_comment->type) { return; @@ -716,8 +716,12 @@ public static function assignTypeFromVarDocblock( $statements_analyzer->getParentFQCLN() ); - $var_comment_type = $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setProperties([ + 'from_docblock' => true, + 'by_ref' => $by_ref + ]); + /** @psalm-suppress UnusedMethodCall This actually has the side effect of generating issues */ $var_comment_type->check( $statements_analyzer, new CodeLocation($statements_analyzer->getSource(), $stmt), @@ -786,7 +790,7 @@ public static function assignTypeFromVarDocblock( } $parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes ?? []; - $var_comment_type->parent_nodes = $parent_nodes; + $var_comment_type = $var_comment_type->setParentNodes($parent_nodes); $context->vars_in_scope[$var_comment->var_id] = $var_comment_type; } catch (UnexpectedValueException $e) { @@ -804,7 +808,7 @@ public static function assignTypeFromVarDocblock( * @param array $added_taints */ private static function taintAssignment( - Union $type, + Union &$type, DataFlowGraph $data_flow_graph, string $var_id, CodeLocation $var_location, @@ -827,7 +831,7 @@ private static function taintAssignment( ); } - $type->parent_nodes = $new_parent_nodes; + $type = $type->setParentNodes($new_parent_nodes, false); } public static function analyzeAssignmentOperation( @@ -994,7 +998,8 @@ public static function analyzeAssignmentRef( $lhs_node = DataFlowNode::getForAssignment($lhs_var_id, $lhs_location); - $context->vars_in_scope[$lhs_var_id]->parent_nodes[$lhs_node->id] = $lhs_node; + $context->vars_in_scope[$lhs_var_id] = + $context->vars_in_scope[$lhs_var_id]->addParentNodes([$lhs_node->id => $lhs_node]); if ($stmt->var instanceof ArrayDimFetch && $stmt->var->dim !== null) { // Analyze offset so that variables in the offset get marked as used @@ -1102,11 +1107,9 @@ public static function assignByRefParam( $statements_analyzer ); - $by_ref_out_type = clone $by_ref_out_type; - - if ($existing_type->parent_nodes) { - $by_ref_out_type->parent_nodes += $existing_type->parent_nodes; - } + $by_ref_out_type = $by_ref_out_type->addParentNodes( + $existing_type->parent_nodes + ); if (!$context->inside_conditional) { $context->vars_in_scope[$var_id] = $by_ref_out_type; @@ -1114,7 +1117,7 @@ public static function assignByRefParam( if (!($stmt_type = $statements_analyzer->node_data->getType($stmt)) || $stmt_type->isNever() ) { - $statements_analyzer->node_data->setType($stmt, clone $by_ref_type); + $statements_analyzer->node_data->setType($stmt, $by_ref_type); } return; @@ -1128,7 +1131,7 @@ public static function assignByRefParam( $stmt_type = $statements_analyzer->node_data->getType($stmt); if (!$stmt_type || $stmt_type->isNever()) { - $statements_analyzer->node_data->setType($stmt, clone $by_ref_type); + $statements_analyzer->node_data->setType($stmt, $by_ref_type); } if ($var_not_in_scope && $stmt instanceof PhpParser\Node\Expr\Variable) { @@ -1229,8 +1232,7 @@ private static function analyzeDestructuringAssignment( $statements_analyzer->getSuppressedIssues() ); - $value_type = clone $value_type; - $value_type->possibly_undefined = false; + $value_type = $value_type->setPossiblyUndefined(false); } if ($statements_analyzer->data_flow_graph @@ -1248,12 +1250,13 @@ private static function analyzeDestructuringAssignment( $keyed_array_var_id = $assign_value_id . '[\'' . $offset_value . '\']'; } + $temp = Type::getString((string) $offset_value); ArrayFetchAnalyzer::taintArrayFetch( $statements_analyzer, $assign_value, $keyed_array_var_id, $value_type, - Type::getString((string)$offset_value) + $temp ); } @@ -1341,7 +1344,7 @@ private static function analyzeDestructuringAssignment( } $array_value_type = $assign_value_atomic_type instanceof TArray - ? clone $assign_value_atomic_type->type_params[1] + ? $assign_value_atomic_type->type_params[1] : Type::getMixed(); self::analyze( @@ -1385,31 +1388,33 @@ private static function analyzeDestructuringAssignment( } if ($assign_value_atomic_type instanceof TArray) { - $new_assign_type = clone $assign_value_atomic_type->type_params[1]; + $new_assign_type = $assign_value_atomic_type->type_params[1]; if ($statements_analyzer->data_flow_graph && $assign_value ) { + $temp = Type::getArrayKey(); ArrayFetchAnalyzer::taintArrayFetch( $statements_analyzer, $assign_value, null, $new_assign_type, - Type::getArrayKey() + $temp ); } $can_be_empty = !$assign_value_atomic_type instanceof TNonEmptyArray; } elseif ($assign_value_atomic_type instanceof TList) { - $new_assign_type = clone $assign_value_atomic_type->type_param; + $new_assign_type = $assign_value_atomic_type->type_param; if ($statements_analyzer->data_flow_graph && $assign_value) { + $temp = Type::getArrayKey(); ArrayFetchAnalyzer::taintArrayFetch( $statements_analyzer, $assign_value, null, $new_assign_type, - Type::getArrayKey() + $temp ); } @@ -1420,7 +1425,7 @@ private static function analyzeDestructuringAssignment( && isset($assign_value_atomic_type->properties[$assign_var_item->key->value]) ) { $new_assign_type = - clone $assign_value_atomic_type->properties[$assign_var_item->key->value]; + $assign_value_atomic_type->properties[$assign_var_item->key->value]; if ($new_assign_type->possibly_undefined) { IssueBuffer::maybeAdd( @@ -1431,17 +1436,18 @@ private static function analyzeDestructuringAssignment( $statements_analyzer->getSuppressedIssues() ); - $new_assign_type->possibly_undefined = false; + $new_assign_type = $new_assign_type->setPossiblyUndefined(false); } } if ($statements_analyzer->data_flow_graph && $assign_value && $new_assign_type) { + $temp = Type::getArrayKey(); ArrayFetchAnalyzer::taintArrayFetch( $statements_analyzer, $assign_value, null, $new_assign_type, - Type::getArrayKey() + $temp ); } @@ -1526,14 +1532,17 @@ private static function analyzeDestructuringAssignment( $var_location ); - $context->vars_in_scope[$list_var_id]->parent_nodes = [ - $assignment_node->id => $assignment_node - ]; + $context->vars_in_scope[$list_var_id] = + $context->vars_in_scope[$list_var_id]->setParentNodes([ + $assignment_node->id => $assignment_node + ]) + ; } else { if ($data_flow_graph instanceof TaintFlowGraph && in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { - $context->vars_in_scope[$list_var_id]->parent_nodes = []; + $context->vars_in_scope[$list_var_id] = + $context->vars_in_scope[$list_var_id]->setParentNodes([]); } else { $event = new AddRemoveTaintsEvent($var, $context, $statements_analyzer, $codebase); @@ -1720,7 +1729,7 @@ private static function analyzeAssignmentToVariable( } if (isset($context->byref_constraints[$var_id])) { - $assign_value_type->by_ref = true; + $assign_value_type = $assign_value_type->setByRef(true); } if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 4e4668dd4ac..522c4b0ed5d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -377,8 +377,10 @@ private static function analyzeOperands( if ($left_type_part instanceof TNumericString || ($left_type_part instanceof TLiteralString && is_numeric($left_type_part->value)) ) { - $new_result_type = new Union([new TFloat(), new TInt()]); - $new_result_type->from_calculation = true; + $new_result_type = new Union( + [new TFloat(), new TInt()], + ['from_calculation' => true] + ); } else { $new_result_type = Type::getNonEmptyString(); $has_string_increment = true; @@ -565,10 +567,12 @@ private static function analyzeOperands( $properties[$key] = Type::combineUnionTypes( $properties[$key], $type, - $codebase + $codebase, + false, + true, + 500, + $type->possibly_undefined ); - - $properties[$key]->possibly_undefined = $type->possibly_undefined; } } @@ -1295,8 +1299,10 @@ private static function analyzePowBetweenIntRange( } elseif ($right_type_part->isNegative()) { $new_result_type = Type::getFloat(); } else { - $new_result_type = new Union([new TFloat(), new TLiteralInt(0), new TLiteralInt(1)]); - $new_result_type->from_calculation = true; + $new_result_type = new Union( + [new TFloat(), new TLiteralInt(0), new TLiteralInt(1)], + ['from_calculation' => true] + ); } } else { //$left_type_part may be a mix of positive, negative and 0 diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php index f5c7580791d..8db1df84e6f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php @@ -10,6 +10,8 @@ use Psalm\Node\Expr\VirtualTernary; use Psalm\Node\Expr\VirtualVariable; use Psalm\Type; +use Psalm\Type\Atomic\TMixed; +use Psalm\Type\Union; use function substr; @@ -48,10 +50,15 @@ public static function analyze( ExpressionAnalyzer::analyze($statements_analyzer, $left_expr, $cloned); - $condition_type = $statements_analyzer->node_data->getType($left_expr) ?? Type::getMixed(); - if ($root_expr !== $left_expr) { - $condition_type->possibly_undefined = true; + $condition_type = $statements_analyzer->node_data->getType($left_expr); + if ($condition_type) { + $condition_type = $condition_type->setPossiblyUndefined(true); + } else { + $condition_type = new Union([new TMixed()], ['possibly_undefined' => true]); + } + } else { + $condition_type = $statements_analyzer->node_data->getType($left_expr) ?? Type::getMixed(); } $context->vars_in_scope[$left_var_id] = $condition_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php index 4504a137dcb..8177e730605 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php @@ -118,7 +118,7 @@ public static function analyze( foreach ($left_context->vars_in_scope as $var_id => $type) { if (!isset($context->vars_in_scope[$var_id])) { if (isset($left_context->assigned_var_ids[$var_id])) { - $context->vars_in_scope[$var_id] = clone $type; + $context->vars_in_scope[$var_id] = $type; } } else { $context->vars_in_scope[$var_id] = Type::combineUnionTypes( @@ -335,7 +335,7 @@ public static function analyze( if ($var_id && isset($left_context->vars_in_scope[$var_id])) { $left_inferred_reconciled = AssertionReconciler::reconcile( new Truthy(), - clone $left_context->vars_in_scope[$var_id], + $left_context->vars_in_scope[$var_id], '', $statements_analyzer, $context->inside_loop, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index 667583344e9..2380f1201d8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -159,9 +159,9 @@ public static function analyze( $new_parent_node = DataFlowNode::getForAssignment('concat', $var_location); $statements_analyzer->data_flow_graph->addNode($new_parent_node); - $stmt_type->parent_nodes = [ + $stmt_type = $stmt_type->setParentNodes([ $new_parent_node->id => $new_parent_node - ]; + ]); $codebase = $statements_analyzer->getCodebase(); $event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase); @@ -402,9 +402,10 @@ public static function addDataFlow( $new_parent_node = DataFlowNode::getForAssignment($type, $var_location); $statements_analyzer->data_flow_graph->addNode($new_parent_node); - $result_type->parent_nodes = [ + $result_type = $result_type->setParentNodes([ $new_parent_node->id => $new_parent_node - ]; + ]); + $statements_analyzer->node_data->setType($stmt, $result_type); if ($stmt_left_type && $stmt_left_type->parent_nodes) { foreach ($stmt_left_type->parent_nodes as $parent_node) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php index 0aae409e996..9fa093ca4d7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -114,9 +114,10 @@ private static function addDataFlow( $new_parent_node = DataFlowNode::getForAssignment('bitwisenot', $var_location); $statements_analyzer->data_flow_graph->addNode($new_parent_node); - $result_type->parent_nodes = [ + $result_type = $result_type->setParentNodes([ $new_parent_node->id => $new_parent_node, - ]; + ]); + $statements_analyzer->node_data->setType($stmt, $result_type); if ($stmt_value_type && $stmt_value_type->parent_nodes) { foreach ($stmt_value_type->parent_nodes as $parent_node) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index 6a2078b02d9..f97c320d8ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -7,6 +7,10 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Type; +use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TFalse; +use Psalm\Type\Atomic\TTrue; +use Psalm\Type\Union; /** * @internal @@ -30,15 +34,20 @@ public static function analyze( $expr_type = $statements_analyzer->node_data->getType($stmt->expr); - $stmt_type = Type::getBool(); if ($expr_type) { if ($expr_type->isAlwaysTruthy()) { - $stmt_type = Type::getFalse($expr_type->from_docblock); + $stmt_type = new TFalse($expr_type->from_docblock); } elseif ($expr_type->isAlwaysFalsy()) { - $stmt_type = Type::getTrue($expr_type->from_docblock); + $stmt_type = new TTrue($expr_type->from_docblock); + } else { + $stmt_type = new TBool(); } - $stmt_type->parent_nodes = $expr_type->parent_nodes; + $stmt_type = new Union([$stmt_type], [ + 'parent_nodes' => $expr_type->parent_nodes + ]); + } else { + $stmt_type = Type::getBool(); } $statements_analyzer->node_data->setType($stmt, $stmt_type); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 5342ed14419..da45f6fa04e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -57,6 +57,7 @@ use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralString; +use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; @@ -248,7 +249,7 @@ private static function checkFunctionLikeTypeMatches( $param_type = Type::getMixed(); } else { - $param_type = clone $function_param->type; + $param_type = $function_param->type; } $bindable_template_params = []; @@ -354,8 +355,9 @@ private static function checkFunctionLikeTypeMatches( } if (!$arg_type_param) { - $arg_type_param = Type::getMixed(); - $arg_type_param->parent_nodes = $arg_value_type->parent_nodes; + $arg_type_param = new Union([ + new TMixed() + ], ['parent_nodes' => $arg_value_type->parent_nodes]); } } @@ -386,7 +388,7 @@ private static function checkFunctionLikeTypeMatches( )) { $template_result->lower_bounds[$template_type->param_name][$template_type->defining_class] = [ new TemplateBound( - clone $template_result->upper_bounds + $template_result->upper_bounds [$template_type->param_name] [$template_type->defining_class]->type ) @@ -394,7 +396,7 @@ private static function checkFunctionLikeTypeMatches( } else { $template_result->lower_bounds[$template_type->param_name][$template_type->defining_class] = [ new TemplateBound( - clone $template_type->as + $template_type->as ) ]; } @@ -488,11 +490,11 @@ private static function checkFunctionLikeTypeMatches( && $allow_named_args && isset($unpacked_atomic_array->properties[$function_param->name]) ) { - $arg_value_type = clone $unpacked_atomic_array->properties[$function_param->name]; + $arg_value_type = $unpacked_atomic_array->properties[$function_param->name]; } elseif ($unpacked_atomic_array->is_list && isset($unpacked_atomic_array->properties[$unpacked_argument_offset]) ) { - $arg_value_type = clone $unpacked_atomic_array->properties[$unpacked_argument_offset]; + $arg_value_type = $unpacked_atomic_array->properties[$unpacked_argument_offset]; } elseif ($function_param->is_optional && $function_param->default_type) { if ($function_param->default_type instanceof Union) { $arg_value_type = $function_param->default_type; @@ -696,7 +698,7 @@ public static function verifyType( $codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset] = Type::combineUnionTypes( $codebase->analyzer->possible_method_param_types[$id_lc][$argument_offset] ?? null, - clone $input_type, + $input_type, $codebase ); } @@ -829,7 +831,7 @@ public static function verifyType( if ($function_param->by_ref || $function_param->is_optional) { //if the param is optional or a ref, we'll allow the input to be possibly_undefined - $param_type->possibly_undefined = true; + $param_type = $param_type->setPossiblyUndefined(true); } if ($param_type->hasCallableType() && $param_type->isSingle()) { @@ -893,8 +895,7 @@ public static function verifyType( ); if ($function_param->assert_untainted) { - $input_type = clone $input_type; - $input_type->parent_nodes = []; + $input_type = $input_type->setParentNodes([]); $replace_input_type = true; } } @@ -1378,6 +1379,7 @@ private static function coerceValueAfterGatekeeperArgument( } } } + unset($input_atomic_type); } } @@ -1412,14 +1414,11 @@ private static function coerceValueAfterGatekeeperArgument( $was_cloned = true; $parent_nodes = $input_type->parent_nodes; $by_ref = $input_type->by_ref; - $input_type = clone $signature_param_type; - - if ($input_type->isNullable()) { - $input_type->ignore_nullable_issues = true; - } - - $input_type->parent_nodes = $parent_nodes; - $input_type->by_ref = $by_ref; + $input_type = $signature_param_type->setProperties([ + 'ignore_nullable_issues' => $signature_param_type->isNullable(), + 'parent_nodes' => $parent_nodes, + 'by_ref' => $by_ref + ]); } if ($context->inside_conditional && !isset($context->assigned_var_ids[$var_id])) { @@ -1432,11 +1431,11 @@ private static function coerceValueAfterGatekeeperArgument( if ($unpack) { if ($unpacked_atomic_array instanceof TList) { - $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParam($input_type); + $unpacked_atomic_array = $unpacked_atomic_array->setTypeParam($input_type); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TArray) { - $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParams([ + $unpacked_atomic_array = $unpacked_atomic_array->setTypeParams([ $unpacked_atomic_array->type_params[0], $input_type ]); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index a3d5f7f46ea..22a63b7831e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -158,7 +158,7 @@ public static function analyze( $by_ref_type = null; if ($by_ref) { - $by_ref_type = $param->type ? clone $param->type : Type::getMixed(); + $by_ref_type = $param->type ?: Type::getMixed(); } if ($by_ref @@ -269,7 +269,7 @@ public static function analyze( $codebase = $statements_analyzer->getCodebase(); TemplateStandinTypeReplacer::fillTemplateResult( - clone $param->type, + $param->type, $template_result, $codebase, $statements_analyzer, @@ -484,7 +484,7 @@ private static function handleHighOrderFuncCallArg( return null; } - $replaced_container_hof_atomic = new Union([clone $container_hof_atomic]); + $replaced_container_hof_atomic = new Union([$container_hof_atomic]); // Replaces all input args in container function. // @@ -514,7 +514,7 @@ private static function handleHighOrderFuncCallArg( isset($container_hof_atomic->params[$offset]) ) { TemplateStandinTypeReplacer::fillTemplateResult( - clone $actual_func_param->type, + $actual_func_param->type, $high_order_template_result, $codebase, null, @@ -573,7 +573,7 @@ private static function handleClosureArg( ) ]); } else { - $replaced_type = clone $param->type; + $replaced_type = $param->type; } $replace_template_result = new TemplateResult( @@ -684,12 +684,13 @@ private static function handleClosureArg( } if ($param_storage->type && ($method_id === 'array_map' || $method_id === 'array_filter')) { + $temp = Type::getMixed(); ArrayFetchAnalyzer::taintArrayFetch( $statements_analyzer, $args[1 - $argument_offset]->value, null, $param_storage->type, - Type::getMixed() + $temp ); } } @@ -792,7 +793,7 @@ public static function checkArgumentsMatch( foreach ($template_result->lower_bounds as $template_name => $type_map) { foreach ($type_map as $class => $lower_bounds) { if (count($lower_bounds) === 1) { - $class_generic_params[$template_name][$class] = clone reset($lower_bounds)->type; + $class_generic_params[$template_name][$class] = reset($lower_bounds)->type; } } } @@ -1210,10 +1211,10 @@ private static function handlePossiblyMatchingByRefParam( } if ($function_param->type) { - $by_ref_type = clone $function_param->type; + $by_ref_type = $function_param->type; } if ($function_param->out_type) { - $by_ref_out_type = clone $function_param->out_type; + $by_ref_out_type = $function_param->out_type; } if ($by_ref_type && $by_ref_type->isNullable()) { @@ -1221,10 +1222,10 @@ private static function handlePossiblyMatchingByRefParam( } if ($template_result && $by_ref_type) { - $original_by_ref_type = clone $by_ref_type; + $original_by_ref_type = $by_ref_type; $by_ref_type = TemplateStandinTypeReplacer::replace( - clone $by_ref_type, + $by_ref_type, $template_result, $codebase, $statements_analyzer, @@ -1246,10 +1247,10 @@ private static function handlePossiblyMatchingByRefParam( } if ($template_result && $by_ref_out_type) { - $original_by_ref_out_type = clone $by_ref_out_type; + $original_by_ref_out_type = $by_ref_out_type; $by_ref_out_type = TemplateStandinTypeReplacer::replace( - clone $by_ref_out_type, + $by_ref_out_type, $template_result, $codebase, $statements_analyzer, @@ -1494,7 +1495,7 @@ private static function handleByRefFunctionArg( $array_type = new TArray([Type::getInt(), $array_type->type_param]); } - $by_ref_type = new Union([clone $array_type]); + $by_ref_type = new Union([$array_type]); AssignmentAnalyzer::assignByRefParam( $statements_analyzer, @@ -1786,7 +1787,7 @@ private static function checkArgCount( && $template_result ) { if ($param->default_type instanceof Union) { - $default_type = clone $param->default_type; + $default_type = $param->default_type; } else { $default_type_atomic = ConstantTypeResolver::resolve( $codebase->classlikes, @@ -1798,7 +1799,7 @@ private static function checkArgCount( } TemplateStandinTypeReplacer::fillTemplateResult( - clone $param->type, + $param->type, $template_result, $codebase, $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 87174eab776..9205d8866b1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -230,7 +230,7 @@ public static function handleAddition( if ($array_type instanceof TKeyedArray) { if ($array_type->is_list) { - $objectlike_list = clone $array_type; + $objectlike_list = $array_type; } $array_type = $array_type->getGenericArrayType(); @@ -244,7 +244,7 @@ public static function handleAddition( } } - $by_ref_type = new Union([clone $array_type]); + $by_ref_type = new Union([$array_type]); foreach ($args as $argument_offset => $arg) { if ($argument_offset === 0) { @@ -309,7 +309,7 @@ public static function handleAddition( $by_ref_type, new Union( [ - new TNonEmptyList(clone $arg_value_type), + new TNonEmptyList($arg_value_type), ] ) ); @@ -321,7 +321,7 @@ public static function handleAddition( new TNonEmptyArray( [ $new_offset_type, - clone $arg_value_type + $arg_value_type ] ), ] @@ -743,6 +743,7 @@ private static function checkClosureType( $array_arg_types ); } + unset($closure_type); } /** diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index a86f701f2f7..6c40038c95b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -309,8 +309,8 @@ public static function analyze( } foreach ($function_call_info->defined_constants as $const_name => $const_type) { - $context->constants[$const_name] = clone $const_type; - $context->vars_in_scope[$const_name] = clone $const_type; + $context->constants[$const_name] = $const_type; + $context->vars_in_scope[$const_name] = $const_type; } foreach ($function_call_info->global_variables as $var_id => $_) { @@ -998,7 +998,7 @@ private static function processAssertFunctionEffects( } if (isset($op_vars_in_scope[$var_id])) { - $op_vars_in_scope[$var_id]->from_docblock = true; + $op_vars_in_scope[$var_id] = $op_vars_in_scope[$var_id]->setProperties(['from_docblock' => true]); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index a084fb7380a..0498efc4ec8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -162,7 +162,7 @@ public static function fetch( try { if ($function_storage && $function_storage->return_type) { - $return_type = clone $function_storage->return_type; + $return_type = $function_storage->return_type; if ($template_result->lower_bounds && $function_storage->template_types) { $return_type = TypeExpander::expandUnion( @@ -214,14 +214,15 @@ public static function fetch( ); } + $return_type = $return_type->setByRef($function_storage->returns_by_ref); $stmt_type = $return_type; - $return_type->by_ref = $function_storage->returns_by_ref; // only check the type locally if it's defined externally if ($return_type_location && !$is_stubbed && // makes lookups or array_* functions quicker !$config->isInProjectDirs($return_type_location->file_path) ) { + /** @psalm-suppress UnusedMethodCall Actually generates issues */ $return_type->check( $statements_analyzer, new CodeLocation($statements_analyzer->getSource(), $stmt), @@ -431,9 +432,7 @@ private static function getReturnTypeFromCallMapWithArgs( case 'hrtime': if (($first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value))) { if ((string) $first_arg_type === 'true') { - $int = Type::getInt(); - $int->from_calculation = true; - return $int; + return Type::getInt(true); } $keyed_array = new TKeyedArray([ @@ -451,9 +450,7 @@ private static function getReturnTypeFromCallMapWithArgs( ]); } - $int = Type::getInt(); - $int->from_calculation = true; - return $int; + return Type::getInt(true); case 'min': case 'max': @@ -469,11 +466,11 @@ private static function getReturnTypeFromCallMapWithArgs( } if ($array_type instanceof TArray) { - return clone $array_type->type_params[1]; + return $array_type->type_params[1]; } if ($array_type instanceof TList) { - return clone $array_type->type_param; + return $array_type->type_param; } } elseif ($first_arg_type->hasScalarType() && ($second_arg = ($call_args[1]->value ?? null)) @@ -501,8 +498,9 @@ private static function getReturnTypeFromCallMapWithArgs( $string_type = new Union([ new TString, new TNull + ], [ + 'ignore_nullable_issues' => true ]); - $string_type->ignore_nullable_issues = true; $call_map_return_type = new Union([ new TNonEmptyList( @@ -510,16 +508,11 @@ private static function getReturnTypeFromCallMapWithArgs( ), new TFalse, new TNull + ], [ + 'ignore_nullable_issues' => $codebase->config->ignore_internal_nullable_issues, + 'ignore_falsable_issues' => $codebase->config->ignore_internal_falsable_issues, ]); - if ($codebase->config->ignore_internal_nullable_issues) { - $call_map_return_type->ignore_nullable_issues = true; - } - - if ($codebase->config->ignore_internal_falsable_issues) { - $call_map_return_type->ignore_falsable_issues = true; - } - return $call_map_return_type; case 'mb_strtolower': if (count($call_args) < 2) { @@ -534,9 +527,7 @@ private static function getReturnTypeFromCallMapWithArgs( } } - $stmt_type = $callmap_callable->return_type - ? clone $callmap_callable->return_type - : Type::getMixed(); + $stmt_type = $callmap_callable->return_type ?: Type::getMixed(); switch ($function_id) { case 'mb_strpos': @@ -558,7 +549,7 @@ private static function getReturnTypeFromCallMapWithArgs( if ($stmt_type->isFalsable() && $codebase->config->ignore_internal_falsable_issues ) { - $stmt_type->ignore_falsable_issues = true; + $stmt_type = $stmt_type->setProperties(['ignore_falsable_issues' => true]); } } @@ -570,7 +561,7 @@ private static function taintReturnType( PhpParser\Node\Expr\FuncCall $stmt, string $function_id, FunctionLikeStorage $function_storage, - Union $stmt_type, + Union &$stmt_type, TemplateResult $template_result, Context $context ): ?DataFlowNode { @@ -609,7 +600,7 @@ private static function taintReturnType( foreach ($function_storage->conditionally_removed_taints as $conditionally_removed_taint) { $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( - clone $conditionally_removed_taint, + $conditionally_removed_taint, $template_result, $codebase ); @@ -646,9 +637,9 @@ private static function taintReturnType( [...$removed_taints, ...$conditionally_removed_taints] ); - $stmt_type->parent_nodes[$assignment_node->id] = $assignment_node; + $stmt_type = $stmt_type->addParentNodes([$assignment_node->id => $assignment_node]); } else { - $stmt_type->parent_nodes[$function_call_node->id] = $function_call_node; + $stmt_type = $stmt_type->addParentNodes([$function_call_node->id => $function_call_node]); } if ($function_storage->return_source_params diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 70ecf984149..aabb7498b03 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -768,13 +768,13 @@ private static function handleTemplatedMixins( true, $context->insideUse() )) { - $lhs_type_part = clone $lhs_type_part_new; + $lhs_type_part = $lhs_type_part_new; $class_storage = $mixin_class_storage; $naive_method_exists = true; $method_id = $new_method_id; } elseif (isset($mixin_class_storage->pseudo_methods[$method_name_lc])) { - $lhs_type_part = clone $lhs_type_part_new; + $lhs_type_part = $lhs_type_part_new; $class_storage = $mixin_class_storage; $method_id = $new_method_id; } 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 795b6b1c02c..ac1ff1b8cf6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -208,7 +208,7 @@ public static function analyze( $method_template_result = new TemplateResult($method_storage->template_types ?: [], []); TemplateStandinTypeReplacer::fillTemplateResult( - clone $method_storage->if_this_is_type, + $method_storage->if_this_is_type, $method_template_result, $codebase, null, @@ -301,7 +301,7 @@ public static function analyze( if ($method_storage->if_this_is_type) { $class_type = new Union([$lhs_type_part]); $if_this_is_type = TemplateInferredTypeReplacer::replace( - clone $method_storage->if_this_is_type, + $method_storage->if_this_is_type, $template_result, $codebase ); @@ -319,7 +319,7 @@ public static function analyze( } if ($method_storage->self_out_type && $lhs_var_id) { - $self_out_candidate = clone $method_storage->self_out_type; + $self_out_candidate = $method_storage->self_out_type; if ($template_result->lower_bounds) { $self_out_candidate = TypeExpander::expandUnion( @@ -658,7 +658,7 @@ private static function getMagicGetterOrSetterProperty( } if (isset($class_storage->pseudo_property_get_types['$' . $prop_name])) { - return clone $class_storage->pseudo_property_get_types['$' . $prop_name]; + return $class_storage->pseudo_property_get_types['$' . $prop_name]; } break; 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 d62ee2d694f..e94fffeff12 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -139,7 +139,7 @@ public static function fetch( && ($method_storage = ($class_storage->methods[$method_id->method_name] ?? null)) && $method_storage->return_type ) { - $return_type_candidate = clone $method_storage->return_type; + $return_type_candidate = $method_storage->return_type; $return_type_candidate = self::replaceTemplateTypes( $return_type_candidate, @@ -159,7 +159,9 @@ public static function fetch( } if ($return_type_candidate->isFalsable()) { - $return_type_candidate->ignore_falsable_issues = true; + $return_type_candidate = $return_type_candidate->setProperties([ + 'ignore_falsable_issues' => true + ]); } $return_type_candidate = TypeExpander::expandUnion( @@ -184,8 +186,6 @@ public static function fetch( ); if ($return_type_candidate) { - $return_type_candidate = clone $return_type_candidate; - if ($template_result->lower_bounds) { $return_type_candidate = TypeExpander::expandUnion( $codebase, @@ -235,6 +235,7 @@ public static function fetch( // only check the type locally if it's defined externally if ($return_type_location && !$config->isInProjectDirs($return_type_location->file_path)) { + /** @psalm-suppress UnusedMethodCall Actually generates issues */ $return_type_candidate->check( $statements_analyzer, new CodeLocation($statements_analyzer, $stmt), @@ -277,7 +278,7 @@ public static function fetch( */ public static function taintMethodCallResult( StatementsAnalyzer $statements_analyzer, - Union $return_type_candidate, + Union &$return_type_candidate, PhpParser\Node $name_expr, PhpParser\Node\Expr $var_expr, array $args, @@ -443,12 +444,12 @@ public static function taintMethodCallResult( } } - $return_type_candidate->parent_nodes = $method_call_nodes; - - $stmt_var_type = clone $context->vars_in_scope[$var_id]; - - $stmt_var_type->parent_nodes = $var_nodes; + $return_type_candidate = $return_type_candidate->setParentNodes($method_call_nodes); + $stmt_var_type = $context->vars_in_scope[$var_id]->setParentNodes( + $var_nodes + ); + $context->vars_in_scope[$var_id] = $stmt_var_type; } else { $method_call_node = DataFlowNode::getForMethodReturn( @@ -482,9 +483,9 @@ public static function taintMethodCallResult( $statements_analyzer->data_flow_graph->addNode($method_call_node); - $return_type_candidate->parent_nodes = [ + $return_type_candidate = $return_type_candidate->setParentNodes([ $method_call_node->id => $method_call_node - ]; + ]); } } else { $method_call_node = DataFlowNode::getForMethodReturn( @@ -520,9 +521,9 @@ public static function taintMethodCallResult( $statements_analyzer->data_flow_graph->addNode($method_call_node); - $return_type_candidate->parent_nodes = [ + $return_type_candidate = $return_type_candidate->setParentNodes([ $method_call_node->id => $method_call_node - ]; + ]); } if (!$statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { 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 ac2346d95ee..1982bcf707b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -146,7 +146,7 @@ public static function handleMagicMethod( ); if ($pseudo_method_storage->return_type) { - $return_type_candidate = clone $pseudo_method_storage->return_type; + $return_type_candidate = $pseudo_method_storage->return_type; if ($found_generic_params) { $return_type_candidate = TemplateInferredTypeReplacer::replace( @@ -312,7 +312,7 @@ public static function handleMissingOrMagicMethod( } if ($pseudo_method_storage->return_type) { - $return_type_candidate = clone $pseudo_method_storage->return_type; + $return_type_candidate = $pseudo_method_storage->return_type; if ($found_generic_params) { $return_type_candidate = TemplateInferredTypeReplacer::replace( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 864b353b725..45e9dfb811b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -214,10 +214,11 @@ public static function analyze( $method_var_id = $lhs_var_id . '->' . strtolower($stmt->name->name) . '()'; if (isset($context->vars_in_scope[$method_var_id])) { - $result->return_type = clone $context->vars_in_scope[$method_var_id]; + $result->return_type = $context->vars_in_scope[$method_var_id]; } elseif ($result->return_type !== null) { - $context->vars_in_scope[$method_var_id] = $result->return_type; - $context->vars_in_scope[$method_var_id]->has_mutations = false; + $context->vars_in_scope[$method_var_id] = $result->return_type->setProperties([ + 'has_mutations' => false + ]); } if ($result->can_memoize) { @@ -362,7 +363,7 @@ public static function analyze( $statements_analyzer->node_data->setType($stmt, $stmt_type); } - $stmt_type->by_ref = $result->returns_by_ref; + $stmt_type = $stmt_type->setByRef($result->returns_by_ref); } if ($codebase->store_node_types diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index e0c8dd51d42..d4029dd5c63 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -36,6 +36,7 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TLowercaseString; +use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; @@ -221,8 +222,9 @@ public static function handle( continue; } - $mixed_type = Type::getMixed(); - $mixed_type->parent_nodes = $context->vars_in_scope[$var_id]->parent_nodes; + $mixed_type = new Union([new TMixed()], [ + 'parent_nodes' => $context->vars_in_scope[$var_id]->parent_nodes + ]); $context->vars_in_scope[$var_id] = $mixed_type; $context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos'); @@ -546,7 +548,7 @@ private static function handleDependentTypeFunction( foreach ($var_type->getAtomicTypes() as $class_type) { if ($class_type instanceof TNamedObject) { - $class_string_types[] = new TClassString($class_type->value, clone $class_type); + $class_string_types[] = new TClassString($class_type->value, $class_type); } elseif ($class_type instanceof TTemplateParam && $class_type->as->isSingle() ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index d15daac482f..43f91dadd1a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -512,13 +512,13 @@ private static function analyzeNamedConstructor( if ($fq_class_name === 'SplObjectStorage') { $generic_param_type = Type::getNever(); } else { - $generic_param_type = clone array_values($base_type)[0]; + $generic_param_type = array_values($base_type)[0]; } } - $generic_param_type->had_template = true; - - $generic_param_types[] = $generic_param_type; + $generic_param_types[] = $generic_param_type->setProperties([ + 'had_template' => true + ]); } } @@ -549,7 +549,7 @@ private static function analyzeNamedConstructor( $fq_class_name, array_values( array_map( - static fn($map) => clone reset($map), + static fn($map) => reset($map), $storage->template_types ) ), @@ -568,7 +568,10 @@ private static function analyzeNamedConstructor( $stmt_type = $statements_analyzer->node_data->getType($stmt); if ($stmt_type) { - $stmt_type->reference_free = true; + $stmt_type = $stmt_type->setProperties([ + 'reference_free' => true + ]); + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } @@ -605,7 +608,8 @@ private static function analyzeNamedConstructor( $statements_analyzer->data_flow_graph->addNode($method_source); - $stmt_type->parent_nodes = [$method_source->id => $method_source]; + $stmt_type = $stmt_type->setParentNodes([$method_source->id => $method_source]); + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } @@ -769,7 +773,7 @@ private static function analyzeConstructorExpression( if (!$statements_analyzer->node_data->getType($stmt)) { if ($lhs_type_part instanceof TClassString) { $generated_type = $lhs_type_part->as_type - ? clone $lhs_type_part->as_type + ? $lhs_type_part->as_type : new TObject(); if ($lhs_type_part->as_type diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index e1754c01c93..bcbed3e9c4e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -90,7 +90,7 @@ public static function analyze( } elseif ($context->self) { if ($stmt->class->parts[0] === 'static' && isset($context->vars_in_scope['$this'])) { $fq_class_name = (string) $context->vars_in_scope['$this']; - $lhs_type = clone $context->vars_in_scope['$this']; + $lhs_type = $context->vars_in_scope['$this']; } else { $fq_class_name = $context->self; } @@ -252,7 +252,7 @@ public static function taintReturnType( PhpParser\Node\Expr\StaticCall $stmt, MethodIdentifier $method_id, string $cased_method_id, - Union $return_type_candidate, + Union &$return_type_candidate, ?MethodStorage $method_storage, ?TemplateResult $template_result, ?Context $context = null @@ -299,7 +299,7 @@ public static function taintReturnType( if ($method_storage && $template_result) { foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) { $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( - clone $conditionally_removed_taint, + $conditionally_removed_taint, $template_result, $codebase ); @@ -345,9 +345,9 @@ public static function taintReturnType( [...$conditionally_removed_taints, ...$removed_taints] ); - $return_type_candidate->parent_nodes[$assignment_node->id] = $assignment_node; + $return_type_candidate = $return_type_candidate->addParentNodes([$assignment_node->id => $assignment_node]); } else { - $return_type_candidate->parent_nodes = [$method_source->id => $method_source]; + $return_type_candidate = $return_type_candidate->setParentNodes([$method_source->id => $method_source]); } if ($method_storage 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 c1afc0fa770..e6474f9baa6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -413,11 +413,11 @@ private static function handleNamedCall( )) { $mixin_candidates = []; foreach ($class_storage->templatedMixins as $mixin_candidate) { - $mixin_candidates[] = clone $mixin_candidate; + $mixin_candidates[] = $mixin_candidate; } foreach ($class_storage->namedMixins as $mixin_candidate) { - $mixin_candidates[] = clone $mixin_candidate; + $mixin_candidates[] = $mixin_candidate; } $mixin_candidates_no_generic = array_filter( @@ -969,7 +969,7 @@ private static function checkPseudoMethod( } if ($pseudo_method_storage->return_type) { - $return_type_candidate = clone $pseudo_method_storage->return_type; + $return_type_candidate = $pseudo_method_storage->return_type; $return_type_candidate = TypeExpander::expandUnion( $statements_analyzer->getCodebase(), 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 8cdfd6c7560..613f2f98715 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -165,7 +165,7 @@ public static function analyze( foreach ($self_class_storage->template_extended_params as $template_fq_class_name => $extended_types) { foreach ($extended_types as $type_key => $extended_type) { if (isset($found_generic_params[$type_key][$template_fq_class_name])) { - $found_generic_params[$type_key][$template_fq_class_name] = clone $extended_type; + $found_generic_params[$type_key][$template_fq_class_name] = $extended_type; continue; } @@ -177,7 +177,7 @@ public static function analyze( = $found_generic_params[$t->param_name][$t->defining_class]; } else { $found_generic_params[$type_key][$template_fq_class_name] - = clone $extended_type; + = $extended_type; break; } } @@ -486,8 +486,6 @@ private static function getMethodReturnType( ); if ($return_type_candidate) { - $return_type_candidate = clone $return_type_candidate; - if ($template_result->template_types) { $bindable_template_types = $return_type_candidate->getTemplateTypes(); @@ -610,6 +608,7 @@ private static function getMethodReturnType( // only check the type locally if it's defined externally if ($return_type_location && !$config->isInProjectDirs($return_type_location->file_path)) { + /** @psalm-suppress UnusedMethodCall Actually generates issues */ $return_type_candidate->check( $statements_analyzer, new CodeLocation($statements_analyzer, $stmt), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index a3f34607a6b..1ce76a0d316 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -755,7 +755,7 @@ public static function applyAssertionsToContext( if ($assertion_type_atomic) { $assertion_type = TemplateInferredTypeReplacer::replace( - new Union([clone $assertion_type_atomic]), + new Union([$assertion_type_atomic]), $template_result, $codebase ); @@ -1050,7 +1050,7 @@ public static function checkTemplateResult( } else { $template_result->lower_bounds[$template_name][$defining_id] = [ new TemplateBound( - clone $upper_bound->type + $upper_bound->type ) ]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index a435018739f..42f8f912984 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -143,11 +143,13 @@ public static function analyze( } } - $type = Type::getBool(); - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; + $type = new Union([new TBool()], [ + 'parent_nodes' => $maybe_type->parent_nodes ?? [] + ]); + } else { + $type = Type::getBool(); } $statements_analyzer->node_data->setType($stmt, $type); @@ -219,7 +221,7 @@ public static function analyze( if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph ) { - $type->parent_nodes = $stmt_expr_type->parent_nodes ?? []; + $type = $type->setParentNodes($stmt_expr_type->parent_nodes ?? []); } $statements_analyzer->node_data->setType($stmt, $type); @@ -252,7 +254,7 @@ public static function analyze( || $type instanceof TList || $type instanceof TKeyedArray ) { - $permissible_atomic_types[] = clone $type; + $permissible_atomic_types[] = $type; } else { $all_permissible = false; break; @@ -267,7 +269,7 @@ public static function analyze( } if ($statements_analyzer->data_flow_graph) { - $type->parent_nodes = $stmt_expr_type->parent_nodes ?? []; + $type = $type->setParentNodes($stmt_expr_type->parent_nodes ?? []); } $statements_analyzer->node_data->setType($stmt, $type); @@ -479,7 +481,7 @@ public static function castIntAttempt( } if ($statements_analyzer->data_flow_graph) { - $int_type->parent_nodes = $parent_nodes; + $int_type = $int_type->setParentNodes($parent_nodes); } return $int_type; @@ -665,7 +667,7 @@ public static function castFloatAttempt( } if ($statements_analyzer->data_flow_graph) { - $float_type->parent_nodes = $parent_nodes; + $float_type = $float_type->setParentNodes($parent_nodes); } return $float_type; @@ -858,7 +860,7 @@ public static function castStringAttempt( } if ($statements_analyzer->data_flow_graph) { - $str_type->parent_nodes = $parent_nodes; + $str_type = $str_type->setParentNodes($parent_nodes); } return $str_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 8360fb5ead0..6926f6ed0b1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -392,7 +392,7 @@ public static function analyzeFetch( } if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock) { - $stmt_type = clone $class_constant_type; + $stmt_type = $class_constant_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); $context->vars_in_scope[$const_id] = $stmt_type; @@ -427,7 +427,7 @@ public static function analyzeFetch( if ($lhs_atomic_type instanceof TNamedObject) { $class_string_types[] = new TClassString( $lhs_atomic_type->value, - clone $lhs_atomic_type + $lhs_atomic_type ); } elseif ($lhs_atomic_type instanceof TTemplateParam && $lhs_atomic_type->as->isSingle()) { @@ -668,7 +668,7 @@ public static function analyzeFetch( } if ($const_class_storage->final || $lhs_type_definite_class === true) { - $stmt_type = clone $class_constant_type; + $stmt_type = $class_constant_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); $context->vars_in_scope[$const_id] = $stmt_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php index 0774533b4e6..4cd3d5ac280 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CloneAnalyzer.php @@ -146,14 +146,13 @@ public static function analyze( return true; } - $statements_analyzer->node_data->setType($stmt, $stmt_expr_type); - if ($immutable_cloned) { - $stmt_expr_type = clone $stmt_expr_type; - $statements_analyzer->node_data->setType($stmt, $stmt_expr_type); - $stmt_expr_type->reference_free = true; - $stmt_expr_type->allow_mutations = true; + $stmt_expr_type = $stmt_expr_type->setProperties([ + 'reference_free' => true, + 'allow_mutations' => true, + ]); } + $statements_analyzer->node_data->setType($stmt, $stmt_expr_type); } return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 26b8718c932..e95ae414798 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -10,7 +10,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; -use Psalm\Type; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; @@ -18,9 +17,9 @@ use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; -use function assert; use function in_array; /** @@ -33,7 +32,7 @@ public static function analyze( PhpParser\Node\Scalar\Encapsed $stmt, Context $context ): bool { - $stmt_type = Type::getString(); + $parent_nodes = []; $non_empty = false; @@ -90,7 +89,7 @@ public static function analyze( $new_parent_node = DataFlowNode::getForAssignment('concat', $var_location); $statements_analyzer->data_flow_graph->addNode($new_parent_node); - $stmt_type->parent_nodes[$new_parent_node->id] = $new_parent_node; + $parent_nodes[$new_parent_node->id] = $new_parent_node; $codebase = $statements_analyzer->getCodebase(); $event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase); @@ -123,19 +122,31 @@ public static function analyze( if ($non_empty) { if ($literal_string !== null) { - $new_type = Type::getString($literal_string); + $stmt_type = new Union( + [new TLiteralString($literal_string)], + ['parent_nodes' => $parent_nodes] + ); } elseif ($all_literals) { - $new_type = new Union([new TNonEmptyNonspecificLiteralString()]); + $stmt_type = new Union( + [new TNonEmptyNonspecificLiteralString()], + ['parent_nodes' => $parent_nodes] + ); } else { - $new_type = new Union([new TNonEmptyString()]); + $stmt_type = new Union( + [new TNonEmptyString()], + ['parent_nodes' => $parent_nodes] + ); } } elseif ($all_literals) { - $new_type = new Union([new TNonspecificLiteralString()]); - } - if (isset($new_type)) { - assert($new_type instanceof Union); - $new_type->parent_nodes = $stmt_type->parent_nodes; - $stmt_type = $new_type; + $stmt_type = new Union( + [new TNonspecificLiteralString()], + ['parent_nodes' => $parent_nodes] + ); + } else { + $stmt_type = new Union( + [new TString()], + ['parent_nodes' => $parent_nodes] + ); } $statements_analyzer->node_data->setType($stmt, $stmt_type); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 5b45cdc1d74..453f45e23a8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -170,12 +170,7 @@ public static function analyze( && $stmt_var_type && !$stmt_var_type->hasClassStringMap() ) { - $stmt_type = clone $context->vars_in_scope[$keyed_array_var_id]; - - $statements_analyzer->node_data->setType( - $stmt, - $stmt_type - ); + $stmt_type = $context->vars_in_scope[$keyed_array_var_id]; self::taintArrayFetch( $statements_analyzer, @@ -186,6 +181,15 @@ public static function analyze( $context ); + if ($stmt->dim && $statements_analyzer->node_data->getType($stmt->dim)) { + $statements_analyzer->node_data->setType($stmt->dim, $used_key_type); + } + + $statements_analyzer->node_data->setType( + $stmt, + $stmt_type + ); + return true; } @@ -311,7 +315,6 @@ public static function analyze( if (!($stmt_type = $statements_analyzer->node_data->getType($stmt))) { $stmt_type = Type::getMixed(); - $statements_analyzer->node_data->setType($stmt, $stmt_type); } else { if ($stmt_type->possibly_undefined && !$context->inside_isset @@ -328,21 +331,13 @@ public static function analyze( ); } - $stmt_type->possibly_undefined = false; + $stmt_type = $stmt_type->setPossiblyUndefined(false); } if ($context->inside_isset && $dim_var_id && $new_offset_type && !$new_offset_type->isUnionEmpty()) { $context->vars_in_scope[$dim_var_id] = $new_offset_type; } - if ($keyed_array_var_id && !$context->inside_isset && $can_store_result) { - $context->vars_in_scope[$keyed_array_var_id] = $stmt_type; - $context->vars_possibly_in_scope[$keyed_array_var_id] = true; - - // reference the variable too - $context->hasVariable($keyed_array_var_id); - } - self::taintArrayFetch( $statements_analyzer, $stmt->var, @@ -352,6 +347,20 @@ public static function analyze( $context ); + $statements_analyzer->node_data->setType($stmt, $stmt_type); + + if ($stmt->dim && $statements_analyzer->node_data->getType($stmt->dim)) { + $statements_analyzer->node_data->setType($stmt->dim, $used_key_type); + } + + if ($keyed_array_var_id && !$context->inside_isset && $can_store_result) { + $context->vars_in_scope[$keyed_array_var_id] = $stmt_type; + $context->vars_possibly_in_scope[$keyed_array_var_id] = true; + + // reference the variable too + $context->hasVariable($keyed_array_var_id); + } + return true; } @@ -362,8 +371,8 @@ public static function taintArrayFetch( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $var, ?string $keyed_array_var_id, - Union $stmt_type, - Union $offset_type, + Union &$stmt_type, + Union &$offset_type, ?Context $context = null ): void { if ($statements_analyzer->data_flow_graph @@ -373,7 +382,7 @@ public static function taintArrayFetch( if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { - $stmt_var_type->parent_nodes = []; + $statements_analyzer->node_data->setType($var, $stmt_var_type->setParentNodes([])); return; } @@ -444,10 +453,10 @@ public static function taintArrayFetch( } } - $stmt_type->parent_nodes = [$new_parent_node->id => $new_parent_node]; + $stmt_type = $stmt_type->setParentNodes([$new_parent_node->id => $new_parent_node]); if ($array_key_node) { - $offset_type->parent_nodes = [$array_key_node->id => $array_key_node]; + $offset_type = $offset_type->setParentNodes([$array_key_node->id => $array_key_node]); } } } @@ -581,7 +590,7 @@ public static function getArrayAccessTypeGivenOffset( continue; } - $type = clone $type->as->getSingleAtomic(); + $type = $type->as->getSingleAtomic(); $original_type = $type; } @@ -592,7 +601,7 @@ public static function getArrayAccessTypeGivenOffset( if ($in_assignment) { if ($replacement_type) { - $array_access_type = Type::combineUnionTypes($array_access_type, clone $replacement_type); + $array_access_type = Type::combineUnionTypes($array_access_type, $replacement_type); } else { IssueBuffer::maybeAdd( new PossiblyNullArrayAssignment( @@ -872,7 +881,7 @@ public static function getArrayAccessTypeGivenOffset( } if ($array_type->by_ref) { - $array_access_type->by_ref = true; + return $array_access_type->setByRef(true); } return $array_access_type; @@ -1069,9 +1078,9 @@ public static function handleMixedArrayAccess( ); } - $stmt_var_type->parent_nodes = [ + $statements_analyzer->node_data->setType($stmt->var, $stmt_var_type->setParentNodes([ $new_parent_node->id => $new_parent_node - ]; + ])); } } @@ -1255,7 +1264,7 @@ private static function handleArrayAccessOnTArray( // if we're assigning to an empty array with a key offset, refashion that array if ($in_assignment) { if ($type->isEmptyArray()) { - $type = $type->replaceTypeParams([ + $type = $type->setTypeParams([ $offset_type->isMixed() ? Type::getArrayKey() : $offset_type->freeze(), @@ -1283,7 +1292,7 @@ private static function handleArrayAccessOnTArray( && $offset_as->param_name === $original_type->param_name && $offset_as->defining_class === $original_type->defining_class ) { - $type = $type->replaceTypeParams([ + $type = $type->setTypeParams([ $type->type_params[0], new Union([ new TTemplateIndexedAccess( @@ -1535,14 +1544,14 @@ private static function handleArrayAccessOnKeyedArray( $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $properties[$key_value->value] + $properties[$key_value->value] ); } elseif ($in_assignment) { $properties[$key_value->value] = new Union([new TNever]); $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $properties[$key_value->value] + $properties[$key_value->value] ); } elseif ($type->previous_value_type) { if ($codebase->config->ensure_array_string_offsets_exist) { @@ -1567,9 +1576,9 @@ private static function handleArrayAccessOnKeyedArray( ); } - $properties[$key_value->value] = clone $type->previous_value_type; + $properties[$key_value->value] = $type->previous_value_type; - $array_access_type = clone $type->previous_value_type; + $array_access_type = $type->previous_value_type; } elseif ($hasMixed) { $has_valid_offset = true; @@ -1669,7 +1678,7 @@ private static function handleArrayAccessOnKeyedArray( $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $generic_params + $generic_params ); } else { $array_access_type = Type::combineUnionTypes( @@ -1749,7 +1758,7 @@ private static function handleArrayAccessOnList( } if ($in_assignment && $replacement_type) { - $type = $type->replaceTypeParam(Type::combineUnionTypes( + $type = $type->setTypeParam(Type::combineUnionTypes( $type->type_param, $replacement_type, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index ef24481d64e..79bbe947414 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -260,7 +260,7 @@ public static function analyze( || isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) ) { $fq_class_name = $mixin->value; - $lhs_type_part = clone $mixin; + $lhs_type_part = $mixin; $class_storage = $new_class_storage; if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) { @@ -477,7 +477,9 @@ public static function analyze( ); if ($class_storage->mutation_free) { - $class_property_type->has_mutations = false; + $class_property_type = $class_property_type->setProperties([ + 'has_mutations' => false + ]); } $stmt_type = $statements_analyzer->node_data->getType($stmt); @@ -567,7 +569,7 @@ private static function propertyFetchCanBeAnalyzed( if (isset($class_storage->pseudo_property_get_types['$' . $prop_name])) { $stmt_type = TypeExpander::expandUnion( $codebase, - clone $class_storage->pseudo_property_get_types['$' . $prop_name], + $class_storage->pseudo_property_get_types['$' . $prop_name], $class_storage->name, $class_storage->name, $class_storage->parent_class @@ -590,8 +592,6 @@ private static function propertyFetchCanBeAnalyzed( ); } - $statements_analyzer->node_data->setType($stmt, $stmt_type); - self::processTaints( $statements_analyzer, $stmt, @@ -602,6 +602,8 @@ private static function propertyFetchCanBeAnalyzed( $context ); + $statements_analyzer->node_data->setType($stmt, $stmt_type); + return false; } @@ -758,7 +760,7 @@ public static function localizePropertyType( public static function processTaints( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\PropertyFetch $stmt, - Union $type, + Union &$type, string $property_id, ClassLikeStorage $class_storage, bool $in_assignment, @@ -801,7 +803,7 @@ public static function processTaints( && $var_type && in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()) ) { - $var_type->parent_nodes = []; + $statements_analyzer->node_data->setType($stmt->var, $var_type->setParentNodes([])); return; } @@ -843,7 +845,7 @@ public static function processTaints( } } - $type->parent_nodes = [$property_node->id => $property_node]; + $type = $type->setParentNodes([$property_node->id => $property_node], true); } } else { self::processUnspecialTaints( @@ -865,7 +867,7 @@ public static function processTaints( public static function processUnspecialTaints( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $stmt, - Union $type, + Union &$type, string $property_id, bool $in_assignment, ?array $added_taints, @@ -919,7 +921,7 @@ public static function processUnspecialTaints( ); } - $type->parent_nodes = [$localized_property_node->id => $localized_property_node]; + $type = $type->setParentNodes([$localized_property_node->id => $localized_property_node], true); } private static function handleEnumName( @@ -1126,7 +1128,7 @@ private static function handleNonExistentProperty( if ($config->use_phpdoc_property_without_magic_or_parent && isset($class_storage->pseudo_property_get_types['$' . $prop_name]) ) { - $stmt_type = clone $class_storage->pseudo_property_get_types['$' . $prop_name]; + $stmt_type = $class_storage->pseudo_property_get_types['$' . $prop_name]; if (count($template_types = $class_storage->getClassTemplateTypes()) !== 0) { if (!$lhs_type_part instanceof TGenericObject) { @@ -1145,8 +1147,6 @@ private static function handleNonExistentProperty( ); } - $statements_analyzer->node_data->setType($stmt, $stmt_type); - self::processTaints( $statements_analyzer, $stmt, @@ -1157,6 +1157,8 @@ private static function handleNonExistentProperty( $context ); + $statements_analyzer->node_data->setType($stmt, $stmt_type); + return; } @@ -1216,7 +1218,7 @@ private static function getClassPropertyType( } else { $class_property_type = TypeExpander::expandUnion( $codebase, - clone $class_property_type, + $class_property_type, $declaring_class_storage->name, $declaring_class_storage->name, $declaring_class_storage->parent_class diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php index abe02428fd3..f1775239ba6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ConstFetchAnalyzer.php @@ -88,7 +88,7 @@ public static function analyze( ); if ($const_type) { - $statements_analyzer->node_data->setType($stmt, clone $const_type); + $statements_analyzer->node_data->setType($stmt, $const_type); } elseif ($context->check_consts) { IssueBuffer::maybeAdd( new UndefinedConstant( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index 2e25c2cd7e5..d40eaa7bb7b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -262,12 +262,13 @@ public static function analyze( $stmt_type = $statements_analyzer->node_data->getType($stmt); if ($stmt_var_type->isNullable() && !$context->inside_isset && $stmt_type) { - $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); - $statements_analyzer->node_data->setType($stmt, $stmt_type); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull); if ($stmt_var_type->ignore_nullable_issues) { $stmt_type->ignore_nullable_issues = true; } + $stmt_type = $stmt_type->freeze(); + $statements_analyzer->node_data->setType($stmt, $stmt_type); } if ($codebase->store_node_types @@ -378,7 +379,9 @@ private static function handleScopedProperty( && $context->vars_in_scope[$var_id]->isNullable() ) ) { - $stmt_type->initialized = true; + $stmt_type = $stmt_type->setProperties(['initialized' => true]); + $statements_analyzer->node_data->setType($stmt, $stmt_type); + $context->vars_in_scope[$var_id] = $stmt_type; } else { IssueBuffer::maybeAdd( new UninitializedProperty( @@ -422,6 +425,9 @@ private static function handleScopedProperty( $class_storage, $in_assignment ); + + $context->vars_in_scope[$var_id] = $stmt_type; + $statements_analyzer->node_data->setType($stmt, $stmt_type); $declaring_property_class = $codebase->properties->getDeclaringClassForProperty( $property_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index abb687d96cf..538e43bd48d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -203,7 +203,17 @@ public static function analyze( if ($var_id && $context->hasVariable($var_id)) { $stmt_type = $context->vars_in_scope[$var_id]; - // we don't need to check anything + AtomicPropertyFetchAnalyzer::processUnspecialTaints( + $statements_analyzer, + $stmt, + $stmt_type, + $property_id, + false, + [], + [] + ); + + $context->vars_in_scope[$var_id] = $stmt_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($codebase->collect_references) { @@ -222,7 +232,6 @@ public static function analyze( if ($codebase->store_node_types && !$context->collect_initializations && !$context->collect_mutations - && ($stmt_type = $statements_analyzer->node_data->getType($stmt)) ) { $codebase->analyzer->addNodeType( $statements_analyzer->getFilePath(), @@ -231,16 +240,6 @@ public static function analyze( ); } - AtomicPropertyFetchAnalyzer::processUnspecialTaints( - $statements_analyzer, - $stmt, - $stmt_type, - $property_id, - false, - [], - [] - ); - return true; } @@ -376,7 +375,7 @@ public static function analyze( if ($property->type) { $context->vars_in_scope[$var_id] = TypeExpander::expandUnion( $codebase, - clone $property->type, + $property->type, $class_storage->name, $class_storage->name, $class_storage->parent_class @@ -385,8 +384,19 @@ public static function analyze( $context->vars_in_scope[$var_id] = Type::getMixed(); } - $stmt_type = clone $context->vars_in_scope[$var_id]; + $stmt_type = $context->vars_in_scope[$var_id]; + AtomicPropertyFetchAnalyzer::processUnspecialTaints( + $statements_analyzer, + $stmt, + $stmt_type, + $property_id, + false, + [], + [] + ); + + $context->vars_in_scope[$var_id] = $stmt_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($codebase->store_node_types @@ -399,16 +409,6 @@ public static function analyze( $stmt_type->getId() ); } - - AtomicPropertyFetchAnalyzer::processUnspecialTaints( - $statements_analyzer, - $stmt, - $stmt_type, - $property_id, - false, - [], - [] - ); } else { $statements_analyzer->node_data->setType($stmt, Type::getMixed()); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index ee327d72eeb..ffe867e5aa6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -13,7 +13,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSource; -use Psalm\Internal\Type\TypeCombiner; use Psalm\Issue\ImpureVariable; use Psalm\Issue\InvalidScope; use Psalm\Issue\PossiblyUndefinedGlobalVariable; @@ -108,7 +107,7 @@ public static function analyze( return true; } - $statements_analyzer->node_data->setType($stmt, clone $context->vars_in_scope['$this']); + $statements_analyzer->node_data->setType($stmt, $context->vars_in_scope['$this']); if ($codebase->store_node_types && !$context->collect_initializations @@ -150,11 +149,12 @@ public static function analyze( $context->vars_possibly_in_scope[$var_name] = true; $statements_analyzer->node_data->setType($stmt, Type::getMixed()); } else { - $stmt_type = clone $context->vars_in_scope[$var_name]; - - $statements_analyzer->node_data->setType($stmt, $stmt_type); + $stmt_type = $context->vars_in_scope[$var_name]; self::addDataFlowToVariable($statements_analyzer, $stmt, $var_name, $stmt_type, $context); + + $context->vars_in_scope[$var_name] = $stmt_type; + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } else { $statements_analyzer->node_data->setType($stmt, Type::getMixed()); @@ -167,10 +167,11 @@ public static function analyze( $var_name = '$' . $stmt->name; if (isset($context->vars_in_scope[$var_name])) { - $type = clone $context->vars_in_scope[$var_name]; + $type = $context->vars_in_scope[$var_name]; self::taintVariable($statements_analyzer, $var_name, $type, $stmt); - + + $context->vars_in_scope[$var_name] = $type; $statements_analyzer->node_data->setType($stmt, $type); return true; @@ -181,7 +182,7 @@ public static function analyze( self::taintVariable($statements_analyzer, $var_name, $type, $stmt); $statements_analyzer->node_data->setType($stmt, $type); - $context->vars_in_scope[$var_name] = clone $type; + $context->vars_in_scope[$var_name] = $type; $context->vars_possibly_in_scope[$var_name] = true; $codebase->analyzer->addNodeReference( @@ -363,21 +364,22 @@ public static function analyze( $stmt_type = Type::getMixed(); - $statements_analyzer->node_data->setType($stmt, $stmt_type); - self::addDataFlowToVariable($statements_analyzer, $stmt, $var_name, $stmt_type, $context); + $statements_analyzer->node_data->setType($stmt, $stmt_type); + $statements_analyzer->registerPossiblyUndefinedVariable($var_name, $stmt); return true; } } else { - $stmt_type = clone $context->vars_in_scope[$var_name]; - - $statements_analyzer->node_data->setType($stmt, $stmt_type); + $stmt_type = $context->vars_in_scope[$var_name]; self::addDataFlowToVariable($statements_analyzer, $stmt, $var_name, $stmt_type, $context); + $context->vars_in_scope[$var_name] = $stmt_type; + $statements_analyzer->node_data->setType($stmt, $stmt_type); + if ($stmt_type->possibly_undefined_from_try && !$context->inside_isset) { if ($context->is_global) { IssueBuffer::maybeAdd( @@ -435,7 +437,7 @@ private static function addDataFlowToVariable( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Variable $stmt, string $var_name, - Union $stmt_type, + Union &$stmt_type, Context $context ): void { $codebase = $statements_analyzer->getCodebase(); @@ -455,9 +457,9 @@ private static function addDataFlowToVariable( new CodeLocation($statements_analyzer->getSource(), $stmt) ); - $stmt_type->parent_nodes = [ + $stmt_type = $stmt_type->setParentNodes([ $assignment_node->id => $assignment_node - ]; + ]); } foreach ($stmt_type->parent_nodes as $parent_node) { @@ -509,7 +511,7 @@ private static function addDataFlowToVariable( private static function taintVariable( StatementsAnalyzer $statements_analyzer, string $var_name, - Union $type, + Union &$type, PhpParser\Node\Expr\Variable $stmt ): void { if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph @@ -532,9 +534,9 @@ private static function taintVariable( $statements_analyzer->data_flow_graph->addSource($server_taint_source); - $type->parent_nodes = [ + $type = $type->setParentNodes([ $server_taint_source->id => $server_taint_source - ]; + ]); } } } @@ -551,6 +553,9 @@ public static function isSuperGlobal(string $var_id): bool ); } + /** @var array|'$_FILES full path'|'$argv'|'$argc', Union> */ + private static array $globalCache = []; + public static function getGlobalType(string $var_id, int $codebase_analysis_php_version_id): Union { $config = Config::getInstance(); @@ -559,6 +564,36 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ return Type::parseString($config->globals[$var_id]); } + if (!self::$globalCache) { + foreach (self::SUPER_GLOBALS as $v) { + self::$globalCache[$v] = self::getGlobalTypeInner($v); + } + self::$globalCache['$_FILES full path'] = self::getGlobalTypeInner( + '$_FILES', + true + ); + self::$globalCache['$argv'] = self::getGlobalTypeInner('$argv'); + self::$globalCache['$argc'] = self::getGlobalTypeInner('$argc'); + } + + if ($codebase_analysis_php_version_id >= 8_01_00 && $var_id === '$_FILES') { + $var_id = '$_FILES full path'; + } + + if (isset(self::$globalCache[$var_id])) { + return self::$globalCache[$var_id]; + } + + return Type::getMixed(); + } + + /** + * @psalm-suppress InaccessibleProperty Always acting on new types + * + * @param value-of|'$argv'|'$argc' $var_id + */ + private static function getGlobalTypeInner(string $var_id, bool $files_full_path = false): Union + { if ($var_id === '$argv') { // only in CLI, null otherwise $argv_nullable = new Union([ @@ -585,10 +620,6 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ return $argc_nullable; } - if (!self::isSuperGlobal($var_id)) { - return Type::getMixed(); - } - if ($var_id === '$http_response_header') { return new Union([ new TList(Type::getNonEmptyString()) @@ -757,60 +788,35 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ } if ($var_id === '$_FILES') { + $str = Type::getString(); $values = [ - 'name' => new Union([ - new TString(), - new TNonEmptyList(Type::getString()), - ]), - 'type' => new Union([ - new TString(), - new TNonEmptyList(Type::getString()), - ]), - 'size' => new Union([ - new TIntRange(0, null), - new TNonEmptyList(Type::getInt()), - ]), - 'tmp_name' => new Union([ - new TString(), - new TNonEmptyList(Type::getString()), - ]), - 'error' => new Union([ - new TIntRange(0, 8), - new TNonEmptyList(Type::getInt()), - ]), + 'name' => $str, + 'type' => $str, + 'tmp_name' => $str, + 'size' => new Union([new TIntRange(0, null)]), + 'error' => new Union([new TIntRange(0, 8)]), ]; - if ($codebase_analysis_php_version_id >= 8_10_00) { - $values['full_path'] = new Union([ - new TString(), - new TNonEmptyList(Type::getString()), - ]); + if ($files_full_path) { + $values['full_path'] = $str; } + + $type = new Union([new TKeyedArray($values)]); + $parent = new TArray([Type::getNonEmptyString(), $type]); - $type = new TKeyedArray($values); - - // $_FILES['userfile']['...'] case - $named_type = new TArray([Type::getNonEmptyString(), new Union([$type])]); - - // by default $_FILES is an empty array - $default_type = new TArray([Type::getNever(), Type::getNever()]); - - // ideally we would have 4 separate arrays with distinct types, but that isn't possible with psalm atm - return TypeCombiner::combine([$default_type, $type, $named_type]); + return new Union([$parent]); } - if ($var_id === '$_SESSION') { - // keys must be string - $type = new Union([ - new TArray([ - Type::getNonEmptyString(), - Type::getMixed(), - ]) - ]); - $type->possibly_undefined = true; - return $type; - } - - return Type::getMixed(); + // $var_id === $_SESSION + + // keys must be string + $type = new Union([ + new TArray([ + Type::getNonEmptyString(), + Type::getMixed(), + ]) + ]); + $type->possibly_undefined = true; + return $type; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 3d94b60c310..929aa796253 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -314,7 +314,7 @@ public static function infer( && $existing_class_constants[$stmt->name->name]->type ) { if ($stmt->class->parts === ['self']) { - return clone $existing_class_constants[$stmt->name->name]->type; + return $existing_class_constants[$stmt->name->name]->type; } } @@ -331,7 +331,7 @@ public static function infer( && isset($existing_class_constants[$stmt->name->name]) && $existing_class_constants[$stmt->name->name]->type ) { - return clone $existing_class_constants[$stmt->name->name]->type; + return $existing_class_constants[$stmt->name->name]->type; } if (strtolower($stmt->name->name) === 'class') { @@ -350,7 +350,7 @@ public static function infer( ); if ($foreign_class_constant) { - return clone $foreign_class_constant; + return $foreign_class_constant; } return null; @@ -483,7 +483,7 @@ public static function infer( foreach ($array_type->getAtomicTypes() as $array_atomic_type) { if ($array_atomic_type instanceof TKeyedArray) { if (isset($array_atomic_type->properties[$dim_value])) { - return clone $array_atomic_type->properties[$dim_value]; + return $array_atomic_type->properties[$dim_value]; } return null; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php index 3a4f70870c8..9014263e058 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php @@ -308,7 +308,7 @@ static function (Clause $c) use ($mixed_var_ids, $cond_object_id): Clause { } elseif ($stmt_cond_type) { $if_return_type_reconciled = AssertionReconciler::reconcile( new Truthy(), - clone $stmt_cond_type, + $stmt_cond_type, '', $statements_analyzer, $context->inside_loop, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php index 60b8e3f6593..f18de5dac7e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php @@ -119,9 +119,12 @@ private static function addDataFlow( $new_parent_node = DataFlowNode::getForAssignment($type, $var_location); $statements_analyzer->data_flow_graph->addNode($new_parent_node); - $result_type->parent_nodes = [ - $new_parent_node->id => $new_parent_node, - ]; + $statements_analyzer->node_data->setType( + $stmt, + $result_type->setParentNodes([ + $new_parent_node->id => $new_parent_node, + ]) + ); if ($stmt_value_type && $stmt_value_type->parent_nodes) { foreach ($stmt_value_type->parent_nodes as $parent_node) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index afb4da37625..a9710c3b42f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -114,7 +114,9 @@ public static function analyze( } if (isset($context->vars_in_scope[$var_comment->var_id])) { - $comment_type->parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes; + $comment_type = $comment_type->setParentNodes( + $context->vars_in_scope[$var_comment->var_id]->parent_nodes + ); } $context->vars_in_scope[$var_comment->var_id] = $comment_type; @@ -137,9 +139,9 @@ public static function analyze( $context->inside_call = false; if ($var_comment_type) { - $expression_type = clone $var_comment_type; + $expression_type = $var_comment_type; } elseif ($stmt_var_type = $statements_analyzer->node_data->getType($stmt->value)) { - $expression_type = clone $stmt_var_type; + $expression_type = $stmt_var_type; } else { $expression_type = Type::getMixed(); } @@ -165,8 +167,8 @@ public static function analyze( $declaring_classlike_storage = $classlike_storage->declaring_yield_fqcn ? $codebase->classlike_storage_provider->get($classlike_storage->declaring_yield_fqcn) : $classlike_storage; - - $yield_candidate_type = clone $classlike_storage->yield; + + $yield_candidate_type = $classlike_storage->yield; $yield_candidate_type = !$yield_candidate_type->isMixed() ? TypeExpander::expandUnion( $codebase, @@ -193,7 +195,7 @@ public static function analyze( $type_params = []; foreach ($class_template_params as $type_map) { - $type_params[] = clone array_values($type_map)[0]; + $type_params[] = array_values($type_map)[0]; } $expression_atomic_type = new TGenericObject($expression_atomic_type->value, $type_params); @@ -239,7 +241,7 @@ public static function analyze( if (!$atomic_return_type->type_params[2]->isVoid()) { $statements_analyzer->node_data->setType( $stmt, - clone $atomic_return_type->type_params[2] + $atomic_return_type->type_params[2] ); } } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php index 16a7cf247f4..a00dab6ef2d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldFromAnalyzer.php @@ -63,9 +63,9 @@ public static function analyze( && strtolower($atomic_type->value) === 'generator' && isset($atomic_type->type_params[3]) ) { - $yield_from_type = clone $atomic_type->type_params[3]; + $yield_from_type = $atomic_type->type_params[3]; } elseif ($atomic_type instanceof TArray) { - $yield_from_type = clone $atomic_type->type_params[1]; + $yield_from_type = $atomic_type->type_params[1]; } elseif ($atomic_type instanceof TKeyedArray) { $yield_from_type = $atomic_type->getGenericValueType(); } diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index accdcc67fd0..7173a232e32 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -51,12 +51,12 @@ public static function analyze( $context->vars_in_scope[$var_id] = VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); } elseif (isset($function_storage->global_types[$var_id])) { - $context->vars_in_scope[$var_id] = clone $function_storage->global_types[$var_id]; + $context->vars_in_scope[$var_id] = $function_storage->global_types[$var_id]; $context->vars_possibly_in_scope[$var_id] = true; } else { $context->vars_in_scope[$var_id] = $global_context && $global_context->hasVariable($var_id) - ? clone $global_context->vars_in_scope[$var_id] + ? $global_context->vars_in_scope[$var_id] : VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); $context->vars_possibly_in_scope[$var_id] = true; @@ -68,9 +68,9 @@ public static function analyze( $var_id, new CodeLocation($statements_analyzer, $var) ); - $context->vars_in_scope[$var_id]->parent_nodes = [ + $context->vars_in_scope[$var_id] = $context->vars_in_scope[$var_id]->setParentNodes([ $assignment_node->id => $assignment_node, - ]; + ]); $context->references_to_external_scope[$var_id] = true; if (isset($context->references_in_scope[$var_id])) { diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 2cf90031d27..4b4e761a7f1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -132,7 +132,9 @@ public static function analyze( } if (isset($context->vars_in_scope[$var_comment->var_id])) { - $comment_type->parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes; + $comment_type = $comment_type->setParentNodes( + $context->vars_in_scope[$var_comment->var_id]->parent_nodes + ); } $context->vars_in_scope[$var_comment->var_id] = $comment_type; @@ -165,7 +167,7 @@ public static function analyze( $stmt_type = $var_comment_type; if ($stmt_expr_type && $stmt_expr_type->parent_nodes) { - $stmt_type->parent_nodes = $stmt_expr_type->parent_nodes; + $stmt_type = $stmt_type->setParentNodes($stmt_expr_type->parent_nodes); } $statements_analyzer->node_data->setType($stmt, $var_comment_type); @@ -199,7 +201,7 @@ public static function analyze( $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($context->finally_scope) { - foreach ($context->vars_in_scope as $var_id => $type) { + foreach ($context->vars_in_scope as $var_id => &$type) { if (isset($context->finally_scope->vars_in_scope[$var_id])) { $context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->finally_scope->vars_in_scope[$var_id], @@ -207,9 +209,8 @@ public static function analyze( $statements_analyzer->getCodebase() ); } else { + $type = $type->setPossiblyUndefined(true, true); $context->finally_scope->vars_in_scope[$var_id] = $type; - $type->possibly_undefined = true; - $type->possibly_undefined_from_try = true; } } } @@ -285,7 +286,7 @@ public static function analyze( } $local_return_type = TemplateInferredTypeReplacer::replace( - clone $local_return_type, + $local_return_type, new TemplateResult([], $found_generic_params), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index b3882121827..800a55a9833 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -101,6 +101,7 @@ public static function analyze( $var_comment_type = $var_comment_type->setFromDocblock(); + /** @psalm-suppress UnusedMethodCall */ $var_comment_type->check( $statements_analyzer, new CodeLocation($statements_analyzer->getSource(), $var), @@ -173,7 +174,7 @@ public static function analyze( } if ($context->check_variables) { - $context->vars_in_scope[$var_id] = $comment_type ? clone $comment_type : Type::getMixed(); + $context->vars_in_scope[$var_id] = $comment_type ? $comment_type : Type::getMixed(); $context->vars_possibly_in_scope[$var_id] = true; $context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos'); $statements_analyzer->byref_uses[$var_id] = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php index 27adedc1eed..43a72aa3d19 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php @@ -35,7 +35,7 @@ public static function analyze( $context->has_returned = true; if ($context->finally_scope) { - foreach ($context->vars_in_scope as $var_id => $type) { + foreach ($context->vars_in_scope as $var_id => &$type) { if (isset($context->finally_scope->vars_in_scope[$var_id])) { $context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes( $context->finally_scope->vars_in_scope[$var_id], @@ -43,9 +43,8 @@ public static function analyze( $statements_analyzer->getCodebase() ); } else { + $type = $type->setPossiblyUndefined(true, true); $context->finally_scope->vars_in_scope[$var_id] = $type; - $type->possibly_undefined = true; - $type->possibly_undefined_from_try = true; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 3916fcccbb5..c40a37ad3f0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -87,9 +87,9 @@ public static function analyze( $root_types [] = new TArray([ $atomic_root_type->previous_key_type - ? clone $atomic_root_type->previous_key_type + ? $atomic_root_type->previous_key_type : new Union([new TArrayKey]), - clone $atomic_root_type->previous_value_type, + $atomic_root_type->previous_value_type, ]) ; } else { @@ -113,8 +113,7 @@ public static function analyze( } else { $properties = []; foreach ($atomic_root_type->properties as $key => $type) { - $properties[$key] = clone $type; - $properties[$key]->possibly_undefined = true; + $properties[$key] = $type->setPossiblyUndefined(true); } $root_types []= new TKeyedArray( $properties, diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 284fd2fabf4..8aba70acd41 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -706,6 +706,7 @@ private static function analyzeStatement( try { $checked_type = $context->vars_in_scope[$checked_var_id]; $check_type = Type::parseString($check_type_string); + /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; if ($check_type->possibly_undefined !== $checked_type->possibly_undefined @@ -1001,7 +1002,8 @@ public function registerPossiblyUndefinedVariable( $stmt_type = $this->node_data->getType($stmt); if ($stmt_type) { - $stmt_type->parent_nodes[$use_node->id] = $use_node; + $stmt_type = $stmt_type->addParentNodes([$use_node->id => $use_node]); + $this->node_data->setType($stmt, $stmt_type); } foreach ($this->unused_var_locations as [$var_id, $original_location]) { @@ -1141,6 +1143,7 @@ public function getParsedDocblock(): ?ParsedDocblock return $this->parsed_docblock; } + /** @psalm-mutation-free */ public function getFQCLN(): ?string { if ($this->fake_this_class) { diff --git a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php index 1260c3471dc..815c9e11ecc 100644 --- a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php @@ -34,17 +34,20 @@ public function __construct( $this->aliases = $aliases; } + /** @psalm-mutation-free */ public function getNamespace(): ?string { return $this->aliases->namespace; } + /** @psalm-mutation-free */ public function getAliases(): Aliases { return $this->aliases; } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlipped(): array @@ -53,6 +56,7 @@ public function getAliasedClassesFlipped(): array } /** + * @psalm-mutation-free * @return array */ public function getAliasedClassesFlippedReplaceable(): array diff --git a/src/Psalm/Internal/Clause.php b/src/Psalm/Internal/Clause.php index 30ec50acb0c..4f4f66e449c 100644 --- a/src/Psalm/Internal/Clause.php +++ b/src/Psalm/Internal/Clause.php @@ -3,6 +3,7 @@ namespace Psalm\Internal; use Psalm\Storage\Assertion; +use Psalm\Storage\ImmutableNonCloneableTrait; use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TEnumCase; use Psalm\Type\Atomic\TLiteralFloat; @@ -28,6 +29,7 @@ */ class Clause { + use ImmutableNonCloneableTrait; /** @var int */ public $creating_conditional_id; diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index cb4ed8ffd50..664605ff5c4 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -833,6 +833,7 @@ public function addClassAlias(string $fq_class_name, string $alias_name): void $this->existing_classlike_aliases[$alias_name] = true; } + /** @psalm-mutation-free */ public function getUnAliasedName(string $alias_name): string { $alias_name_lc = strtolower($alias_name); @@ -1657,6 +1658,7 @@ public function getClassConstantType( } if ($constant_storage->unresolved_node) { + /** @psalm-suppress InaccessibleProperty Lazy resolution */ $constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve( $this, $constant_storage->unresolved_node, @@ -1664,6 +1666,7 @@ public function getClassConstantType( $visited_constant_ids )]); if ($constant_storage->type === null || !$constant_storage->type->from_docblock) { + /** @psalm-suppress InaccessibleProperty Lazy resolution */ $constant_storage->type = $constant_storage->inferred_type; } } @@ -2022,7 +2025,7 @@ private function findPossibleMethodParamTypes(ClassLikeStorage $classlike_storag if ($method_storage->params[$offset]->default_type) { if ($method_storage->params[$offset]->default_type instanceof Union) { - $default_type = clone $method_storage->params[$offset]->default_type; + $default_type = $method_storage->params[$offset]->default_type; } else { $default_type_atomic = ConstantTypeResolver::resolve( $codebase->classlikes, @@ -2055,7 +2058,7 @@ private function findPossibleMethodParamTypes(ClassLikeStorage $classlike_storag ); if ($has_variable_calls) { - $possible_type->from_docblock = true; + $possible_type = $possible_type->setProperties(['from_docblock' => true]); } if ($function_analyzer) { diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index e92134bcf82..a7ba48dd184 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -474,7 +474,7 @@ public function getMethodParams( ) { $params[$i] = clone $param; /** @var Union $params[$i]->type */ - $params[$i]->type = clone $overridden_storage->params[$i]->type; + $params[$i]->type = $overridden_storage->params[$i]->type; if ($source) { $overridden_class_storage = $this->classlike_storage_provider->get($overriding_fq_class_name); @@ -534,7 +534,7 @@ public static function getExtendedTemplatedTypes( $extra_added_types = []; if (isset($extends[$atomic_type->defining_class][$atomic_type->param_name])) { - $extended_param = clone $extends[$atomic_type->defining_class][$atomic_type->param_name]; + $extended_param = $extends[$atomic_type->defining_class][$atomic_type->param_name]; foreach ($extended_param->getAtomicTypes() as $extended_atomic_type) { if ($extended_atomic_type instanceof TTemplateParam) { @@ -676,7 +676,7 @@ public function getMethodReturnType( if ($atomic_type instanceof TCallable || $atomic_type instanceof TClosure ) { - $callable_type = clone $atomic_type; + $callable_type = $atomic_type; return new Union([new TClosure( 'Closure', @@ -712,7 +712,9 @@ public function getMethodReturnType( $return_type_candidate = $callmap_callables[0]->return_type; if ($return_type_candidate->isFalsable()) { - $return_type_candidate->ignore_falsable_issues = true; + return $return_type_candidate->setProperties([ + 'ignore_falsable_issues' => true + ]); } return $return_type_candidate; @@ -725,7 +727,7 @@ public function getMethodReturnType( $candidate_type = $storage->return_type; if ($candidate_type && $candidate_type->isVoid()) { - return clone $candidate_type; + return $candidate_type; } if (isset($class_storage->documenting_method_ids[$appearing_method_name])) { @@ -736,7 +738,7 @@ public function getMethodReturnType( && $storage->return_type && $storage->return_type === $storage->signature_return_type ) { - return clone $storage->return_type; + return $storage->return_type; } $overridden_storage = $this->getStorage($overridden_method_id); @@ -749,13 +751,13 @@ public function getMethodReturnType( if (!$candidate_type || !$source_analyzer) { $self_class = $overridden_method_id->fq_class_name; - return clone $overridden_storage->return_type; + return $overridden_storage->return_type; } if ($candidate_type->getId() === $overridden_storage->return_type->getId()) { $self_class = $appearing_fq_class_storage->name; - return clone $candidate_type; + return $candidate_type; } $overridden_class_storage = @@ -763,7 +765,7 @@ public function getMethodReturnType( $overridden_storage_return_type = TypeExpander::expandUnion( $source_analyzer->getCodebase(), - clone $overridden_storage->return_type, + $overridden_storage->return_type, $overridden_method_id->fq_class_name, $appearing_fq_class_name, $overridden_class_storage->parent_class, @@ -814,25 +816,25 @@ public function getMethodReturnType( $self_class = $appearing_fq_class_storage->name; - return clone $candidate_type; + return $candidate_type; } if ($old_contained_by_new) { $self_class = $appearing_fq_class_storage->name; - return clone $candidate_type; + return $candidate_type; } $self_class = $overridden_method_id->fq_class_name; - return clone $overridden_storage->return_type; + return $overridden_storage->return_type; } } if ($candidate_type) { $self_class = $appearing_fq_class_storage->name; - return clone $candidate_type; + return $candidate_type; } if (!isset($class_storage->overridden_method_ids[$appearing_method_name])) { @@ -859,7 +861,7 @@ public function getMethodReturnType( $overridden_class_storage = $this->classlike_storage_provider->get($fq_overridden_class); - $overridden_return_type = clone $overridden_storage->return_type; + $overridden_return_type = $overridden_storage->return_type; $self_class = $overridden_class_storage->name; @@ -992,6 +994,7 @@ public function setAppearingMethodId( ); } + /** @psalm-mutation-free */ public function getDeclaringMethodId( MethodIdentifier $method_id ): ?MethodIdentifier { @@ -1115,6 +1118,7 @@ public function getClassLikeStorageForMethod(MethodIdentifier $method_id): Class return $this->classlike_storage_provider->get($declaring_fq_class_name); } + /** @psalm-mutation-free */ public function getStorage(MethodIdentifier $method_id): MethodStorage { try { @@ -1134,6 +1138,7 @@ public function getStorage(MethodIdentifier $method_id): MethodStorage return $class_storage->methods[$method_name]; } + /** @psalm-mutation-free */ public function hasStorage(MethodIdentifier $method_id): bool { try { diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index a2e0f228f5f..54db81288e3 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -650,8 +650,7 @@ private static function extendTemplateParams( } else { foreach ($parent_storage->template_types as $template_name => $template_type_map) { foreach ($template_type_map as $template_type) { - $default_param = clone $template_type; - $default_param->from_docblock = false; + $default_param = $template_type->setProperties(['from_docblock' => false]); $storage->template_extended_params[$parent_storage->name][$template_name] = $default_param; } } diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index 7bd20893404..26a6f4892de 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -159,6 +159,7 @@ public function registerClass(ReflectionClass $reflected_class): void $type = Type::parseString($type_string); if ($property_id === 'DateInterval::$days') { + /** @psalm-suppress InaccessibleProperty We just parsed this type */ $type->ignore_falsable_issues = true; } @@ -304,6 +305,7 @@ public function extractReflectionMethodInfo(ReflectionMethod $method): void foreach ($callables[0]->params as $param) { if ($param->type) { + /** @psalm-suppress UnusedMethodCall */ $param->type->queueClassLikesForScanning($this->codebase); } } @@ -311,6 +313,7 @@ public function extractReflectionMethodInfo(ReflectionMethod $method): void $storage->setParams($callables[0]->params); $storage->return_type = $callables[0]->return_type; + /** @psalm-suppress UnusedMethodCall */ $storage->return_type->queueClassLikesForScanning($this->codebase); } else { $params = $method->getParameters(); diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index 0fd3e6a1a4b..ab2d7742a8f 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -286,6 +286,7 @@ public function queueClassLikeForScanning( foreach ($public_mapped_properties as $public_mapped_property) { $property_type = Type::parseString($public_mapped_property); + /** @psalm-suppress UnusedMethodCall */ $property_type->queueClassLikesForScanning( $this->codebase, null, diff --git a/src/Psalm/Internal/DataFlow/Path.php b/src/Psalm/Internal/DataFlow/Path.php index e1fb988508f..e28eecb91fd 100644 --- a/src/Psalm/Internal/DataFlow/Path.php +++ b/src/Psalm/Internal/DataFlow/Path.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\DataFlow; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,6 +11,8 @@ */ class Path { + use ImmutableNonCloneableTrait; + public $type; public $unescaped_taints; diff --git a/src/Psalm/Internal/Diff/DiffElem.php b/src/Psalm/Internal/Diff/DiffElem.php index d8ac0d29af2..6d506c70111 100644 --- a/src/Psalm/Internal/Diff/DiffElem.php +++ b/src/Psalm/Internal/Diff/DiffElem.php @@ -4,6 +4,8 @@ namespace Psalm\Internal\Diff; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @internal * @@ -11,6 +13,8 @@ */ class DiffElem { + use ImmutableNonCloneableTrait; + public const TYPE_KEEP = 0; public const TYPE_REMOVE = 1; public const TYPE_ADD = 2; diff --git a/src/Psalm/Internal/FileManipulation/CodeMigration.php b/src/Psalm/Internal/FileManipulation/CodeMigration.php index 83e3c79ceed..31d7be9a99f 100644 --- a/src/Psalm/Internal/FileManipulation/CodeMigration.php +++ b/src/Psalm/Internal/FileManipulation/CodeMigration.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\FileManipulation; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,6 +11,7 @@ */ class CodeMigration { + use ImmutableNonCloneableTrait; /** @var string */ public $source_file_path; diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 3eddbad6244..3541046cdca 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -357,6 +357,7 @@ private function getDocblock(): string break; } } + unset($param_block); } if (!$found_in_params) { @@ -383,6 +384,7 @@ private function getDocblock(): string break; } } + unset($param_block); } if (!$found_in_params) { diff --git a/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php b/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php index 4f955b2bde9..ab407ff5ba5 100644 --- a/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php +++ b/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Fork; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,6 +11,7 @@ */ class ForkProcessDoneMessage implements ForkMessage { + use ImmutableNonCloneableTrait; /** @var mixed */ public $data; diff --git a/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php b/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php index 26c620d5001..f187a63a927 100644 --- a/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php +++ b/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Fork; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,6 +11,7 @@ */ class ForkProcessErrorMessage implements ForkMessage { + use ImmutableNonCloneableTrait; /** @var string */ public $message; diff --git a/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php b/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php index ccfba7ff107..af545dcfc39 100644 --- a/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php +++ b/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Fork; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,6 +11,8 @@ */ class ForkTaskDoneMessage implements ForkMessage { + use ImmutableNonCloneableTrait; + /** @var mixed */ public $data; diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index 2c1dde57c95..c82d2ba93f9 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -3,6 +3,7 @@ namespace Psalm\Internal; use InvalidArgumentException; +use Psalm\Storage\ImmutableNonCloneableTrait; use function explode; use function is_string; @@ -17,6 +18,8 @@ */ class MethodIdentifier { + use ImmutableNonCloneableTrait; + public $fq_class_name; public $method_name; diff --git a/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php b/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php index 6adde5b60f5..19eb0350eb2 100644 --- a/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php @@ -34,7 +34,7 @@ public function enterNode(Node $node): Node $node_type = $this->type_provider->getType($origNode); if ($node_type) { - $this->type_provider->setType($node, clone $node_type); + $this->type_provider->setType($node, $node_type); } return $node; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index b67b7e18a54..0d71276b42e 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -502,6 +502,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $this->type_aliases, true ); + /** @psalm-suppress UnusedMethodCall */ $yield_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -562,6 +563,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $this->type_aliases, true ); + /** @psalm-suppress UnusedMethodCall */ $pseudo_property_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -664,6 +666,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool true ); + /** @psalm-suppress UnusedMethodCall */ $mixin_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -774,6 +777,7 @@ public function finish(PhpParser\Node\Stmt\ClassLike $node): ClassLikeStorage foreach ($mapped_properties as $property_name => $public_mapped_property) { $property_type = Type::parseString($public_mapped_property); + /** @psalm-suppress UnusedMethodCall */ $property_type->queueClassLikesForScanning($this->codebase, $this->file_storage); if (!isset($classlike_storage->properties[$property_name])) { @@ -941,6 +945,7 @@ private function extendTemplatedType( ); } + /** @psalm-suppress UnusedMethodCall */ $extended_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1026,6 +1031,7 @@ private function implementTemplatedType( return; } + /** @psalm-suppress UnusedMethodCall */ $implemented_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1111,6 +1117,7 @@ private function useTemplatedType( return; } + /** @psalm-suppress UnusedMethodCall */ $used_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1271,28 +1278,21 @@ private function visitClassConstDeclaration( $const_type = $inferred_type; } - $storage->constants[$const->name->name] = $constant_storage = new ClassConstantStorage( - $const_type, - $inferred_type, - $stmt->isProtected() - ? ClassLikeAnalyzer::VISIBILITY_PROTECTED - : ($stmt->isPrivate() - ? ClassLikeAnalyzer::VISIBILITY_PRIVATE - : ClassLikeAnalyzer::VISIBILITY_PUBLIC), - new CodeLocation( - $this->file_scanner, - $const->name - ) - ); - $constant_storage->suppressed_issues = $suppressed_issues; - - $constant_storage->type_location = $type_location; - - $constant_storage->stmt_location = new CodeLocation( - $this->file_scanner, - $const - ); - + $attributes = []; + foreach ($stmt->attrGroups as $attr_group) { + foreach ($attr_group->attrs as $attr) { + $attributes[] = AttributeResolver::resolve( + $this->codebase, + $this->file_scanner, + $this->file_storage, + $this->aliases, + $attr, + $this->storage->name ?? null + ); + } + } + + $unresolved_node = null; if ($inferred_type && !( $const->value instanceof Concat @@ -1300,8 +1300,9 @@ private function visitClassConstDeclaration( && get_class($inferred_type->getSingleAtomic()) === TString::class ) ) { - $existing_constants[$const->name->name] = $constant_storage; + $exists = true; } else { + $exists = false; $unresolved_const_expr = ExpressionResolver::getUnresolvedClassConstExpr( $const->value, $this->aliases, @@ -1310,30 +1311,38 @@ private function visitClassConstDeclaration( ); if ($unresolved_const_expr) { - $constant_storage->unresolved_node = $unresolved_const_expr; + $unresolved_node = $unresolved_const_expr; } else { - $constant_storage->type = Type::getMixed(); + $const_type = Type::getMixed(); } } + $storage->constants[$const->name->name] = $constant_storage = new ClassConstantStorage( + $const_type, + $inferred_type, + $stmt->isProtected() + ? ClassLikeAnalyzer::VISIBILITY_PROTECTED + : ($stmt->isPrivate() + ? ClassLikeAnalyzer::VISIBILITY_PRIVATE + : ClassLikeAnalyzer::VISIBILITY_PUBLIC), + new CodeLocation( + $this->file_scanner, + $const->name + ), + $type_location, + new CodeLocation( + $this->file_scanner, + $const + ), + $deprecated, + $stmt->isFinal(), + $unresolved_node, + $attributes, + $suppressed_issues, + $description + ); - if ($deprecated) { - $constant_storage->deprecated = true; - } - - $constant_storage->description = $description; - $constant_storage->final = $stmt->isFinal(); - - foreach ($stmt->attrGroups as $attr_group) { - foreach ($attr_group->attrs as $attr) { - $constant_storage->attributes[] = AttributeResolver::resolve( - $this->codebase, - $this->file_scanner, - $this->file_storage, - $this->aliases, - $attr, - $this->storage->name ?? null - ); - } + if ($exists) { + $existing_constants[$const->name->name] = $constant_storage; } } } @@ -1537,6 +1546,7 @@ private function visitPropertyDeclaration( $doc_var_group_type = $var_comment->type ?? null; if ($doc_var_group_type) { + /** @psalm-suppress UnusedMethodCall */ $doc_var_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage); } @@ -1593,7 +1603,7 @@ private function visitPropertyDeclaration( if ($doc_var_group_type) { $property_storage->type = count($stmt->props) === 1 ? $doc_var_group_type - : clone $doc_var_group_type; + : $doc_var_group_type; } } @@ -1618,6 +1628,7 @@ private function visitPropertyDeclaration( } if ($all_typehint_types_match) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $property_storage->type->from_docblock = false; } @@ -1628,6 +1639,7 @@ private function visitPropertyDeclaration( } } + /** @psalm-suppress UnusedMethodCall */ $property_storage->type->queueClassLikesForScanning($this->codebase, $this->file_storage); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 98b4146d945..c9a268a260a 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -115,6 +115,7 @@ private static function registerClassMapFunctionCall( foreach ($callable->params as $function_param) { if ($function_param->type) { + /** @psalm-suppress UnusedMethodCall */ $function_param->type->queueClassLikesForScanning( $codebase, $file_storage @@ -123,6 +124,7 @@ private static function registerClassMapFunctionCall( } if ($callable->return_type && !$callable->return_type->hasMixed()) { + /** @psalm-suppress UnusedMethodCall */ $callable->return_type->queueClassLikesForScanning($codebase, $file_storage); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 2b9316afa4c..5452d899e6b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -147,6 +147,7 @@ public static function addDocblockInfo( || !in_array($file_storage->file_path, $codebase->config->internal_stubs) ) ) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->ignore_nullable_issues = true; } @@ -157,6 +158,7 @@ public static function addDocblockInfo( || !in_array($file_storage->file_path, $codebase->config->internal_stubs) ) ) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->ignore_falsable_issues = true; } @@ -465,7 +467,7 @@ private static function getConditionalSanitizedTypeTokens( $param_type_mapping[$token_body] = $template_name; } else { $template_as_type = $param_storage->type - ? clone $param_storage->type + ? $param_storage->type : Type::getMixed(); $storage->template_types[$template_name] = [ @@ -635,6 +637,7 @@ private static function getAssertionParts( return null; } + /** @psalm-suppress UnusedMethodCall */ $namespaced_type->queueClassLikesForScanning( $codebase, $file_storage, @@ -751,10 +754,11 @@ private static function improveParamsFromDocblock( $function, null, true, - CodeLocation::FUNCTION_PARAM_VAR + CodeLocation::FUNCTION_PARAM_VAR, + null, + $docblock_param['line_number'] ); - $param_location->setCommentLine($docblock_param['line_number']); $unused_docblock_params[$param_name] = $param_location; if (!$docblock_param_variadic || $storage->params || $file_scanner->will_analyze) { @@ -802,6 +806,7 @@ private static function improveParamsFromDocblock( $storage_param->has_docblock_type = true; + /** @psalm-suppress UnusedMethodCall */ $new_param_type->queueClassLikesForScanning( $codebase, $file_storage, @@ -880,6 +885,7 @@ private static function improveParamsFromDocblock( && $type instanceof TArray && $type->type_params[0]->hasArrayKey() ) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->type_params[0]->from_docblock = false; } } else { @@ -888,6 +894,7 @@ private static function improveParamsFromDocblock( } if ($all_typehint_types_match) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $new_param_type->from_docblock = false; } @@ -950,7 +957,10 @@ private static function handleReturn( !$fake_method ? CodeLocation::FUNCTION_PHPDOC_RETURN_TYPE : CodeLocation::FUNCTION_PHPDOC_METHOD, - $docblock_info->return_type + $docblock_info->return_type, + $docblock_info->return_type_line_number && !$fake_method + ? $docblock_info->return_type_line_number + : null ); } @@ -992,6 +1002,7 @@ private static function handleReturn( } if ($all_typehint_types_match) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->from_docblock = false; if ($storage instanceof MethodStorage) { @@ -1019,6 +1030,7 @@ private static function handleReturn( } } + /** @psalm-suppress UnusedMethodCall */ $storage->return_type->queueClassLikesForScanning($codebase, $file_storage); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1034,6 +1046,7 @@ private static function handleReturn( || !in_array($file_storage->file_path, $codebase->config->internal_stubs) ) ) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->ignore_nullable_issues = true; } @@ -1044,17 +1057,15 @@ private static function handleReturn( || !in_array($file_storage->file_path, $codebase->config->internal_stubs) ) ) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->ignore_falsable_issues = true; } if ($stmt->returnsByRef() && $storage->return_type) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->by_ref = true; } - if ($docblock_info->return_type_line_number && !$fake_method) { - $storage->return_type_location->setCommentLine($docblock_info->return_type_line_number); - } - $storage->return_type_description = $docblock_info->return_type_description; } @@ -1169,6 +1180,7 @@ private static function handleRemovedTaint( $type_aliases ); + /** @psalm-suppress UnusedMethodCall */ $removed_taint->queueClassLikesForScanning($codebase, $file_storage); $removed_taint_single = $removed_taint->getSingleAtomic(); @@ -1388,6 +1400,7 @@ private static function handleParamOut( return; } + /** @psalm-suppress UnusedMethodCall */ $out_type->queueClassLikesForScanning( $codebase, $file_storage, @@ -1480,8 +1493,15 @@ private static function handleUnexpectedTags( ): void { foreach ($docblock_info->unexpected_tags as $tag => $details) { foreach ($details['lines'] as $line) { - $tag_location = new CodeLocation($file_scanner, $stmt, null, true); - $tag_location->setCommentLine($line); + $tag_location = new CodeLocation( + $file_scanner, + $stmt, + null, + true, + null, + null, + $line + ); $message = 'Docblock tag @' . $tag . ' is not recognized in the function docblock ' . 'for ' . $cased_function_id; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index ef7c2f7f207..487b60a8d9e 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -447,6 +447,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal ); if ($stmt->returnsByRef()) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $storage->return_type->by_ref = true; } @@ -812,7 +813,7 @@ private function inferPropertyTypeFromConstructor( $storage->external_mutation_free = true; foreach ($assigned_properties as $property_name => $property_type) { - $classlike_storage->properties[$property_name]->type = clone $property_type; + $classlike_storage->properties[$property_name]->type = $property_type; } } diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 85fa7351967..94422b17e01 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -381,6 +381,7 @@ public function enterNode(PhpParser\Node $node): ?int } $var_type = $var_comment->type; + /** @psalm-suppress UnusedMethodCall */ $var_type->queueClassLikesForScanning($this->codebase, $this->file_storage); } } @@ -613,26 +614,31 @@ public function leaveNode(PhpParser\Node $node) return null; } + /** @psalm-mutation-free */ public function getFilePath(): string { return $this->file_path; } + /** @psalm-mutation-free */ public function getFileName(): string { return $this->file_scanner->getFileName(); } + /** @psalm-mutation-free */ public function getRootFilePath(): string { return $this->file_scanner->getRootFilePath(); } + /** @psalm-mutation-free */ public function getRootFileName(): string { return $this->file_scanner->getRootFileName(); } + /** @psalm-mutation-free */ public function getAliases(): Aliases { return $this->aliases; diff --git a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php index 85aa00f574f..0616249b1c0 100644 --- a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php +++ b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php @@ -73,6 +73,7 @@ public function enterNode(Node $node): ?int foreach ($node->implements as &$interface) { $interface = $this->resolveClassName($interface); } + unset($interface); $this->resolveAttrGroups($node); if (null !== $node->name) { $this->addNamespacedName($node); @@ -114,6 +115,7 @@ public function enterNode(Node $node): ?int foreach ($node->types as &$type) { $type = $this->resolveClassName($type); } + unset($type); } elseif ($node instanceof Expr\FuncCall) { if ($node->name instanceof Name) { $node->name = $this->resolveName($node->name, Stmt\Use_::TYPE_FUNCTION); @@ -126,6 +128,7 @@ public function enterNode(Node $node): ?int foreach ($node->traits as &$trait) { $trait = $this->resolveClassName($trait); } + unset($trait); foreach ($node->adaptations as $adaptation) { if (null !== $adaptation->trait) { @@ -136,6 +139,7 @@ public function enterNode(Node $node): ?int foreach ($adaptation->insteadof as &$insteadof) { $insteadof = $this->resolveClassName($insteadof); } + unset($insteadof); } } } diff --git a/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php b/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php index da987822342..593a64ee03d 100644 --- a/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php @@ -36,7 +36,7 @@ public function enterNode(Node $node) if ($node_type) { /** @psalm-suppress ArgumentTypeCoercion */ - $this->real_type_provider->setType($origNode, clone $node_type); + $this->real_type_provider->setType($origNode, $node_type); } return null; diff --git a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php index 0a678986374..0d01a34f970 100644 --- a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php +++ b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php @@ -43,8 +43,8 @@ public function enterNode(Node $node): ?int $generator_type = new TGenericObject( 'Generator', [ - $key_type ? clone $key_type : Type::getInt(), - clone $value_type, + $key_type ? $key_type : Type::getInt(), + $value_type, Type::getMixed(), Type::getMixed() ] diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php index bedf9b64667..11f847dccd9 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php @@ -37,15 +37,18 @@ public function __construct(?ClassLikeStorageCacheProvider $cache = null) } /** + * @psalm-mutation-free * @throws InvalidArgumentException when class does not exist */ public function get(string $fq_classlike_name): ClassLikeStorage { $fq_classlike_name_lc = strtolower($fq_classlike_name); + /** @psalm-suppress ImpureStaticProperty Used only for caching */ if (!isset(self::$storage[$fq_classlike_name_lc])) { throw new InvalidArgumentException('Could not get class storage for ' . $fq_classlike_name_lc); } + /** @psalm-suppress ImpureStaticProperty Used only for caching */ return self::$storage[$fq_classlike_name_lc]; } diff --git a/src/Psalm/Internal/Provider/FakeFileProvider.php b/src/Psalm/Internal/Provider/FakeFileProvider.php index dcae2821b02..06405e386f5 100644 --- a/src/Psalm/Internal/Provider/FakeFileProvider.php +++ b/src/Psalm/Internal/Provider/FakeFileProvider.php @@ -25,6 +25,7 @@ public function fileExists(string $file_path): bool return isset($this->fake_files[$file_path]) || parent::fileExists($file_path); } + /** @psalm-external-mutation-free */ public function getContents(string $file_path, bool $go_to_source = false): string { if (!$go_to_source && isset($this->temp_files[$file_path])) { diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index bb3feba755c..9c4119644b5 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -33,26 +33,32 @@ class FileProvider */ protected static $open_files = []; + /** @psalm-mutation-free */ public function getContents(string $file_path, bool $go_to_source = false): string { if (!$go_to_source && isset($this->temp_files[$file_path])) { return $this->temp_files[$file_path]; } + /** @psalm-suppress ImpureStaticProperty Used only for caching */ if (isset(self::$open_files[$file_path])) { return self::$open_files[$file_path]; } + /** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */ if (!file_exists($file_path)) { throw new UnexpectedValueException('File ' . $file_path . ' should exist to get contents'); } + /** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */ if (is_dir($file_path)) { throw new UnexpectedValueException('File ' . $file_path . ' is a directory'); } + /** @psalm-suppress ImpureFunctionCall For our purposes, this should not mutate external state */ $file_contents = (string) file_get_contents($file_path); + /** @psalm-suppress ImpureStaticProperty Used only for caching */ self::$open_files[$file_path] = $file_contents; return $file_contents; diff --git a/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php b/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php index eb2f6652dcd..ae6419f83dc 100644 --- a/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php +++ b/src/Psalm/Internal/Provider/PropertyTypeProvider/DomDocumentPropertyTypeProvider.php @@ -17,13 +17,15 @@ */ class DomDocumentPropertyTypeProvider implements PropertyTypeProviderInterface { + private static ?Union $cache = null; public static function getPropertyType(PropertyTypeProviderEvent $event): ?Union { if (strtolower($event->getPropertyName()) === 'documentelement') { - $type = new Union([new TNamedObject('DOMElement'), new TNull()]); - $type->ignore_nullable_issues = true; + self::$cache ??= new Union([new TNamedObject('DOMElement'), new TNull()], [ + 'ignore_nullable_issues' => true + ]); - return $type; + return self::$cache; } return null; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index 37151d19692..b23b8737f9e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -117,7 +117,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $have_at_least_one_res = true; } //array_column skips undefined elements so resulting type is necessarily defined - $result_element_type->possibly_undefined = false; + $result_element_type = $result_element_type->setPossiblyUndefined(false); } elseif (!$value_column_name_is_null) { $result_element_type = Type::getMixed(); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php index 07dd6089498..5c0e8f2b272 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php @@ -38,7 +38,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $second_arg_type = isset($call_args[1]) ? $statements_source->node_data->getType($call_args[1]->value) : null; $third_arg_type = isset($call_args[2]) ? $statements_source->node_data->getType($call_args[2]->value) : null; - $value_type_from_third_arg = $third_arg_type ? clone $third_arg_type : Type::getMixed(); + $value_type_from_third_arg = $third_arg_type ? $third_arg_type : Type::getMixed(); if ($first_arg_type && $first_arg_type->isSingleIntLiteral() diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index 759053d5cd0..7b2112a93ae 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -77,7 +77,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($first_arg_array instanceof TArray) { $inner_type = $first_arg_array->type_params[1]; - $key_type = clone $first_arg_array->type_params[0]; + $key_type = $first_arg_array->type_params[0]; } elseif ($first_arg_array instanceof TList) { $inner_type = $first_arg_array->type_param; $key_type = Type::getInt(); @@ -95,7 +95,7 @@ static function ($keyed_type) use ($statements_source, $context) { $keyed_type = AssertionReconciler::reconcile( new Truthy(), - clone $keyed_type, + $keyed_type, '', $statements_source, $context->inside_loop, @@ -104,9 +104,7 @@ static function ($keyed_type) use ($statements_source, $context) { $statements_source->getSuppressedIssues() ); - $keyed_type->possibly_undefined = !$prev_keyed_type->isAlwaysTruthy(); - - return $keyed_type; + return $keyed_type->setPossiblyUndefined(!$prev_keyed_type->isAlwaysTruthy()); }, $first_arg_array->properties ), @@ -131,7 +129,7 @@ static function ($keyed_type) use ($statements_source, $context) { if (!isset($call_args[1])) { $inner_type = AssertionReconciler::reconcile( new Truthy(), - clone $inner_type, + $inner_type, '', $statements_source, $context->inside_loop, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 46a6bf10bd1..5c550b18792 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -82,7 +82,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $call_arg_type = $statements_source->node_data->getType($call_arg->value); if ($call_arg_type) { - $array_arg_types[] = clone $call_arg_type; + $array_arg_types[] = $call_arg_type; } else { $array_arg_types[] = Type::getMixed(); break; @@ -134,7 +134,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $closure_return_type = Type::getNull(); } - $mapping_return_type = clone $closure_return_type; + $mapping_return_type = $closure_return_type; } elseif ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_ || $function_call_arg->value instanceof PhpParser\Node\Expr\Array_ || $function_call_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat @@ -195,7 +195,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($array_arg_atomic_type instanceof TKeyedArray && count($call_args) === 2) { $atomic_type = new TKeyedArray( array_map( - static fn(Union $_): Union => clone $mapping_return_type, + static fn(Union $_): Union => $mapping_return_type, $array_arg_atomic_type->properties ), null, @@ -416,7 +416,7 @@ public static function getReturnTypeFromMappingIds( && count($atomic_type->properties) === 2 && isset($atomic_type->properties[0]) ) { - $lhs_instance_type = clone $atomic_type->properties[0]; + $lhs_instance_type = $atomic_type->properties[0]; } } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 36970468665..91a9a7b5426 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -130,10 +130,12 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $generic_properties[$key] = Type::combineUnionTypes( $generic_properties[$key], $type, - $codebase + $codebase, + false, + true, + 500, + $was_possibly_undefined ); - - $generic_properties[$key]->possibly_undefined = $was_possibly_undefined; } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index f89b9012e08..80bd3a7c638 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -66,10 +66,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($atomic_type instanceof TArray) { - $value_type = clone $atomic_type->type_params[1]; + $value_type = $atomic_type->type_params[1]; $definitely_has_items = $atomic_type instanceof TNonEmptyArray; } elseif ($atomic_type instanceof TList) { - $value_type = clone $atomic_type->type_param; + $value_type = $atomic_type->type_param; $definitely_has_items = $atomic_type instanceof TNonEmptyList; } elseif ($atomic_type instanceof TKeyedArray) { $value_type = $atomic_type->getGenericValueType(); @@ -86,21 +86,24 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($value_type->isNever()) { $value_type = Type::getFalse(); } elseif (($function_id !== 'reset' && $function_id !== 'end') || !$definitely_has_items) { - $value_type = $value_type->getBuilder()->addType(new TFalse)->freeze(); + $value_type = $value_type->getBuilder()->addType(new TFalse); $codebase = $statements_source->getCodebase(); if ($codebase->config->ignore_internal_falsable_issues) { $value_type->ignore_falsable_issues = true; } + + $value_type = $value_type->freeze(); } + $temp = Type::getMixed(); ArrayFetchAnalyzer::taintArrayFetch( $statements_source, $first_arg, null, $value_type, - Type::getMixed() + $temp ); return $value_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php index 64266953d8f..a524439414a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -56,7 +56,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $nullable = false; if ($first_arg_array instanceof TArray) { - $value_type = clone $first_arg_array->type_params[1]; + $value_type = $first_arg_array->type_params[1]; if ($first_arg_array->isEmptyArray()) { return Type::getNull(); @@ -66,7 +66,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $nullable = true; } } elseif ($first_arg_array instanceof TList) { - $value_type = clone $first_arg_array->type_param; + $value_type = $first_arg_array->type_param; if (!$first_arg_array instanceof TNonEmptyList) { $nullable = true; @@ -74,7 +74,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } else { // special case where we know the type of the first element if ($function_id === 'array_shift' && $first_arg_array->is_list && isset($first_arg_array->properties[0])) { - $value_type = clone $first_arg_array->properties[0]; + $value_type = $first_arg_array->properties[0]; } else { $value_type = $first_arg_array->getGenericValueType(); @@ -85,13 +85,15 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($nullable) { - $value_type = $value_type->getBuilder()->addType(new TNull)->freeze(); + $value_type = $value_type->getBuilder()->addType(new TNull); $codebase = $statements_source->getCodebase(); if ($codebase->config->ignore_internal_nullable_issues) { $value_type->ignore_nullable_issues = true; } + + $value_type = $value_type->freeze(); } return $value_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php index 41830ca965d..5d06518edd6 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php @@ -51,7 +51,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($first_arg_array instanceof TArray) { - $key_type = clone $first_arg_array->type_params[0]; + $key_type = $first_arg_array->type_params[0]; } elseif ($first_arg_array instanceof TList) { $key_type = Type::getInt(); } else { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php index 86601f80bac..b399c51f77f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php @@ -17,6 +17,7 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; +use Psalm\Type\Atomic\TNull; use Psalm\Type\Union; use function count; @@ -81,14 +82,15 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } elseif ($array_arg_atomic_type instanceof TList) { $array_arg_atomic_type = new TArray([ Type::getInt(), - clone $array_arg_atomic_type->type_param + $array_arg_atomic_type->type_param ]); } } if (!isset($call_args[2])) { - $reduce_return_type = Type::getNull(); - $reduce_return_type->ignore_nullable_issues = true; + $reduce_return_type = new Union([new TNull()], [ + 'ignore_nullable_issues' => true, + ]); } else { $reduce_return_type = $statements_source->node_data->getType($call_args[2]->value); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php index 4c2d2705df3..ea95b1e9a7b 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php @@ -49,7 +49,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($first_arg_array instanceof TArray) { - return new Union([clone $first_arg_array]); + return new Union([$first_arg_array]); } if ($first_arg_array instanceof TList) { @@ -60,10 +60,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && $second_arg_type->isFalse() ) ) { - return new Union([clone $first_arg_array]); + return new Union([$first_arg_array]); } - return new Union([new TArray([Type::getInt(), clone $first_arg_array->type_param])]); + return new Union([new TArray([Type::getInt(), $first_arg_array->type_param])]); } return new Union([$first_arg_array->getGenericArrayType()]); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php index e0adab140c0..283ba2438c9 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php @@ -60,24 +60,17 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev continue; } - $already_cloned = false; - if ($atomic_type instanceof TKeyedArray) { - $already_cloned = true; $atomic_type = $atomic_type->getGenericArrayType(); } if ($atomic_type instanceof TArray) { - if (!$already_cloned) { - $atomic_type = clone $atomic_type; - } - $return_atomic_type = new TArray($atomic_type->type_params); continue; } if ($atomic_type instanceof TList) { - $return_atomic_type = new TArray([Type::getInt(), clone $atomic_type->type_param]); + $return_atomic_type = new TArray([Type::getInt(), $atomic_type->type_param]); continue; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php index 125c42ee2c9..ddca344cddf 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySpliceReturnTypeProvider.php @@ -48,20 +48,14 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getArray(); } - $already_cloned = false; - if ($first_arg_array instanceof TKeyedArray) { - $already_cloned = true; $first_arg_array = $first_arg_array->getGenericArrayType(); } if ($first_arg_array instanceof TArray) { - if (!$already_cloned) { - $first_arg_array = clone $first_arg_array; - } $array_type = new TArray($first_arg_array->type_params); } else { - $array_type = new TArray([Type::getInt(), clone $first_arg_array->type_param]); + $array_type = new TArray([Type::getInt(), $first_arg_array->type_param]); } if (!$array_type->type_params[0]->hasString()) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php index 766895b9c4e..74253323a58 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php @@ -63,7 +63,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return new Union([ new TNonEmptyArray([ Type::getInt(), - clone $first_arg_array->type_param + $first_arg_array->type_param ]) ]); } @@ -71,7 +71,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return new Union([ new TArray([ Type::getInt(), - clone $first_arg_array->type_param + $first_arg_array->type_param ]) ]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php index c08a570762d..e714f74c7c5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DomNodeAppendChild.php @@ -37,7 +37,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) if (($first_arg_type = $source->node_data->getType($call_args[0]->value)) && $first_arg_type->hasObjectType() ) { - return clone $first_arg_type; + return $first_arg_type; } return null; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExplodeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExplodeReturnTypeProvider.php index af6beb4c8a7..b08dfab4c04 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExplodeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExplodeReturnTypeProvider.php @@ -83,11 +83,10 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev ? new TList($inner_type) : new TNonEmptyList($inner_type), new TFalse + ], [ + 'ignore_falsable_issues' => + $statements_source->getCodebase()->config->ignore_internal_falsable_issues ]); - - if ($statements_source->getCodebase()->config->ignore_internal_falsable_issues) { - $array_type->ignore_falsable_issues = true; - } } else { $array_type = new Union([ $can_return_empty diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index afa872b72ca..8ad87eb9f0d 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -165,7 +165,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev 'arg' ); - $filter_type->parent_nodes = [$function_return_sink->id => $function_return_sink]; + return $filter_type->setParentNodes([$function_return_sink->id => $function_return_sink]); } return $filter_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php index 2a3d18e8048..f7a5c63104a 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php @@ -41,9 +41,8 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return new Union([new TString]); } - $return_type = new Union([new TString, new TNull]); - $return_type->ignore_nullable_issues = true; - - return $return_type; + return new Union([new TString, new TNull], [ + 'ignore_nullable_issues' => true + ]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php index 4c4a61c6e65..d1d00943441 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/InArrayReturnTypeProvider.php @@ -41,6 +41,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } $false = Type::getFalse(); + /** @psalm-suppress InaccessibleProperty We just created these types */ $false->from_docblock = $bool->from_docblock = $needle_type->from_docblock || $haystack_type->from_docblock; if (!isset($call_args[2])) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php index a8d37a92465..92b80af98d6 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MktimeReturnTypeProvider.php @@ -37,15 +37,11 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!($call_arg_type = $statements_source->node_data->getType($call_arg->value)) || !$call_arg_type->isInt() ) { - $value_type = new Union([new TInt, new TFalse]); - $codebase = $statements_source->getCodebase(); - if ($codebase->config->ignore_internal_falsable_issues) { - $value_type->ignore_falsable_issues = true; - } - - return $value_type; + return new Union([new TInt, new TFalse], [ + 'ignore_falsable_issues' => $codebase->config->ignore_internal_falsable_issues + ]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php index 02641f87248..0abaa72c790 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ParseUrlReturnTypeProvider.php @@ -15,6 +15,8 @@ use Psalm\Type\Atomic\TString; use Psalm\Type\Union; +use function array_fill_keys; + use const PHP_URL_FRAGMENT; use const PHP_URL_HOST; use const PHP_URL_PASS; @@ -37,6 +39,13 @@ public static function getFunctionIds(): array return ['parse_url']; } + private static ?Union $acceptable_int_component_type = null; + private static ?Union $acceptable_string_component_type = null; + private static ?Union $nullable_falsable_int = null; + private static ?Union $nullable_falsable_string = null; + private static ?Union $nullable_string_or_int = null; + private static ?Union $return_type = null; + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union { $statements_source = $event->getStatementsSource(); @@ -51,7 +60,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!$component_type->hasMixed()) { $codebase = $statements_source->getCodebase(); - $acceptable_string_component_type = new Union([ + self::$acceptable_string_component_type ??= new Union([ new TLiteralInt(PHP_URL_SCHEME), new TLiteralInt(PHP_URL_USER), new TLiteralInt(PHP_URL_PASS), @@ -61,56 +70,44 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev new TLiteralInt(PHP_URL_FRAGMENT), ]); - $acceptable_int_component_type = new Union([ + self::$acceptable_int_component_type ??= new Union([ new TLiteralInt(PHP_URL_PORT), ]); if (UnionTypeComparator::isContainedBy( $codebase, $component_type, - $acceptable_string_component_type + self::$acceptable_string_component_type )) { - $nullable_falsable_string = new Union([ + self::$nullable_falsable_string ??= new Union([ new TString, new TFalse, new TNull, + ], [ + 'ignore_nullable_issues' => $statements_source->getCodebase() + ->config->ignore_internal_nullable_issues, + 'ignore_falsable_issues' => $statements_source->getCodebase() + ->config->ignore_internal_falsable_issues, ]); - - $codebase = $statements_source->getCodebase(); - - if ($codebase->config->ignore_internal_nullable_issues) { - $nullable_falsable_string->ignore_nullable_issues = true; - } - - if ($codebase->config->ignore_internal_falsable_issues) { - $nullable_falsable_string->ignore_falsable_issues = true; - } - - return $nullable_falsable_string; + return self::$nullable_falsable_string; } if (UnionTypeComparator::isContainedBy( $codebase, $component_type, - $acceptable_int_component_type + self::$acceptable_int_component_type )) { - $nullable_falsable_int = new Union([ + self::$nullable_falsable_int ??= new Union([ new TInt, new TFalse, new TNull, + ], [ + 'ignore_nullable_issues' => $statements_source->getCodebase() + ->config->ignore_internal_nullable_issues, + 'ignore_falsable_issues' => $statements_source->getCodebase() + ->config->ignore_internal_falsable_issues, ]); - - $codebase = $statements_source->getCodebase(); - - if ($codebase->config->ignore_internal_nullable_issues) { - $nullable_falsable_int->ignore_nullable_issues = true; - } - - if ($codebase->config->ignore_internal_falsable_issues) { - $nullable_falsable_int->ignore_falsable_issues = true; - } - - return $nullable_falsable_int; + return self::$nullable_falsable_int; } if ($component_type->isSingleIntLiteral()) { @@ -121,46 +118,41 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if (!$is_default_component) { - $nullable_string_or_int = new Union([ + self::$nullable_string_or_int ??= new Union([ new TString, new TInt, new TNull, + ], [ + 'ignore_nullable_issues' => $statements_source->getCodebase() + ->config->ignore_internal_nullable_issues, ]); - - $codebase = $statements_source->getCodebase(); - - if ($codebase->config->ignore_internal_nullable_issues) { - $nullable_string_or_int->ignore_nullable_issues = true; - } - - return $nullable_string_or_int; + return self::$nullable_string_or_int; } } - $component_types = [ - 'scheme' => Type::getString(), - 'user' => Type::getString(), - 'pass' => Type::getString(), - 'host' => Type::getString(), - 'port' => Type::getInt(), - 'path' => Type::getString(), - 'query' => Type::getString(), - 'fragment' => Type::getString(), - ]; - - foreach ($component_types as $component_type) { - $component_type->possibly_undefined = true; - } - - $return_type = new Union([ - new TKeyedArray($component_types), - new TFalse(), - ]); - - if ($statements_source->getCodebase()->config->ignore_internal_falsable_issues) { - $return_type->ignore_falsable_issues = true; + if (!self::$return_type) { + $component_types = array_fill_keys( + [ + 'scheme', + 'user', + 'pass', + 'host', + 'path', + 'query', + 'fragment', + ], + new Union([new TString()], ['possibly_undefined' => true]) + ); + $component_types['port'] = new Union([new TInt()], ['possibly_undefined' => true]); + + self::$return_type = new Union([ + new TKeyedArray($component_types), + new TFalse(), + ], [ + 'ignore_falsable_issues' => $statements_source->getCodebase()->config->ignore_internal_falsable_issues + ]); } - return $return_type; + return self::$return_type; } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php index 5dbd1083af8..56f202ee2a8 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php @@ -72,13 +72,11 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $return_type = Type::getString($replaced_string); } } elseif (in_array($function_id, ['preg_replace', 'preg_replace_callback'], true)) { - $return_type = new Union([new TString, new TNull()]); - $codebase = $statements_source->getCodebase(); - if ($codebase->config->ignore_internal_nullable_issues) { - $return_type->ignore_nullable_issues = true; - } + $return_type = new Union([new TString, new TNull()], [ + 'ignore_nullable_issues' => $codebase->config->ignore_internal_nullable_issues + ]); } return $return_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php index 6e73cff96d3..c5279e01fe8 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php @@ -67,7 +67,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev ); } - $type->parent_nodes = [$function_return_sink->id => $function_return_sink]; + return $type->setParentNodes([$function_return_sink->id => $function_return_sink]); } return $type; diff --git a/src/Psalm/Internal/Scanner/FileScanner.php b/src/Psalm/Internal/Scanner/FileScanner.php index b77a8794556..a8b3f72bddf 100644 --- a/src/Psalm/Internal/Scanner/FileScanner.php +++ b/src/Psalm/Internal/Scanner/FileScanner.php @@ -90,26 +90,31 @@ public function scan( $file_storage->deep_scan = $this->will_analyze; } + /** @psalm-mutation-free */ public function getFilePath(): string { return $this->file_path; } + /** @psalm-mutation-free */ public function getFileName(): string { return $this->file_name; } + /** @psalm-mutation-free */ public function getRootFilePath(): string { return $this->file_path; } + /** @psalm-mutation-free */ public function getRootFileName(): string { return $this->file_name; } + /** @psalm-mutation-free */ public function getAliases(): Aliases { return new Aliases(); diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index 95cce5e791a..10d4156fb77 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -134,7 +134,7 @@ static function ( if ($mapped_type = $map[$offset_arg_value] ?? null) { if ($mapped_type instanceof Union) { - return clone $mapped_type; + return $mapped_type; } } @@ -185,7 +185,7 @@ static function ( && ($call_arg_type = $statements_analyzer->node_data->getType($call_args[$type_offset]->value)) ) { - return clone $call_arg_type; + return $call_arg_type; } return null; @@ -237,7 +237,7 @@ static function ( return $array_atomic_type->type_param; } - return clone $array_atomic_type->type_params[1]; + return $array_atomic_type->type_params[1]; } return null; @@ -284,7 +284,7 @@ static function ( if ($mapped_type = $map[$offset_arg_value] ?? null) { if ($mapped_type instanceof Union) { - return clone $mapped_type; + return $mapped_type; } } @@ -332,7 +332,7 @@ static function ( && ($call_arg_type = $statements_analyzer->node_data->getType($call_args[$type_offset]->value)) ) { - return clone $call_arg_type; + return $call_arg_type; } $storage = $statements_analyzer->getCodebase()->functions->getStorage( @@ -381,7 +381,7 @@ static function ( return $array_atomic_type->type_param; } - return clone $array_atomic_type->type_params[1]; + return $array_atomic_type->type_params[1]; } $storage = $statements_analyzer->getCodebase()->functions->getStorage( diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php index adaa1b10ba0..25966b7ff19 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Scanner\UnresolvedConstant; use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Storage\ImmutableNonCloneableTrait; /** * @psalm-immutable @@ -11,6 +12,8 @@ */ abstract class UnresolvedBinaryOp extends UnresolvedConstantComponent { + use ImmutableNonCloneableTrait; + /** @var UnresolvedConstantComponent */ public $left; diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php index 29899ab3b8f..ea89992649f 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedConcatOp.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Scanner\UnresolvedConstant; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,4 +11,5 @@ */ class UnresolvedConcatOp extends UnresolvedBinaryOp { + use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php index 9efa28eb7c4..90b827a9fea 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedDivisionOp.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Scanner\UnresolvedConstant; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,4 +11,5 @@ */ class UnresolvedDivisionOp extends UnresolvedBinaryOp { + use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php index d2572f9344a..38417e145e1 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedMultiplicationOp.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Scanner\UnresolvedConstant; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,4 +11,5 @@ */ class UnresolvedMultiplicationOp extends UnresolvedBinaryOp { + use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php index 87ceb46c9c0..27d25ccea6c 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedSubtractionOp.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Scanner\UnresolvedConstant; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,4 +11,5 @@ */ class UnresolvedSubtractionOp extends UnresolvedBinaryOp { + use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php index ede42e27232..c2af781b860 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Scanner\UnresolvedConstant; use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Storage\ImmutableNonCloneableTrait; /** * @psalm-immutable @@ -11,6 +12,8 @@ */ class UnresolvedTernary extends UnresolvedConstantComponent { + use ImmutableNonCloneableTrait; + /** @var UnresolvedConstantComponent */ public $cond; /** @var UnresolvedConstantComponent|null */ diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstantComponent.php b/src/Psalm/Internal/Scanner/UnresolvedConstantComponent.php index 5d7bf494b03..746fa6c456a 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstantComponent.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstantComponent.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Scanner; +use Psalm\Storage\ImmutableNonCloneableTrait; + /** * @psalm-immutable * @@ -9,4 +11,5 @@ */ abstract class UnresolvedConstantComponent { + use ImmutableNonCloneableTrait; } diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index d39fe490e64..9f7fa389fc1 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -196,7 +196,7 @@ public static function reconcile( if ($assertion instanceof IsClassEqual) { $new_type_part = Atomic::create($assertion->type, null, $template_type_map); } elseif ($assertion_type = $assertion->getAtomicType()) { - $new_type_part = clone $assertion_type; + $new_type_part = $assertion_type; } else { $new_type_part = new TMixed(); } @@ -262,7 +262,7 @@ private static function getMissingType( $assertion_type = $assertion->getAtomicType(); if ($assertion_type) { - return new Union([clone $assertion_type]); + return new Union([$assertion_type]); } } @@ -331,14 +331,14 @@ private static function refine( if ($existing_var_type_part instanceof TNamedObject || $existing_var_type_part instanceof TTemplateParam ) { - $acceptable_atomic_types[] = clone $existing_var_type_part; + $acceptable_atomic_types[] = $existing_var_type_part; } else { if (AtomicTypeComparator::isContainedBy( $codebase, $existing_var_type_part, $new_as_atomic )) { - $acceptable_atomic_types[] = clone $existing_var_type_part; + $acceptable_atomic_types[] = $existing_var_type_part; } } } @@ -393,7 +393,7 @@ private static function refine( $existing_var_type_part, $new_type_part )) { - $acceptable_atomic_types[] = clone $existing_var_type_part; + $acceptable_atomic_types[] = $existing_var_type_part; continue; } @@ -533,8 +533,6 @@ private static function filterTypeWithAnother( ): ?Union { $matching_atomic_types = []; - $new_type = clone $new_type; - $existing_types = $existing_type->getAtomicTypes(); foreach ($new_type->getAtomicTypes() as $new_type_part) { foreach ($existing_types as &$existing_type_part) { @@ -549,6 +547,7 @@ private static function filterTypeWithAnother( $matching_atomic_types[] = $matching_atomic_type; } } + unset($existing_type_part); } $existing_type = $existing_type->setTypes($existing_types); @@ -640,7 +639,7 @@ private static function filterAtomicWithAnother( $type_2_value, $any_scalar_type_match_found ); - $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_type_param); + $type_1_atomic = $type_1_atomic->setTypeParam($type_1_type_param); if ($type_2_value === null) { return null; @@ -726,7 +725,7 @@ private static function filterAtomicWithAnother( } /** @psalm-suppress ArgumentTypeCoercion */ - $type_1_atomic = $type_1_atomic->replaceTypeParams( + $type_1_atomic = $type_1_atomic->setTypeParams( $type_1_params ); @@ -754,9 +753,9 @@ private static function filterAtomicWithAnother( } if ($type_1_param->getId() !== $type_2_param->getId()) { - $type_1_atomic = $type_1_atomic->replaceTypeParam($type_2_param); + $type_1_atomic = $type_1_atomic->setTypeParam($type_2_param); } elseif ($type_1_param !== $type_1_atomic->type_param) { - $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_param); + $type_1_atomic = $type_1_atomic->setTypeParam($type_1_param); } $matching_atomic_type = $type_1_atomic; @@ -786,6 +785,7 @@ private static function filterAtomicWithAnother( $type_1_param = $type_2_param; } } + unset($type_1_param); $matching_atomic_type = $type_1_atomic->setProperties($type_1_properties); $atomic_comparison_results->type_coerced = true; @@ -852,7 +852,7 @@ private static function refineContainedAtomicWithAnother( return $type_1_atomic->replaceAs($type_1_as); } else { - return clone $type_2_atomic; + return $type_2_atomic; } } @@ -1004,9 +1004,9 @@ private static function handleLiteralEqualityWithInt( $value = $assertion_type->value; // we create the literal that is being asserted. We'll return this when we're sure this is the resulting type - $literal_asserted_type = new Union([new TLiteralInt($value)]); - $literal_asserted_type->from_docblock = $existing_var_type->from_docblock; - + $literal_asserted_type = new Union([new TLiteralInt($value)], [ + 'from_docblock' => $existing_var_type->from_docblock + ]); $compatible_int_type = self::getCompatibleIntType( $existing_var_type, $existing_var_atomic_types, @@ -1145,8 +1145,9 @@ private static function handleLiteralEqualityWithString( $value = $assertion_type->value; // we create the literal that is being asserted. We'll return this when we're sure this is the resulting type - $literal_asserted_type_string = new Union([clone $assertion_type]); - $literal_asserted_type_string->from_docblock = $existing_var_type->from_docblock; + $literal_asserted_type_string = new Union([$assertion_type], [ + 'from_docblock' => $existing_var_type->from_docblock + ]); $compatible_string_type = self::getCompatibleStringType( $existing_var_type, @@ -1287,8 +1288,9 @@ private static function handleLiteralEqualityWithFloat( $value = $assertion_type->value; // we create the literal that is being asserted. We'll return this when we're sure this is the resulting type - $literal_asserted_type = new Union([new TLiteralFloat($value)]); - $literal_asserted_type->from_docblock = $existing_var_type->from_docblock; + $literal_asserted_type = new Union([new TLiteralFloat($value)], [ + 'from_docblock' => $existing_var_type->from_docblock + ]); $compatible_float_type = self::getCompatibleFloatType( $existing_var_type, @@ -1428,9 +1430,9 @@ private static function getCompatibleIntType( return $existing_var_type; } - $assertion_type = new Union([clone $assertion_type]); - $assertion_type->from_docblock = $existing_var_type->from_docblock; - return $assertion_type; + return new Union([$assertion_type], [ + 'from_docblock' => $existing_var_type->from_docblock + ]); } } @@ -1455,9 +1457,9 @@ private static function getCompatibleStringType( return $existing_var_type; } - $assertion_type = new Union([clone $assertion_type]); - $assertion_type->from_docblock = $existing_var_type->from_docblock; - return $assertion_type; + return new Union([$assertion_type], [ + 'from_docblock' => $existing_var_type->from_docblock + ]); } } @@ -1482,9 +1484,9 @@ private static function getCompatibleFloatType( return $existing_var_type; } - $assertion_type = new Union([clone $assertion_type]); - $assertion_type->from_docblock = $existing_var_type->from_docblock; - return $assertion_type; + return new Union([$assertion_type], [ + 'from_docblock' => $existing_var_type->from_docblock + ]); } } @@ -1510,15 +1512,15 @@ private static function handleIsA( if ($existing_var_type->hasMixed()) { if (!$assertion_type instanceof TNamedObject) { - return [clone $assertion_type]; + return [$assertion_type]; } - $types = [clone $assertion_type]; + $types = [$assertion_type]; if ($allow_string_comparison) { $types[] = new TClassString( $assertion_type->value, - clone $assertion_type + $assertion_type ); } @@ -1533,7 +1535,7 @@ private static function handleIsA( if ($assertion_type instanceof TTemplateParamClass) { return [new TTemplateParam( $assertion_type->param_name, - new Union([$assertion_type->as_type ? clone $assertion_type->as_type : new TObject()]), + new Union([$assertion_type->as_type ? $assertion_type->as_type : new TObject()]), $assertion_type->defining_class )]; } @@ -1554,7 +1556,7 @@ private static function handleIsA( return [new TMixed()]; } else { if (!$assertion_type instanceof TNamedObject) { - return [clone $assertion_type]; + return [$assertion_type]; } $new_type_has_interface_string = $codebase->interfaceExists($assertion_type->value); @@ -1590,7 +1592,7 @@ private static function handleIsA( ) ) ) { - $new_type_part = clone $assertion_type; + $new_type_part = $assertion_type; $acceptable_atomic_types = []; @@ -1614,7 +1616,7 @@ private static function handleIsA( $existing_var_type_part, $new_type_part )) { - $acceptable_atomic_types[] = clone $existing_var_type_part; + $acceptable_atomic_types[] = $existing_var_type_part; continue; } diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index 5948512b93b..09ca3cabc48 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -64,8 +64,7 @@ public static function isContainedBy( foreach ($input_type_part->type_params[0]->getAtomicTypes() as $atomic_key_type) { if ($atomic_key_type instanceof TLiteralString || $atomic_key_type instanceof TLiteralInt) { - $properties[$atomic_key_type->value] = clone $input_type_part->type_params[1]; - $properties[$atomic_key_type->value]->possibly_undefined = true; + $properties[$atomic_key_type->value] = $input_type_part->type_params[1]->setPossiblyUndefined(true); } else { $all_string_int_literals = false; } @@ -167,14 +166,14 @@ public static function isContainedBy( if ($input_type_part instanceof TClassStringMap) { $input_type_part = new TArray([ $input_type_part->getStandinKeyParam(), - clone $input_type_part->value_param + $input_type_part->value_param ]); } if ($container_type_part instanceof TClassStringMap) { $container_type_part = new TArray([ $container_type_part->getStandinKeyParam(), - clone $container_type_part->value_param + $container_type_part->value_param ]); } @@ -185,7 +184,7 @@ public static function isContainedBy( $atomic_comparison_result->type_coerced = true; } - $container_type_part = new TArray([Type::getInt(), clone $container_type_part->type_param]); + $container_type_part = new TArray([Type::getInt(), $container_type_part->type_param]); } if ($input_type_part instanceof TList) { @@ -199,13 +198,13 @@ public static function isContainedBy( $input_type_part = new TNonEmptyArray([ new Union($literal_ints), - clone $input_type_part->type_param + $input_type_part->type_param ]); } else { - $input_type_part = new TNonEmptyArray([Type::getInt(), clone $input_type_part->type_param]); + $input_type_part = new TNonEmptyArray([Type::getInt(), $input_type_part->type_param]); } } else { - $input_type_part = new TArray([Type::getInt(), clone $input_type_part->type_param]); + $input_type_part = new TArray([Type::getInt(), $input_type_part->type_param]); } } diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index d67e7087d41..b1a73a9fe43 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -211,7 +211,9 @@ public static function isNotExplicitlyCallableTypeCallable( return false; } - $codebase->methods->getStorage($method_id); + if (!$codebase->methods->hasStorage($method_id)) { + return false; + } } catch (Exception $e) { return false; } @@ -260,7 +262,7 @@ public static function getCallableFromAtomic( foreach ($function_storage->params as $param) { if ($param->type) { - $param = $param->replaceType( + $param = $param->setType( TypeExpander::expandUnion( $codebase, $param->type, @@ -326,7 +328,7 @@ public static function getCallableFromAtomic( } } - $matching_callable = clone InternalCallMapHandler::getCallableFromCallMapById( + $matching_callable = InternalCallMapHandler::getCallableFromCallMapById( $codebase, $input_type_part->value, $args, @@ -335,14 +337,13 @@ public static function getCallableFromAtomic( $must_use = false; - /** @psalm-suppress InaccessibleProperty We just cloned this object */ - $matching_callable->is_pure = $codebase->functions->isCallMapFunctionPure( + $matching_callable = $matching_callable->setIsPure($codebase->functions->isCallMapFunctionPure( $codebase, $statements_analyzer->node_data ?? null, $input_type_part->value, null, $must_use - ); + )); return $matching_callable; } @@ -443,7 +444,7 @@ public static function getCallableFromAtomic( if ($template_result) { $callable = TemplateInferredTypeReplacer::replace( - new Union([clone $callable]), + new Union([$callable]), $template_result, $codebase )->getSingleAtomic(); diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index c6de9ca06be..64028cdb42a 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -60,7 +60,7 @@ public static function isContainedBy( $atomic_comparison_result_type_params = null; if ($atomic_comparison_result) { if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; + $atomic_comparison_result->replacement_atomic_type = $input_type_part; } if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { @@ -76,7 +76,7 @@ public static function isContainedBy( if ($input_param->isNever()) { if ($atomic_comparison_result_type_params !== null) { - $atomic_comparison_result_type_params[$i] = clone $container_param; + $atomic_comparison_result_type_params[$i] = $container_param; } continue; @@ -139,7 +139,7 @@ public static function isContainedBy( ) { if ($input_param->containsAnyLiteral()) { if ($atomic_comparison_result_type_params !== null) { - $atomic_comparison_result_type_params[$i] = clone $container_param; + $atomic_comparison_result_type_params[$i] = $container_param; } } else { if (!($container_type_params_covariant[$i] ?? false) @@ -180,7 +180,7 @@ public static function isContainedBy( /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ $atomic_comparison_result->replacement_atomic_type = $atomic_comparison_result->replacement_atomic_type - ->replaceTypeParams($atomic_comparison_result_type_params); + ->setTypeParams($atomic_comparison_result_type_params); } if ($all_types_contain) { diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index 96ed14f299c..6ade9af6b1b 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -50,7 +50,11 @@ public static function isContainedByUnion( Union $container_type ): bool { $container_atomic_types = $container_type->getAtomicTypes(); - $reduced_range = clone $input_type_part; + $reduced_range = new TIntRange( + $input_type_part->min_bound, + $input_type_part->max_bound, + $input_type_part->from_docblock + ); if (isset($container_atomic_types['int'])) { if (get_class($container_atomic_types['int']) === TInt::class) { @@ -85,7 +89,7 @@ public static function isContainedByUnion( * The goal is to use values in atomics in order to reduce the range. * Once the range is empty, it means that every value in range was covered by some atomics combination * - * @psalm-suppress InaccessibleProperty $reduced_range was already cloned + * @psalm-suppress InaccessibleProperty $reduced_range was just re-created * @param array $container_atomic_types */ private static function reduceRangeIncrementally(array &$container_atomic_types, TIntRange $reduced_range): ?bool diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index dfbf026a917..5f9980a8c06 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -184,8 +184,8 @@ public static function reconcile( [ $iterable->type_params[0]->hasMixed() ? Type::getArrayKey() - : clone $iterable->type_params[0], - clone $iterable->type_params[1], + : $iterable->type_params[0], + $iterable->type_params[1], ] )); } elseif ($assertion_type !== null && get_class($assertion_type) === TInt::class @@ -259,7 +259,7 @@ public static function reconcile( && ($key !== '$this' || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)) ) { - $assertion_type = new Union([clone $assertion->type]); + $assertion_type = new Union([$assertion->type]); if ($key && $code_location @@ -382,7 +382,7 @@ private static function handleLiteralNegatedEquality( $existing_var_type->addType(new Type\Atomic\TIntRange($assertion_type->value + 1, null));*/ } } else { - $scalar_var_type = clone $assertion_type; + $scalar_var_type = $assertion_type; } } elseif ($assertion_type instanceof TLiteralString) { if ($existing_var_type->hasString()) { @@ -396,7 +396,7 @@ private static function handleLiteralNegatedEquality( $existing_var_type->addType(new TNonEmptyString()); } } elseif (get_class($assertion_type) === TLiteralString::class) { - $scalar_var_type = clone $assertion_type; + $scalar_var_type = $assertion_type; } } elseif ($assertion_type instanceof TLiteralFloat) { if ($existing_var_type->hasFloat()) { @@ -408,7 +408,7 @@ private static function handleLiteralNegatedEquality( } } } else { - $scalar_var_type = clone $assertion_type; + $scalar_var_type = $assertion_type; } } else { $fq_enum_name = $assertion_type->value; @@ -423,7 +423,7 @@ private static function handleLiteralNegatedEquality( $enum_storage = $codebase->classlike_storage_provider->get($fq_enum_name); if (!$enum_storage->is_enum || !$enum_storage->enum_cases) { - $scalar_var_type = clone $assertion_type; + $scalar_var_type = $assertion_type; } else { $existing_var_type->removeType($atomic_type->getKey()); $did_remove_type = true; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 95a2bc30c04..f5b26ce19d9 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -129,9 +129,7 @@ public static function reconcile( } if ($assertion instanceof ArrayKeyExists) { - $existing_var_type->possibly_undefined = false; - - return $existing_var_type; + return $existing_var_type->setPossiblyUndefined(false); } if ($assertion instanceof InArray) { @@ -481,7 +479,7 @@ public static function reconcile( || $atomic_type->as->hasObject() ) { unset($types[$k]); - $atomic_type = $atomic_type->replaceAs(new Union([clone $assertion_type])); + $atomic_type = $atomic_type->replaceAs(new Union([$assertion_type])); $types[$atomic_type->getKey()] = $atomic_type; return new Union($types); } @@ -637,7 +635,7 @@ private static function reconcileNonEmptyCountable( $properties = $array_atomic_type->properties; for ($i = $prop_count; $i < $assertion->count; $i++) { $properties[$i] - = clone ($array_atomic_type->previous_value_type ?: Type::getMixed()); + = ($array_atomic_type->previous_value_type ?: Type::getMixed()); } $array_atomic_type = $array_atomic_type->setProperties($properties); $existing_var_type->removeType('array'); @@ -1554,7 +1552,7 @@ private static function reconcileInArray( array $suppressed_issues, int &$failed_reconciliation ): Union { - $new_var_type = clone $assertion->type; + $new_var_type = $assertion->type; if ($new_var_type->isSingle() && $new_var_type->getSingleAtomic() instanceof TClassConstant) { // Can't do assertion on const with non-literal type @@ -1603,7 +1601,12 @@ private static function reconcileHasArrayKey( } if (isset($atomic_type->properties[$assertion])) { - $atomic_type->properties[$assertion]->possibly_undefined = false; + $atomic_type = $atomic_type->setProperties(array_merge( + $atomic_type->properties, + [ + $assertion => $atomic_type->properties[$assertion]->setPossiblyUndefined(false) + ] + )); } else { $atomic_type = new TKeyedArray( array_merge( @@ -1622,6 +1625,7 @@ private static function reconcileHasArrayKey( } } } + unset($atomic_type); return $existing_var_type->setTypes($types); } @@ -1872,8 +1876,7 @@ private static function reconcileTraversable( if ($type->hasTraversableInterface($codebase)) { $traversable_types[] = $type; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - $traversable_types[] = new TGenericObject('Traversable', $clone_type->type_params); + $traversable_types[] = new TGenericObject('Traversable', $type->type_params); $did_remove_type = true; } elseif ($type instanceof TObject) { $traversable_types[] = new TNamedObject('Traversable'); @@ -2072,8 +2075,7 @@ private static function reconcileList( $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - $array_types[] = new TList($clone_type->type_params[1]); + $array_types[] = new TList($type->type_params[1]); $did_remove_type = true; } else { @@ -2287,17 +2289,14 @@ private static function reconcileCallable( $callable_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TArray) { - $type = clone $type; $type = new TCallableArray($type->type_params); $callable_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TList) { - $type = clone $type; $type = new TCallableList($type->type_param); $callable_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TKeyedArray && count($type->properties) === 2) { - $type = clone $type; $type = new TCallableKeyedArray($type->properties); $callable_types[] = $type; $did_remove_type = true; @@ -2534,11 +2533,11 @@ private static function reconcileTruthyOrNonEmpty( } $new = $existing_var_type->setTypes($types); if ($new === $existing_var_type && ($new->possibly_undefined || $new->possibly_undefined_from_try)) { - $new = clone $existing_var_type; - $new->possibly_undefined = false; - $new->possibly_undefined_from_try = false; + $new = $existing_var_type->setPossiblyUndefined(false, false); } else { + /** @psalm-suppress InaccessibleProperty We just created this type */ $new->possibly_undefined = false; + /** @psalm-suppress InaccessibleProperty We just created this type */ $new->possibly_undefined_from_try = false; } return $new; diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index e744f456ad1..1aa8772279a 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -155,7 +155,7 @@ public static function reconcile( } if ($assertion instanceof NotInArray) { - $new_var_type = clone $assertion->type; + $new_var_type = $assertion->type; $intersection = Type::intersectUnionTypes( $new_var_type, @@ -618,6 +618,7 @@ private static function reconcileNull( $type = $new; } } + unset($type); if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { @@ -689,6 +690,7 @@ private static function reconcileFalse( $type = $new; } } + unset($type); if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { @@ -978,11 +980,11 @@ private static function reconcileScalar( } if ($non_scalar_types) { - $type = new Union($non_scalar_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_scalar_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1079,11 +1081,11 @@ private static function reconcileObject( } if ($non_object_types) { - $type = new Union($non_object_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_object_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock, + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1171,11 +1173,11 @@ private static function reconcileNumeric( } if ($non_numeric_types) { - $type = new Union($non_numeric_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_numeric_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock, + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1273,11 +1275,11 @@ private static function reconcileInt( } if ($non_int_types) { - $type = new Union($non_int_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_int_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock, + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1370,11 +1372,11 @@ private static function reconcileFloat( } if ($non_float_types) { - $type = new Union($non_float_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_float_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock, + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1476,11 +1478,11 @@ private static function reconcileString( } if ($non_string_types) { - $type = new Union($non_string_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_string_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock, + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1579,11 +1581,11 @@ private static function reconcileArray( } if ($non_array_types) { - $type = new Union($non_array_types); - $type->ignore_falsable_issues = $existing_var_type->ignore_falsable_issues; - $type->ignore_nullable_issues = $existing_var_type->ignore_nullable_issues; - $type->from_docblock = $existing_var_type->from_docblock; - return $type; + return new Union($non_array_types, [ + 'ignore_falsable_issues' => $existing_var_type->ignore_falsable_issues, + 'ignore_nullable_issues' => $existing_var_type->ignore_nullable_issues, + 'from_docblock' => $existing_var_type->from_docblock, + ]); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1633,6 +1635,7 @@ private static function reconcileResource( $type = $new; } } + unset($type); if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 7e0e76f0597..fd61ef298d0 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -83,7 +83,7 @@ public static function replace( } } elseif ($atomic_type instanceof TTemplateParamClass) { $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) - ? clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + ? TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( $inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], $codebase ) @@ -152,7 +152,7 @@ public static function replace( || $offset_template_type instanceof TLiteralInt) && isset($array_template_type->properties[$offset_template_type->value]) ) { - $template_type = clone $array_template_type->properties[$offset_template_type->value]; + $template_type = $array_template_type->properties[$offset_template_type->value]; } } } @@ -311,7 +311,7 @@ private static function replaceTemplateParam( $template_name = (string) $param_map[$key]; if (isset($inferred_lower_bounds[$template_name][$template_class])) { $template_type - = clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + = TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( $inferred_lower_bounds[$template_name][$template_class], $codebase ); @@ -341,7 +341,7 @@ private static function replaceTemplateKeyOfValueOf( return null; } - $template_type = clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + $template_type = TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( $inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], $codebase ); @@ -349,13 +349,13 @@ private static function replaceTemplateKeyOfValueOf( if ($atomic_type instanceof TTemplateKeyOf && TKeyOf::isViableTemplateType($template_type) ) { - return new TKeyOf(clone $template_type); + return new TKeyOf($template_type); } if ($atomic_type instanceof TTemplateValueOf && TValueOf::isViableTemplateType($template_type) ) { - return new TValueOf(clone $template_type); + return new TValueOf($template_type); } return null; @@ -373,7 +373,7 @@ private static function replaceTemplatePropertiesOf( return null; } - $template_type = clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + $template_type = TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( $inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], $codebase ); @@ -399,7 +399,7 @@ private static function replaceConditional( array $inferred_lower_bounds ): Union { $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) - ? clone TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( + ? TemplateStandinTypeReplacer::getMostSpecificTypeFromBounds( $inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class], $codebase ) @@ -471,7 +471,7 @@ private static function replaceConditional( false ) ) { - $if_template_type = clone $if_type; + $if_template_type = $if_type; $refined_template_result = clone $template_result; @@ -501,7 +501,7 @@ private static function replaceConditional( false ) ) { - $else_template_type = clone $else_type; + $else_template_type = $else_type; $refined_template_result = clone $template_result; @@ -546,7 +546,7 @@ private static function replaceConditional( ); } - $atomic_type = $atomic_type->replaceTypes( + $atomic_type = $atomic_type->setTypes( $as_type, $conditional_type, $if_type, diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 68a61684cf0..e92cb86d8f6 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -165,23 +165,23 @@ public static function replace( } if (count($atomic_types) > 1) { - $new_union_type = TypeCombiner::combine( + return TypeCombiner::combine( $atomic_types, $codebase - ); - } else { - $new_union_type = new Union($atomic_types); - } - - $new_union_type->ignore_nullable_issues = $union_type->ignore_nullable_issues; - $new_union_type->ignore_falsable_issues = $union_type->ignore_falsable_issues; - $new_union_type->possibly_undefined = $union_type->possibly_undefined; - - if ($had_template) { - $new_union_type->had_template = true; + )->setProperties([ + 'ignore_nullable_issues' => $union_type->ignore_nullable_issues, + 'ignore_falsable_issues' => $union_type->ignore_falsable_issues, + 'possibly_undefined' => $union_type->possibly_undefined, + 'had_template' => $had_template + ]); } - - return $new_union_type; + + return new Union($atomic_types, [ + 'ignore_nullable_issues' => $union_type->ignore_nullable_issues, + 'ignore_falsable_issues' => $union_type->ignore_falsable_issues, + 'possibly_undefined' => $union_type->possibly_undefined, + 'had_template' => $had_template + ]); } return $union_type; @@ -287,7 +287,7 @@ private static function handleAtomicStandin( $include_first = false; $replacement_type - = clone $array_template_type->properties[$offset_template_type->value]; + = $array_template_type->properties[$offset_template_type->value]; foreach ($replacement_type->getAtomicTypes() as $replacement_atomic_type) { $atomic_types[] = $replacement_atomic_type; @@ -333,15 +333,15 @@ private static function handleAtomicStandin( } elseif ($template_atomic instanceof TList) { $template_atomic = Type::getInt(); } else { - $template_atomic = clone $template_atomic->type_params[0]; + $template_atomic = $template_atomic->type_params[0]; } } else { if ($template_atomic instanceof TKeyedArray) { $template_atomic = $template_atomic->getGenericValueType(); } elseif ($template_atomic instanceof TList) { - $template_atomic = clone $template_atomic->type_param; + $template_atomic = $template_atomic->type_param; } else { - $template_atomic = clone $template_atomic->type_params[1]; + $template_atomic = $template_atomic->type_params[1]; } } @@ -676,7 +676,7 @@ private static function handleTemplateParamStandin( && !$atomic_type->as->hasMixed() ) { foreach ($atomic_type->as->getAtomicTypes() as $as_atomic_type) { - $atomic_types[] = clone $as_atomic_type; + $atomic_types[] = $as_atomic_type; } } else { $replacement_type = TypeExpander::expandUnion( @@ -734,12 +734,12 @@ private static function handleTemplateParamStandin( $replacements_found = true; foreach ($key_type->getAtomicTypes() as $key_type_atomic) { - $atomic_types[] = clone $key_type_atomic; + $atomic_types[] = $key_type_atomic; } $existing_lower_bound = reset($template_result->lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]); - $existing_lower_bound->type = clone $key_type; + $existing_lower_bound->type = $key_type; } } @@ -749,13 +749,13 @@ private static function handleTemplateParamStandin( ) { foreach ($replacement_atomic_type->as->getAtomicTypes() as $nested_type_atomic) { $replacements_found = true; - $atomic_types[] = clone $nested_type_atomic; + $atomic_types[] = $nested_type_atomic; } } // @codingStandardsIgnoreEnd if (!$replacements_found) { - $atomic_types[] = clone $replacement_atomic_type; + $atomic_types[] = $replacement_atomic_type; } $had_template = true; @@ -877,6 +877,7 @@ private static function handleTemplateParamStandin( $t = reset($extra_types)->setIntersectionTypes(array_slice($extra_types, 1)); } } + unset($t); return $atomic_types; } @@ -1293,20 +1294,19 @@ public static function getMappedGenericTypeParams( ); $candidate_param_type = $input_type_params[$old_params_offset] ?? Type::getMixed(); + $candidate_param_type = $candidate_param_type->setProperties([ + 'from_template_default' => true + ]); } else { - $candidate_param_type = new Union([clone $et]); + $candidate_param_type = new Union([$et], ['from_template_default' => true]); } - $candidate_param_type->from_template_default = true; - $new_input_param = Type::combineUnionTypes( $new_input_param, $candidate_param_type ); } - $new_input_param = clone $new_input_param; - $new_input_param = TemplateInferredTypeReplacer::replace( $new_input_param, new TemplateResult([], $replacement_templates), diff --git a/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php index 1e1f99fe320..44a8ac30ab8 100644 --- a/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias; +use Psalm\Storage\ImmutableNonCloneableTrait; /** * @psalm-immutable @@ -11,6 +12,8 @@ */ class InlineTypeAlias implements TypeAlias { + use ImmutableNonCloneableTrait; + /** * @var list */ diff --git a/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php index bdd13819e6c..efe89975005 100644 --- a/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias; +use Psalm\Storage\ImmutableNonCloneableTrait; /** * @psalm-immutable @@ -11,6 +12,8 @@ */ class LinkableTypeAlias implements TypeAlias { + use ImmutableNonCloneableTrait; + public $declaring_fq_classlike_name; public $alias_name; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index b0348134cdf..3c9076dae8f 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -101,7 +101,7 @@ public static function combine( int $literal_limit = 500 ): Union { if (count($types) === 1) { - return new Union([$types[0]], $types[0]->from_docblock); + return new Union([$types[0]]); } $combination = new TypeCombination(); @@ -122,7 +122,7 @@ public static function combine( if ($result) { if ($from_docblock) { - $result->from_docblock = true; + return $result->setProperties(['from_docblock' => true]); } return $result; @@ -360,7 +360,7 @@ public static function combine( } if ($from_docblock) { - $union_type->from_docblock = true; + return $union_type->setProperties(['from_docblock' => true]); } return $union_type; @@ -675,7 +675,7 @@ private static function scrapeTypeProperties( } $existing_objectlike_entries = (bool) $combination->objectlike_entries; - $possibly_undefined_entries = $combination->objectlike_entries; + $missing_entries = $combination->objectlike_entries; $combination->objectlike_sealed = $combination->objectlike_sealed && $type->sealed; if ($type->previous_value_type) { @@ -702,10 +702,9 @@ private static function scrapeTypeProperties( $value_type = $combination->objectlike_entries[$candidate_property_name] ?? null; if (!$value_type) { - $combination->objectlike_entries[$candidate_property_name] = clone $candidate_property_type; - // it's possibly undefined if there are existing objectlike entries and - $combination->objectlike_entries[$candidate_property_name]->possibly_undefined - = $existing_objectlike_entries || $candidate_property_type->possibly_undefined; + $combination->objectlike_entries[$candidate_property_name] = $candidate_property_type + ->setPossiblyUndefined($existing_objectlike_entries + || $candidate_property_type->possibly_undefined); } else { $combination->objectlike_entries[$candidate_property_name] = Type::combineUnionTypes( $value_type, @@ -715,13 +714,11 @@ private static function scrapeTypeProperties( ); } - if (!$type->previous_value_type) { - unset($possibly_undefined_entries[$candidate_property_name]); - } - if (!$candidate_property_type->possibly_undefined) { $has_defined_keys = true; } + + unset($missing_entries[$candidate_property_name]); } if (!$has_defined_keys) { @@ -742,8 +739,9 @@ private static function scrapeTypeProperties( $combination->array_min_counts[$min_prop_count] = true; } - foreach ($possibly_undefined_entries as $possibly_undefined_type) { - $possibly_undefined_type->possibly_undefined = true; + foreach ($missing_entries as $k => $_) { + $combination->objectlike_entries[$k] = $combination->objectlike_entries[$k] + ->setPossiblyUndefined(true); } if (!$type->is_list) { @@ -1348,9 +1346,10 @@ private static function handleKeyedArrayEntries( && ($combination->array_type_params[1]->isNever() || $combination->array_type_params[1]->isMixed())) ) { - foreach ($combination->objectlike_entries as $objectlike_entry) { - $objectlike_entry->possibly_undefined = true; + foreach ($combination->objectlike_entries as &$objectlike_entry) { + $objectlike_entry = $objectlike_entry->setPossiblyUndefined(true); } + unset($objectlike_entry); } if ($combination->objectlike_value_type @@ -1437,10 +1436,13 @@ private static function getArrayTypeFromGenericParams( foreach ($combination->objectlike_entries as $property_name => $property_type) { $objectlike_generic_type = Type::combineUnionTypes( - clone $property_type, + $property_type, $objectlike_generic_type, $codebase, - $overwrite_empty_array + $overwrite_empty_array, + true, + 500, + false ); if (is_int($property_name)) { @@ -1457,12 +1459,13 @@ private static function getArrayTypeFromGenericParams( $combination->objectlike_value_type, $objectlike_generic_type, $codebase, - $overwrite_empty_array + $overwrite_empty_array, + true, + 500, + false ); } - $objectlike_generic_type->possibly_undefined = false; - $objectlike_key_type = new Union(array_values($objectlike_keys)); $objectlike_key_type = Type::combineUnionTypes( diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 8e0b130a21e..17e7abb6528 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -58,6 +58,7 @@ class TypeExpander { /** + * @psalm-suppress InaccessibleProperty We just created the type * @param string|TNamedObject|TTemplateParam|null $static_class_type */ public static function expandUnion( @@ -73,8 +74,6 @@ public static function expandUnion( bool $expand_templates = false, bool $throw_on_unresolvable_constant = false ): Union { - $return_type = clone $return_type; - $new_return_type_parts = []; $had_split_values = false; @@ -214,16 +213,15 @@ public static function expandAtomic( ); if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on clone */ - $return_type->as_type = $new_as_type; - /** @psalm-suppress InaccessibleProperty Acting on clone */ - $return_type->as = $new_as_type->value; + $return_type = $return_type->setAs( + $new_as_type->value, + $new_as_type + ); } } elseif ($return_type instanceof TTemplateParam) { $new_as_type = self::expandUnion( $codebase, - clone $return_type->as, + $return_type->as, $self_class, $static_class_type, $parent_class, @@ -297,8 +295,6 @@ public static function expandAtomic( if ($class_constant) { if ($class_constant->isSingle()) { - $class_constant = clone $class_constant; - $matching_constant_types = array_merge( array_values($class_constant->getAtomicTypes()), $matching_constant_types @@ -495,8 +491,9 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + unset($type_param); /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ - $return_type = $return_type->replaceTypeParams($type_params); + $return_type = $return_type->setTypeParams($type_params); } elseif ($return_type instanceof TKeyedArray) { $properties = $return_type->properties; foreach ($properties as &$property_type) { @@ -514,9 +511,10 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + unset($property_type); $return_type = $return_type->setProperties($properties); } elseif ($return_type instanceof TList) { - $return_type = $return_type->replaceTypeParam(self::expandUnion( + $return_type = $return_type->setTypeParam(self::expandUnion( $codebase, $return_type->type_param, $self_class, @@ -548,6 +546,7 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + unset($property_type); $return_type = $return_type->setProperties($properties); } @@ -558,7 +557,7 @@ public static function expandAtomic( if ($params) { foreach ($params as &$param) { if ($param->type) { - $param = $param->replaceType(self::expandUnion( + $param = $param->setType(self::expandUnion( $codebase, $param->type, $self_class, @@ -573,6 +572,7 @@ public static function expandAtomic( )); } } + unset($param); } $sub_return_type = $return_type->return_type; if ($sub_return_type) { @@ -591,13 +591,10 @@ public static function expandAtomic( ); } - if ($sub_return_type !== $return_type->return_type || $params !== $return_type->params) { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty We just cloned this */ - $return_type->return_type = $sub_return_type; - /** @psalm-suppress InaccessibleProperty We just cloned this */ - $return_type->params = $params; - } + $return_type = $return_type->replace( + $params, + $sub_return_type + ); } return [$return_type]; @@ -605,6 +602,7 @@ public static function expandAtomic( /** * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param-out TNamedObject|TTemplateParam $return_type * @return TNamedObject|TTemplateParam */ private static function expandNamedObject( @@ -636,7 +634,7 @@ private static function expandNamedObject( $return_type->value, array_values( array_map( - static fn($type_map) => clone reset($type_map), + static fn($type_map) => reset($type_map), $container_class_storage->template_types ) ) @@ -650,28 +648,36 @@ private static function expandNamedObject( $return_type_lc = strtolower($return_type->value); if ($static_class_type && ($return_type_lc === 'static' || $return_type_lc === '$this')) { + $is_static = $return_type->is_static; + $is_static_resolved = null; + if (!$final) { + $is_static = true; + $is_static_resolved = true; + } if (is_string($static_class_type)) { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->value = $static_class_type; + $return_type = $return_type->setValueIsStatic( + $static_class_type, + $is_static, + $is_static_resolved + ); } else { if ($return_type instanceof TGenericObject && $static_class_type instanceof TGenericObject ) { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->value = $static_class_type->value; + $return_type = $return_type->setValueIsStatic( + $static_class_type->value, + $is_static, + $is_static_resolved + ); + } elseif ($static_class_type instanceof TNamedObject) { + $return_type = $static_class_type->setIsStatic( + $is_static, + $is_static_resolved + ); } else { - $return_type = clone $static_class_type; + $return_type = $static_class_type; } } - - if (!$final && $return_type instanceof TNamedObject) { - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->is_static = true; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->is_static_resolved = true; - } } elseif ($return_type->is_static && !$return_type->is_static_resolved && ($static_class_type instanceof TNamedObject || $static_class_type instanceof TTemplateParam) @@ -686,35 +692,25 @@ private static function expandNamedObject( foreach ($extra_static as $extra_static_type) { if ($extra_static_type->getKey(false) !== $return_type->getKey(false)) { - $return_type_types[$extra_static_type->getKey()] = clone $extra_static_type; + $return_type_types[$extra_static_type->getKey()] = $extra_static_type; } } $return_type = $return_type->setIntersectionTypes($return_type_types) ->setIsStatic(true, true); } elseif ($return_type->is_static && is_string($static_class_type) && $final) { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->value = $static_class_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->is_static = false; + $return_type = $return_type->setValueIsStatic( + $static_class_type, + false + ); } elseif ($self_class && $return_type_lc === 'self') { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->value = $self_class; + $return_type = $return_type->setValue($self_class); } elseif ($parent_class && $return_type_lc === 'parent') { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->value = $parent_class; + $return_type = $return_type->setValue($parent_class); } else { $new_value = $codebase->classlikes->getUnAliasedName($return_type->value); - if ($return_type->value !== $new_value) { - $return_type = clone $return_type; - /** @psalm-suppress InaccessibleProperty Acting on a clone */ - $return_type->value = $new_value; - } + $return_type = $return_type->setValue($new_value); } - /** @psalm-suppress ReferenceConstraintViolation Psalm bug, we are never assigning a TTemplateParam to $return_type */ return $return_type; } @@ -880,13 +876,13 @@ private static function expandConditional( $codebase ); - $return_type = $return_type->replaceTypes($new_as_type); + $return_type = $return_type->setTypes($new_as_type); return array_values($combined->getAtomicTypes()); } } - $return_type = $return_type->replaceTypes( + $return_type = $return_type->setTypes( $new_as_type, self::expandUnion( $codebase, diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index e810718075d..8790c0f9ab7 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -142,7 +142,7 @@ public static function parseTokens( $from_docblock ); - return new Union([$atomic], $from_docblock); + return new Union([$atomic], ['from_docblock' => $from_docblock]); } } @@ -158,7 +158,7 @@ public static function parseTokens( ); if (!($parsed_type instanceof Union)) { - $parsed_type = new Union([$parsed_type], $from_docblock); + $parsed_type = new Union([$parsed_type], ['from_docblock' => $from_docblock]); } return $parsed_type; @@ -247,7 +247,7 @@ public static function getTypeFromTree( $callable_type->return_type = $return_type instanceof Union ? $return_type - : new Union([$return_type], $from_docblock) + : new Union([$return_type], ['from_docblock' => $from_docblock]) ; return $callable_type; @@ -370,15 +370,15 @@ public static function getTypeFromTree( ); if ($conditional_type instanceof Atomic) { - $conditional_type = new Union([$conditional_type], $from_docblock); + $conditional_type = new Union([$conditional_type], ['from_docblock' => $from_docblock]); } if ($if_type instanceof Atomic) { - $if_type = new Union([$if_type], $from_docblock); + $if_type = new Union([$if_type], ['from_docblock' => $from_docblock]); } if ($else_type instanceof Atomic) { - $else_type = new Union([$else_type], $from_docblock); + $else_type = new Union([$else_type], ['from_docblock' => $from_docblock]); } return new TConditional( @@ -605,7 +605,10 @@ private static function getTypeFromGenericTree( } } - $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type], $from_docblock); + $generic_params[] = $tree_type instanceof Union + ? $tree_type + : new Union([$tree_type], ['from_docblock' => $from_docblock]) + ; } $generic_type_value = TypeTokenizer::fixScalarTerms($generic_type); @@ -1125,7 +1128,7 @@ private static function getTypeFromIntersectionTree( foreach ($intersection_types as $intersection_type) { foreach ($intersection_type->properties as $property => $property_type) { if (!array_key_exists($property, $properties)) { - $properties[$property] = clone $property_type; + $properties[$property] = $property_type; continue; } @@ -1426,7 +1429,7 @@ private static function getTypeFromKeyedArrayTree( } if (!$property_type instanceof Union) { - $property_type = new Union([$property_type], $from_docblock); + $property_type = new Union([$property_type], ['from_docblock' => $from_docblock]); } if ($property_maybe_undefined) { diff --git a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php index c1bc1280fd4..651251380ae 100644 --- a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php +++ b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php @@ -6,15 +6,15 @@ use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\MutableTypeVisitor; use Psalm\Type\TypeNode; -use Psalm\Type\TypeVisitor; use function strtolower; /** * @internal */ -class ClasslikeReplacer extends TypeVisitor +class ClasslikeReplacer extends MutableTypeVisitor { private string $old; private string $new; @@ -27,25 +27,30 @@ public function __construct( $this->new = $new; } - /** - * @psalm-suppress InaccessibleProperty Acting on clones - */ protected function enterNode(TypeNode &$type): ?int { if ($type instanceof TClassConstant) { if (strtolower($type->fq_classlike_name) === $this->old) { - $type = clone $type; - $type->fq_classlike_name = $this->new; + $type = new TClassConstant( + $this->new, + $type->const_name, + $type->from_docblock + ); } } elseif ($type instanceof TClassString) { if ($type->as !== 'object' && strtolower($type->as) === $this->old) { - $type = clone $type; - $type->as = $this->new; + $type = new TClassString( + $this->new, + $type->as_type, + $type->is_loaded, + $type->is_interface, + $type->is_enum, + $type->from_docblock + ); } } elseif ($type instanceof TNamedObject || $type instanceof TLiteralClassString) { if (strtolower($type->value) === $this->old) { - $type = clone $type; - $type->value = $this->new; + $type = $type->setValue($this->new); } } return null; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php index 472f61f93d6..d5da9e62642 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php @@ -5,7 +5,6 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use Psalm\Type\TypeVisitor; @@ -14,7 +13,7 @@ /** * @internal */ -class ContainsClassLikeVisitor extends ImmutableTypeVisitor +class ContainsClassLikeVisitor extends TypeVisitor { /** * @var lowercase-string @@ -43,21 +42,21 @@ protected function enterNode(TypeNode $type): ?int if ($type instanceof TNamedObject) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return TypeVisitor::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } } if ($type instanceof TClassConstant) { if (strtolower($type->fq_classlike_name) === $this->fq_classlike_name) { $this->contains_classlike = true; - return TypeVisitor::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } } if ($type instanceof TLiteralClassString) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return TypeVisitor::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } } diff --git a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php index c340207e57d..0b905253afa 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php @@ -8,14 +8,13 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TTrue; -use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use Psalm\Type\TypeVisitor; /** * @internal */ -class ContainsLiteralVisitor extends ImmutableTypeVisitor +class ContainsLiteralVisitor extends TypeVisitor { /** * @var bool @@ -31,12 +30,12 @@ protected function enterNode(TypeNode $type): ?int || $type instanceof TFalse ) { $this->contains_literal = true; - return TypeVisitor::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } if ($type instanceof TArray && $type->isEmptyArray()) { $this->contains_literal = true; - return TypeVisitor::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php index da0a266b0e7..c8ef3fdd8a4 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php @@ -3,14 +3,13 @@ namespace Psalm\Internal\TypeVisitor; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use Psalm\Type\TypeVisitor; /** * @internal */ -class ContainsStaticVisitor extends ImmutableTypeVisitor +class ContainsStaticVisitor extends TypeVisitor { private bool $contains_static = false; @@ -18,7 +17,7 @@ protected function enterNode(TypeNode $type): ?int { if ($type instanceof TNamedObject && ($type->value === 'static' || $type->is_static)) { $this->contains_static = true; - return TypeVisitor::STOP_TRAVERSAL; + return self::STOP_TRAVERSAL; } return null; } diff --git a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php index ce7250a7150..5e624820a9d 100644 --- a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php +++ b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php @@ -4,15 +4,15 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\MutableTypeVisitor; use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; -use Psalm\Type\TypeVisitor; use Psalm\Type\Union; /** * @internal */ -class FromDocblockSetter extends TypeVisitor +class FromDocblockSetter extends MutableTypeVisitor { private bool $from_docblock; public function __construct(bool $from_docblock) @@ -30,14 +30,18 @@ protected function enterNode(TypeNode &$type): ?int if ($type->from_docblock === $this->from_docblock) { return null; } - $type = clone $type; - /** @psalm-suppress InaccessibleProperty Acting on clone */ - $type->from_docblock = $this->from_docblock; + if ($type instanceof MutableUnion) { + $type->from_docblock = true; + } elseif ($type instanceof Union) { + $type = $type->setProperties(['from_docblock' => $this->from_docblock]); + } else { + $type = $type->setFromDocblock($this->from_docblock); + } if ($type instanceof TTemplateParam && $type->as->isMixed() ) { - return TypeVisitor::DONT_TRAVERSE_CHILDREN; + return self::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php index de02f7b70f5..41d3acb8e67 100644 --- a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php +++ b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php @@ -6,14 +6,14 @@ use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; /** * @internal */ -class TemplateTypeCollector extends ImmutableTypeVisitor +class TemplateTypeCollector extends TypeVisitor { /** * @var list diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index dcf43591cd2..46b29015068 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -25,7 +25,6 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; use Psalm\Type\TypeVisitor; @@ -42,7 +41,7 @@ /** * @internal */ -class TypeChecker extends ImmutableTypeVisitor +class TypeChecker extends TypeVisitor { /** * @var StatementsSource @@ -118,7 +117,7 @@ protected function enterNode(TypeNode $type): ?int } if ($type->checked) { - return TypeVisitor::DONT_TRAVERSE_CHILDREN; + return self::DONT_TRAVERSE_CHILDREN; } if ($type instanceof TNamedObject) { diff --git a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php index 658cf6c9da0..4bfb4ac5250 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php +++ b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php @@ -6,9 +6,9 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; +use Psalm\Type\MutableTypeVisitor; use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; -use Psalm\Type\TypeVisitor; use Psalm\Type\Union; use function array_values; @@ -17,7 +17,7 @@ /** * @internal */ -class TypeLocalizer extends TypeVisitor +class TypeLocalizer extends MutableTypeVisitor { /** * @var array> @@ -36,9 +36,6 @@ public function __construct( $this->base_fq_class_name = $base_fq_class_name; } - /** - * @psalm-suppress InaccessibleProperty Acting on clones - */ protected function enterNode(TypeNode &$type): ?int { if ($type instanceof TTemplateParamClass) { @@ -49,11 +46,15 @@ protected function enterNode(TypeNode &$type): ?int $types = array_values($extended_param->getAtomicTypes()); if (count($types) === 1 && $types[0] instanceof TNamedObject) { - $type = clone $type; - $type->as_type = $types[0]; + $type = $type->setAs( + $type->as, + $types[0] + ); } elseif ($type->as_type !== null) { - $type = clone $type; - $type->as_type = null; + $type = $type->setAs( + $type->as, + null + ); } } } diff --git a/src/Psalm/Internal/TypeVisitor/TypeScanner.php b/src/Psalm/Internal/TypeVisitor/TypeScanner.php index 73b2687124c..cad58880ef4 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeScanner.php +++ b/src/Psalm/Internal/TypeVisitor/TypeScanner.php @@ -7,15 +7,15 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use function strtolower; /** * @internal */ -class TypeScanner extends ImmutableTypeVisitor +class TypeScanner extends TypeVisitor { private Scanner $scanner; diff --git a/src/Psalm/Storage/AttributeArg.php b/src/Psalm/Storage/AttributeArg.php index 549dcc21461..0472ad6b94b 100644 --- a/src/Psalm/Storage/AttributeArg.php +++ b/src/Psalm/Storage/AttributeArg.php @@ -6,8 +6,12 @@ use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class AttributeArg { + use ImmutableNonCloneableTrait; /** * @var ?string * @psalm-suppress PossiblyUnusedProperty It's part of the public API for now diff --git a/src/Psalm/Storage/AttributeStorage.php b/src/Psalm/Storage/AttributeStorage.php index 21d92cdcdb3..45651ec8eb5 100644 --- a/src/Psalm/Storage/AttributeStorage.php +++ b/src/Psalm/Storage/AttributeStorage.php @@ -4,8 +4,12 @@ use Psalm\CodeLocation; +/** + * @psalm-immutable + */ final class AttributeStorage { + use ImmutableNonCloneableTrait; /** * @var string */ diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index 1c10941dd44..66917f816cc 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -9,83 +9,83 @@ /** * @psalm-suppress PossiblyUnusedProperty + * @psalm-immutable */ final class ClassConstantStorage { + /** @psalm-suppress MutableDependency Mutable by design */ use CustomMetadataTrait; + use ImmutableNonCloneableTrait; - /** - * @var ?CodeLocation - */ - public $type_location; + public ?CodeLocation $type_location; /** * The type from an annotation, or the inferred type if no annotation exists. - * - * @var ?Union */ - public $type; + public ?Union $type; /** * The type inferred from the value. - * - * @var ?Union */ - public $inferred_type; + public ?Union $inferred_type; /** * @var ClassLikeAnalyzer::VISIBILITY_* */ - public $visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC; + public int $visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC; - /** - * @var ?CodeLocation - */ - public $location; + public ?CodeLocation $location; - /** - * @var ?CodeLocation - */ - public $stmt_location; + public ?CodeLocation $stmt_location; - /** - * @var ?UnresolvedConstantComponent - */ - public $unresolved_node; + public ?UnresolvedConstantComponent $unresolved_node; - /** - * @var bool - */ - public $deprecated = false; + public bool $deprecated = false; - /** - * @var bool - */ - public $final = false; + public bool $final = false; /** * @var list */ - public $attributes = []; + public array $attributes = []; /** * @var array */ - public $suppressed_issues = []; + public array $suppressed_issues = []; - /** - * @var ?string - */ - public $description; + public ?string $description; /** * @param ClassLikeAnalyzer::VISIBILITY_* $visibility + * @param list $attributes + * @param array $suppressed_issues */ - public function __construct(?Union $type, ?Union $inferred_type, int $visibility, ?CodeLocation $location) - { + public function __construct( + ?Union $type, + ?Union $inferred_type, + int $visibility, + ?CodeLocation $location, + ?CodeLocation $type_location = null, + ?CodeLocation $stmt_location = null, + bool $deprecated = false, + bool $final = false, + ?UnresolvedConstantComponent $unresolved_node = null, + array $attributes = [], + array $suppressed_issues = [], + ?string $description = null + ) { $this->visibility = $visibility; $this->location = $location; $this->type = $type; $this->inferred_type = $inferred_type; + $this->type_location = $type_location; + $this->stmt_location = $stmt_location; + $this->deprecated = $deprecated; + $this->final = $final; + $this->unresolved_node = $unresolved_node; + $this->attributes = $attributes; + $this->suppressed_issues = $suppressed_issues; + $this->description = $description; } } diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 8a9b25bfffd..7bd81b13abc 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -486,7 +486,7 @@ public function getClassTemplateTypes(): array $type_params = []; foreach ($this->template_types ?? [] as $type_map) { - $type_params[] = clone array_values($type_map)[0]; + $type_params[] = array_values($type_map)[0]; } return $type_params; diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index f7bc9ca0662..3526dca3851 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -4,7 +4,9 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Type\MutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; final class FunctionLikeParameter implements HasAttributesInterface, TypeNode @@ -151,7 +153,7 @@ public function getId(): string } /** @psalm-mutation-free */ - public function replaceType(Union $type): self + public function setType(Union $type): self { if ($this->type === $type) { return $this; @@ -161,14 +163,52 @@ public function replaceType(Union $type): self return $cloned; } - /** @psalm-mutation-free */ - public function getChildNodeKeys(): array + /** + * @internal Should only be used by the MutableTypeVisitor. + * @psalm-mutation-free + */ + public function visit(TypeVisitor $visitor): bool + { + if ($this->type && !$visitor->traverse($this->type)) { + return false; + } + if ($this->signature_type && !$visitor->traverse($this->signature_type)) { + return false; + } + if ($this->out_type && !$visitor->traverse($this->out_type)) { + return false; + } + if ($this->default_type instanceof Union && !$visitor->traverse($this->default_type)) { + return false; + } + + return true; + } + public static function visitMutable(MutableTypeVisitor $visitor, &$node, bool $cloned): bool { - $result = ['type', 'signature_type', 'out_type']; - if ($this->default_type instanceof Union) { - $result []= 'default_type'; + foreach (['type', 'signature_type', 'out_type', 'default_type'] as $key) { + if (!$node->{$key} instanceof TypeNode) { + continue; + } + + /** @var TypeNode */ + $value = $node->{$key}; + $value_orig = $value; + $result = $visitor->traverse($value); + if ($value !== $value_orig) { + if (!$cloned) { + $node = clone $node; + $cloned = true; + } + $node->{$key} = $value; + } + + if (!$result) { + return false; + } } - return $result; + + return true; } /** diff --git a/src/Psalm/Storage/Possibilities.php b/src/Psalm/Storage/Possibilities.php index 0ea16daec3d..15ec483918b 100644 --- a/src/Psalm/Storage/Possibilities.php +++ b/src/Psalm/Storage/Possibilities.php @@ -44,7 +44,7 @@ public function getUntemplatedCopy( $assertion_type = $assertion->getAtomicType(); if ($assertion_type) { - $union = new Union([clone $assertion_type]); + $union = new Union([$assertion_type]); $union = TemplateInferredTypeReplacer::replace( $union, $template_result, diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 7dcfc543a5b..b00696b0b79 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -182,15 +182,13 @@ public static function getStringFromFQCLN( public static function getInt(bool $from_calculation = false, ?int $value = null): Union { if ($value !== null) { - $union = new Union([new TLiteralInt($value)]); - } else { - $union = new Union([new TInt()]); + return new Union([new TLiteralInt($value)], [ + 'from_calculation' => $from_calculation + ]); } - - /** @psalm-suppress ImpurePropertyAssignment We just created this object */ - $union->from_calculation = $from_calculation; - - return $union; + return new Union([new TInt()], [ + 'from_calculation' => $from_calculation + ]); } /** @@ -523,6 +521,7 @@ public static function combineUnionTypeArray(array $union_types, ?Codebase $code * @psalm-external-mutation-free * * @psalm-suppress ImpurePropertyAssignment We're not mutating external instances + * @psalm-suppress InaccessibleProperty We're not mutating external instances */ public static function combineUnionTypes( ?Union $type_1, @@ -530,21 +529,31 @@ public static function combineUnionTypes( ?Codebase $codebase = null, bool $overwrite_empty_array = false, bool $allow_mixed_union = true, - int $literal_limit = 500 + int $literal_limit = 500, + ?bool $possibly_undefined = null ): Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); } if ($type_1 === null) { + if ($possibly_undefined !== null) { + return $type_2->setPossiblyUndefined($possibly_undefined); + } return $type_2; } if ($type_2 === null) { + if ($possibly_undefined !== null) { + return $type_1->setPossiblyUndefined($possibly_undefined); + } return $type_1; } if ($type_1 === $type_2) { + if ($possibly_undefined !== null) { + return $type_1->setPossiblyUndefined($possibly_undefined); + } return $type_1; } @@ -557,16 +566,16 @@ public static function combineUnionTypes( if ($type_2->failed_reconciliation) { $both_failed_reconciliation = true; } else { - $type_2 = clone $type_2; - $type_2->parent_nodes += $type_1->parent_nodes; - - return $type_2; + return $type_2->setProperties([ + 'parent_nodes' => array_merge($type_2->parent_nodes, $type_1->parent_nodes), + 'possibly_undefined' => $possibly_undefined ?? $type_2->possibly_undefined + ]); } } elseif ($type_2->failed_reconciliation) { - $type_1 = clone $type_1; - $type_1->parent_nodes += $type_2->parent_nodes; - - return $type_1; + return $type_1->setProperties([ + 'parent_nodes' => array_merge($type_1->parent_nodes, $type_2->parent_nodes), + 'possibly_undefined' => $possibly_undefined ?? $type_1->possibly_undefined + ]); } $combined_type = TypeCombiner::combine( @@ -613,7 +622,9 @@ public static function combineUnionTypes( } } - if ($type_1->possibly_undefined || $type_2->possibly_undefined) { + if ($possibly_undefined !== null) { + $combined_type->possibly_undefined = $possibly_undefined; + } elseif ($type_1->possibly_undefined || $type_2->possibly_undefined) { $combined_type->possibly_undefined = true; } @@ -662,8 +673,10 @@ public static function intersectUnionTypes( $type_1_mixed = $type_1->isMixed(); $type_2_mixed = $type_2->isMixed(); + $possibly_undefined = $type_1->possibly_undefined && $type_2->possibly_undefined; + if ($type_1_mixed && $type_2_mixed) { - $combined_type = self::getMixed(); + $combined_type = new Union([new TMixed()], ['possibly_undefined' => $possibly_undefined]); } else { $both_failed_reconciliation = false; @@ -678,10 +691,10 @@ public static function intersectUnionTypes( } if ($type_1_mixed) { - $combined_type = clone $type_2; + $combined_type = $type_2->getBuilder(); $intersection_performed = true; } elseif ($type_2_mixed) { - $combined_type = clone $type_1; + $combined_type = $type_1->getBuilder(); $intersection_performed = true; } else { $combined_type = null; @@ -703,9 +716,6 @@ public static function intersectUnionTypes( } } } - if ($combined_type) { - $combined_type = $combined_type->freeze(); - } } //if a type is contained by the other, the intersection is the narrowest type @@ -714,10 +724,10 @@ public static function intersectUnionTypes( $type_2_in_1 = UnionTypeComparator::isContainedBy($codebase, $type_2, $type_1); if ($type_1_in_2) { $intersection_performed = true; - $combined_type = $type_1; + $combined_type = $type_1->getBuilder(); } elseif ($type_2_in_1) { $intersection_performed = true; - $combined_type = $type_2; + $combined_type = $type_2->getBuilder(); } } @@ -749,6 +759,10 @@ public static function intersectUnionTypes( if ($both_failed_reconciliation) { $combined_type->failed_reconciliation = true; } + + $combined_type->possibly_undefined = $possibly_undefined; + + $combined_type = $combined_type->freeze(); } } @@ -756,10 +770,6 @@ public static function intersectUnionTypes( return null; } - if ($type_1->possibly_undefined && $type_2->possibly_undefined && $combined_type !== null) { - $combined_type->possibly_undefined = true; - } - return $combined_type; } @@ -778,14 +788,14 @@ private static function intersectAtomicTypes( && get_class($type_1_atomic) === TNamedObject::class && get_class($type_2_atomic) !== TNamedObject::class) ) { - $intersection_atomic = clone $type_2_atomic; + $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; $intersection_performed = true; } elseif (($type_1_atomic->value === $type_2_atomic->value && get_class($type_2_atomic) === TNamedObject::class && get_class($type_1_atomic) !== TNamedObject::class) ) { - $intersection_atomic = clone $type_1_atomic; + $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; $intersection_performed = true; } @@ -814,7 +824,7 @@ private static function intersectAtomicTypes( $type_2_atomic, $type_1_atomic )) { - $intersection_atomic = clone $type_2_atomic; + $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; $intersection_performed = true; } elseif (AtomicTypeComparator::isContainedBy( @@ -822,7 +832,7 @@ private static function intersectAtomicTypes( $type_1_atomic, $type_2_atomic )) { - $intersection_atomic = clone $type_1_atomic; + $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; $intersection_performed = true; } @@ -849,7 +859,7 @@ private static function intersectAtomicTypes( } } if ($intersection_atomic === null && $wider_type === null) { - $intersection_atomic = clone $type_1_atomic; + $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; } if ($intersection_atomic === null || $wider_type === null) { @@ -881,7 +891,7 @@ private static function intersectAtomicTypes( foreach ($wider_type_intersection_types as $wider_type_intersection_type) { $final_intersection[$wider_type_intersection_type->getKey()] - = clone $wider_type_intersection_type; + = $wider_type_intersection_type; } return $intersection_atomic->setIntersectionTypes($final_intersection); diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 3ec161a2b3a..26f003c8979 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -72,6 +72,7 @@ use function array_filter; use function array_keys; use function get_class; +use function is_array; use function is_numeric; use function strpos; use function strtolower; @@ -85,6 +86,10 @@ public function __construct(bool $from_docblock = false) { $this->from_docblock = $from_docblock; } + protected function __clone() + { + } + /** * Whether or not the type has been checked yet * @@ -592,7 +597,82 @@ public function hasArrayAccessInterface(Codebase $codebase): bool ); } - public function getChildNodeKeys(): array + public function visit(TypeVisitor $visitor): bool + { + foreach ($this->getChildNodeKeys() as $key) { + /** @psalm-suppress MixedAssignment */ + $value = $this->{$key}; + if (is_array($value)) { + /** @psalm-suppress MixedAssignment */ + foreach ($value as $type) { + if (!$type instanceof TypeNode) { + continue; + } + + if ($visitor->traverse($type) === false) { + return false; + } + } + } elseif ($value instanceof TypeNode) { + if ($visitor->traverse($value) === false) { + return false; + } + } + } + return true; + } + + public static function visitMutable(MutableTypeVisitor $visitor, &$node, bool $cloned): bool + { + foreach ($node->getChildNodeKeys() as $key) { + /** @psalm-suppress MixedAssignment */ + $value = $node->{$key}; + $result = true; + if (is_array($value)) { + $changed = false; + /** @psalm-suppress MixedAssignment */ + foreach ($value as &$type) { + if (!$type instanceof TypeNode) { + continue; + } + + $type_orig = $type; + $result = $visitor->traverse($type); + $changed = $changed || $type !== $type_orig; + } + unset($type); + } elseif ($value instanceof TypeNode) { + $value_orig = $value; + $result = $visitor->traverse($value); + $changed = $value !== $value_orig; + } else { + continue; + } + + if ($changed) { + if (!$cloned) { + $node = clone $node; + $cloned = true; + } + if ($key === 'extra_types') { + /** @var array $value */ + $new = []; + foreach ($value as $type) { + $new[$type->getKey()] = $type; + } + $value = $new; + } + $node->{$key} = $value; + } + if ($result === false) { + return false; + } + } + return true; + } + + /** @return list */ + protected function getChildNodeKeys(): array { return []; } diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 17980ae840e..f53d3734217 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -53,6 +53,30 @@ public function __construct( $this->from_docblock = $from_docblock; } + /** + * @param list|null $params + * @return static + */ + public function replace(?array $params, ?Union $return_type): self + { + if ($this->params === $params && $this->return_type === $return_type) { + return $this; + } + $cloned = clone $this; + $cloned->params = $params; + $cloned->return_type = $return_type; + return $cloned; + } + /** @return static */ + public function setIsPure(bool $is_pure): self + { + if ($this->is_pure === $is_pure) { + return $this; + } + $cloned = clone $this; + $cloned->is_pure = $is_pure; + return $cloned; + } public function getKey(bool $include_extra = true): string { $param_string = ''; @@ -211,7 +235,7 @@ protected function replaceCallableTemplateTypesWithStandins( $input_param_type = $input_type->params[$offset]->type; } - $new_param = $param->replaceType(TemplateStandinTypeReplacer::replace( + $new_param = $param->setType(TemplateStandinTypeReplacer::replace( $param->type, $template_result, $codebase, @@ -269,7 +293,7 @@ protected function replaceCallableTemplateTypesWithArgTypes( if ($params) { foreach ($params as $k => $param) { if ($param->type) { - $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( + $new_param = $param->setType(TemplateInferredTypeReplacer::replace( $param->type, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index 87767a38fec..9167c064485 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -29,7 +29,7 @@ trait GenericTrait * * @return static */ - public function replaceTypeParams(array $type_params): self + public function setTypeParams(array $type_params): self { if ($this->type_params === $type_params) { return $this; diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 7889a0111a8..30c78184321 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -112,9 +112,9 @@ protected function replaceIntersectionTemplateTypesWithArgTypes( foreach ($template_type->getAtomicTypes() as $template_type_part) { if ($template_type_part instanceof TNamedObject) { - $new_types[$template_type_part->getKey()] = clone $template_type_part; + $new_types[$template_type_part->getKey()] = $template_type_part; } elseif ($template_type_part instanceof TTemplateParam) { - $new_types[$template_type_part->getKey()] = clone $template_type_part; + $new_types[$template_type_part->getKey()] = $template_type_part; } } } else { diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 0b51e220fbf..7a15176dac7 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -157,7 +157,7 @@ public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result return $this; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['type_params']; } diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index 18c62ed283e..994520585dc 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -91,7 +91,7 @@ public function replaceTemplateTypesWithStandins( ); } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return $this->getCallableChildNodeKeys(); } diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index b8eec740027..a56ea416de2 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -55,6 +55,19 @@ public function __construct( $this->is_enum = $is_enum; $this->from_docblock = $from_docblock; } + /** + * @return static + */ + public function setAs(string $as, ?TNamedObject $as_type): self + { + if ($this->as === $as && $this->as_type === $as_type) { + return $this; + } + $cloned = clone $this; + $cloned->as = $as; + $cloned->as_type = $as_type; + return $cloned; + } public function getKey(bool $include_extra = true): string { if ($this->is_interface) { @@ -135,7 +148,7 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return $this->as_type ? ['as_type'] : []; } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index fe3e1c3e39c..8f6eacd912b 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -142,7 +142,7 @@ public function replaceTemplateTypesWithStandins( && isset($input_type->type_params[$offset]) ) { - $input_type_param = clone $input_type->type_params[$offset]; + $input_type_param = $input_type->type_params[$offset]; } elseif ($input_type instanceof TKeyedArray) { if ($offset === 0) { $input_type_param = $input_type->getGenericKeyType(); @@ -154,7 +154,7 @@ public function replaceTemplateTypesWithStandins( continue; } - $input_type_param = clone $input_type->type_param; + $input_type_param = $input_type->type_param; } $value_param = TemplateStandinTypeReplacer::replace( @@ -203,7 +203,7 @@ public function replaceTemplateTypesWithArgTypes( ); } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['value_param']; } diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index 7b0345e9eb3..8b17a63963d 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -124,7 +124,7 @@ public function replaceTemplateTypesWithStandins( ); } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return array_merge(parent::getChildNodeKeys(), $this->getCallableChildNodeKeys()); } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index b1c2d6b9794..3322400e9a0 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -62,7 +62,7 @@ public function __construct( $this->from_docblock = $from_docblock; } - public function replaceTypes( + public function setTypes( ?Union $as_type, ?Union $conditional_type = null, ?Union $if_type = null, @@ -135,7 +135,7 @@ public function toNamespacedString( return ''; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['conditional_type', 'if_type', 'else_type']; } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 7e78bd1324e..347d07a7b70 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -121,7 +121,7 @@ public function getAssertionString(): string return $this->value; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return array_merge(parent::getChildNodeKeys(), ['type_params']); } diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index b1fd8c987f0..33b9d8c3869 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -47,7 +47,7 @@ public function toNamespacedString( . '>'; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['value']; } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index 91f75bc9fb8..8df955ff520 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -122,7 +122,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['type_params', 'extra_types']; } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 99e14a64deb..be933b211dd 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -205,7 +205,7 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function getGenericKeyType(): Union + public function getGenericKeyType(bool $possibly_undefined = false): Union { $key_types = []; @@ -221,22 +221,29 @@ public function getGenericKeyType(): Union $key_type = TypeCombiner::combine($key_types); - $key_type->possibly_undefined = false; + /** @psalm-suppress InaccessibleProperty We just created this type */ + $key_type->possibly_undefined = $possibly_undefined; return Type::combineUnionTypes($this->previous_key_type, $key_type); } - public function getGenericValueType(): Union + public function getGenericValueType(bool $possibly_undefined = false): Union { $value_type = null; foreach ($this->properties as $property) { - $value_type = Type::combineUnionTypes(clone $property, $value_type); + $value_type = Type::combineUnionTypes($property, $value_type); } - $value_type = Type::combineUnionTypes($this->previous_value_type, $value_type); - - $value_type->possibly_undefined = false; + $value_type = Type::combineUnionTypes( + $this->previous_value_type, + $value_type, + null, + false, + true, + 500, + $possibly_undefined + ); return $value_type; } @@ -260,7 +267,7 @@ public function getGenericArrayType(bool $allow_non_empty = true): TArray $key_types[] = new TLiteralString($key); } - $value_type = Type::combineUnionTypes(clone $property, $value_type); + $value_type = Type::combineUnionTypes($property, $value_type); if (!$property->possibly_undefined) { $has_defined_keys = true; @@ -272,7 +279,7 @@ public function getGenericArrayType(bool $allow_non_empty = true): TArray $value_type = Type::combineUnionTypes($this->previous_value_type, $value_type); $key_type = Type::combineUnionTypes($this->previous_key_type, $key_type); - $value_type->possibly_undefined = false; + $value_type = $value_type->setPossiblyUndefined(false); if ($allow_non_empty && ($this->previous_value_type || $has_defined_keys)) { $array_type = new TNonEmptyArray([$key_type, $value_type]); @@ -373,7 +380,7 @@ public function replaceTemplateTypesWithArgTypes( return $this; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['properties']; } diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 78beb798b34..8ead8881d61 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -43,7 +43,7 @@ public function __construct(Union $type_param, bool $from_docblock = false) /** * @return static */ - public function replaceTypeParam(Union $type_param): self + public function setTypeParam(Union $type_param): self { if ($type_param === $this->type_param) { return $this; @@ -138,7 +138,7 @@ public function replaceTemplateTypesWithStandins( && isset($input_type->type_params[$offset]) ) { - $input_type_param = clone $input_type->type_params[$offset]; + $input_type_param = $input_type->type_params[$offset]; } elseif ($input_type instanceof TKeyedArray) { if ($offset === 0) { $input_type_param = $input_type->getGenericKeyType(); @@ -150,7 +150,7 @@ public function replaceTemplateTypesWithStandins( continue; } - $input_type_param = clone $input_type->type_param; + $input_type_param = $input_type->type_param; } $type_param = TemplateStandinTypeReplacer::replace( @@ -184,7 +184,7 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): self { - return $this->replaceTypeParam(TemplateInferredTypeReplacer::replace( + return $this->setTypeParam(TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase @@ -213,7 +213,7 @@ public function getAssertionString(): string return $this->getId(); } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['type_param']; } diff --git a/src/Psalm/Type/Atomic/TLiteralString.php b/src/Psalm/Type/Atomic/TLiteralString.php index 9f000b90aa9..b8adfc6df7d 100644 --- a/src/Psalm/Type/Atomic/TLiteralString.php +++ b/src/Psalm/Type/Atomic/TLiteralString.php @@ -21,6 +21,20 @@ public function __construct(string $value, bool $from_docblock = false) $this->from_docblock = $from_docblock; } + /** + * @psalm-suppress PossiblyUnusedMethod + * @return static + */ + public function setValue(string $value): self + { + if ($value === $this->value) { + return $this; + } + $cloned = clone $this; + $cloned->value = $value; + return $cloned; + } + public function getKey(bool $include_extra = true): string { return 'string(' . $this->value . ')'; diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 732d09253dd..acd8192b7fe 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -64,6 +64,9 @@ public function __construct( $this->from_docblock = $from_docblock; } + /** + * @return static + */ public function setIsStatic(bool $is_static, ?bool $is_static_resolved = null): self { $is_static_resolved ??= $this->is_static_resolved; @@ -76,6 +79,42 @@ public function setIsStatic(bool $is_static, ?bool $is_static_resolved = null): return $cloned; } + /** + * @return static + */ + public function setValue(string $value): self + { + if ($value[0] === '\\') { + $value = substr($value, 1); + } + if ($value === $this->value) { + return $this; + } + $cloned = clone $this; + $cloned->value = $value; + return $cloned; + } + /** + * @return static + */ + public function setValueIsStatic(string $value, bool $is_static, ?bool $is_static_resolved = null): self + { + $is_static_resolved ??= $this->is_static_resolved; + if ($value[0] === '\\') { + $value = substr($value, 1); + } + if ($value === $this->value + && $this->is_static === $is_static + && $this->is_static_resolved === $is_static_resolved + ) { + return $this; + } + $cloned = clone $this; + $cloned->value = $value; + $cloned->is_static = $is_static; + $cloned->is_static_resolved = $is_static; + return $cloned; + } public function getKey(bool $include_extra = true): string { if ($include_extra && $this->extra_types) { @@ -211,7 +250,7 @@ public function replaceTemplateTypesWithStandins( } return $this; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['extra_types']; } diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 96f1e2d805e..be4bbcfec02 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -289,7 +289,7 @@ public function replaceTemplateTypesWithArgTypes( ); } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['properties', 'extra_types']; } diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index ffee4f946c5..57f02a9c394 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -89,7 +89,7 @@ public static function tokenNameForFilter(?int $visibility_filter): string } } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['classlike_type']; } diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index 775f52993e2..7bf2f330748 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -137,7 +137,7 @@ public function toNamespacedString( return $this->param_name . $intersection_types; } - public function getChildNodeKeys(): array + protected function getChildNodeKeys(): array { return ['as', 'extra_types']; } diff --git a/src/Psalm/Type/ImmutableTypeVisitor.php b/src/Psalm/Type/ImmutableTypeVisitor.php deleted file mode 100644 index 7578277f2c7..00000000000 --- a/src/Psalm/Type/ImmutableTypeVisitor.php +++ /dev/null @@ -1,67 +0,0 @@ -enterNode($node); - - if ($visitor_result === self::DONT_TRAVERSE_CHILDREN) { - return true; - } - - if ($visitor_result === self::STOP_TRAVERSAL) { - return false; - } - - foreach ($node->getChildNodeKeys() as $key) { - if ($node instanceof Union || $node instanceof MutableUnion) { - $child_node = $node->getAtomicTypes(); - } else { - /** @var TypeNode|non-empty-array|null */ - $child_node = $node->{$key}; - } - if ($child_node === null) { - continue; - } - if (is_array($child_node)) { - $visitor_result = $this->traverseArray($child_node); - } else { - $visitor_result = $this->traverse($child_node); - } - if ($visitor_result === false) { - return false; - } - } - - return true; - } - - /** - * @param non-empty-array $nodes - */ - public function traverseArray(array $nodes): bool - { - foreach ($nodes as $node) { - if ($this->traverse($node) === false) { - return false; - } - } - return true; - } -} diff --git a/src/Psalm/Type/MutableTypeVisitor.php b/src/Psalm/Type/MutableTypeVisitor.php new file mode 100644 index 00000000000..aa9be575caf --- /dev/null +++ b/src/Psalm/Type/MutableTypeVisitor.php @@ -0,0 +1,55 @@ +enterNode($node); + + if ($result === self::DONT_TRAVERSE_CHILDREN) { + return true; + } + + if ($result === self::STOP_TRAVERSAL) { + return false; + } + + return $node::visitMutable($this, $node, $node !== $nodeOrig); + } + + /** + * @template T as array + * @param T $nodes + * @param-out T $nodes + */ + public function traverseArray(array &$nodes): void + { + foreach ($nodes as &$node) { + if ($this->traverse($node) === false) { + return; + } + } + unset($node); + } +} diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php index a187a3be98e..407e9658cdc 100644 --- a/src/Psalm/Type/MutableUnion.php +++ b/src/Psalm/Type/MutableUnion.php @@ -222,6 +222,7 @@ public function setTypes(array $types): self $this->literal_int_types = []; $this->literal_string_types = []; $this->typed_class_strings = []; + $this->checked = false; $from_docblock = false; $keyed_types = []; @@ -469,32 +470,27 @@ public function getBuilder(): self */ 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; + return new Union($this->getAtomicTypes(), get_object_vars($this)); + } + + public static function visitMutable(MutableTypeVisitor $visitor, &$node, bool $cloned): bool + { + $result = true; + $changed = false; + foreach ($node->types as &$type) { + $type_orig = $type; + $result = $visitor->traverse($type); + $changed = $changed || $type_orig !== $type; + if (!$result) { + break; } - /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ - $union->{$key} = $value; } - return $union; + unset($type); + + if ($changed) { + $node->setTypes($node->types); + } + + return $result; } } diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index c1585ac75ca..8f6fff0a887 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -129,7 +129,7 @@ public static function reconcileKeyedTypes( $cloned_referenceds = []; foreach ($existing_references as $reference => $referenced) { if (!isset($cloned_referenceds[$referenced])) { - $existing_types[$referenced] = clone $old_existing_types[$referenced]; + $existing_types[$referenced] = $old_existing_types[$referenced]; $cloned_referenceds[$referenced] = true; } $existing_types[$reference] = &$existing_types[$referenced]; @@ -209,26 +209,24 @@ public static function reconcileKeyedTypes( $has_object_array_access = false; - $result_type = isset($existing_types[$key]) - ? clone $existing_types[$key] - : self::getValueForKey( - $codebase, - $key, - $existing_types, - $new_types, - $code_location, - $has_isset, - $has_inverted_isset, - $has_empty, - $inside_loop, - $has_object_array_access - ); + $result_type = $existing_types[$key] ?? self::getValueForKey( + $codebase, + $key, + $existing_types, + $new_types, + $code_location, + $has_isset, + $has_inverted_isset, + $has_empty, + $inside_loop, + $has_object_array_access + ); if ($result_type && $result_type->isUnionEmpty()) { throw new InvalidArgumentException('Union::$types cannot be empty after get value for ' . $key); } - $before_adjustment = $result_type ? clone $result_type : null; + $before_adjustment = $result_type; $failed_reconciliation = self::RECONCILIATION_OK; @@ -266,7 +264,7 @@ public static function reconcileKeyedTypes( $result_type_candidate = AssertionReconciler::reconcile( $new_type_part_part, - $result_type ? clone $result_type : null, + $result_type, $key, $statements_analyzer, $inside_loop, @@ -309,19 +307,24 @@ public static function reconcileKeyedTypes( || $statements_analyzer->data_flow_graph instanceof VariableUseGraph ) { if ($before_adjustment && $before_adjustment->parent_nodes) { - $result_type->parent_nodes = $before_adjustment->parent_nodes; + $result_type = $result_type->setParentNodes($before_adjustment->parent_nodes); } elseif (!$did_type_exist && $code_location) { - $result_type->parent_nodes = $statements_analyzer->getParentNodesForPossiblyUndefinedVariable( - $key + $result_type = $result_type->setParentNodes( + $statements_analyzer->getParentNodesForPossiblyUndefinedVariable( + $key + ) ); } } if ($before_adjustment && $before_adjustment->by_ref) { - $result_type->by_ref = true; + $result_type = $result_type->setByRef(true); } - $type_changed = !$before_adjustment || !$result_type->equals($before_adjustment); + $type_changed = !$before_adjustment + || !$result_type->equals($before_adjustment) + || $result_type->different + || $before_adjustment->different; $key_parts = self::breakUpPathIntoParts($key); if ($type_changed || $failed_reconciliation) { @@ -379,7 +382,7 @@ public static function reconcileKeyedTypes( } if ($failed_reconciliation === self::RECONCILIATION_EMPTY) { - $result_type->failed_reconciliation = true; + $result_type = $result_type->setProperties(['failed_reconciliation' => true]); } if (!$has_object_array_access) { @@ -644,7 +647,7 @@ private static function getValueForKey( $key_parts = self::breakUpPathIntoParts($key); if (count($key_parts) === 1) { - return isset($existing_keys[$key_parts[0]]) ? clone $existing_keys[$key_parts[0]] : null; + return $existing_keys[$key_parts[0]] ?? null; } $base_key = array_shift($key_parts); @@ -670,7 +673,7 @@ private static function getValueForKey( ); if ($class_constant) { - $existing_keys[$base_key] = clone $class_constant; + $existing_keys[$base_key] = $class_constant; } else { return null; } @@ -706,7 +709,7 @@ private static function getValueForKey( return null; } - $new_base_type_candidate = clone $existing_key_type_part->type_params[1]; + $new_base_type_candidate = $existing_key_type_part->type_params[1]; if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset) { return $new_base_type_candidate; @@ -716,27 +719,28 @@ private static function getValueForKey( 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->possibly_undefined = true; $new_base_type_candidate = $new_base_type_candidate->freeze(); + } else { + $new_base_type_candidate = $new_base_type_candidate->setPossiblyUndefined(true); } - - $new_base_type_candidate->possibly_undefined = true; } } elseif ($existing_key_type_part instanceof TList) { if ($has_empty) { return null; } - $new_base_type_candidate = clone $existing_key_type_part->type_param; + $new_base_type_candidate = $existing_key_type_part->type_param; 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() - ->addType(new TNull) - ->freeze(); + $new_base_type_candidate = $new_base_type_candidate->getBuilder(); + $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate->possibly_undefined = true; + $new_base_type_candidate = $new_base_type_candidate->freeze(); + } else { + $new_base_type_candidate = $new_base_type_candidate->setPossiblyUndefined(true); } - - $new_base_type_candidate->possibly_undefined = true; } } elseif ($existing_key_type_part instanceof TNull || $existing_key_type_part instanceof TFalse @@ -744,6 +748,7 @@ private static function getValueForKey( $new_base_type_candidate = Type::getNull(); if ($existing_keys[$base_key]->ignore_nullable_issues) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $new_base_type_candidate->ignore_nullable_issues = true; } } elseif ($existing_key_type_part instanceof TClassStringMap) { @@ -778,13 +783,13 @@ private static function getValueForKey( if (!isset($array_properties[$key_parts_key])) { if ($existing_key_type_part->previous_value_type) { - $new_base_type_candidate = clone $existing_key_type_part->previous_value_type; - $new_base_type_candidate->different = true; + $new_base_type_candidate = $existing_key_type_part + ->previous_value_type->setDifferent(true); } else { return null; } } else { - $new_base_type_candidate = clone $array_properties[$key_parts_key]; + $new_base_type_candidate = $array_properties[$key_parts_key]; } } @@ -858,7 +863,7 @@ private static function getValueForKey( if ($method_return_type) { $class_property_type = TypeExpander::expandUnion( $codebase, - clone $method_return_type, + $method_return_type, $declaring_class, $declaring_class, null @@ -927,7 +932,7 @@ private static function getPropertyType( ); if (isset($declaring_class_storage->pseudo_property_get_types['$' . $property_name])) { - return clone $declaring_class_storage->pseudo_property_get_types['$' . $property_name]; + return $declaring_class_storage->pseudo_property_get_types['$' . $property_name]; } return null; @@ -956,7 +961,7 @@ private static function getPropertyType( if ($class_property_type) { return TypeExpander::expandUnion( $codebase, - clone $class_property_type, + $class_property_type, $declaring_class_storage->name, $declaring_class_storage->name, null @@ -1126,15 +1131,15 @@ private static function adjustTKeyedArrayType( || $base_atomic_type instanceof TList || $base_atomic_type instanceof TClassStringMap ) { - $new_base_type = clone $existing_types[$base_key]; + $new_base_type = $existing_types[$base_key]; if ($base_atomic_type instanceof TArray) { - $previous_key_type = clone $base_atomic_type->type_params[0]; - $previous_value_type = clone $base_atomic_type->type_params[1]; + $previous_key_type = $base_atomic_type->type_params[0]; + $previous_value_type = $base_atomic_type->type_params[1]; $base_atomic_type = new TKeyedArray( [ - $array_key_offset => clone $result_type, + $array_key_offset => $result_type, ], null, false, @@ -1143,11 +1148,11 @@ private static function adjustTKeyedArrayType( ); } elseif ($base_atomic_type instanceof TList) { $previous_key_type = Type::getInt(); - $previous_value_type = clone $base_atomic_type->type_param; + $previous_value_type = $base_atomic_type->type_param; $base_atomic_type = new TKeyedArray( [ - $array_key_offset => clone $result_type, + $array_key_offset => $result_type, ], null, false, @@ -1159,7 +1164,7 @@ private static function adjustTKeyedArrayType( // do nothing } else { $properties = $base_atomic_type->properties; - $properties[$array_key_offset] = clone $result_type; + $properties[$array_key_offset] = $result_type; $base_atomic_type = $base_atomic_type->setProperties($properties); } diff --git a/src/Psalm/Type/TypeNode.php b/src/Psalm/Type/TypeNode.php index f8d8cf4a506..978eace6954 100644 --- a/src/Psalm/Type/TypeNode.php +++ b/src/Psalm/Type/TypeNode.php @@ -4,8 +4,12 @@ interface TypeNode { + /** @internal Should only be used by the TypeVisitor */ + public function visit(TypeVisitor $visitor): bool; /** - * @return list + * @param static $node + * @param-out static $node + * @internal Should only be used by the MutableTypeVisitor */ - public function getChildNodeKeys(): array; + public static function visitMutable(MutableTypeVisitor $visitor, &$node, bool $cloned): bool; } diff --git a/src/Psalm/Type/TypeVisitor.php b/src/Psalm/Type/TypeVisitor.php index c4e98bcf517..8e9122ff455 100644 --- a/src/Psalm/Type/TypeVisitor.php +++ b/src/Psalm/Type/TypeVisitor.php @@ -2,103 +2,44 @@ namespace Psalm\Type; -use function is_array; - abstract class TypeVisitor { public const STOP_TRAVERSAL = 1; public const DONT_TRAVERSE_CHILDREN = 2; /** - * @template T as TypeNode - * @param T $type - * @param-out T $type + * @internal Can only be called by a TypeNode + * * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ - abstract protected function enterNode(TypeNode &$type): ?int; + abstract protected function enterNode(TypeNode $type): ?int; - /** - * @template T as TypeNode - * @param T $node - * @param-out T $node - * @return bool - true if we want to continue traversal, false otherwise - * - * @psalm-suppress ReferenceConstraintViolation, ConflictingReferenceConstraint - */ - public function traverse(TypeNode &$node): bool + /** @psalm-external-mutation-free */ + public function traverse(TypeNode $node): bool { - $old = $node; - $visitor_result = $this->enterNode($node); + $result = $this->enterNode($node); - if ($visitor_result === self::DONT_TRAVERSE_CHILDREN) { + if ($result === self::DONT_TRAVERSE_CHILDREN) { return true; } - if ($visitor_result === self::STOP_TRAVERSAL) { + if ($result === self::STOP_TRAVERSAL) { return false; } - $cloned = $node !== $old; - foreach ($node->getChildNodeKeys() as $key) { - if ($node instanceof Union || $node instanceof MutableUnion) { - $child_node = $node->getAtomicTypes(); - } else { - /** @var TypeNode|non-empty-array|null */ - $child_node = $node->{$key}; - } - if ($child_node === null) { - continue; - } - $orig = $child_node; - if (is_array($child_node)) { - $visitor_result = $this->traverseArray($child_node); - } else { - $visitor_result = $this->traverse($child_node); - } - if ($child_node !== $orig) { - if ($node instanceof Union) { - /** @var non-empty-array $child_node */ - $node = $node->getBuilder()->setTypes($child_node)->freeze(); - } elseif ($node instanceof MutableUnion) { - // This mutates in-place - /** @var non-empty-array $child_node */ - $node->setTypes($child_node); - } else { - if (!$cloned) { - $cloned = true; - $node = clone $node; - } - if ($key === 'extra_types' && is_array($child_node)) { - $new = []; - /** @var Union */ - foreach ($child_node as $value) { - $new[$value->getKey()] = $value; - } - $child_node = $new; - } - $node->{$key} = $child_node; - } - } - if ($visitor_result === false) { - return false; - } - } - - return true; + return $node->visit($this); } /** - * @template T as array - * @param T $nodes - * @param-out T $nodes + * @psalm-external-mutation-free + * @param non-empty-array $nodes */ - public function traverseArray(array &$nodes): bool + public function traverseArray(array $nodes): void { - foreach ($nodes as &$node) { + foreach ($nodes as $node) { if ($this->traverse($node) === false) { - return false; + return; } } - return true; } } diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index cdcfc5d9ed0..f20744e50d4 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -4,6 +4,7 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\TypeVisitor\FromDocblockSetter; +use Psalm\Storage\ImmutableNonCloneableTrait; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; @@ -12,8 +13,35 @@ use function get_object_vars; +/** + * @psalm-immutable + * @psalm-type TProperties=array{ + * from_docblock?: bool, + * from_calculation?: bool, + * from_property?: bool, + * from_static_property?: bool, + * initialized?: bool, + * initialized_class?: ?string, + * checked?: bool, + * failed_reconciliation?: bool, + * ignore_nullable_issues?: bool, + * ignore_falsable_issues?: bool, + * ignore_isset?: bool, + * possibly_undefined?: bool, + * possibly_undefined_from_try?: bool, + * had_template?: bool, + * from_template_default?: bool, + * by_ref?: bool, + * reference_free?: bool, + * allow_mutations?: bool, + * has_mutations?: bool, + * different?: bool, + * parent_nodes?: array + * } + */ final class Union implements TypeNode, Stringable { + use ImmutableNonCloneableTrait; use UnionTrait; /** @@ -192,11 +220,105 @@ final class Union implements TypeNode, Stringable */ public $parent_nodes = []; + public bool $propagate_parent_nodes = false; + /** * @var bool */ public $different = false; + /** + * @param TProperties $properties + * @return static + */ + public function setProperties(array $properties): self + { + $obj = null; + foreach ($properties as $key => $value) { + if ($this->{$key} !== $value) { + if ($obj === null) { + $obj = clone $this; + } + /** @psalm-suppress ImpurePropertyAssignment We just cloned this object */ + $obj->{$key} = $value; + } + } + return $obj ?? $this; + } + + /** + * @return static + */ + public function setDifferent(bool $different): self + { + if ($different === $this->different) { + return $this; + } + $cloned = clone $this; + $cloned->different = $different; + return $cloned; + } + + /** + * @param array $parent_nodes + * @return static + */ + public function setParentNodes(array $parent_nodes, bool $propagate_changes = false): self + { + if ($parent_nodes === $this->parent_nodes) { + return $this; + } + $cloned = clone $this; + $cloned->parent_nodes = $parent_nodes; + $cloned->propagate_parent_nodes = $propagate_changes; + return $cloned; + } + + + /** + * @param array $parent_nodes + * @return static + */ + public function addParentNodes(array $parent_nodes): self + { + if (!$parent_nodes) { + return $this; + } + $parent_nodes = $this->parent_nodes + $parent_nodes; + if ($parent_nodes === $this->parent_nodes) { + return $this; + } + $cloned = clone $this; + $cloned->parent_nodes = $parent_nodes; + return $cloned; + } + + /** @return static */ + public function setPossiblyUndefined(bool $possibly_undefined, ?bool $from_try = null): self + { + $from_try ??= $this->possibly_undefined_from_try; + if ($this->possibly_undefined === $possibly_undefined + && $this->possibly_undefined_from_try == $from_try + ) { + return $this; + } + $cloned = clone $this; + $cloned->possibly_undefined = $possibly_undefined; + $cloned->possibly_undefined_from_try = $from_try; + return $cloned; + } + + /** @return static */ + public function setByRef(bool $by_ref) + { + if ($by_ref === $this->by_ref) { + return $this; + } + $cloned = clone $this; + $cloned->by_ref = $by_ref; + return $cloned; + } + /** * @psalm-mutation-free * @param non-empty-array $types @@ -214,33 +336,7 @@ public function setTypes(array $types): self */ public function getBuilder(): MutableUnion { - $union = new MutableUnion($this->getAtomicTypes()); - foreach (get_object_vars($this) as $key => $value) { - if ($key === 'types') { - continue; - } - if ($key === 'id') { - continue; - } - if ($key === 'exact_id') { - continue; - } - if ($key === 'literal_string_types') { - continue; - } - if ($key === 'typed_class_strings') { - continue; - } - if ($key === 'literal_int_types') { - continue; - } - if ($key === 'literal_float_types') { - continue; - } - /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ - $union->{$key} = $value; - } - return $union; + return new MutableUnion($this->getAtomicTypes(), get_object_vars($this)); } /** @@ -253,4 +349,26 @@ public function setFromDocblock(bool $fromDocblock = true): self (new FromDocblockSetter($fromDocblock))->traverse($cloned); return $cloned; } + + public static function visitMutable(MutableTypeVisitor $visitor, &$node, bool $cloned): bool + { + $result = true; + $changed = false; + $types = $node->types; + foreach ($types as &$type) { + $type_orig = $type; + $result = $visitor->traverse($type); + $changed = $changed || $type_orig !== $type; + if (!$result) { + break; + } + } + unset($type); + + if ($changed) { + $node = $node->setTypes($types); + } + + return $result; + } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index ce7ff78965f..2df7db1515e 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -49,6 +49,10 @@ use function sort; use function strpos; +/** + * @psalm-immutable + * @psalm-import-type TProperties from Union + */ trait UnionTrait { /** @@ -57,11 +61,24 @@ trait UnionTrait * @psalm-external-mutation-free * * @param non-empty-array $types + * @param TProperties $properties */ - public function __construct(array $types, bool $from_docblock = false) + public function __construct(array $types, array $properties = []) { + foreach ($properties as $key => $value) { + $this->{$key} = $value; + } + $this->literal_int_types = []; + $this->literal_string_types = []; + $this->literal_float_types = []; + $this->typed_class_strings = []; + $this->checked = false; + $this->id = null; + $this->exact_id = null; + $keyed_types = []; + $from_docblock = $this->from_docblock; foreach ($types as $type) { $key = $type->getKey(); $keyed_types[$key] = $type; @@ -81,9 +98,8 @@ public function __construct(array $types, bool $from_docblock = false) $from_docblock = $from_docblock || $type->from_docblock; } - $this->types = $keyed_types; - $this->from_docblock = $from_docblock; + $this->types = $keyed_types; } /** @@ -205,10 +221,10 @@ public function getId(bool $exact = true): string $id = implode('|', $types); if ($exact) { - /** @psalm-suppress ImpurePropertyAssignment Cache */ + /** @psalm-suppress ImpurePropertyAssignment, InaccessibleProperty Cache */ $this->exact_id = $id; } else { - /** @psalm-suppress ImpurePropertyAssignment Cache */ + /** @psalm-suppress ImpurePropertyAssignment, InaccessibleProperty Cache */ $this->id = $id; } @@ -586,6 +602,14 @@ public function hasBool(): bool return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); } + /** + * @psalm-mutation-free + */ + public function hasNull(): bool + { + return isset($this->types['null']); + } + /** * @psalm-mutation-free */ @@ -1214,6 +1238,7 @@ public function check( $checker->traverseArray($this->types); + /** @psalm-suppress InaccessibleProperty, ImpurePropertyAssignment Does not affect anything else */ $this->checked = true; return !$checker->hasErrors(); @@ -1291,8 +1316,11 @@ public function getTemplateTypes(): array /** * @psalm-mutation-free */ - public function equals(self $other_type, bool $ensure_source_equality = true): bool - { + public function equals( + self $other_type, + bool $ensure_source_equality = true, + bool $ensure_parent_node_equality = true + ): bool { if ($other_type === $this) { return true; } @@ -1333,7 +1361,7 @@ public function equals(self $other_type, bool $ensure_source_equality = true): b return false; } - if ($this->parent_nodes !== $other_type->parent_nodes) { + if ($ensure_parent_node_equality && $this->parent_nodes !== $other_type->parent_nodes) { return false; } @@ -1399,15 +1427,6 @@ public function getLiteralFloats(): array return $this->literal_float_types; } - /** - * @psalm-mutation-free - * @return list - */ - public function getChildNodeKeys(): array - { - return ['types']; - } - /** * @psalm-mutation-free * @return bool true if this is a float literal with only one possible value @@ -1466,4 +1485,15 @@ public function isUnionEmpty(): bool { return $this->types === []; } + + public function visit(TypeVisitor $visitor): bool + { + foreach ($this->types as $type) { + if ($visitor->traverse($type) === false) { + return false; + } + } + + return true; + } } diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 0fd92b6342d..6ec62630b9a 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -408,6 +408,55 @@ public static function getStateLabelIf(int $state): string { } }' ], + 'arrayKeyExistsComplex2' => [ + 'code' => ' + * }>, + * formatted_address: string, + * geometry: array{ + * location: array{ lat: float, lng: float }, + * location_type: string, + * viewport: array{ + * northeast: array{ lat: float, lng: float }, + * southwest: array{ lat: float, lng: float } + * } + * }, + * partial_match: bool, + * types: list + * } + */ + $data = []; + $cmp = []; + foreach ($data["address_components"] as $component) { + foreach ($component["types"] as $type) { + $cmp[$type] = $component["long_name"]; + } + } + + if (!\array_key_exists("locality", $cmp)) { + $cmp["locality"] = ""; + } + + if (!\array_key_exists("administrative_area_level_1", $cmp)) { + $cmp["administrative_area_level_1"] = ""; + } + if ($cmp["administrative_area_level_1"] === "test") { + $cmp["administrative_area_level_1"] = ""; + }' + ] ]; } diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 8ab726ddeef..6bc30bfd38d 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -764,6 +764,16 @@ function far(array $arr): void { } }', ], + 'arraySubAppend' => [ + 'code' => ' []]; + foreach ($rules as $rule) { + $report["runs"][] = $rule; + } + echo(count($report));' + ], 'arrayAssignmentInFunctionCoerced' => [ 'code' => '