diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 1b8519d849c..b39b834ec5c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -13,6 +13,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; +use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodCallReturnTypeFetcher; use Psalm\Internal\Analyzer\Statements\Expression\Call\Method\MethodVisibilityAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; @@ -23,6 +24,7 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; +use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\AbstractInstantiation; use Psalm\Issue\DeprecatedClass; use Psalm\Issue\ImpureMethodCall; @@ -60,6 +62,7 @@ use function array_map; use function array_values; +use function count; use function in_array; use function md5; use function preg_match; @@ -431,6 +434,8 @@ private static function analyzeNamedConstructor( $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id); + $method_storage = null; + if ($declaring_method_id) { $method_storage = $codebase->methods->getStorage($declaring_method_id); @@ -502,6 +507,7 @@ private static function analyzeNamedConstructor( } $generic_param_types = null; + $self_out_candidate = null; if ($storage->template_types) { foreach ($storage->template_types as $template_name => $base_type) { @@ -539,9 +545,49 @@ private static function analyzeNamedConstructor( 'had_template' => true, ]); } + + if ($method_storage && $method_storage->self_out_type) { + $self_out_candidate = $method_storage->self_out_type; + + if ($template_result->lower_bounds) { + $self_out_candidate = TypeExpander::expandUnion( + $codebase, + $self_out_candidate, + $fq_class_name, + null, + $storage->parent_class, + true, + false, + false, + true, + ); + } + + $self_out_candidate = MethodCallReturnTypeFetcher::replaceTemplateTypes( + $self_out_candidate, + $template_result, + $method_id, + count($stmt->getArgs()), + $codebase, + ); + + $self_out_candidate = TypeExpander::expandUnion( + $codebase, + $self_out_candidate, + $fq_class_name, + $fq_class_name, + $storage->parent_class, + true, + false, + false, + true, + ); + $statements_analyzer->node_data->setType($stmt, $self_out_candidate); + } } - if ($generic_param_types) { + // XXX: what if we need both? + if ($generic_param_types && !$self_out_candidate) { $result_atomic_type = new TGenericObject( $fq_class_name, $generic_param_types, 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 85f4d465fa8..d770de533cd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -32,6 +32,7 @@ use Psalm\Issue\InvalidStringClass; use Psalm\Issue\MixedMethodCall; use Psalm\Issue\UndefinedClass; +use Psalm\Issue\UndefinedMethod; use Psalm\IssueBuffer; use Psalm\Node\Expr\VirtualArray; use Psalm\Node\Expr\VirtualMethodCall; @@ -492,6 +493,7 @@ private static function handleNamedCall( $method_name_lc, ); + if ($stmt->isFirstClassCallable()) { if ($found_method_and_class_storage) { [ $method_storage ] = $found_method_and_class_storage; @@ -517,8 +519,31 @@ private static function handleNamedCall( $codebase->getMethodReturnType($method_id, $fq_class_name), $codebase->methods->getStorage($declaring_method_id)->pure, )]); + } elseif ($codebase->methodExists( + $call_static_method_id = new MethodIdentifier($method_id->fq_class_name, '__callstatic'), + new CodeLocation($statements_analyzer, $stmt), + null, + null, + false, + )) { + $return_type_candidate = new Union([new TClosure( + 'Closure', + null, + $codebase->getMethodReturnType($call_static_method_id, $fq_class_name), + $codebase->methods->getStorage($call_static_method_id)->pure, + )]); } else { - // FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception? + if (IssueBuffer::accepts( + new UndefinedMethod( + 'Method ' . $method_id . ' does not exist', + new CodeLocation($statements_analyzer, $stmt), + (string) $method_id, + ), + $statements_analyzer->getSuppressedIssues(), + )) { + return false; + } + $return_type_candidate = Type::getClosure(); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index de5cd1a2fa5..e5fd54725ea 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -78,6 +78,7 @@ use function assert; use function count; use function implode; +use function ltrim; use function preg_match; use function preg_split; use function strtolower; @@ -1925,7 +1926,7 @@ private static function getTypeAliasesFromCommentLines( continue; } - if ($var_line_parts[0] === ' ') { + while (isset($var_line_parts[0]) && $var_line_parts[0] === ' ') { array_shift($var_line_parts); } @@ -1941,11 +1942,12 @@ private static function getTypeAliasesFromCommentLines( continue; } - if ($var_line_parts[0] === ' ') { + while (isset($var_line_parts[0]) && $var_line_parts[0] === ' ') { array_shift($var_line_parts); } $type_string = implode('', $var_line_parts); + $type_string = ltrim($type_string, "* \n\r"); try { $type_string = CommentAnalyzer::splitDocLine($type_string)[0]; } catch (DocblockParseException $e) { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 03a5d8c8d0f..64f1bce052c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -242,6 +242,13 @@ public static function parse( if (count($param_parts) >= 2) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; + } else { + IssueBuffer::maybeAdd( + new InvalidDocblock( + '@psalm-taint-sink expects 2 arguments', + $code_location, + ), + ); } } } @@ -283,6 +290,13 @@ public static function parse( if ($param_parts[0]) { $info->taint_source_types[] = $param_parts[0]; + } else { + IssueBuffer::maybeAdd( + new InvalidDocblock( + '@psalm-taint-source expects 1 argument', + $code_location, + ), + ); } } } elseif (isset($parsed_docblock->tags['return-taint'])) { diff --git a/stubs/extensions/random.phpstub b/stubs/extensions/random.phpstub index 01cc8fb8028..27135afc376 100644 --- a/stubs/extensions/random.phpstub +++ b/stubs/extensions/random.phpstub @@ -87,10 +87,20 @@ namespace Random */ public function getBytes(int $length): string {} + /** + * @template TValue + * @param array $array + * @return list + */ public function shuffleArray(array $array): array {} public function shuffleBytes(string $bytes): string {} + /** + * @template TKey as array-key + * @param array $array + * @return list + */ public function pickArrayKeys(array $array, int $num): array {} public function __serialize(): array {} diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index 3e254899342..9179c2fa445 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -515,6 +515,28 @@ function example(string $_x) : void {}', */ class A {}', ], + 'multipeLineGenericArray2' => [ + 'code' => ' + */ + class A { + /** @return TRelAlternate */ + public function ret(): array { return []; } + } + + $_ = (new A)->ret(); + ', + 'assertions' => [ + '$_===' => 'list', + ], + ], 'builtInClassInAShape' => [ 'code' => ' 'TooManyArguments', ], + 'firstClassCallableWithUnknownStaticMethod' => [ + 'code' => <<<'PHP' + 'UndefinedMethod', + ], + 'firstClassCallableWithUnknownInstanceMethod' => [ + 'code' => <<<'PHP' + foo(...); + PHP, + 'error_message' => 'UndefinedMethod', + ], ]; } } diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 7698f3ce966..24d8a1beacf 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -786,7 +786,7 @@ private function getBody() : string { * @template TSource as self::SOURCE_* * @param TSource $source * @return (TSource is "BODY" ? object|list : array) - * @psalm-taint-source + * @psalm-taint-source input */ public function getParams( string $source = self::SOURCE_GET diff --git a/tests/ThisOutTest.php b/tests/ThisOutTest.php index adc1ad20b99..737b927a913 100644 --- a/tests/ThisOutTest.php +++ b/tests/ThisOutTest.php @@ -86,6 +86,26 @@ public function getData(): array { return $this->data; } '$data3===' => 'list<2|3>', ], ], + 'provideDefaultTypeToTypeArguments' => [ + 'code' => <<<'PHP' + */ + public function __construct() {} + + /** + * @psalm-if-this-is self<'idle'> + * @psalm-this-out self<'running'> + */ + public function start(): void {} + } + $app = new App(); + PHP, + 'assertions' => [ + '$app===' => "App<'idle'>", + ], + ], ]; } } diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index bcb9947e630..55613f0d9e9 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -940,6 +940,22 @@ public function bar(array $foo): void {} } PHP, ], + 'typeWithMultipleSpaces' => [ + 'code' => <<<'PHP' +