diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php index 70b1fad7d..d3a3f7ce0 100644 --- a/dictionaries/ImpureFunctionsList.php +++ b/dictionaries/ImpureFunctionsList.php @@ -67,6 +67,8 @@ return [ 'socket_set_block' => true, 'socket_set_nonblock' => true, 'socket_listen' => true, + 'stream_socket_shutdown' => true, + 'socket_shutdown' => true, // meta calls 'call_user_func' => true, 'call_user_func_array' => true, @@ -93,7 +95,6 @@ return [ 'mcrypt_generic_deinit' => true, 'mcrypt_module_close' => true, // internal optimisation - 'opcache_compile_file' => true, 'clearstatcache' => true, // process-related 'pcntl_signal' => true, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 3c0b6f2bd..4e93704e2 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1260,6 +1260,17 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer ); } + if ($param_type->isNever()) { + IssueBuffer::maybeAdd( + new ReservedWord( + 'Parameter cannot be never', + $function_param->type_location, + 'never', + ), + $this->suppressed_issues, + ); + } + if ($param_type->check( $this->source, $function_param->type_location, diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 82e1c6c48..d595df158 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -313,6 +313,16 @@ class MethodComparator ); } + if ($guide_method_storage->returns_by_ref && !$implementer_method_storage->returns_by_ref) { + IssueBuffer::maybeAdd( + new MethodSignatureMismatch( + 'Method ' . $cased_implementer_method_id . ' must return by-reference', + $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); + } + if ($guide_method_storage->external_mutation_free && !$implementer_method_storage->external_mutation_free && !$guide_method_storage->mutation_free_inferred diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index dfe109cc0..55e80d446 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -678,7 +678,11 @@ class StatementsAnalyzer extends SourceAnalyzer } else { try { $checked_type = $context->vars_in_scope[$checked_var_id]; - $check_type = Type::parseString($check_type_string); + $fq_check_type_string = Type::getFQCLNFromString( + $check_type_string, + $statements_analyzer->getAliases(), + ); + $check_type = Type::parseString($fq_check_type_string); /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index f4de672c9..ab7da8730 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -122,6 +122,11 @@ class NegatedAssertionReconciler extends Reconciler $existing_var_type->removeType('array'); } + if ($assertion instanceof IsNotType && $assertion_type instanceof TClassString) { + $existing_var_type->removeType(TClassString::class); + $existing_var_type->addType(new TString); + } + if (!$is_equality && isset($existing_var_atomic_types['int']) && $existing_var_type->from_calculation diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 9fdd3fcc4..0ea260804 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -235,6 +235,46 @@ class ParseTreeCreator $this->current_leaf = $new_parent_leaf; } + /** + * @param array{0: string, 1: int, 2?: string} $current_token + */ + private function parseCallableParam(array $current_token, ParseTree $current_parent): void + { + $variadic = false; + $has_default = false; + + if ($current_token[0] === '&') { + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '...') { + $variadic = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '=') { + $has_default = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } + + if (!$current_token || $current_token[0][0] !== '$') { + throw new TypeParseTreeException('Unexpected token after space'); + } + + $new_leaf = new CallableParamTree($current_parent); + $new_leaf->has_default = $has_default; + $new_leaf->variadic = $variadic; + + if ($current_parent !== $this->current_leaf) { + $new_leaf->children = [$this->current_leaf]; + array_pop($current_parent->children); + } + $current_parent->children[] = $new_leaf; + + $this->current_leaf = $new_leaf; + } + private function handleLessThan(): void { if (!$this->current_leaf instanceof FieldEllipsis) { @@ -553,24 +593,27 @@ class ParseTreeCreator $current_parent = $this->current_leaf->parent; - if ($current_parent instanceof CallableTree) { - return; - } - - while ($current_parent && !$current_parent instanceof MethodTree) { + //while ($current_parent && !$method_or_callable_parent) { + while ($current_parent && !$current_parent instanceof MethodTree && !$current_parent instanceof CallableTree) { $this->current_leaf = $current_parent; $current_parent = $current_parent->parent; } $next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; - if (!$current_parent instanceof MethodTree || !$next_token) { + if (!($current_parent instanceof MethodTree || $current_parent instanceof CallableTree) || !$next_token) { throw new TypeParseTreeException('Unexpected space'); } - ++$this->t; - $this->createMethodParam($next_token, $current_parent); + if ($current_parent instanceof MethodTree) { + ++$this->t; + $this->createMethodParam($next_token, $current_parent); + } + if ($current_parent instanceof CallableTree) { + ++$this->t; + $this->parseCallableParam($next_token, $current_parent); + } } private function handleQuestionMark(): void diff --git a/src/Psalm/Issue/InternalClass.php b/src/Psalm/Issue/InternalClass.php index 827c90134..087eece02 100644 --- a/src/Psalm/Issue/InternalClass.php +++ b/src/Psalm/Issue/InternalClass.php @@ -3,6 +3,7 @@ namespace Psalm\Issue; use function array_pop; +use function array_unique; use function count; use function implode; use function reset; @@ -15,6 +16,7 @@ final class InternalClass extends ClassIssue /** @param non-empty-list $words */ public static function listToPhrase(array $words): string { + $words = array_unique($words); if (count($words) === 1) { return reset($words); } diff --git a/tests/CheckTypeTest.php b/tests/CheckTypeTest.php index a3f0aabbf..457c496db 100644 --- a/tests/CheckTypeTest.php +++ b/tests/CheckTypeTest.php @@ -18,6 +18,26 @@ class CheckTypeTest extends TestCase $foo = 1; ', ]; + yield 'allowNamespace' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + '$className===' => 'string', + ], + + ], 'createNewObjectFromGetClass' => [ 'code' => ' 'InvalidScalarArgument', ], + 'disallowNeverTypeForParam' => [ + 'code' => ' 'ReservedWord', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 4c65719ee..61cd41b9b 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -929,6 +929,34 @@ class MethodSignatureTest extends TestCase } ', ], + 'allowByRefReturn' => [ + 'code' => 'x; + } + } + ', + ], + 'descendantAddsByRefReturn' => [ + 'code' => 'x; + } + } + ', + ], ]; } @@ -1586,6 +1614,20 @@ class MethodSignatureTest extends TestCase 'ignored_issues' => [], 'php_version' => '8.1', ], + 'absentByRefReturnInDescendant' => [ + 'code' => ' 'MethodSignatureMismatch', + ], ]; } } diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 8cd1fb218..29ee8f658 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -679,6 +679,68 @@ class TypeAnnotationTest extends TestCase '$output===' => 'callable():int', ], ], + 'callableFormats' => [ + 'code' => '): array + * @psalm-type I callable(array $e): array + * @psalm-type J callable(array ...): string + * @psalm-type K callable(array ...$e): string + * @psalm-type L \Closure(int, int): string + * + * @method ma(): A + * @method mb(): B + * @method mc(): C + * @method md(): D + * @method me(): E + * @method mf(): F + * @method mg(): G + * @method mh(): H + * @method mi(): I + * @method mj(): J + * @method mk(): K + * @method ml(): L + */ + class Foo { + public function __call(string $method, array $params) { return 1; } + } + + $foo = new \Foo(); + $output_ma = $foo->ma(); + $output_mb = $foo->mb(); + $output_mc = $foo->mc(); + $output_md = $foo->md(); + $output_me = $foo->me(); + $output_mf = $foo->mf(); + $output_mg = $foo->mg(); + $output_mh = $foo->mh(); + $output_mi = $foo->mi(); + $output_mj = $foo->mj(); + $output_mk = $foo->mk(); + $output_ml = $foo->ml(); + ', + 'assertions' => [ + '$output_ma===' => 'callable(int, int):string', + '$output_mb===' => 'callable(int, int=):string', + '$output_mc===' => 'callable(int, string):void', + '$output_md===' => 'callable(string):mixed', + '$output_me===' => 'callable(string):mixed', + '$output_mf===' => 'callable(float...):(int|null)', + '$output_mg===' => 'callable(float...):(int|null)', + '$output_mh===' => 'callable(array):array', + '$output_mi===' => 'callable(array):array', + '$output_mj===' => 'callable(array...):string', + '$output_mk===' => 'callable(array...):string', + '$output_ml===' => 'Closure(int, int):string', + ], + ], 'unionOfStringsContainingBraceChar' => [ 'code' => '