From b671117417578dc4df45642879317b38f1b2a65a Mon Sep 17 00:00:00 2001 From: AndrolGenhald Date: Sat, 25 Jun 2022 02:03:26 -0500 Subject: [PATCH] Improve @psalm-internal and prevent usage of IssueBuffer::add(). --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 14 +- .../Internal/Analyzer/CommentAnalyzer.php | 11 +- src/Psalm/Internal/Analyzer/FileAnalyzer.php | 2 +- .../Analyzer/FunctionLikeAnalyzer.php | 2 +- .../Internal/Analyzer/InterfaceAnalyzer.php | 2 +- .../Internal/Analyzer/NamespaceAnalyzer.php | 120 ++++++++++++++++-- .../Statements/Expression/ArrayAnalyzer.php | 2 +- .../Statements/Expression/AssertionFinder.php | 2 +- .../InstancePropertyAssignmentAnalyzer.php | 9 +- .../ExistingAtomicMethodCallAnalyzer.php | 2 +- .../Method/MethodCallProhibitionAnalyzer.php | 9 +- .../Expression/Call/NewAnalyzer.php | 17 +-- .../StaticMethod/AtomicStaticCallAnalyzer.php | 4 +- .../ExistingAtomicStaticCallAnalyzer.php | 2 +- .../Statements/Expression/CallAnalyzer.php | 8 +- .../Fetch/AtomicPropertyFetchAnalyzer.php | 5 +- .../Fetch/ClassConstFetchAnalyzer.php | 14 +- .../Internal/Analyzer/StatementsAnalyzer.php | 23 ++++ src/Psalm/Internal/Codebase/Populator.php | 10 +- .../Reflector/ClassLikeDocblockParser.php | 10 +- .../Reflector/ClassLikeNodeScanner.php | 29 +++-- .../Reflector/FunctionLikeDocblockParser.php | 10 +- .../Reflector/FunctionLikeDocblockScanner.php | 16 +-- .../Reflector/FunctionLikeNodeScanner.php | 13 +- .../Internal/Provider/StatementsProvider.php | 2 +- .../Scanner/ClassLikeDocblockComment.php | 4 +- src/Psalm/Internal/Scanner/DocblockParser.php | 41 ++++++ .../Scanner/FunctionDocblockComment.php | 4 +- .../Internal/Scanner/VarDocblockComment.php | 4 +- src/Psalm/Issue/InternalClass.php | 23 ++++ src/Psalm/IssueBuffer.php | 6 +- src/Psalm/Storage/ClassLikeStorage.php | 4 +- src/Psalm/Storage/FunctionLikeStorage.php | 4 +- src/Psalm/Storage/PropertyStorage.php | 4 +- tests/InternalAnnotationTest.php | 53 ++++++++ 35 files changed, 347 insertions(+), 138 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 6895a08727b..cb4e63491ba 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -135,6 +135,7 @@ public function __construct(PhpParser\Node\Stmt $class, SourceAnalyzer $source, } } + /** @return non-empty-string */ public static function getAnonymousClassName(PhpParser\Node\Stmt\Class_ $class, string $file_path): string { return preg_replace('/[^A-Za-z0-9]/', '_', $file_path) @@ -251,7 +252,7 @@ public function analyze( } foreach ($storage->docblock_issues as $docblock_issue) { - IssueBuffer::add($docblock_issue); + IssueBuffer::maybeAdd($docblock_issue); } $classlike_storage_provider = $codebase->classlike_storage_provider; @@ -1647,7 +1648,7 @@ private function analyzeClassMethod( $config = Config::getInstance(); if ($stmt->stmts === null && !$stmt->isAbstract()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Non-abstract class method must have statements', new CodeLocation($this, $stmt) @@ -1660,7 +1661,7 @@ private function analyzeClassMethod( try { $method_analyzer = new MethodAnalyzer($stmt, $source); } catch (UnexpectedValueException $e) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Problem loading method: ' . $e->getMessage(), new CodeLocation($this, $stmt) @@ -2506,11 +2507,12 @@ private function checkParentClass( ); } - if (!NamespaceAnalyzer::isWithin($fq_class_name, $parent_class_storage->internal)) { + if (!NamespaceAnalyzer::isWithinAny($fq_class_name, $parent_class_storage->internal)) { IssueBuffer::maybeAdd( new InternalClass( - $parent_fq_class_name . ' is internal to ' . $parent_class_storage->internal - . ' but called from ' . $fq_class_name, + $parent_fq_class_name . ' is internal to ' + . InternalClass::listToPhrase($parent_class_storage->internal) + . ' but called from ' . $fq_class_name, $code_location, $parent_fq_class_name ), diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 7466e5557ce..5af3a43e8a3 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -9,6 +9,7 @@ use Psalm\Exception\IncorrectDocblockException; use Psalm\Exception\TypeParseTreeException; use Psalm\FileSource; +use Psalm\Internal\Scanner\DocblockParser; use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Internal\Scanner\VarDocblockComment; use Psalm\Internal\Type\TypeAlias; @@ -21,7 +22,6 @@ use function preg_match; use function preg_replace; use function preg_split; -use function reset; use function rtrim; use function str_replace; use function strlen; @@ -236,14 +236,7 @@ private static function decorateVarDocblockComment( } } - if (isset($parsed_docblock->tags['psalm-internal'])) { - $psalm_internal = trim(reset($parsed_docblock->tags['psalm-internal'])); - - if (!$psalm_internal) { - throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); - } - - $var_comment->psalm_internal = $psalm_internal; + if (count($var_comment->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) { $var_comment->internal = true; } diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index e59a1790789..0d57d4c24e1 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -196,7 +196,7 @@ public function analyze( $statements_analyzer = new StatementsAnalyzer($this, $this->node_data); foreach ($file_storage->docblock_issues as $docblock_issue) { - IssueBuffer::add($docblock_issue); + IssueBuffer::maybeAdd($docblock_issue); } // if there are any leftover statements, evaluate them, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 6e3d216c61e..f9cafde5e92 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -209,7 +209,7 @@ public function analyze( } foreach ($storage->docblock_issues as $docblock_issue) { - IssueBuffer::add($docblock_issue); + IssueBuffer::maybeAdd($docblock_issue); } $function_information = $this->getFunctionInformation( diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index c77781a0006..9c68e2eddab 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -133,7 +133,7 @@ public function analyze(): void ); } } elseif ($stmt instanceof PhpParser\Node\Stmt\Property) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Interfaces cannot have properties', new CodeLocation($this, $stmt) diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index 436e47bcd7f..06413e6bbbf 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -12,11 +12,13 @@ use ReflectionProperty; use UnexpectedValueException; +use function assert; +use function count; use function implode; use function preg_replace; use function strpos; use function strtolower; -use function trim; +use function substr; /** * @internal @@ -152,33 +154,127 @@ public function getFileAnalyzer(): FileAnalyzer } /** - * Returns true if $className is the same as, or starts with $namespace, in a case-insensitive comparison. + * Returns true if $calling_identifier is the same as, or is within with $identifier, in a + * case-insensitive comparison. Identifiers can be namespaces, classlikes, functions, or methods. * + * @psalm-pure + * + * @throws InvalidArgumentException if $identifier is not a valid identifier + */ + public static function isWithin(string $calling_identifier, string $identifier): bool + { + $normalized_calling_ident = self::normalizeIdentifier($calling_identifier); + $normalized_ident = self::normalizeIdentifier($identifier); + + if ($normalized_calling_ident === $normalized_ident) { + return true; + } + + $normalized_calling_ident_parts = self::getIdentifierParts($normalized_calling_ident); + $normalized_ident_parts = self::getIdentifierParts($normalized_ident); + + if (count($normalized_calling_ident_parts) < count($normalized_ident_parts)) { + return false; + } + + for ($i = 0; $i < count($normalized_ident_parts); ++$i) { + if ($normalized_ident_parts[$i] !== $normalized_calling_ident_parts[$i]) { + return false; + } + } + + return true; + } + + /** + * Returns true if $calling_identifier is the same as or is within any identifier + * in $identifiers in a case-insensitive comparison, or if $identifiers is empty. + * Identifiers can be namespaces, classlikes, functions, or methods. * * @psalm-pure + * + * @psalm-assert-if-false !empty $identifiers + * + * @param list $identifiers */ - public static function isWithin(string $calling_namespace, string $namespace): bool + public static function isWithinAny(string $calling_identifier, array $identifiers): bool { - if ($namespace === '') { - return true; // required to prevent a warning from strpos with empty needle in PHP < 8 + if (count($identifiers) === 0) { + return true; } - $calling_namespace = strtolower(trim($calling_namespace, '\\') . '\\'); - $namespace = strtolower(trim($namespace, '\\') . '\\'); + foreach ($identifiers as $identifier) { + if (self::isWithin($calling_identifier, $identifier)) { + return true; + } + } - return $calling_namespace === $namespace - || strpos($calling_namespace, $namespace) === 0; + return false; } /** - * @param string $fullyQualifiedClassName, e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer' + * @param non-empty-string $fullyQualifiedClassName, e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer' * - * @return string , e.g. 'Psalm' + * @return non-empty-string , e.g. 'Psalm' * * @psalm-pure */ public static function getNameSpaceRoot(string $fullyQualifiedClassName): string { - return preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); + $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); + if ($root_namespace === "") { + throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\""); + } + return $root_namespace; + } + + /** + * @return ($lowercase is true ? lowercase-string : string) + * + * @psalm-pure + */ + public static function normalizeIdentifier(string $identifier, bool $lowercase = true): string + { + if ($identifier === "") { + return ""; + } + + $identifier = $identifier[0] === "\\" ? substr($identifier, 1) : $identifier; + return $lowercase ? strtolower($identifier) : $identifier; + } + + /** + * Splits an identifier into parts, eg `Foo\Bar::baz` becomes ["Foo", "\\", "Bar", "::", "baz"]. + * + * @return list + * + * @psalm-pure + */ + public static function getIdentifierParts(string $identifier): array + { + $parts = []; + while (($pos = strpos($identifier, "\\")) !== false) { + if ($pos > 0) { + $part = substr($identifier, 0, $pos); + assert($part !== ""); + $parts[] = $part; + } + $parts[] = "\\"; + $identifier = substr($identifier, $pos + 1); + } + if (($pos = strpos($identifier, "::")) !== false) { + if ($pos > 0) { + $part = substr($identifier, 0, $pos); + assert($part !== ""); + $parts[] = $part; + } + $parts[] = "::"; + $identifier = substr($identifier, $pos + 2); + } + if ($identifier !== "") { + $parts[] = $identifier; + } + + return $parts; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 8ad68bffe30..0a92b1a1cc3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -74,7 +74,7 @@ public static function analyze( foreach ($stmt->items as $item) { if ($item === null) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Array element cannot be empty', new CodeLocation($statements_analyzer, $stmt) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index e2fdfa395ae..69ae960dfd9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -959,7 +959,7 @@ protected static function processCustomAssertion( } } elseif ($assertion->var_id === '$this') { if (!$expr instanceof PhpParser\Node\Expr\MethodCall) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Assertion of $this can be done only on method of a class', new CodeLocation($source, $expr) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index 12a09c8d7b7..f5ebf74b12e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -37,6 +37,7 @@ use Psalm\Issue\ImplicitToStringCast; use Psalm\Issue\ImpurePropertyAssignment; use Psalm\Issue\InaccessibleProperty; +use Psalm\Issue\InternalClass; use Psalm\Issue\InternalProperty; use Psalm\Issue\InvalidPropertyAssignment; use Psalm\Issue\InvalidPropertyAssignmentValue; @@ -420,7 +421,7 @@ public static function analyzeStatement( foreach ($stmt->props as $prop) { if ($prop->default) { if ($stmt->isReadonly()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidPropertyAssignment( 'Readonly property ' . $context->self . '::$' . $prop->name->name . ' cannot have a default', @@ -1258,11 +1259,11 @@ private static function analyzeAtomicAssignment( ); } - if ($context->self && !NamespaceAnalyzer::isWithin($context->self, $property_storage->internal)) { + if ($context->self && !NamespaceAnalyzer::isWithinAny($context->self, $property_storage->internal)) { IssueBuffer::maybeAdd( new InternalProperty( - $property_id . ' is internal to ' . $property_storage->internal - . ' but called from ' . $context->self, + $property_id . ' is internal to ' . InternalClass::listToPhrase($property_storage->internal) + . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $property_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 26faf4e593e..6dee09a63e3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -271,7 +271,7 @@ public static function analyze( $codebase, $context, $method_id, - $statements_analyzer->getNamespace(), + $statements_analyzer->getFullyQualifiedFunctionMethodOrNamespaceName(), $name_code_location, $statements_analyzer->getSuppressedIssues() ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php index a3f4660a6ae..29c8bf22344 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallProhibitionAnalyzer.php @@ -8,6 +8,7 @@ use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\MethodIdentifier; use Psalm\Issue\DeprecatedMethod; +use Psalm\Issue\InternalClass; use Psalm\Issue\InternalMethod; use Psalm\IssueBuffer; @@ -22,7 +23,7 @@ public static function analyze( Codebase $codebase, Context $context, MethodIdentifier $method_id, - ?string $namespace, + ?string $caller_identifier, CodeLocation $code_location, array $suppressed_issues ): ?bool { @@ -51,12 +52,12 @@ public static function analyze( if (!$context->collect_initializations && !$context->collect_mutations ) { - if (!NamespaceAnalyzer::isWithin($namespace ?: '', $storage->internal)) { + if (!NamespaceAnalyzer::isWithinAny($caller_identifier ?? "", $storage->internal)) { IssueBuffer::maybeAdd( new InternalMethod( 'The method ' . $codebase_methods->getCasedMethodId($method_id) - . ' is internal to ' . $storage->internal - . ' but called from ' . ($context->self ?: 'root namespace'), + . ' is internal to ' . InternalClass::listToPhrase($storage->internal) + . ' but called from ' . ($caller_identifier ?: 'root namespace'), $code_location, (string) $method_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 2f9548c4195..863c38ec099 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -349,12 +349,12 @@ private static function analyzeNamedConstructor( if ($context->self && !$context->collect_initializations && !$context->collect_mutations - && !NamespaceAnalyzer::isWithin($context->self, $storage->internal) + && !NamespaceAnalyzer::isWithinAny($context->self, $storage->internal) ) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $storage->internal - . ' but called from ' . $context->self, + $fq_class_name . ' is internal to ' . InternalClass::listToPhrase($storage->internal) + . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name ), @@ -411,16 +411,13 @@ private static function analyzeNamedConstructor( if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); - $namespace = $statements_analyzer->getNamespace() ?: ''; - if (!NamespaceAnalyzer::isWithin( - $namespace, - $method_storage->internal - )) { + $caller_identifier = $statements_analyzer->getFullyQualifiedFunctionMethodOrNamespaceName() ?: ''; + if (!NamespaceAnalyzer::isWithinAny($caller_identifier, $method_storage->internal)) { IssueBuffer::maybeAdd( new InternalMethod( 'Constructor ' . $codebase->methods->getCasedMethodId($declaring_method_id) - . ' is internal to ' . $method_storage->internal - . ' but called from ' . ($namespace ?: 'root namespace'), + . ' is internal to ' . InternalClass::listToPhrase($method_storage->internal) + . ' but called from ' . ($caller_identifier ?: 'root namespace'), new CodeLocation($statements_analyzer, $stmt), (string) $method_id ), 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 4e85c2c0f70..673dc56c505 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -792,10 +792,10 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { ); } - if ($context->self && ! NamespaceAnalyzer::isWithin($context->self, $class_storage->internal)) { + if ($context->self && ! NamespaceAnalyzer::isWithinAny($context->self, $class_storage->internal)) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $class_storage->internal + $fq_class_name . ' is internal to ' . InternalClass::listToPhrase($class_storage->internal) . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name 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 6951396f72a..7855200e95e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -71,7 +71,7 @@ public static function analyze( $codebase, $context, $method_id, - $statements_analyzer->getNamespace(), + $statements_analyzer->getFullyQualifiedFunctionMethodOrNamespaceName(), new CodeLocation($statements_analyzer->getSource(), $stmt), $statements_analyzer->getSuppressedIssues() ) === false) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 7d898f9d31d..9cc9d85098e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -693,7 +693,7 @@ public static function applyAssertionsToContext( $exploded = explode('->', $assertion->var_id); if (count($exploded) < 2) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Assert notation is malformed', new CodeLocation($statements_analyzer, $expr) @@ -707,7 +707,7 @@ public static function applyAssertionsToContext( $var_id = is_numeric($var_id) ? (int) $var_id : $var_id; if (!is_int($var_id) || !isset($args[$var_id])) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Variable ' . $var_id . ' is not an argument so cannot be asserted', new CodeLocation($statements_analyzer, $expr) @@ -722,7 +722,7 @@ public static function applyAssertionsToContext( $arg_var_id = ExpressionIdentifier::getArrayVarId($arg_value, null, $statements_analyzer); if (!$arg_var_id) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock( 'Variable being asserted as argument ' . ($var_id+1) . ' cannot be found in local scope', new CodeLocation($statements_analyzer, $expr) @@ -740,7 +740,7 @@ public static function applyAssertionsToContext( ); if (null !== $failedMessage) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new InvalidDocblock($failedMessage, new CodeLocation($statements_analyzer, $expr)) ); continue; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index afa84a3d7e4..b34e983ed32 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -29,6 +29,7 @@ use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\DeprecatedProperty; use Psalm\Issue\ImpurePropertyFetch; +use Psalm\Issue\InternalClass; use Psalm\Issue\InternalProperty; use Psalm\Issue\MissingPropertyType; use Psalm\Issue\NoInterfaceProperties; @@ -405,10 +406,10 @@ public static function analyze( $property_storage = $declaring_class_storage->properties[$prop_name]; - if ($context->self && !NamespaceAnalyzer::isWithin($context->self, $property_storage->internal)) { + if ($context->self && !NamespaceAnalyzer::isWithinAny($context->self, $property_storage->internal)) { IssueBuffer::maybeAdd( new InternalProperty( - $property_id . ' is internal to ' . $property_storage->internal + $property_id . ' is internal to ' . InternalClass::listToPhrase($property_storage->internal) . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $property_id diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php index e6cb06ca161..8ed56437729 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ClassConstFetchAnalyzer.php @@ -343,12 +343,12 @@ public static function analyze( if ($context->self && !$context->collect_initializations && !$context->collect_mutations - && $const_class_storage->internal - && !NamespaceAnalyzer::isWithin($context->self, $const_class_storage->internal) + && !NamespaceAnalyzer::isWithinAny($context->self, $const_class_storage->internal) ) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $const_class_storage->internal + $fq_class_name . ' is internal to ' + . InternalClass::listToPhrase($const_class_storage->internal) . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name @@ -619,13 +619,13 @@ public static function analyze( if ($context->self && !$context->collect_initializations && !$context->collect_mutations - && $const_class_storage->internal - && !NamespaceAnalyzer::isWithin($context->self, $const_class_storage->internal) + && !NamespaceAnalyzer::isWithinAny($context->self, $const_class_storage->internal) ) { IssueBuffer::maybeAdd( new InternalClass( - $fq_class_name . ' is internal to ' . $const_class_storage->internal - . ' but called from ' . $context->self, + $fq_class_name . ' is internal to ' + . InternalClass::listToPhrase($const_class_storage->internal) + . ' but called from ' . $context->self, new CodeLocation($statements_analyzer->getSource(), $stmt), $fq_class_name ), diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 3c3e040c1e7..dff3d60d7ac 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -65,6 +65,7 @@ use function array_keys; use function array_merge; use function array_search; +use function assert; use function count; use function fwrite; use function get_class; @@ -1050,4 +1051,26 @@ public function getNodeTypeProvider(): NodeTypeProvider { return $this->node_data; } + + public function getFullyQualifiedFunctionMethodOrNamespaceName(): ?string + { + if ($this->source instanceof MethodAnalyzer) { + $fqcn = $this->getFQCLN(); + $method_name = $this->source->getFunctionLikeStorage($this)->cased_name; + assert($fqcn !== null && $method_name !== null); + + return "$fqcn::$method_name"; + } + + if ($this->source instanceof FunctionAnalyzer) { + $namespace = $this->getNamespace(); + $namespace = $namespace === "" ? "" : "$namespace\\"; + $function_name = $this->source->getFunctionLikeStorage($this)->cased_name; + assert($function_name !== null); + + return "{$namespace}{$function_name}"; + } + + return $this->getNamespace(); + } } diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 09f672854ef..9d8328249c9 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -29,7 +29,6 @@ use function count; use function in_array; use function reset; -use function strlen; use function strpos; use function strtolower; @@ -258,16 +257,11 @@ private function populateClassLikeStorage(ClassLikeStorage $storage, array $depe if (!$storage->is_interface && !$storage->is_trait) { foreach ($storage->methods as $method) { - if (strlen($storage->internal) > strlen($method->internal)) { - $method->internal = $storage->internal; - } + $method->internal = array_merge($storage->internal, $method->internal); } - foreach ($storage->properties as $property) { - if (strlen($storage->internal) > strlen($property->internal)) { - $property->internal = $storage->internal; - } + $property->internal = array_merge($storage->internal, $property->internal); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index d2bb03ce945..4572df37da3 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -16,6 +16,7 @@ use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Provider\StatementsProvider; use Psalm\Internal\Scanner\ClassLikeDocblockComment; +use Psalm\Internal\Scanner\DocblockParser; use Psalm\Internal\Type\ParseTree\MethodParamTree; use Psalm\Internal\Type\ParseTree\MethodTree; use Psalm\Internal\Type\ParseTree\MethodWithReturnTypeTree; @@ -212,14 +213,7 @@ public static function parse( $info->consistent_templates = true; } - if (isset($parsed_docblock->tags['psalm-internal'])) { - $psalm_internal = trim(reset($parsed_docblock->tags['psalm-internal'])); - - if (!$psalm_internal) { - throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); - } - - $info->psalm_internal = $psalm_internal; + if (count($info->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) { $info->internal = true; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index d2b0ed25fa8..91fbfbfee14 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -69,8 +69,8 @@ use function array_pop; use function array_shift; use function array_values; +use function assert; use function count; -use function explode; use function get_class; use function implode; use function preg_match; @@ -182,6 +182,7 @@ 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); @@ -251,7 +252,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool && isset($this->aliases->uses[strtolower($class_name)]) && $this->aliases->uses[strtolower($class_name)] !== $fq_classlike_name ) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Class name ' . $class_name . ' clashes with a use statement alias', $name_location ?? $class_location @@ -616,13 +617,10 @@ function (array $l, array $r): int { $storage->deprecated = $docblock_info->deprecated; - if ($docblock_info->internal - && !$docblock_info->psalm_internal - && $this->aliases->namespace - ) { - $storage->internal = explode('\\', $this->aliases->namespace)[0]; - } else { - $storage->internal = $docblock_info->psalm_internal ?? ''; + if (count($docblock_info->psalm_internal) !== 0) { + $storage->internal = $docblock_info->psalm_internal; + } elseif ($docblock_info->internal && $this->aliases->namespace) { + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($this->aliases->namespace)]; } if ($docblock_info->final && !$storage->final) { @@ -738,7 +736,7 @@ function (array $l, array $r): int { } if ($attribute->fq_class_name === 'Psalm\\Internal' && !$storage->internal) { - $storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } if ($attribute->fq_class_name === 'Psalm\\Immutable' @@ -1436,6 +1434,9 @@ private function getAttributeStorageFromStatement( return $storages; } + /** + * @param non-empty-string $fq_classlike_name + */ private function visitPropertyDeclaration( PhpParser\Node\Stmt\Property $stmt, Config $config, @@ -1534,9 +1535,9 @@ private function visitPropertyDeclaration( $property_storage->has_default = (bool)$property->default; $property_storage->deprecated = $var_comment ? $var_comment->deprecated : false; $property_storage->suppressed_issues = $var_comment ? $var_comment->suppressed_issues : []; - $property_storage->internal = $var_comment ? $var_comment->psalm_internal ?? '' : ''; - if (! $property_storage->internal && $var_comment && $var_comment->internal) { - $property_storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $property_storage->internal = $var_comment ? $var_comment->psalm_internal : []; + if (count($property_storage->internal) === 0 && $var_comment && $var_comment->internal) { + $property_storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } $property_storage->readonly = $stmt->isReadonly() || ($var_comment && $var_comment->readonly); $property_storage->allow_private_mutation = $var_comment ? $var_comment->allow_private_mutation : false; @@ -1648,7 +1649,7 @@ private function visitPropertyDeclaration( } if ($attribute->fq_class_name === 'Psalm\\Internal' && !$property_storage->internal) { - $property_storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $property_storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } if ($attribute->fq_class_name === 'Psalm\\Readonly') { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index bb11e8b99a4..341d0f63918 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -8,6 +8,7 @@ use Psalm\Exception\DocblockParseException; use Psalm\Exception\IncorrectDocblockException; use Psalm\Internal\Analyzer\CommentAnalyzer; +use Psalm\Internal\Scanner\DocblockParser; use Psalm\Internal\Scanner\FunctionDocblockComment; use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Issue\InvalidDocblock; @@ -381,14 +382,7 @@ public static function parse( $info->internal = true; } - if (isset($parsed_docblock->tags['psalm-internal'])) { - $psalm_internal = trim(reset($parsed_docblock->tags['psalm-internal'])); - - if (!$psalm_internal) { - throw new DocblockParseException('@psalm-internal annotation used without specifying namespace'); - } - - $info->psalm_internal = $psalm_internal; + if (count($info->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) { $info->internal = true; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index e80c441ee82..d78bff90c63 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -10,6 +10,7 @@ use Psalm\Config; use Psalm\Exception\InvalidMethodOverrideException; use Psalm\Exception\TypeParseTreeException; +use Psalm\Internal\Analyzer\NamespaceAnalyzer; use Psalm\Internal\Scanner\FileScanner; use Psalm\Internal\Scanner\FunctionDocblockComment; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -96,17 +97,10 @@ public static function addDocblockInfo( $storage->deprecated = true; } - if ($docblock_info->internal - && !$docblock_info->psalm_internal - && $aliases->namespace - ) { - $storage->internal = explode('\\', $aliases->namespace)[0]; - } elseif (!$classlike_storage - || ($docblock_info->psalm_internal - && strlen($docblock_info->psalm_internal) > strlen($classlike_storage->internal) - ) - ) { - $storage->internal = $docblock_info->psalm_internal ?? ''; + if (count($docblock_info->psalm_internal) !== 0) { + $storage->internal = $docblock_info->psalm_internal; + } elseif ($docblock_info->internal && $aliases->namespace) { + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($aliases->namespace)]; } if (($storage->internal || ($classlike_storage && $classlike_storage->internal)) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index bb654dc77b8..969633e2e54 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -51,6 +51,7 @@ use UnexpectedValueException; use function array_keys; +use function array_merge; use function array_pop; use function array_search; use function count; @@ -60,7 +61,6 @@ use function in_array; use function is_string; use function spl_object_id; -use function strlen; use function strpos; use function strtolower; @@ -458,11 +458,8 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $doc_comment = $stmt->getDocComment(); - if ($classlike_storage - && !$classlike_storage->is_trait - && strlen($classlike_storage->internal) > strlen($storage->internal) - ) { - $storage->internal = $classlike_storage->internal; + if ($classlike_storage && !$classlike_storage->is_trait) { + $storage->internal = array_merge($classlike_storage->internal, $storage->internal); } if ($doc_comment) { @@ -594,7 +591,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal } if (isset($classlike_storage->properties[$param_storage->name]) && $param_storage->location) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Promoted property ' . $param_storage->name . ' clashes with an existing property', $param_storage->location @@ -722,7 +719,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal } if ($attribute->fq_class_name === 'Psalm\\Internal' && !$storage->internal && $fq_classlike_name) { - $storage->internal = NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name); + $storage->internal = [NamespaceAnalyzer::getNameSpaceRoot($fq_classlike_name)]; } if ($attribute->fq_class_name === 'Psalm\\ExternalMutationFree' diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index 57456f6e5e2..d0b255e4e6e 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -498,7 +498,7 @@ public static function parseStatements( foreach ($error_handler->getErrors() as $error) { if ($error->hasColumnInfo()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( $error->getMessage(), new ParseErrorLocation( diff --git a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php index b91edd73e9a..a9168cc39fd 100644 --- a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php +++ b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php @@ -33,9 +33,9 @@ class ClassLikeDocblockComment /** * If set, the class is internal to the given namespace. * - * @var null|string + * @var list */ - public $psalm_internal; + public $psalm_internal = []; /** * @var string[] diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 416f487e5a2..2ddf134e62c 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -2,8 +2,16 @@ namespace Psalm\Internal\Scanner; +use Psalm\Exception\DocblockParseException; + +use function array_filter; +use function array_map; +use function array_values; +use function assert; +use function count; use function explode; use function implode; +use function is_string; use function min; use function preg_match; use function preg_replace; @@ -256,4 +264,37 @@ private static function resolveTags(ParsedDocblock $docblock): void + ($docblock->tags['psalm-param-out'] ?? []); } } + + /** + * @return list + * @throws DocblockParseException when a @psalm-internal tag doesn't include a namespace + */ + public static function handlePsalmInternal(ParsedDocblock $parsed_docblock): array + { + if (isset($parsed_docblock->tags['psalm-internal'])) { + $psalm_internal = array_map("trim", $parsed_docblock->tags['psalm-internal']); + + if (count($psalm_internal) !== count(array_filter($psalm_internal))) { + throw new DocblockParseException('psalm-internal annotation used without specifying namespace'); + } + // assert($psalm_internal === array_filter($psalm_internal)); // TODO get this to work + assert(self::assertArrayOfNonEmptyString($psalm_internal)); + + return array_values($psalm_internal); + } + + return []; + } + + /** @psalm-assert-if-true array $arr */ + private static function assertArrayOfNonEmptyString(array $arr): bool + { + foreach ($arr as $val) { + if (!is_string($val) || $val === "") { + return false; + } + } + + return true; + } } diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 89e41dbb8fd..a3819b4acf7 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -77,9 +77,9 @@ class FunctionDocblockComment /** * If set, the function is internal to the given namespace. * - * @var null|string + * @var list */ - public $psalm_internal; + public $psalm_internal = []; /** * Whether or not the function is internal diff --git a/src/Psalm/Internal/Scanner/VarDocblockComment.php b/src/Psalm/Internal/Scanner/VarDocblockComment.php index 7c38a339be1..8797530a664 100644 --- a/src/Psalm/Internal/Scanner/VarDocblockComment.php +++ b/src/Psalm/Internal/Scanner/VarDocblockComment.php @@ -51,9 +51,9 @@ class VarDocblockComment /** * If set, the property is internal to the given namespace. * - * @var null|string + * @var list */ - public $psalm_internal; + public $psalm_internal = []; /** * Whether or not the property is readonly diff --git a/src/Psalm/Issue/InternalClass.php b/src/Psalm/Issue/InternalClass.php index 26e2df0b64f..8062f1df436 100644 --- a/src/Psalm/Issue/InternalClass.php +++ b/src/Psalm/Issue/InternalClass.php @@ -2,8 +2,31 @@ namespace Psalm\Issue; +use function array_pop; +use function count; +use function implode; +use function reset; + class InternalClass extends ClassIssue { public const ERROR_LEVEL = 4; public const SHORTCODE = 174; + + /** @param non-empty-list $words */ + public static function listToPhrase(array $words): string + { + if (count($words) === 1) { + return reset($words); + } + + if (count($words) === 2) { + return implode(" and ", $words); + } + + $last_word = array_pop($words); + $phrase = implode(", ", $words); + $phrase = "$phrase, and $last_word"; + + return $phrase; + } } diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 5c666574ffe..0fea8f035fc 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -244,7 +244,11 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) } /** - * Add an issue to be emitted + * Add an issue to be emitted. This method should normally not be used! Use IssueBuffer::maybeAdd instead. + * + * @psalm-internal Psalm\IssueBuffer + * @psalm-internal Psalm\Type\Reconciler::getValueForKey + * * @throws CodeException */ public static function add(CodeIssue $e, bool $is_fixable = false): bool diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 01b36968975..a5252d8aaae 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -45,9 +45,9 @@ class ClassLikeStorage implements HasAttributesInterface public $deprecated = false; /** - * @var string + * @var list */ - public $internal = ''; + public $internal = []; /** * @var TTemplateParam[] diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index aba5fc68b36..fe23ea05059 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -74,9 +74,9 @@ abstract class FunctionLikeStorage implements HasAttributesInterface public $deprecated; /** - * @var string + * @var list */ - public $internal = ''; + public $internal = []; /** * @var bool diff --git a/src/Psalm/Storage/PropertyStorage.php b/src/Psalm/Storage/PropertyStorage.php index cbe8bf6c86c..b309a7f1356 100644 --- a/src/Psalm/Storage/PropertyStorage.php +++ b/src/Psalm/Storage/PropertyStorage.php @@ -78,9 +78,9 @@ class PropertyStorage implements HasAttributesInterface public $allow_private_mutation = false; /** - * @var string + * @var list */ - public $internal = ''; + public $internal = []; /** * @var ?string diff --git a/tests/InternalAnnotationTest.php b/tests/InternalAnnotationTest.php index dc6047713c2..604b6516d54 100644 --- a/tests/InternalAnnotationTest.php +++ b/tests/InternalAnnotationTest.php @@ -556,6 +556,59 @@ public function batBat() : void { } }', ], + 'psalmInternalMultipleNamespaces' => [ + ' [ + 'bar(); + } + } + } + ' + ], ]; }