diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d1103373cb..e94a3740cb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,7 @@ jobs: echo "chunks=$(php -r 'echo json_encode(range(1, ${{ env.CHUNK_COUNT }} ));')" >> $GITHUB_OUTPUT tests: - name: "Unit Tests - PHP ${{ matrix.php-version }} ${{ matrix.chunk }}/${{ matrix.count }}" + name: "Unit Tests - PHP ${{ matrix.php-version }} ${{ matrix.dependencies }} ${{ matrix.chunk }}/${{ matrix.count }}" runs-on: ubuntu-latest needs: @@ -129,6 +129,9 @@ jobs: - "8.1" - "8.2" - "8.3" + dependencies: + - highest + - lowest count: ${{ fromJson(needs.chunk-matrix.outputs.count) }} chunk: ${{ fromJson(needs.chunk-matrix.outputs.chunks) }} @@ -158,8 +161,17 @@ jobs: echo "files_cache=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT echo "vcs_cache=$(composer config cache-vcs-dir)" >> $GITHUB_OUTPUT - - name: Generate composer.lock + - name: Generate highest composer.lock + if: ${{ matrix.dependencies == 'highest' }} + run: | + composer update --no-install + env: + COMPOSER_ROOT_VERSION: dev-master + + - name: Generate lowest composer.lock + if: ${{ matrix.dependencies == 'lowest' }} run: | + composer require nikic/php-parser ^4.16 composer update --no-install env: COMPOSER_ROOT_VERSION: dev-master diff --git a/composer.json b/composer.json index aed943f1dd5..0d60df0ba87 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "felixfbecker/language-server-protocol": "^1.5.2", "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.17", + "nikic/php-parser": "^4.17 || ^5.1", "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 987aab124ad..2fc61f0a912 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -134,6 +134,9 @@ + + + @@ -372,6 +375,15 @@ + + + + + + + + |null]]> + getArgs()[1]]]> leftover_statements[0]]]> @@ -457,8 +469,6 @@ - - @@ -797,6 +807,17 @@ + + value, + null, + false, + $arg->getAttributes(), + )]]> + + + + @@ -825,6 +846,9 @@ + + + getFQCLN()]]> @@ -935,7 +959,6 @@ - @@ -1014,6 +1037,12 @@ + + getAttributes())]]> + + + + @@ -1045,6 +1074,11 @@ type_start]]> + + + + + @@ -1062,6 +1096,16 @@ type_start]]> + + + + + + + + + + @@ -1081,6 +1125,9 @@ + + + expr->getArgs()[0]]]> @@ -1093,6 +1140,24 @@ vars_to_initialize]]> + + + [ + 'comments', 'startLine', 'startFilePos', 'endFilePos', + ], + 'phpVersion' => $major_version . '.' . $minor_version, + ]]]> + + + + + + + + + + @@ -1386,6 +1451,10 @@ + + + + @@ -1397,7 +1466,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1511,6 +1619,13 @@ + + + + + + + stmts[0]]]> @@ -1537,6 +1652,10 @@ + + + + aliases->uses_start]]> aliases->uses_start]]> @@ -1553,6 +1672,9 @@ + + + @@ -1562,6 +1684,16 @@ start_change]]> + + + + + + + + + + @@ -1608,6 +1740,11 @@ + + + + + @@ -1733,6 +1870,30 @@ + + + + + + + + + + + + + + + + suggested_type + ? StubsGenerator::getExpressionFromType($property_storage->suggested_type) + : null + ) + ]]]> + cased_name]]> template_types]]> @@ -1741,6 +1902,9 @@ + + + cased_name]]> cased_name]]> @@ -2002,6 +2166,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + other_references]]> @@ -2351,9 +2644,19 @@ - - - - + + + + + + + + + + + + + + diff --git a/src/Psalm/CodeLocation/ParseErrorLocation.php b/src/Psalm/CodeLocation/ParseErrorLocation.php index 1714ff7fead..a5b91201d75 100644 --- a/src/Psalm/CodeLocation/ParseErrorLocation.php +++ b/src/Psalm/CodeLocation/ParseErrorLocation.php @@ -17,9 +17,9 @@ public function __construct( string $file_path, string $file_name ) { - /** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */ + /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $this->file_start = (int)$error->getAttributes()['startFilePos']; - /** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */ + /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $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/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index 2d99b3435af..3d23cc1dcb7 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -6,6 +6,7 @@ use PhpParser\NodeTraverser; use Psalm\Codebase; use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer; +use Psalm\Internal\BCHelper; use Psalm\Internal\PhpVisitor\YieldTypeCollector; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Type; @@ -76,14 +77,15 @@ public static function getReturnTypes( break; } - if ($stmt instanceof PhpParser\Node\Stmt\Throw_) { + if (BCHelper::isThrowStatement($stmt)) { $return_types[] = Type::getNever(); break; } if ($stmt instanceof PhpParser\Node\Stmt\Expression) { - if ($stmt->expr instanceof PhpParser\Node\Expr\Exit_) { + if ($stmt->expr instanceof PhpParser\Node\Expr\Exit_ + || (!BCHelper::usePHPParserV4() && $stmt->expr instanceof PhpParser\Node\Expr\Throw_)) { $return_types[] = Type::getNever(); break; diff --git a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php index 808010d09bc..54bdd54c540 100644 --- a/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Analyzer; use PhpParser; +use Psalm\Internal\BCHelper; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\NodeTypeProvider; @@ -48,7 +49,7 @@ public static function getControlActions( foreach ($stmts as $stmt) { if ($stmt instanceof PhpParser\Node\Stmt\Return_ || - $stmt instanceof PhpParser\Node\Stmt\Throw_ || + BCHelper::isThrow($stmt) || ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_) ) { if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) { @@ -406,7 +407,7 @@ public static function onlyThrowsOrExits(NodeTypeProvider $type_provider, array for ($i = count($stmts) - 1; $i >= 0; --$i) { $stmt = $stmts[$i]; - if ($stmt instanceof PhpParser\Node\Stmt\Throw_ + if (BCHelper::isThrow($stmt) || ($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_) ) { @@ -436,7 +437,7 @@ public static function onlyThrows(array $stmts): bool } foreach ($stmts as $stmt) { - if ($stmt instanceof PhpParser\Node\Stmt\Throw_) { + if (BCHelper::isThrow($stmt)) { return true; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php index 54b7e7f3f01..c274bbd6095 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php @@ -691,8 +691,8 @@ private static function simplifyCaseEqualityExpression( } /** - * @param array $in_array_values - * @return ?array + * @param array $in_array_values + * @return array|null */ private static function getOptionsFromNestedOr( PhpParser\Node\Expr $case_equality_expr, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 679abaa347a..95f17603e4b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -896,9 +896,10 @@ public static function analyzeAssignmentOperation( public static function analyzeAssignmentRef( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\AssignRef $stmt, - Context $context + Context $context, + ?PhpParser\Node\Stmt $from_stmt ): bool { - ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context, false, null, false, null, true); + ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context, false, null, null, null, true); $lhs_var_id = ExpressionIdentifier::getExtendedVarId( $stmt->var, @@ -912,7 +913,7 @@ public static function analyzeAssignmentRef( $statements_analyzer, ); - $doc_comment = $stmt->getDocComment(); + $doc_comment = $stmt->getDocComment() ?? ($from_stmt ? $from_stmt->getDocComment() : null); if ($doc_comment) { try { $var_comments = CommentAnalyzer::getTypeFromComment( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 3d825668dc9..900c529f934 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -240,7 +240,7 @@ public static function analyze( $context, false, null, - false, + null, $high_order_template_result, ) === false) { $context->inside_isset = $was_inside_isset; 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 22f483c1bcd..f41469e7ec3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -201,7 +201,8 @@ public static function handleMagicMethod( $result->existent_method_ids[$method_id->__toString()] = true; $array_values = array_map( - static fn(PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem => new VirtualArrayItem( + /** @return PhpParser\Node\Expr\ArrayItem|PhpParser\Node\ArrayItem */ + static fn(PhpParser\Node\Arg $arg) => new VirtualArrayItem( $arg->value, null, false, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 63c815ff521..af3f1f22305 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; +use PhpParser\Node\Expr; use PhpParser\Node\Scalar\EncapsedStringPart; use Psalm\CodeLocation; use Psalm\Context; @@ -42,7 +43,7 @@ public static function analyze( $literal_string = ""; foreach ($stmt->parts as $part) { - if (ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) { + if ($part instanceof Expr && ExpressionAnalyzer::analyze($statements_analyzer, $part, $context) === false) { return false; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php index ffaa0f5b387..2f4153e285a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php @@ -316,6 +316,7 @@ public static function analyze( /** * @param non-empty-list $conds + * @param array $attributes */ private static function convertCondsToConditional( array $conds, @@ -331,7 +332,8 @@ private static function convertCondsToConditional( } $array_items = array_map( - static fn(PhpParser\Node\Expr $cond): PhpParser\Node\Expr\ArrayItem => + /** @return PhpParser\Node\Expr\ArrayItem|PhpParser\Node\ArrayItem */ + static fn(PhpParser\Node\Expr $cond) => new VirtualArrayItem($cond, null, false, $cond->getAttributes()), $conds, ); diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index c19c8df51a6..41c1ea8fd4a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -42,6 +42,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\YieldAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\YieldFromAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\BCHelper; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\Type\TemplateResult; use Psalm\Issue\RiskyTruthyFalsyComparison; @@ -77,7 +78,7 @@ public static function analyze( Context $context, bool $array_assignment = false, ?Context $global_context = null, - bool $from_stmt = false, + ?PhpParser\Node\Stmt $from_stmt = null, ?TemplateResult $template_result = null, bool $assigned_to_reference = false ): bool { @@ -182,7 +183,7 @@ private static function handleExpression( Context $context, bool $array_assignment, ?Context $global_context, - bool $from_stmt, + ?PhpParser\Node\Stmt $from_stmt, ?TemplateResult $template_result = null, bool $assigned_to_reference = false ): bool { @@ -292,7 +293,7 @@ private static function handleExpression( $stmt, $context, 0, - $from_stmt, + $from_stmt !== null, ); } @@ -379,7 +380,7 @@ private static function handleExpression( } if ($stmt instanceof PhpParser\Node\Expr\AssignRef) { - if (!AssignmentAnalyzer::analyzeAssignmentRef($statements_analyzer, $stmt, $context)) { + if (!AssignmentAnalyzer::analyzeAssignmentRef($statements_analyzer, $stmt, $context, $from_stmt)) { IssueBuffer::maybeAdd( new UnsupportedReferenceUsage( "This reference cannot be analyzed by Psalm", @@ -456,7 +457,9 @@ private static function handleExpression( return MatchAnalyzer::analyze($statements_analyzer, $stmt, $context); } - if ($stmt instanceof PhpParser\Node\Expr\Throw_ && $analysis_php_version_id >= 8_00_00) { + if ($stmt instanceof PhpParser\Node\Expr\Throw_ + && ($analysis_php_version_id >= 8_00_00 || !BCHelper::usePHPParserV4()) + ) { return ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context); } @@ -496,7 +499,7 @@ private static function analyzeAssignment( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $stmt, Context $context, - bool $from_stmt + ?PhpParser\Node\Stmt $from_stmt ): bool { $assignment_type = AssignmentAnalyzer::analyze( $statements_analyzer, @@ -504,7 +507,7 @@ private static function analyzeAssignment( $stmt->expr, null, $context, - $stmt->getDocComment(), + $stmt->getDocComment() ?? ($from_stmt ? $from_stmt->getDocComment() : null), [], !$from_stmt ? $stmt : null, ); diff --git a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php index 6ae148f8b9e..bd39a52d2dd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ThrowAnalyzer.php @@ -19,11 +19,11 @@ final class ThrowAnalyzer { /** - * @param PhpParser\Node\Stmt\Throw_|PhpParser\Node\Expr\Throw_ $stmt + * @param PhpParser\Node\Expr\Throw_|PhpParser\Node\Stmt\Throw_ $stmt */ public static function analyze( StatementsAnalyzer $statements_analyzer, - PhpParser\Node $stmt, + $stmt, Context $context ): bool { $context->inside_throw = true; diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index caf95b3801f..a9799125d27 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -36,6 +36,7 @@ use Psalm\Internal\Analyzer\Statements\ThrowAnalyzer; use Psalm\Internal\Analyzer\Statements\UnsetAnalyzer; use Psalm\Internal\Analyzer\Statements\UnusedAssignmentRemover; +use Psalm\Internal\BCHelper; use Psalm\Internal\Codebase\DataFlowGraph; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; @@ -543,7 +544,7 @@ private static function analyzeStatement( UnsetAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) { ReturnAnalyzer::analyze($statements_analyzer, $stmt, $context); - } elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) { + } elseif (BCHelper::isThrowStatement($stmt)) { ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context); } elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) { SwitchAnalyzer::analyze($statements_analyzer, $stmt, $context); @@ -566,7 +567,7 @@ private static function analyzeStatement( $context, false, $global_context, - true, + $stmt, ) === false) { return false; } diff --git a/src/Psalm/Internal/BCHelper.php b/src/Psalm/Internal/BCHelper.php new file mode 100644 index 00000000000..316148d96e4 --- /dev/null +++ b/src/Psalm/Internal/BCHelper.php @@ -0,0 +1,76 @@ + Node\ArrayItem::class, + Node\Expr\ClosureUse::class => Node\ClosureUse::class, + Node\Scalar\DNumber::class => Node\Scalar\Float_::class, + Node\Scalar\Encapsed::class => Node\Scalar\InterpolatedString::class, + Node\Scalar\EncapsedStringPart::class => Node\InterpolatedStringPart::class, + Node\Scalar\LNumber::class => Node\Scalar\Int_::class, + Node\Stmt\DeclareDeclare::class => Node\DeclareItem::class, + Node\Stmt\PropertyProperty::class => Node\PropertyItem::class, + Node\Stmt\StaticVar::class => Node\StaticVar::class, + Node\Stmt\UseUse::class => Node\UseItem::class, + ]; + + public static function getPHPParserClassName(string $className): string + { + if (isset(self::CLASS_MAP[$className]) && class_exists(self::CLASS_MAP[$className])) { + return self::CLASS_MAP[$className]; + } + + return $className; + } + + public static function usePHPParserV4(): bool + { + return class_exists('\PhpParser\Node\Stmt\Throw_'); + } + + public static function isThrow(Node $stmt): bool + { + if (self::usePHPParserV4()) { + return $stmt instanceof PhpParser\Node\Stmt\Throw_; + } + + return $stmt instanceof PhpParser\Node\Stmt\Expression + && $stmt->expr instanceof PhpParser\Node\Expr\Throw_; + } + + public static function isThrowStatement(Node $node): bool + { + if (self::usePHPParserV4()) { + return $node instanceof PhpParser\Node\Stmt\Throw_; + } + + return false; + } + + public static function createEmulative(int $major_version, int $minor_version): PhpParser\Lexer\Emulative + { + if (class_exists(PhpVersion::class)) { + return new Emulative(PhpVersion::fromComponents($major_version, $minor_version)); + } + + return new Emulative([ + 'usedAttributes' => [ + 'comments', 'startLine', 'startFilePos', 'endFilePos', + ], + 'phpVersion' => $major_version . '.' . $minor_version, + ]); + } +} diff --git a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php index f1e2673572d..085302993dd 100644 --- a/src/Psalm/Internal/PhpTraverser/CustomTraverser.php +++ b/src/Psalm/Internal/PhpTraverser/CustomTraverser.php @@ -1,5 +1,9 @@ customTraverseNode($node); + + return $node; + } + } +} else { + /** + * @internal + */ + final class CustomTraverser extends InternalCustomTraverser + { + protected function traverseNode(Node $node): void + { + $this->customTraverseNode($node); + } + } +} + + /** * @internal */ -final class CustomTraverser extends NodeTraverser +abstract class InternalCustomTraverser extends NodeTraverser { public function __construct() { @@ -27,9 +59,8 @@ public function __construct() * Recursively traverse a node. * * @param Node $node node to traverse - * @return Node Result of traversal (may be original node or new one) */ - protected function traverseNode(Node $node): Node + protected function customTraverseNode(Node $node): void { foreach ($node->getSubNodeNames() as $name) { $subNode = &$node->$name; @@ -60,7 +91,7 @@ protected function traverseNode(Node $node): Node } if ($traverseChildren) { - $subNode = $this->traverseNode($subNode); + $this->traverseNode($subNode); if ($this->stopTraversal) { break; } @@ -88,8 +119,6 @@ protected function traverseNode(Node $node): Node } } } - - return $node; } /** @@ -124,7 +153,7 @@ protected function traverseArray(array $nodes): array } if ($traverseChildren) { - $node = $this->traverseNode($node); + $this->traverseNode($node); if ($this->stopTraversal) { break; } diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index 4179ac0d0e6..12d2f44b0fe 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -63,6 +63,9 @@ public function enterNode(PhpParser\Node $node): ?int || $node instanceof PhpParser\Node\Expr\StaticPropertyFetch) { return PhpParser\NodeTraverser::STOP_TRAVERSAL; } + } elseif ($node instanceof PhpParser\Node\ClosureUse) { + $this->has_non_trivial_expr = true; + return PhpParser\NodeTraverser::STOP_TRAVERSAL; } return null; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index dd97adf9b80..ff88705c606 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -73,7 +73,6 @@ use function array_pop; use function array_shift; use function array_values; -use function assert; use function count; use function get_class; use function implode; @@ -169,7 +168,6 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $fq_classlike_name = ($this->aliases->namespace ? $this->aliases->namespace . '\\' : '') . $node->name->name; - assert($fq_classlike_name !== ""); $fq_classlike_name_lc = strtolower($fq_classlike_name); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index fa2d09f2a98..7444d7d7db4 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -122,8 +122,11 @@ public function __construct( * @param bool $fake_method in the case of @method annotations we do something a little strange * @return FunctionStorage|MethodStorage|false */ - public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = false) - { + public function start( + PhpParser\Node\FunctionLike $stmt, + bool $fake_method = false, + PhpParser\Comment\Doc $doc_comment = null + ) { if ($stmt instanceof PhpParser\Node\Expr\Closure || $stmt instanceof PhpParser\Node\Expr\ArrowFunction ) { @@ -433,7 +436,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $storage->returns_by_ref = true; } - $doc_comment = $stmt->getDocComment(); + $doc_comment = $stmt->getDocComment() ?? $doc_comment; if ($classlike_storage && !$classlike_storage->is_trait) { diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 188e073153e..f20be2fe4dd 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -32,6 +32,7 @@ use Psalm\Storage\FileStorage; use Psalm\Storage\MethodStorage; use Psalm\Type; +use SplObjectStorage; use UnexpectedValueException; use function array_merge; @@ -92,6 +93,11 @@ final class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements Fi private array $bad_classes = []; private EventDispatcher $eventDispatcher; + /** + * @var SplObjectStorage + */ + private SplObjectStorage $closure_statements; + public function __construct( Codebase $codebase, FileScanner $file_scanner, @@ -104,6 +110,7 @@ public function __construct( $this->file_storage = $file_storage; $this->aliases = $this->file_storage->aliases = new Aliases(); $this->eventDispatcher = $this->codebase->config->eventDispatcher; + $this->closure_statements = new SplObjectStorage(); } public function enterNode(PhpParser\Node $node): ?int @@ -171,13 +178,34 @@ public function enterNode(PhpParser\Node $node): ?int } } } - } elseif ($node instanceof PhpParser\Node\FunctionLike) { + } elseif ($node instanceof PhpParser\Node\FunctionLike + || ($node instanceof PhpParser\Node\Stmt\Expression + && ($node->expr instanceof PhpParser\Node\Expr\ArrowFunction + || $node->expr instanceof PhpParser\Node\Expr\Closure)) + || ($node instanceof PhpParser\Node\Arg + && ($node->value instanceof PhpParser\Node\Expr\ArrowFunction + || $node->value instanceof PhpParser\Node\Expr\Closure)) + ) { + $doc_comment = null; if ($node instanceof PhpParser\Node\Stmt\Function_ || $node instanceof PhpParser\Node\Stmt\ClassMethod ) { if ($this->skip_if_descendants) { return null; } + } elseif ($node instanceof PhpParser\Node\Stmt\Expression) { + $doc_comment = $node->getDocComment(); + /** @var PhpParser\Node\FunctionLike */ + $node = $node->expr; + $this->closure_statements->attach($node); + } elseif ($node instanceof PhpParser\Node\Arg) { + $doc_comment = $node->getDocComment(); + /** @var PhpParser\Node\FunctionLike */ + $node = $node->value; + $this->closure_statements->attach($node); + } elseif ($this->closure_statements->contains($node)) { + // This is a closure that was already processed at the statement level. + return null; } $classlike_storage = null; @@ -204,7 +232,7 @@ public function enterNode(PhpParser\Node $node): ?int $functionlike_types, ); - $functionlike_node_scanner->start($node); + $functionlike_node_scanner->start($node, false, $doc_comment); $this->functionlike_node_scanners[] = $functionlike_node_scanner; diff --git a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php index 1eca6d3fd10..68a10ed5176 100644 --- a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php +++ b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php @@ -144,6 +144,9 @@ public function enterNode(Node $node): ?int return null; } + /** + * @param Stmt\Use_::TYPE_* $type + */ private function addAlias(Stmt\UseUse $use, int $type, ?Name $prefix = null): void { // Add prefix for group uses diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php index 353bdb3f408..e7488df50e6 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use LogicException; +use Psalm\Internal\BCHelper; use Psalm\Storage\ClassLikeStorage; use function array_merge; @@ -39,7 +40,7 @@ public function __construct(?ClassLikeStorageCacheProvider $cache = null) */ public function get(string $fq_classlike_name): ClassLikeStorage { - $fq_classlike_name_lc = strtolower($fq_classlike_name); + $fq_classlike_name_lc = $this->formatClassName($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); @@ -49,12 +50,21 @@ public function get(string $fq_classlike_name): ClassLikeStorage return self::$storage[$fq_classlike_name_lc]; } + /** + * @psalm-mutation-free + * @return lowercase-string + */ + private function formatClassName(string $class): string + { + return strtolower(BCHelper::getPHPParserClassName($class)); + } + /** * @psalm-mutation-free */ public function has(string $fq_classlike_name): bool { - $fq_classlike_name_lc = strtolower($fq_classlike_name); + $fq_classlike_name_lc = $this->formatClassName($fq_classlike_name); /** @psalm-suppress ImpureStaticProperty Used only for caching */ return isset(self::$storage[$fq_classlike_name_lc]); @@ -62,7 +72,7 @@ public function has(string $fq_classlike_name): bool public function exhume(string $fq_classlike_name, string $file_path, string $file_contents): ClassLikeStorage { - $fq_classlike_name_lc = strtolower($fq_classlike_name); + $fq_classlike_name_lc = $this->formatClassName($fq_classlike_name); if (isset(self::$storage[$fq_classlike_name_lc])) { return self::$storage[$fq_classlike_name_lc]; @@ -112,7 +122,7 @@ public function makeNew(string $fq_classlike_name_lc): void public function create(string $fq_classlike_name): ClassLikeStorage { - $fq_classlike_name_lc = strtolower($fq_classlike_name); + $fq_classlike_name_lc = $this->formatClassName($fq_classlike_name); $storage = new ClassLikeStorage($fq_classlike_name); self::$storage[$fq_classlike_name_lc] = $storage; @@ -123,7 +133,7 @@ public function create(string $fq_classlike_name): ClassLikeStorage public function remove(string $fq_classlike_name): void { - $fq_classlike_name_lc = strtolower($fq_classlike_name); + $fq_classlike_name_lc = $this->formatClassName($fq_classlike_name); unset(self::$storage[$fq_classlike_name_lc]); } diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index bd8e34a4b8c..e896baf9623 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -10,6 +10,7 @@ use Psalm\CodeLocation\ParseErrorLocation; use Psalm\Codebase; use Psalm\Config; +use Psalm\Internal\BCHelper; use Psalm\Internal\Diff\FileDiffer; use Psalm\Internal\Diff\FileStatementsDiffer; use Psalm\Internal\PhpTraverser\CustomTraverser; @@ -390,21 +391,15 @@ public static function parseStatements( ?array $existing_statements = null, ?array $file_changes = null ): array { - $attributes = [ - 'comments', 'startLine', 'startFilePos', 'endFilePos', - ]; - if (!self::$lexer) { $major_version = Codebase::transformPhpVersionId($analysis_php_version_id, 10_000); $minor_version = Codebase::transformPhpVersionId($analysis_php_version_id % 10_000, 100); - self::$lexer = new Emulative([ - 'usedAttributes' => $attributes, - 'phpVersion' => $major_version . '.' . $minor_version, - ]); + + self::$lexer = BCHelper::createEmulative($major_version, $minor_version); } if (!self::$parser) { - self::$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::ONLY_PHP7, self::$lexer); + self::$parser = new Parser\Php7(self::$lexer); } $used_cached_statements = false; diff --git a/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php b/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php index 97934728826..a70664b581e 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php @@ -2,6 +2,7 @@ namespace Psalm\Plugin\EventHandler\Event; +use PhpParser\Node\ArrayItem; use PhpParser\Node\Expr; use Psalm\Codebase; use Psalm\Context; @@ -9,7 +10,8 @@ final class AddRemoveTaintsEvent { - private Expr $expr; + /** @var ArrayItem|Expr */ + private $expr; private Context $context; private StatementsSource $statements_source; private Codebase $codebase; @@ -17,10 +19,11 @@ final class AddRemoveTaintsEvent /** * Called after an expression has been checked * + * @param ArrayItem|Expr $expr * @internal */ public function __construct( - Expr $expr, + $expr, Context $context, StatementsSource $statements_source, Codebase $codebase @@ -31,7 +34,10 @@ public function __construct( $this->codebase = $codebase; } - public function getExpr(): Expr + /** + * @return ArrayItem|Expr + */ + public function getExpr() { return $this->expr; } diff --git a/tests/fixtures/SuicidalAutoloader/autoloader.php b/tests/fixtures/SuicidalAutoloader/autoloader.php index ac6e8d54a47..6e28b6c1b78 100644 --- a/tests/fixtures/SuicidalAutoloader/autoloader.php +++ b/tests/fixtures/SuicidalAutoloader/autoloader.php @@ -18,6 +18,8 @@ 'PHPUnit\Framework\DOMElement', 'Stringable', 'AllowDynamicProperties', + 'PhpParser\PhpVersion', // BCHelper for nikic/php-parser v4/5 + 'PhpParser\Node\Stmt\Throw_', // BCHelper for nikic/php-parser v4/5 // https://github.com/symfony/symfony/pull/40203 // these are actually functions, referenced as `if (!function_exists(u::class))`