diff --git a/docs/running_psalm/issues/MixedArgument.md b/docs/running_psalm/issues/MixedArgument.md index 896f01efe43..a7f1f5a1642 100644 --- a/docs/running_psalm/issues/MixedArgument.md +++ b/docs/running_psalm/issues/MixedArgument.md @@ -6,5 +6,5 @@ Emitted when Psalm cannot determine the type of an argument analysis_php_version_id); if (!$type->isMixed()) { return ['type' => 'analysis_php_version_id); self::taintVariable($statements_analyzer, $var_name, $type, $stmt); @@ -522,7 +531,7 @@ public static function isSuperGlobal(string $var_id): bool ); } - public static function getGlobalType(string $var_id): Union + public static function getGlobalType(string $var_id, int $codebase_analysis_php_version_id): Union { $config = Config::getInstance(); @@ -531,26 +540,239 @@ public static function getGlobalType(string $var_id): Union } if ($var_id === '$argv') { - return new Union([ - new TArray([Type::getInt(), Type::getString()]), + // only in CLI, null otherwise + $argv_nullable = new Union([ + new TNonEmptyList(Type::getString()), + new TNull() ]); + // use TNull explicitly instead of this + // as it will cause weird errors due to ignore_nullable_issues true + // e.g. InvalidPropertyAssignmentValue + // $this->argv 'list' cannot be assigned type 'non-empty-list' + // $argv_nullable->possibly_undefined = true; + $argv_nullable->ignore_nullable_issues = true; + return $argv_nullable; } if ($var_id === '$argc') { - return Type::getInt(); + // only in CLI, null otherwise + $argc_nullable = new Union([ + new TIntRange(1, null), + new TNull() + ]); + // $argc_nullable->possibly_undefined = true; + $argc_nullable->ignore_nullable_issues = true; + return $argc_nullable; + } + + if (!self::isSuperGlobal($var_id)) { + return Type::getMixed(); } if ($var_id === '$http_response_header') { return new Union([ - new TList(Type::getString()) + new TList(Type::getNonEmptyString()) ]); } - if (self::isSuperGlobal($var_id)) { - $type = Type::getArray(); - if ($var_id === '$_SESSION') { - $type->possibly_undefined = true; + if ($var_id === '$GLOBALS') { + return new Union([ + new TNonEmptyArray([ + Type::getNonEmptyString(), + Type::getMixed() + ]) + ]); + } + + if ($var_id === '$_COOKIE') { + $type = new TArray( + [ + Type::getNonEmptyString(), + Type::getString(), + ] + ); + + return new Union([$type]); + } + + if (in_array($var_id, array('$_GET', '$_POST', '$_REQUEST'), true)) { + $array_key = new Union([new TNonEmptyString(), new TInt()]); + $array = new TNonEmptyArray( + [ + $array_key, + new Union([ + new TString(), + new TArray([ + $array_key, + Type::getMixed() + ]) + ]) + ] + ); + + $type = new TArray( + [ + $array_key, + new Union([new TString(), $array]), + ] + ); + + return new Union([$type]); + } + + if ($var_id === '$_SERVER' || $var_id === '$_ENV') { + $string_helper = Type::getString(); + $string_helper->possibly_undefined = true; + + $non_empty_string_helper = Type::getNonEmptyString(); + $non_empty_string_helper->possibly_undefined = true; + + $argv_helper = new Union([ + new TNonEmptyList(Type::getString()) + ]); + $argv_helper->possibly_undefined = true; + + $argc_helper = new Union([ + new TIntRange(1, null) + ]); + $argc_helper->possibly_undefined = true; + + $request_time_helper = new Union([ + new TIntRange(time(), null) + ]); + $request_time_helper->possibly_undefined = true; + + $request_time_float_helper = Type::getFloat(); + $request_time_float_helper->possibly_undefined = true; + + $detailed_type = new TKeyedArray([ + // https://www.php.net/manual/en/reserved.variables.server.php + 'PHP_SELF' => $non_empty_string_helper, + 'argv' => $argv_helper, + 'argc' => $argc_helper, + 'GATEWAY_INTERFACE' => $non_empty_string_helper, + 'SERVER_ADDR' => $non_empty_string_helper, + 'SERVER_NAME' => $non_empty_string_helper, + 'SERVER_SOFTWARE' => $non_empty_string_helper, + 'SERVER_PROTOCOL' => $non_empty_string_helper, + 'REQUEST_METHOD' => $non_empty_string_helper, + 'REQUEST_TIME' => $request_time_helper, + 'REQUEST_TIME_FLOAT' => $request_time_float_helper, + 'QUERY_STRING' => $string_helper, + 'DOCUMENT_ROOT' => $non_empty_string_helper, + 'HTTP_ACCEPT' => $non_empty_string_helper, + 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, + 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, + 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, + 'HTTP_CONNECTION' => $non_empty_string_helper, + 'HTTP_HOST' => $non_empty_string_helper, + 'HTTP_REFERER' => $non_empty_string_helper, + 'HTTP_USER_AGENT' => $non_empty_string_helper, + 'HTTPS' => $string_helper, + 'REMOTE_ADDR' => $non_empty_string_helper, + 'REMOTE_HOST' => $non_empty_string_helper, + 'REMOTE_PORT' => $string_helper, + 'REMOTE_USER' => $non_empty_string_helper, + 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, + 'SCRIPT_FILENAME' => $non_empty_string_helper, + 'SERVER_ADMIN' => $non_empty_string_helper, + 'SERVER_PORT' => $non_empty_string_helper, + 'SERVER_SIGNATURE' => $non_empty_string_helper, + 'PATH_TRANSLATED' => $non_empty_string_helper, + 'SCRIPT_NAME' => $non_empty_string_helper, + 'REQUEST_URI' => $non_empty_string_helper, + 'PHP_AUTH_DIGEST' => $non_empty_string_helper, + 'PHP_AUTH_USER' => $non_empty_string_helper, + 'PHP_AUTH_PW' => $non_empty_string_helper, + 'AUTH_TYPE' => $non_empty_string_helper, + 'PATH_INFO' => $non_empty_string_helper, + 'ORIG_PATH_INFO' => $non_empty_string_helper, + // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html + 'CONTENT_LENGTH' => $string_helper, + 'CONTENT_TYPE' => $string_helper, + // common, misc stuff + 'FCGI_ROLE' => $non_empty_string_helper, + 'HOME' => $non_empty_string_helper, + 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, + 'HTTP_COOKIE' => $non_empty_string_helper, + 'HTTP_PRIORITY' => $non_empty_string_helper, + 'PATH' => $non_empty_string_helper, + 'REDIRECT_STATUS' => $non_empty_string_helper, + 'REQUEST_SCHEME' => $non_empty_string_helper, + 'USER' => $non_empty_string_helper, + // common, misc headers + 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, + 'HTTP_CLIENT_IP' => $non_empty_string_helper, + 'HTTP_X_REAL_IP' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, + 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, + 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, + 'HTTP_CF_VISITOR' => $non_empty_string_helper, + 'HTTP_CDN_LOOP' => $non_empty_string_helper, + // common, misc browser headers + 'HTTP_DNT' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA' => $non_empty_string_helper, + ]); + + // generic case for all other elements + $detailed_type->previous_key_type = Type::getNonEmptyString(); + $detailed_type->previous_value_type = Type::getString(); + + return new Union([$detailed_type]); + } + + if ($var_id === '$_FILES') { + $values = [ + 'name' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'type' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'size' => new Union([ + new TInt(), + new TNonEmptyList(Type::getInt()), + ]), + 'tmp_name' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'error' => new Union([ + new TInt(), + new TNonEmptyList(Type::getInt()), + ]), + ]; + + if ($codebase_analysis_php_version_id >= 81000) { + $values['full_path'] = new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]); } + + $type = new TKeyedArray($values); + + return new Union([$type]); + } + + if ($var_id === '$_SESSION') { + // keys must be string + $type = new Union([ + new TArray([ + Type::getNonEmptyString(), + Type::getMixed(), + ]) + ]); + $type->possibly_undefined = true; return $type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index c716bc2bfbc..d4280bd2a6e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -33,6 +33,7 @@ public static function analyze( ); } + $codebase = $statements_analyzer->getCodebase(); $source = $statements_analyzer->getSource(); $function_storage = $source instanceof FunctionLikeAnalyzer ? $source->getFunctionLikeStorage($statements_analyzer) @@ -44,7 +45,8 @@ public static function analyze( $var_id = '$' . $var->name; if ($var->name === 'argv' || $var->name === 'argc') { - $context->vars_in_scope[$var_id] = VariableFetchAnalyzer::getGlobalType($var_id); + $context->vars_in_scope[$var_id] = + VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); } elseif (isset($function_storage->global_types[$var_id])) { $context->vars_in_scope[$var_id] = clone $function_storage->global_types[$var_id]; $context->vars_possibly_in_scope[$var_id] = true; @@ -52,7 +54,7 @@ public static function analyze( $context->vars_in_scope[$var_id] = $global_context && $global_context->hasVariable($var_id) ? clone $global_context->vars_in_scope[$var_id] - : VariableFetchAnalyzer::getGlobalType($var_id); + : VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); $context->vars_possibly_in_scope[$var_id] = true; diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index d1611accfa8..e72954cbaf0 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -117,7 +117,7 @@ public static function reconcile( && is_string($key) && VariableFetchAnalyzer::isSuperGlobal($key) ) { - $existing_var_type = VariableFetchAnalyzer::getGlobalType($key); + $existing_var_type = VariableFetchAnalyzer::getGlobalType($key, $codebase->analysis_php_version_id); } if ($existing_var_type === null) { diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index da697c3284a..6331495cfb2 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -724,7 +724,7 @@ public function offsetGet($offset) { 'mixedSwallowsArrayAssignment' => [ ' 5, "b" => 12, "c" => null], function(?int $i) { - return $_GET["a"]; + return $GLOBALS["a"]; } );', 'error_message' => 'MixedArgumentTypeCoercion', diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 22442e8e1c5..9ecb98208c5 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -375,7 +375,7 @@ function isInvalidString(?string $myVar) : bool { echo "Ma chaine " . $myString; }', ], - 'assertServerVar' => [ + 'assertSessionVar' => [ ' [ @@ -513,7 +513,7 @@ function assertIntOrFoo($b) : void { } /** @psalm-suppress MixedAssignment */ - $a = $_GET["a"]; + $a = $GLOBALS["a"]; assertIntOrFoo($a); diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index 529b8f2ff07..cff307f3669 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -217,7 +217,7 @@ public function foo() { } public function bar() { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -232,7 +232,7 @@ public function foo() : int { } public function bar() { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -247,7 +247,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -268,7 +268,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -285,7 +285,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -303,7 +303,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index be4f4c32b17..e57a1d432b9 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -128,7 +128,7 @@ function foo() { } 'noRedundantConditionAfterMixedOrEmptyArrayCountCheck' => [ ' [ '$a' => 'false|int', @@ -1481,7 +1481,7 @@ function test() : void { $y2 = date("Y", 10000); $F2 = date("F", 10000); /** @psalm-suppress MixedArgument */ - $F3 = date("F", $_GET["F3"]);', + $F3 = date("F", $GLOBALS["F3"]);', [ '$y' => 'numeric-string', '$m' => 'numeric-string', diff --git a/tests/Internal/CliUtilsTest.php b/tests/Internal/CliUtilsTest.php index f6f6ab0511e..928eb0152f1 100644 --- a/tests/Internal/CliUtilsTest.php +++ b/tests/Internal/CliUtilsTest.php @@ -12,7 +12,7 @@ class CliUtilsTest extends TestCase { /** - * @var array + * @var list */ private $argv = []; diff --git a/tests/JsonOutputTest.php b/tests/JsonOutputTest.php index f151cfc820e..975ac545fce 100644 --- a/tests/JsonOutputTest.php +++ b/tests/JsonOutputTest.php @@ -123,11 +123,11 @@ function fooFoo() { 'assertCancelsMixedAssignment' => [ ' 'Docblock-defined type int for $a is always int', + assert(is_string($a)); + if (is_string($a)) {}', + 'message' => 'Docblock-defined type string for $a is always string', 'line' => 4, - 'error' => 'is_int($a)', + 'error' => 'is_string($a)', ], ]; } diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index c5ab588f23f..be928a96dc2 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -78,7 +78,7 @@ function qux(int $a, int $b) : int { return $a + $b; } - $_SERVER;' + $_SESSION;' ); new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); @@ -111,9 +111,9 @@ function qux(int $a, int $b) : int { $this->assertNotNull($information); $this->assertSame("getSymbolInformation('somefile.php', '$_SERVER'); + $information = $codebase->getSymbolInformation('somefile.php', '$_SESSION'); $this->assertNotNull($information); - $this->assertSame("", $information['type']); + $this->assertSame("", $information['type']); $information = $codebase->getSymbolInformation('somefile.php', '$my_global'); $this->assertNotNull($information); diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 9c522ee9df1..5642079d89e 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1213,7 +1213,7 @@ function fooFoo(): A { * @psalm-suppress UndefinedClass */ function fooFoo(): A { - return $_GET["a"]; + return $GLOBALS["a"]; } fooFoo()->bar();', diff --git a/tests/TaintTest.php b/tests/TaintTest.php index 302ca24348f..1c7620e4c06 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -458,13 +458,6 @@ public static function slugify(string $url) : string { echo $a[0]["b"];', ], - 'intUntainted' => [ - ' [ ' [ ' 'TaintedHtml', ], 'foreachArg' => [ diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index ee84dbce884..6b94c0f9db0 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -1425,7 +1425,7 @@ public function __construct(array $elements = []) } /** @psalm-suppress MixedArgument */ - $c = new ArrayCollection($_GET["a"]);', + $c = new ArrayCollection($GLOBALS["a"]);', [ '$c' => 'ArrayCollection', ], diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index f01263a2fdd..15e4e9ee41f 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -40,7 +40,7 @@ public function getAttribute(?string $name, string $default = "") $a = (new A)->getAttribute("colour", "red"); // typed as string $b = (new A)->getAttribute(null); // typed as array /** @psalm-suppress MixedArgument */ - $c = (new A)->getAttribute($_GET["foo"]); // typed as string|array', + $c = (new A)->getAttribute($GLOBALS["foo"]); // typed as string|array', [ '$a' => 'string', '$b' => 'array', diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 3b7d0148bd5..fb1e10efa92 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -768,10 +768,10 @@ function d(?iterable $foo): void { } }', ], - 'isStringServerVar' => [ + 'isStringSessionVar' => [ ' [ diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index 1d92af7130d..873d75c5459 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -200,7 +200,7 @@ function foo(int $t) : void { ' "$_GET['abc']-src/FileWithErrors.php:345-349" - "$_GET['abc']-src/FileWithErrors.php:345-349" -> "coalesce-src/FileWithErrors.php:345-363" + "$_GET:src/FileWithErrors.php:413" -> "$_GET['abc']-src/FileWithErrors.php:413-417" + "$_GET:src/FileWithErrors.php:440" -> "$_GET['abc']-src/FileWithErrors.php:440-444" + "$_GET:src/FileWithErrors.php:456" -> "$_GET['abc']-src/FileWithErrors.php:456-460" + "$_GET['abc']-src/FileWithErrors.php:440-444" -> "call to is_string-src/FileWithErrors.php:440-451" + "$_GET['abc']-src/FileWithErrors.php:456-460" -> "call to echo-src/FileWithErrors.php:407-473" "$s-src/FileWithErrors.php:109-110" -> "variable-use" -> "acme\sampleproject\bar" "$s-src/FileWithErrors.php:162-163" -> "variable-use" -> "acme\sampleproject\baz" "$s-src/FileWithErrors.php:215-216" -> "variable-use" -> "acme\sampleproject\bat" @@ -10,6 +13,8 @@ digraph Taints { "acme\sampleproject\bat#1" -> "$s-src/FileWithErrors.php:215-216" "acme\sampleproject\baz#1" -> "$s-src/FileWithErrors.php:162-163" "acme\sampleproject\foo#1" -> "$s-src/FileWithErrors.php:57-58" - "call to echo-src/FileWithErrors.php:335-364" -> "echo#1-src/filewitherrors.php:330" - "coalesce-src/FileWithErrors.php:345-363" -> "call to echo-src/FileWithErrors.php:335-364" + "call to echo-src/FileWithErrors.php:335-367" -> "echo#1-src/filewitherrors.php:330" + "call to echo-src/FileWithErrors.php:407-473" -> "echo#1-src/filewitherrors.php:402" + "call to is_string-src/FileWithErrors.php:440-451" -> "is_string#1-src/filewitherrors.php:430" + "coalesce-src/FileWithErrors.php:345-366" -> "call to echo-src/FileWithErrors.php:335-367" }