From 17541b415fb164cee528f76092beb6ff47c3a45a Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Thu, 7 Feb 2019 12:25:57 -0500 Subject: [PATCH] Fix #1191 - allow Psalm to check against different versions of PHP --- src/Psalm/Codebase.php | 10 ++ .../Internal/Analyzer/CommentAnalyzer.php | 2 +- .../FunctionLike/ReturnTypeAnalyzer.php | 6 +- .../Analyzer/FunctionLikeAnalyzer.php | 6 +- .../Internal/Analyzer/ProjectAnalyzer.php | 29 ++-- src/Psalm/Internal/CallMap.php | 2 +- src/Psalm/Internal/CallMap_71_delta.php | 51 ++++++ src/Psalm/Internal/CallMap_72_delta.php | 150 ++++++++++++++++++ src/Psalm/Internal/CallMap_73_delta.php | 52 ++++++ src/Psalm/Internal/Codebase/CallMap.php | 51 +++++- src/Psalm/Internal/Scanner/FileScanner.php | 4 +- .../Internal/Visitor/ReflectorVisitor.php | 38 +++-- src/Psalm/Type.php | 60 +++---- src/Psalm/Type/Atomic.php | 70 +++++--- src/Psalm/Type/Reconciler.php | 12 +- src/psalm.php | 9 ++ src/psalter.php | 9 +- tests/ClassTest.php | 2 +- tests/FileManipulationTest.php | 12 +- tests/FileReferenceTest.php | 16 +- tests/FunctionCallTest.php | 36 +++++ tests/InterfaceTest.php | 2 +- tests/IssetTest.php | 18 ++- tests/Php71Test.php | 20 +++ tests/Traits/InvalidCodeAnalysisTestTrait.php | 19 +-- tests/Traits/ValidCodeAnalysisTestTrait.php | 22 ++- tests/TryCatchTest.php | 4 +- tests/TypeParseTest.php | 73 ++++----- tests/TypeReconciliationTest.php | 14 +- tests/UnusedCodeTest.php | 8 +- tests/UnusedVariableTest.php | 8 +- 31 files changed, 590 insertions(+), 225 deletions(-) create mode 100644 src/Psalm/Internal/CallMap_71_delta.php create mode 100644 src/Psalm/Internal/CallMap_72_delta.php create mode 100644 src/Psalm/Internal/CallMap_73_delta.php diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 1df1e65b3e3..27d67a400ee 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -159,6 +159,16 @@ class Codebase */ public $diff_methods = false; + /** + * @var int + */ + public $php_major_version = PHP_MAJOR_VERSION; + + /** + * @var int + */ + public $php_minor_version = PHP_MINOR_VERSION; + /** * @param bool $collect_references * @param bool $debug_output diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 445d580a4d8..7c7ccc9b50a 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -98,7 +98,7 @@ public static function getTypeFromComment( } try { - $defined_type = Type::parseTokens($var_type_tokens, false, $template_type_map ?: []); + $defined_type = Type::parseTokens($var_type_tokens, null, $template_type_map ?: []); } catch (TypeParseTreeException $e) { if (is_int($came_from_line_number)) { throw new DocblockParseException( diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 4b161a35faf..71b000c02c1 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -702,13 +702,13 @@ private static function addOrUpdateReturnType( $function ); $manipulator->setReturnType( - !$docblock_only && $project_analyzer->php_major_version >= 7 + !$docblock_only && $project_analyzer->getCodebase()->php_major_version >= 7 ? $inferred_return_type->toPhpString( $source->getNamespace(), $source->getAliasedClassesFlipped(), $source->getFQCLN(), - $project_analyzer->php_major_version, - $project_analyzer->php_minor_version + $project_analyzer->getCodebase()->php_major_version, + $project_analyzer->getCodebase()->php_minor_version ) : null, $inferred_return_type->toNamespacedString( $source->getNamespace(), diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 5ba3be3751f..2808c4c3cc9 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -875,13 +875,13 @@ private function addOrUpdateParamType( ); $manipulator->setParamType( $param_name, - !$docblock_only && $project_analyzer->php_major_version >= 7 + !$docblock_only && $project_analyzer->getCodebase()->php_major_version >= 7 ? $inferred_return_type->toPhpString( $this->source->getNamespace(), $this->source->getAliasedClassesFlipped(), $this->source->getFQCLN(), - $project_analyzer->php_major_version, - $project_analyzer->php_minor_version + $project_analyzer->getCodebase()->php_major_version, + $project_analyzer->getCodebase()->php_minor_version ) : null, $inferred_return_type->toNamespacedString( $this->source->getNamespace(), diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index d790d2e685b..1a0a00f518d 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -108,16 +108,6 @@ class ProjectAnalyzer */ private $issues_to_fix = []; - /** - * @var int - */ - public $php_major_version = PHP_MAJOR_VERSION; - - /** - * @var int - */ - public $php_minor_version = PHP_MINOR_VERSION; - /** * @var bool */ @@ -743,20 +733,31 @@ public function fileExists($file_path) * @return void */ public function alterCodeAfterCompletion( - $php_major_version, - $php_minor_version, $dry_run = false, $safe_types = false ) { $this->codebase->alter_code = true; $this->codebase->infer_types_from_usage = true; $this->show_issues = false; - $this->php_major_version = $php_major_version; - $this->php_minor_version = $php_minor_version; $this->dry_run = $dry_run; $this->only_replace_php_types_with_non_docblock_types = $safe_types; } + /** + * @return void + */ + public function setPhpVersion(string $version) + { + if (!preg_match('/^(5\.[456]|7\.[01234])(\..*)?$/', $version)) { + throw new \UnexpectedValueException('Expecting a version number in the format x.y'); + } + + list($php_major_version, $php_minor_version) = explode('.', $version); + + $this->codebase->php_major_version = (int) $php_major_version; + $this->codebase->php_minor_version = (int) $php_minor_version; + } + /** * @param array $issues * diff --git a/src/Psalm/Internal/CallMap.php b/src/Psalm/Internal/CallMap.php index 72deb114cdb..bfa8941eace 100644 --- a/src/Psalm/Internal/CallMap.php +++ b/src/Psalm/Internal/CallMap.php @@ -4171,7 +4171,7 @@ 'hash_hmac' => ['string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], 'hash_hmac_algos' => ['array'], 'hash_hmac_file' => ['string', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_init' => ['HashContext|resource', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], +'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], 'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'scontext='=>'?HashContext'], diff --git a/src/Psalm/Internal/CallMap_71_delta.php b/src/Psalm/Internal/CallMap_71_delta.php new file mode 100644 index 00000000000..fd94d3c224b --- /dev/null +++ b/src/Psalm/Internal/CallMap_71_delta.php @@ -0,0 +1,51 @@ + [ + 'Closure::fromCallable' => ['Closure', 'callable'=>'callable'], + 'SQLite3::createFunction' => ['bool', 'name'=>'string', 'callback'=>'callable', 'argument_count='=>'int', 'flags='=>'int'], + 'curl_multi_errno' => ['int', 'mh'=>'resource'], + 'curl_share_errno' => ['int', 'sh'=>'resource'], + 'curl_share_strerror' => ['string', 'code'=>'int'], + 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int', 'context='=>'resource'], + 'getenv\'1' => ['array'], + 'getopt' => ['array|array|array>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], + 'hash_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'is_iterable' => ['bool', 'var'=>'mixed'], + 'openssl_get_curve_names' => ['array'], + 'pcntl_async_signals' => ['bool', 'on='=>'bool'], + 'pcntl_signal_get_handler' => ['int|string', 'signo'=>'int'], + 'pg_fetch_all' => ['array', 'result'=>'resource', 'result_type='=>'int'], + 'pg_last_error' => ['string', 'connection='=>'resource', 'operation='=>'int'], + 'pg_select' => ['mixed', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int', 'result_type='=>'int'], + 'sapi_windows_cp_conv' => ['string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], + 'sapi_windows_cp_get' => ['int'], + 'sapi_windows_cp_is_utf8' => ['bool'], + 'sapi_windows_cp_set' => ['bool', 'code_page'=>'int'], + 'session_create_id' => ['string', 'prefix='=>'string'], + 'session_gc' => ['int'], + 'unpack' => ['array', 'format'=>'string', 'data'=>'string', 'offset='=>'int'], +], +'old' => [ + 'SQLite3::createFunction' => ['bool', 'name'=>'string', 'callback'=>'callable', 'argument_count='=>'int'], + 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int'], + 'getopt' => ['array|array|array>', 'options'=>'string', 'longopts='=>'array'], + 'pg_fetch_all' => ['array', 'result'=>'resource'], + 'pg_last_error' => ['string', 'connection='=>'resource'], + 'pg_select' => ['mixed', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int'], + 'unpack' => ['array', 'format'=>'string', 'data'=>'string'], +], +]; diff --git a/src/Psalm/Internal/CallMap_72_delta.php b/src/Psalm/Internal/CallMap_72_delta.php new file mode 100644 index 00000000000..023154b658f --- /dev/null +++ b/src/Psalm/Internal/CallMap_72_delta.php @@ -0,0 +1,150 @@ + [ + 'DOMNodeList::count' => ['int'], + 'ftp_append' => ['bool', 'ftp'=>'resource', 'remote_file'=>'string', 'local_file'=>'string', 'mode='=>'int'], + 'hash_copy' => ['HashContext', 'context'=>'HashContext'], + 'hash_final' => ['string', 'context'=>'HashContext', 'raw_output='=>'bool'], + 'hash_hmac_algos' => ['array'], + 'hash_init' => ['HashContext', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], + 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], + 'hash_update_file' => ['bool', 'context='=>'HashContext', 'filename'=>'string', 'scontext='=>'?HashContext'], + 'hash_update_stream' => ['int', 'context'=>'HashContext', 'handle'=>'', 'length='=>'int'], + 'imagebmp' => ['bool', 'image'=>'resource', 'to='=>'mixed', 'compressed='=>'bool'], + 'imagecreatefrombmp' => ['resource', 'filename'=>'string'], + 'imageopenpolygon' => ['bool', 'image'=>'resource', 'points'=>'array', 'num_points'=>'int', 'color'=>'int'], + 'imageresolution' => ['mixed', 'image'=>'resource', 'res_x='=>'int', 'res_y='=>'int'], + 'imagesetclip' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int'], + 'ldap_exop' => ['mixed', 'link'=>'resource', 'reqoid'=>'string', 'reqdata='=>'string', 'servercontrols='=>'array', 'retdata='=>'string', 'retoid='=>'string'], + 'ldap_exop_passwd' => ['mixed', 'link'=>'resource', 'user='=>'string', 'oldpw='=>'string', 'newpw='=>'string', 'serverctrls='=>'array'], + 'ldap_exop_refresh' => ['int', 'link'=>'resource', 'dn'=>'string', 'ttl'=>'int'], + 'ldap_exop_whoami' => ['string', 'link'=>'resource'], + 'ldap_parse_exop' => ['bool', 'link'=>'resource', 'result'=>'resource', 'retdata='=>'string', 'retoid='=>'string'], + 'mb_chr' => ['string', 'cp'=>'int', 'encoding='=>'string'], + 'mb_ord' => ['int', 'str'=>'string', 'enc='=>'string'], + 'mb_scrub' => ['string', 'str'=>'string', 'enc='=>'string'], + 'oci_register_taf_callback' => ['bool', 'connection'=>'resource', 'callback='=>'callable'], + 'oci_unregister_taf_callback' => ['bool', 'connection'=>'resource'], + 'ReflectionClass::isIterable' => ['bool'], + 'SQLite3::openBlob' => ['resource', 'table'=>'string', 'column'=>'string', 'rowid'=>'int', 'dbname'=>'string', 'flags='=>'int'], + 'sapi_windows_vt100_support' => ['bool', 'stream'=>'resource', 'enable='=>'bool'], + 'socket_addrinfo_bind' => ['resource', 'addrinfo'=>'resource'], + 'socket_addrinfo_connect' => ['resource', 'addrinfo'=>'resource'], + 'socket_addrinfo_explain' => ['array', 'addrinfo'=>'resource'], + 'socket_addrinfo_lookup' => ['resource[]', 'node'=>'string', 'service='=>'mixed', 'hints='=>'array'], + 'sodium_add' => ['string', 'string_1'=>'string', 'string_2'=>'string'], + 'sodium_base642bin' => ['string', 'base64'=>'string', 'variant'=>'int', 'ignore'=>'string'], + 'sodium_bin2base64' => ['string', 'binary'=>'string', 'variant'=>'int'], + 'sodium_bin2hex' => ['string', 'binary'=>'string'], + 'sodium_compare' => ['int', 'string_1'=>'string', 'string_2'=>'string'], + 'sodium_crypto_aead_aes256gcm_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_aes256gcm_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_aes256gcm_is_available' => ['bool'], + 'sodium_crypto_aead_aes256gcm_keygen' => ['string'], + 'sodium_crypto_aead_chacha20poly1305_decrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_chacha20poly1305_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_chacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_chacha20poly1305_ietf_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_chacha20poly1305_ietf_keygen' => ['string'], + 'sodium_crypto_aead_chacha20poly1305_keygen' => ['string'], + 'sodium_crypto_aead_xchacha20poly1305_ietf_decrypt' => ['string|false', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt' => ['string', 'confidential_message'=>'string', 'public_message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_aead_xchacha20poly1305_ietf_keygen' => ['string'], + 'sodium_crypto_auth' => ['string', 'message'=>'string', 'key'=>'string'], + 'sodium_crypto_auth_keygen' => ['string'], + 'sodium_crypto_auth_verify' => ['bool', 'mac'=>'string', 'message'=>'string', 'key'=>'string'], + 'sodium_crypto_box' => ['string', 'string'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_box_keypair' => ['string'], + 'sodium_crypto_box_keypair_from_secretkey_and_publickey' => ['string', 'secret_key'=>'string', 'public_key'=>'string'], + 'sodium_crypto_box_open' => ['string|false', 'message'=>'string', 'nonce'=>'string', 'message_keypair'=>'string'], + 'sodium_crypto_box_publickey' => ['string', 'keypair'=>'string'], + 'sodium_crypto_box_publickey_from_secretkey' => ['string', 'secretkey'=>'string'], + 'sodium_crypto_box_seal' => ['string', 'message'=>'string', 'publickey'=>'string'], + 'sodium_crypto_box_seal_open' => ['string|false', 'message'=>'string', 'recipient_keypair'=>'string'], + 'sodium_crypto_box_secretkey' => ['string', 'keypair'=>'string'], + 'sodium_crypto_box_seed_keypair' => ['string', 'seed'=>'string'], + 'sodium_crypto_generichash' => ['string', 'msg'=>'string', 'key='=>'?string', 'length='=>'?int'], + 'sodium_crypto_generichash_final' => ['string', 'state'=>'string', 'length='=>'?int'], + 'sodium_crypto_generichash_init' => ['string', 'key='=>'?string', 'length='=>'?int'], + 'sodium_crypto_generichash_keygen' => ['string'], + 'sodium_crypto_generichash_update' => ['bool', 'state'=>'string', 'string'=>'string'], + 'sodium_crypto_kdf_derive_from_key' => ['string', 'subkey_len'=>'int', 'subkey_id'=>'int', 'context'=>'string', 'key'=>'string'], + 'sodium_crypto_kdf_keygen' => ['string'], + 'sodium_crypto_kx_client_session_keys' => ['string', 'client_keypair'=>'string', 'server_key'=>'string'], + 'sodium_crypto_kx_keypair' => ['string'], + 'sodium_crypto_kx_publickey' => ['string', 'keypair'=>'string'], + 'sodium_crypto_kx_secretkey' => ['string', 'keypair'=>'string'], + 'sodium_crypto_kx_seed_keypair' => ['string', 'seed'=>'string'], + 'sodium_crypto_kx_server_session_keys' => ['string', 'server_keypair'=>'string', 'client_key'=>'string'], + 'sodium_crypto_pwhash' => ['string', 'length'=>'int', 'password'=>'string', 'salt'=>'string', 'opslimit'=>'int', 'memlimit'=>'int', 'alg='=>'int'], + 'sodium_crypto_pwhash_scryptsalsa208sha256' => ['string', 'length'=>'int', 'password'=>'string', 'salt'=>'string', 'opslimit'=>'int', 'memlimit'=>'int'], + 'sodium_crypto_pwhash_scryptsalsa208sha256_str' => ['string', 'password'=>'string', 'opslimit'=>'int', 'memlimit'=>'int'], + 'sodium_crypto_pwhash_scryptsalsa208sha256_str_verify' => ['bool', 'hash'=>'string', 'password'=>'string'], + 'sodium_crypto_pwhash_str' => ['string', 'password'=>'string', 'opslimit'=>'int', 'memlimit'=>'int'], + 'sodium_crypto_pwhash_str_needs_rehash' => ['bool', 'password'=>'string', 'opslimit'=>'int', 'memlimit'=>'int'], + 'sodium_crypto_pwhash_str_verify' => ['bool', 'hash'=>'string', 'password'=>'string'], + 'sodium_crypto_scalarmult' => ['string', 'string_1'=>'string', 'string_2'=>'string'], + 'sodium_crypto_scalarmult_base' => ['string', 'string_1'=>'string', 'string_2'=>'string'], + 'sodium_crypto_secretbox' => ['string', 'plaintext'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_secretbox_keygen' => ['string'], + 'sodium_crypto_secretbox_open' => ['string|false', 'ciphertext'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_secretstream_xchacha20poly1305_init_pull' => ['string', 'header'=>'string', 'key'=>'string'], + 'sodium_crypto_secretstream_xchacha20poly1305_init_push' => ['array', 'key'=>'string'], + 'sodium_crypto_secretstream_xchacha20poly1305_keygen' => ['string'], + 'sodium_crypto_secretstream_xchacha20poly1305_pull' => ['array', 'state'=>'string', 'c'=>'string', 'ad='=>'string'], + 'sodium_crypto_secretstream_xchacha20poly1305_push' => ['string', 'state'=>'string', 'msg'=>'string', 'ad='=>'string', 'tag='=>'int'], + 'sodium_crypto_secretstream_xchacha20poly1305_rekey' => ['void', 'state'=>'string'], + 'sodium_crypto_shorthash' => ['string', 'message'=>'string', 'key'=>'string'], + 'sodium_crypto_shorthash_keygen' => ['string'], + 'sodium_crypto_sign' => ['string', 'message'=>'string', 'secretkey'=>'string'], + 'sodium_crypto_sign_detached' => ['string', 'message'=>'string', 'secretkey'=>'string'], + 'sodium_crypto_sign_ed25519_pk_to_curve25519' => ['string', 'ed25519pk'=>'string'], + 'sodium_crypto_sign_ed25519_sk_to_curve25519' => ['string', 'ed25519sk'=>'string'], + 'sodium_crypto_sign_keypair' => ['string'], + 'sodium_crypto_sign_keypair_from_secretkey_and_publickey' => ['string', 'secret_key'=>'string', 'public_key'=>'string'], + 'sodium_crypto_sign_open' => ['string|false', 'message'=>'string', 'publickey'=>'string'], + 'sodium_crypto_sign_publickey' => ['string', 'keypair'=>'string'], + 'sodium_crypto_sign_publickey_from_secretkey' => ['string', 'secretkey'=>'string'], + 'sodium_crypto_sign_secretkey' => ['string', 'keypair'=>'string'], + 'sodium_crypto_sign_seed_keypair' => ['string', 'seed'=>'string'], + 'sodium_crypto_sign_verify_detached' => ['bool', 'signature'=>'string', 'message'=>'string', 'publickey'=>'string'], + 'sodium_crypto_stream' => ['string', 'length'=>'int', 'nonce'=>'string', 'key'=>'string'], + 'sodium_crypto_stream_keygen' => ['string'], + 'sodium_crypto_stream_xor' => ['string', 'message'=>'string', 'nonce'=>'string', 'key'=>'string'], + 'sodium_hex2bin' => ['string', 'hex'=>'string', 'ignore='=>'string'], + 'sodium_increment' => ['string', '&binary_string'=>'string'], + 'sodium_memcmp' => ['int', 'string_1'=>'string', 'string_2'=>'string'], + 'sodium_memzero' => ['void', '&secret'=>'string'], + 'sodium_pad' => ['string', 'unpadded'=>'string', 'length'=>'int'], + 'sodium_unpad' => ['string', 'padded'=>'string', 'length'=>'int'], + 'stream_isatty' => ['bool', 'stream'=>'resource'], + 'ZipArchive::count' => ['int'], + 'ZipArchive::setEncryptionIndex' => ['bool', 'index'=>'int', 'method'=>'string', 'password='=>'string'], + 'ZipArchive::setEncryptionName' => ['bool', 'name'=>'string', 'method'=>'int', 'password='=>'string'], +], +'old' => [ + 'hash_copy' => ['resource', 'context'=>'resource'], + 'hash_final' => ['string', 'context'=>'resource', 'raw_output='=>'bool'], + 'hash_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_init' => ['resource', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], + 'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], + 'hash_update' => ['bool', 'context'=>'resource', 'data'=>'string'], + 'hash_update_file' => ['bool', 'hcontext'=>'resource', 'filename'=>'string', 'scontext='=>'?resource'], + 'hash_update_stream' => ['int', 'context'=>'resource', 'handle'=>'resource', 'length='=>'int'], + 'SQLite3::openBlob' => ['resource', 'table'=>'string', 'column'=>'string', 'rowid'=>'int', 'dbname'=>'string'], +] +]; diff --git a/src/Psalm/Internal/CallMap_73_delta.php b/src/Psalm/Internal/CallMap_73_delta.php new file mode 100644 index 00000000000..ca765fa3700 --- /dev/null +++ b/src/Psalm/Internal/CallMap_73_delta.php @@ -0,0 +1,52 @@ + [ + 'array_key_first' => ['int|string|null', 'array'=>'array'], + 'array_key_last' => ['int|string|null', 'array'=>'array'], + 'DateTime::createFromImmutable' => ['static', 'datetime'=>'DateTimeImmutable'], + 'fpm_get_status' => ['array'], + 'gmp_binomial' => ['GMP', 'n'=>'GMP|string|int', 'k'=>'GMP|string|int'], + 'gmp_lcm' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], + 'gmp_perfect_power' => ['GMP', 'a'=>'GMP|string|int'], + 'gmp_kronecker' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], + 'JsonException::__clone' => [''], + 'JsonException::__construct' => [''], + 'JsonException::__toString' => [''], + 'JsonException::__wakeup' => [''], + 'JsonException::getCode' => [''], + 'JsonException::getFile' => [''], + 'JsonException::getLine' => [''], + 'JsonException::getMessage' => [''], + 'JsonException::getPrevious' => [''], + 'JsonException::getTrace' => [''], + 'JsonException::getTraceAsString' => [''], + 'net_get_interfaces' => ['array>|false'], + 'openssl_pkey_derive' => ['string', 'peer_pub_key'=>'mixed', 'priv_key'=>'mixed', 'keylen='=>'?int'], + 'gc_status' => ['array{runs:int,collected:int,threshold:int,roots:int}'], + 'hrtime' => ['array{0:int,1:int}|int|false', 'get_as_num='=>'bool'], + 'is_countable' => ['bool', 'var'=>'mixed'], + 'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:?string,secure?:bool,httponly?:bool}'], + 'socket_wsaprotocol_info_export' => ['string|false', 'sock='=>'resource','pid'=>'int'], + 'socket_wsaprotocol_info_import' => ['resource|false', 'id'=>'string'], + 'socket_wsaprotocol_info_release' => ['bool', 'id'=>'string'], + 'SplPriorityQueue::isCorrupted' => ['bool'], +], +'old' => [ +] +]; diff --git a/src/Psalm/Internal/Codebase/CallMap.php b/src/Psalm/Internal/Codebase/CallMap.php index 6b8ecdfc109..365f8754302 100644 --- a/src/Psalm/Internal/Codebase/CallMap.php +++ b/src/Psalm/Internal/Codebase/CallMap.php @@ -1,6 +1,7 @@ >|null */ @@ -128,15 +141,23 @@ public static function getReturnTypeFromCallMap($function_id) * @psalm-suppress MixedInferredReturnType as the use of require buggers things up * @psalm-suppress MixedAssignment * @psalm-suppress MixedTypeCoercion + * @psalm-suppress MixedReturnStatement */ public static function getCallMap() { - if (self::$call_map !== null) { + $codebase = ProjectAnalyzer::getInstance()->getCodebase(); + $analyzer_major_version = $codebase->php_major_version; + $analyzer_minor_version = $codebase->php_minor_version; + + if (self::$call_map !== null + && $analyzer_major_version === self::$loaded_php_major_version + && $analyzer_minor_version === self::$loaded_php_minor_version + ) { return self::$call_map; } /** @var array> */ - $call_map = require_once(__DIR__ . '/../CallMap.php'); + $call_map = require(__DIR__ . '/../CallMap.php'); self::$call_map = []; @@ -145,6 +166,32 @@ public static function getCallMap() self::$call_map[$cased_key] = $value; } + if ($analyzer_minor_version < self::PHP_MINOR_VERSION) { + for ($i = self::PHP_MINOR_VERSION; $i > $analyzer_minor_version; $i--) { + /** + * @var array{ + * old: array>, + * new: array> + * } + * @psalm-suppress UnresolvableInclude + */ + $diff_call_map = require(__DIR__ . '/../CallMap_7' . $i . '_delta.php'); + + foreach ($diff_call_map['new'] as $key => $_) { + $cased_key = strtolower($key); + unset(self::$call_map[$cased_key]); + } + + foreach ($diff_call_map['old'] as $key => $value) { + $cased_key = strtolower($key); + self::$call_map[$cased_key] = $value; + } + } + } + + self::$loaded_php_major_version = $analyzer_major_version; + self::$loaded_php_minor_version = $analyzer_minor_version; + return self::$call_map; } diff --git a/src/Psalm/Internal/Scanner/FileScanner.php b/src/Psalm/Internal/Scanner/FileScanner.php index d0eee606ba0..47f760fa0f0 100644 --- a/src/Psalm/Internal/Scanner/FileScanner.php +++ b/src/Psalm/Internal/Scanner/FileScanner.php @@ -86,7 +86,9 @@ public function scan( } $traverser = new NodeTraverser(); - $traverser->addVisitor(new ReflectorVisitor($codebase, $file_storage, $this)); + $traverser->addVisitor( + new ReflectorVisitor($codebase, $file_storage, $this) + ); $traverser->traverse($stmts); $file_storage->deep_scan = $this->will_analyze; diff --git a/src/Psalm/Internal/Visitor/ReflectorVisitor.php b/src/Psalm/Internal/Visitor/ReflectorVisitor.php index efa3b42f981..d015a77c36b 100644 --- a/src/Psalm/Internal/Visitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/Visitor/ReflectorVisitor.php @@ -86,6 +86,12 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse /** @var class-string<\Psalm\Plugin\Hook\AfterClassLikeVisitInterface>[] */ private $after_classlike_check_plugins; + /** @var int */ + private $php_major_version; + + /** @var int */ + private $php_minor_version; + /** * @var array> */ @@ -104,6 +110,8 @@ public function __construct( $this->aliases = $this->file_aliases = new Aliases(); $this->file_storage = $file_storage; $this->after_classlike_check_plugins = $this->config->after_visit_classlikes; + $this->php_major_version = $codebase->php_major_version; + $this->php_minor_version = $codebase->php_minor_version; } /** @@ -1000,7 +1008,7 @@ private function extendTemplatedType( $this->class_template_types, $this->type_aliases ), - false, + null, $this->class_template_types ); } catch (TypeParseTreeException $e) { @@ -1097,7 +1105,7 @@ private function implementTemplatedType( $this->class_template_types, $this->type_aliases ), - false, + null, $this->class_template_types ); } catch (TypeParseTreeException $e) { @@ -1192,7 +1200,7 @@ private function useTemplatedType( $this->class_template_types, $this->type_aliases ), - false, + null, $this->class_template_types ); } catch (TypeParseTreeException $e) { @@ -1651,7 +1659,10 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m $return_type_string = $return_type_fq_classlike_name . $suffix; } - $storage->return_type = Type::parseString($return_type_string, true); + $storage->return_type = Type::parseString( + $return_type_string, + [$this->php_major_version, $this->php_minor_version] + ); $storage->return_type->queueClassLikesForScanning($this->codebase, $this->file_storage); $storage->return_type_location = new CodeLocation( @@ -1934,7 +1945,7 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m $storage->return_type = Type::parseTokens( $fixed_type_tokens, - false, + null, $this->function_template_types + $this->class_template_types ); $storage->return_type->setFromDocblock(); @@ -2005,7 +2016,7 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m null, $this->type_aliases ), - false + null ); } catch (TypeParseTreeException $e) { if (IssueBuffer::accepts( @@ -2042,7 +2053,7 @@ private function registerFunctionLike(PhpParser\Node\FunctionLike $stmt, $fake_m $this->function_template_types + $this->class_template_types, $this->type_aliases ), - false, + null, $this->function_template_types + $this->class_template_types ); @@ -2144,7 +2155,11 @@ public function getTranslatedFunctionParam(PhpParser\Node\Param $param) } if ($param_type_string) { - $param_type = Type::parseString($param_type_string, true, []); + $param_type = Type::parseString( + $param_type_string, + [$this->php_major_version, $this->php_minor_version], + [] + ); if ($is_nullable) { $param_type->addType(new Type\Atomic\TNull); @@ -2266,7 +2281,7 @@ private function improveParamsFromDocblock( $this->function_template_types + $this->class_template_types, $this->type_aliases ), - false, + null, $this->function_template_types + $this->class_template_types ); } catch (TypeParseTreeException $e) { @@ -2455,7 +2470,10 @@ private function visitPropertyDeclaration( $property_type_string = $property_type_fq_classlike_name . $suffix; } - $signature_type = Type::parseString($property_type_string, true); + $signature_type = Type::parseString( + $property_type_string, + [$this->php_major_version, $this->php_minor_version] + ); $signature_type->queueClassLikesForScanning($this->codebase, $this->file_storage); $signature_type_location = new CodeLocation( diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 3429dc139a6..e19250ff676 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -84,31 +84,31 @@ abstract class Type * Parses a string type representation * * @param string $type_string - * @param bool $php_compatible + * @param array{int,int}|null $php_version * @param array $template_type_map * * @return Union */ public static function parseString( $type_string, - $php_compatible = false, + array $php_version = null, array $template_type_map = [] ) { - return self::parseTokens(self::tokenize($type_string), $php_compatible, $template_type_map); + return self::parseTokens(self::tokenize($type_string), $php_version, $template_type_map); } /** * Parses a string type representation * * @param array $type_tokens - * @param bool $php_compatible + * @param array{int,int}|null $php_version * @param array $template_type_map * * @return Union */ public static function parseTokens( array $type_tokens, - $php_compatible = false, + array $php_version = null, array $template_type_map = [] ) { if (count($type_tokens) === 1) { @@ -119,13 +119,13 @@ public static function parseTokens( throw new TypeParseTreeException("Invalid type '$only_token'"); } - $only_token = self::fixScalarTerms($only_token, $php_compatible); + $only_token = self::fixScalarTerms($only_token, $php_version); - return new Union([Atomic::create($only_token, $php_compatible, $template_type_map)]); + return new Union([Atomic::create($only_token, $php_version, $template_type_map)]); } $parse_tree = ParseTree::createFromTokens($type_tokens); - $parsed_type = self::getTypeFromTree($parse_tree, $php_compatible, $template_type_map); + $parsed_type = self::getTypeFromTree($parse_tree, $php_version, $template_type_map); if (!($parsed_type instanceof Union)) { $parsed_type = new Union([$parsed_type]); @@ -136,11 +136,11 @@ public static function parseTokens( /** * @param string $type_string - * @param bool $php_compatible + * @param array{int,int}|null $php_version * * @return string */ - private static function fixScalarTerms($type_string, $php_compatible = false) + private static function fixScalarTerms($type_string, array $php_version = null) { $type_string_lc = strtolower($type_string); @@ -165,14 +165,14 @@ private static function fixScalarTerms($type_string, $php_compatible = false) switch ($type_string) { case 'boolean': - return $php_compatible ? $type_string : 'bool'; + return $php_version !== null ? $type_string : 'bool'; case 'integer': - return $php_compatible ? $type_string : 'int'; + return $php_version !== null ? $type_string : 'int'; case 'double': case 'real': - return $php_compatible ? $type_string : 'float'; + return $php_version !== null ? $type_string : 'float'; } return $type_string; @@ -180,14 +180,14 @@ private static function fixScalarTerms($type_string, $php_compatible = false) /** * @param ParseTree $parse_tree - * @param bool $php_compatible + * @param array{int,int}|null $php_version * @param array $template_type_map * * @return Atomic|TArray|TGenericObject|ObjectLike|Union */ public static function getTypeFromTree( ParseTree $parse_tree, - $php_compatible = false, + array $php_version = null, array $template_type_map = [] ) { if ($parse_tree instanceof ParseTree\GenericTree) { @@ -198,14 +198,14 @@ public static function getTypeFromTree( * @return Union */ function (ParseTree $child_tree) use ($template_type_map) { - $tree_type = self::getTypeFromTree($child_tree, false, $template_type_map); + $tree_type = self::getTypeFromTree($child_tree, null, $template_type_map); return $tree_type instanceof Union ? $tree_type : new Union([$tree_type]); }, $parse_tree->children ); - $generic_type_value = self::fixScalarTerms($generic_type, false); + $generic_type_value = self::fixScalarTerms($generic_type); if ($generic_type_value === 'array' && count($generic_params) === 1) { array_unshift($generic_params, new Union([new TArrayKey])); @@ -276,10 +276,10 @@ function (ParseTree $child_tree) use ($template_type_map) { foreach ($parse_tree->children as $child_tree) { if ($child_tree instanceof ParseTree\NullableTree) { - $atomic_type = self::getTypeFromTree($child_tree->children[0], false, $template_type_map); + $atomic_type = self::getTypeFromTree($child_tree->children[0], null, $template_type_map); $has_null = true; } else { - $atomic_type = self::getTypeFromTree($child_tree, false, $template_type_map); + $atomic_type = self::getTypeFromTree($child_tree, null, $template_type_map); } if ($atomic_type instanceof Union) { @@ -306,7 +306,7 @@ function (ParseTree $child_tree) use ($template_type_map) { * @return Atomic */ function (ParseTree $child_tree) use ($template_type_map) { - $atomic_type = self::getTypeFromTree($child_tree, false, $template_type_map); + $atomic_type = self::getTypeFromTree($child_tree, null, $template_type_map); if (!$atomic_type instanceof Atomic) { throw new TypeParseTreeException( @@ -346,11 +346,11 @@ function (ParseTree $child_tree) use ($template_type_map) { foreach ($parse_tree->children as $i => $property_branch) { if (!$property_branch instanceof ParseTree\ObjectLikePropertyTree) { - $property_type = self::getTypeFromTree($property_branch, false, $template_type_map); + $property_type = self::getTypeFromTree($property_branch, null, $template_type_map); $property_maybe_undefined = false; $property_key = (string)$i; } elseif (count($property_branch->children) === 1) { - $property_type = self::getTypeFromTree($property_branch->children[0], false, $template_type_map); + $property_type = self::getTypeFromTree($property_branch->children[0], null, $template_type_map); $property_maybe_undefined = $property_branch->possibly_undefined; $property_key = $property_branch->value; } else { @@ -386,7 +386,7 @@ function (ParseTree $child_tree) use ($template_type_map) { } if ($parse_tree instanceof ParseTree\CallableWithReturnTypeTree) { - $callable_type = self::getTypeFromTree($parse_tree->children[0], false, $template_type_map); + $callable_type = self::getTypeFromTree($parse_tree->children[0], null, $template_type_map); if (!$callable_type instanceof TCallable && !$callable_type instanceof Type\Atomic\Fn) { throw new \InvalidArgumentException('Parsing callable tree node should return TCallable'); @@ -396,7 +396,7 @@ function (ParseTree $child_tree) use ($template_type_map) { throw new TypeParseTreeException('Invalid return type'); } - $return_type = self::getTypeFromTree($parse_tree->children[1], false, $template_type_map); + $return_type = self::getTypeFromTree($parse_tree->children[1], null, $template_type_map); $callable_type->return_type = $return_type instanceof Union ? $return_type : new Union([$return_type]); @@ -413,11 +413,11 @@ function (ParseTree $child_tree) use ($template_type_map) { $is_optional = false; if ($child_tree instanceof ParseTree\CallableParamTree) { - $tree_type = self::getTypeFromTree($child_tree->children[0], false, $template_type_map); + $tree_type = self::getTypeFromTree($child_tree->children[0], null, $template_type_map); $is_variadic = $child_tree->variadic; $is_optional = $child_tree->has_default; } else { - $tree_type = self::getTypeFromTree($child_tree, false, $template_type_map); + $tree_type = self::getTypeFromTree($child_tree, null, $template_type_map); } $tree_type = $tree_type instanceof Union ? $tree_type : new Union([$tree_type]); @@ -444,7 +444,7 @@ function (ParseTree $child_tree) use ($template_type_map) { } if ($parse_tree instanceof ParseTree\EncapsulationTree) { - return self::getTypeFromTree($parse_tree->children[0], false, $template_type_map); + return self::getTypeFromTree($parse_tree->children[0], null, $template_type_map); } if ($parse_tree instanceof ParseTree\NullableTree) { @@ -452,7 +452,7 @@ function (ParseTree $child_tree) use ($template_type_map) { throw new TypeParseTreeException('Misplaced question mark'); } - $non_nullable_type = self::getTypeFromTree($parse_tree->children[0], false, $template_type_map); + $non_nullable_type = self::getTypeFromTree($parse_tree->children[0], null, $template_type_map); if ($non_nullable_type instanceof Union) { $non_nullable_type->addType(new TNull); @@ -515,9 +515,9 @@ function (ParseTree $child_tree) use ($template_type_map) { throw new TypeParseTreeException('Invalid type \'' . $parse_tree->value . '\''); } - $atomic_type = self::fixScalarTerms($parse_tree->value, $php_compatible); + $atomic_type = self::fixScalarTerms($parse_tree->value, $php_version); - return Atomic::create($atomic_type, $php_compatible, $template_type_map); + return Atomic::create($atomic_type, $php_version, $template_type_map); } private static function getGenericParamClass( diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 8d1a082f723..aecac4aeaae 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -67,14 +67,14 @@ abstract class Atomic /** * @param string $value - * @param bool $php_compatible + * @param array{int,int}|null $php_version * @param array $template_type_map * * @return Atomic */ public static function create( $value, - $php_compatible = false, + array $php_version = null, array $template_type_map = [] ) { switch ($value) { @@ -91,13 +91,27 @@ public static function create( return new TBool(); case 'void': - return new TVoid(); + if ($php_version === null + || ($php_version[0] > 7) + || ($php_version[0] === 7 && $php_version[1] >= 1) + ) { + return new TVoid(); + } + + break; case 'array-key': return new TArrayKey(); case 'iterable': - return new TIterable(); + if ($php_version === null + || ($php_version[0] > 7) + || ($php_version[0] === 7 && $php_version[1] >= 1) + ) { + return new TIterable(); + } + + break; case 'never-return': case 'never-returns': @@ -105,7 +119,14 @@ public static function create( return new TNever(); case 'object': - return new TObject(); + if ($php_version === null + || ($php_version[0] > 7) + || ($php_version[0] === 7 && $php_version[1] >= 2) + ) { + return new TObject(); + } + + break; case 'callable': return new TCallable(); @@ -117,28 +138,28 @@ public static function create( return new TNonEmptyArray([new Union([new TMixed]), new Union([new TMixed])]); case 'resource': - return $php_compatible ? new TNamedObject($value) : new TResource(); + return $php_version !== null ? new TNamedObject($value) : new TResource(); case 'numeric': - return $php_compatible ? new TNamedObject($value) : new TNumeric(); + return $php_version !== null ? new TNamedObject($value) : new TNumeric(); case 'true': - return $php_compatible ? new TNamedObject($value) : new TTrue(); + return $php_version !== null ? new TNamedObject($value) : new TTrue(); case 'false': - return $php_compatible ? new TNamedObject($value) : new TFalse(); + return $php_version !== null ? new TNamedObject($value) : new TFalse(); case 'empty': - return $php_compatible ? new TNamedObject($value) : new TEmpty(); + return $php_version !== null ? new TNamedObject($value) : new TEmpty(); case 'scalar': - return $php_compatible ? new TNamedObject($value) : new TScalar(); + return $php_version !== null ? new TNamedObject($value) : new TScalar(); case 'null': - return $php_compatible ? new TNamedObject($value) : new TNull(); + return $php_version !== null ? new TNamedObject($value) : new TNull(); case 'mixed': - return $php_compatible ? new TNamedObject($value) : new TMixed(); + return $php_version !== null ? new TNamedObject($value) : new TMixed(); case 'class-string': return new TClassString(); @@ -151,22 +172,21 @@ public static function create( case '$this': return new TNamedObject('static'); + } - default: - if (strpos($value, '-') && substr($value, 0, 4) !== 'OCI-') { - throw new \Psalm\Exception\TypeParseTreeException('no hyphens allowed'); - } - - if (is_numeric($value[0])) { - throw new \Psalm\Exception\TypeParseTreeException('First character of type cannot be numeric'); - } + if (strpos($value, '-') && substr($value, 0, 4) !== 'OCI-') { + throw new \Psalm\Exception\TypeParseTreeException('no hyphens allowed'); + } - if (isset($template_type_map[$value])) { - return new TGenericParam($value, $template_type_map[$value][0], $template_type_map[$value][1]); - } + if (is_numeric($value[0])) { + throw new \Psalm\Exception\TypeParseTreeException('First character of type cannot be numeric'); + } - return new TNamedObject($value); + if (isset($template_type_map[$value])) { + return new TGenericParam($value, $template_type_map[$value][0], $template_type_map[$value][1]); } + + return new TNamedObject($value); } /** diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 1b0fbd63926..c37ce2df5a9 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -355,7 +355,7 @@ public static function reconcileTypes( } } - return Type::parseString($new_var_type, false, $template_type_map); + return Type::parseString($new_var_type, null, $template_type_map); } return Type::getMixed(); @@ -886,7 +886,7 @@ public static function reconcileTypes( $existing_has_string = $existing_var_type->hasString(); if ($existing_has_object && !$existing_has_string) { - $new_type = Type::parseString($new_var_type, false, $template_type_map); + $new_type = Type::parseString($new_var_type, null, $template_type_map); } elseif ($existing_has_string && !$existing_has_object) { if (!$allow_string_comparison && $code_location) { if (IssueBuffer::accepts( @@ -932,7 +932,7 @@ public static function reconcileTypes( ) ) ) { - $new_type_part = Atomic::create($new_var_type, false, $template_type_map); + $new_type_part = Atomic::create($new_var_type, null, $template_type_map); $acceptable_atomic_types = []; @@ -983,7 +983,7 @@ public static function reconcileTypes( } } elseif (substr($new_var_type, 0, 9) === 'getclass-') { $new_var_type = substr($new_var_type, 9); - $new_type = Type::parseString($new_var_type, false, $template_type_map); + $new_type = Type::parseString($new_var_type, null, $template_type_map); } else { $bracket_pos = strpos($new_var_type, '('); @@ -1000,7 +1000,7 @@ public static function reconcileTypes( ); } - $new_type = Type::parseString($new_var_type, false, $template_type_map); + $new_type = Type::parseString($new_var_type, null, $template_type_map); } if ($existing_var_type->hasMixed()) { @@ -1033,7 +1033,7 @@ public static function reconcileTypes( } } - $new_type_part = Atomic::create($new_var_type, false, $template_type_map); + $new_type_part = Atomic::create($new_var_type, null, $template_type_map); if ($new_type_part instanceof TNamedObject && (($new_type_has_interface diff --git a/src/psalm.php b/src/psalm.php index 1bb06aefb88..2586efd8daa 100644 --- a/src/psalm.php +++ b/src/psalm.php @@ -49,6 +49,7 @@ 'update-baseline', 'use-ini-defaults', 'version', + 'php-version:', ]; gc_collect_cycles(); @@ -464,6 +465,14 @@ function ($arg) { !isset($options['show-snippet']) || $options['show-snippet'] !== "false" ); +if (isset($options['php-version'])) { + if (!is_string($options['php-version'])) { + die('Expecting a version number in the format x.y' . PHP_EOL); + } + + $project_analyzer->setPhpVersion($options['php-version']); +} + $project_analyzer->getCodebase()->diff_methods = isset($options['diff-methods']); $start_time = microtime(true); diff --git a/src/psalter.php b/src/psalter.php index bd58b07f14e..a05b7ddec60 100644 --- a/src/psalter.php +++ b/src/psalter.php @@ -160,15 +160,12 @@ $keyed_issues = []; } -$php_major_version = PHP_MAJOR_VERSION; -$php_minor_version = PHP_MINOR_VERSION; - if (isset($options['php-version'])) { - if (!is_string($options['php-version']) || !preg_match('/^(5\.[456]|7\.[012])$/', $options['php-version'])) { + if (!is_string($options['php-version'])) { die('Expecting a version number in the format x.y' . PHP_EOL); } - list($php_major_version, $php_minor_version) = explode('.', $options['php-version']); + $project_analyzer->setPhpVersion($options['php-version']); } $plugins = []; @@ -187,8 +184,6 @@ } $project_analyzer->alterCodeAfterCompletion( - (int) $php_major_version, - (int) $php_minor_version, array_key_exists('dry-run', $options), array_key_exists('safe-types', $options) ); diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 9fa03f38928..b62fcc649d7 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -128,7 +128,7 @@ public function __construct($a) { } }', ], - 'PHP7-subclassOfInvalidArgumentExceptionWithSimplerArg' => [ + 'subclassOfInvalidArgumentExceptionWithSimplerArg' => [ 'getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'SKIPPED-') !== false) { + if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } @@ -69,7 +63,7 @@ public function testValidCode($input_code, $output_code, $php_version, array $is $input_code ); - list($php_major_version, $php_minor_version) = explode('.', $php_version); + $this->project_analyzer->setPhpVersion($php_version); $keyed_issues_to_fix = []; @@ -79,8 +73,6 @@ public function testValidCode($input_code, $output_code, $php_version, array $is $this->project_analyzer->setIssuesToFix($keyed_issues_to_fix); $this->project_analyzer->alterCodeAfterCompletion( - (int) $php_major_version, - (int) $php_minor_version, false, $safe_types ); diff --git a/tests/FileReferenceTest.php b/tests/FileReferenceTest.php index 89729825a4f..360a1faa77d 100644 --- a/tests/FileReferenceTest.php +++ b/tests/FileReferenceTest.php @@ -43,13 +43,7 @@ public function setUp() public function testReferenceLocations($input_code, $symbol, $expected_locations) { $test_name = $this->getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'SKIPPED-') !== false) { + if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } @@ -94,13 +88,7 @@ public function testReferenceLocations($input_code, $symbol, $expected_locations public function testReferencedMethods($input_code, array $expected_referenced_methods) { $test_name = $this->getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'SKIPPED-') !== false) { + if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 2e57d6e5b27..a600211907e 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1412,6 +1412,42 @@ class Foo {} '$d' => 'float', '$e' => 'int|float', ], + ], + 'hashInit70' => [ + ' 'resource', + ], + [], + '7.1' + ], + 'hashInit71' => [ + ' 'resource', + ], + [], + '7.1' + ], + 'hashInit72' => [ + ' 'HashContext', + ], + [], + '7.2' + ], + 'hashInit73' => [ + ' 'HashContext', + ], + [], + '7.3' ] ]; } diff --git a/tests/InterfaceTest.php b/tests/InterfaceTest.php index 3d5802034f9..f35f68290ad 100644 --- a/tests/InterfaceTest.php +++ b/tests/InterfaceTest.php @@ -397,7 +397,7 @@ function takesCollection(Collection $c): void { function takesIterable(iterable $i): void {}', ], - 'PHP7-interfaceInstanceofInterfaceOrClass' => [ + 'interfaceInstanceofInterfaceOrClass' => [ ' [ ' 'string|mixed', ], 'error_levels' => [], - 'scope_vars' => [ - '$foo' => \Psalm\Type::getArray(), - ], ], 'issetKeyedOffsetORFalse' => [ ' [ ' [ '$foo[\'a\']' => 'string|mixed', ], 'error_levels' => ['MixedAssignment'], - 'scope_vars' => [ - '$foo' => \Psalm\Type::getArray(), - ], ], 'noRedundantConditionOnMixed' => [ ' 'InvalidArgument', ], + 'voidDoesntWorkIn70' => [ + ' 'ReservedWord', + [], + false, + '7.0' + ], + 'objectDoesntWorkIn71' => [ + ' 'ReservedWord', + [], + false, + '7.0' + ], ]; } } diff --git a/tests/Traits/InvalidCodeAnalysisTestTrait.php b/tests/Traits/InvalidCodeAnalysisTestTrait.php index 0b21bc2366c..6b6ce62b3d4 100644 --- a/tests/Traits/InvalidCodeAnalysisTestTrait.php +++ b/tests/Traits/InvalidCodeAnalysisTestTrait.php @@ -22,16 +22,15 @@ abstract public function providerInvalidCodeParse(); * * @return void */ - public function testInvalidCode($code, $error_message, $error_levels = [], $strict_mode = false) - { + public function testInvalidCode( + $code, + $error_message, + $error_levels = [], + $strict_mode = false, + string $php_version = '7.3' + ) { $test_name = $this->getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'PHP71-') !== false) { + if (strpos($test_name, 'PHP71-') !== false) { if (version_compare(PHP_VERSION, '7.1.0', '<')) { $this->markTestSkipped('Test case requires PHP 7.1.'); @@ -56,6 +55,8 @@ public function testInvalidCode($code, $error_message, $error_levels = [], $stri Config::getInstance()->setCustomErrorLevel($issue_name, $error_level); } + $this->project_analyzer->setPhpVersion($php_version); + $this->expectException('\Psalm\Exception\CodeException'); $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/'); diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index 27ddd40fe5b..6d322bc6271 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -18,22 +18,19 @@ abstract public function providerValidCodeParse(); * @param string $code * @param array $assertions * @param array $error_levels - * @param array $scope_vars * * @small * * @return void */ - public function testValidCode($code, $assertions = [], $error_levels = [], $scope_vars = []) - { + public function testValidCode( + $code, + $assertions = [], + $error_levels = [], + string $php_version = '7.3' + ) { $test_name = $this->getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'PHP71-') !== false) { + if (strpos($test_name, 'PHP71-') !== false) { if (version_compare(PHP_VERSION, '7.1.0', '<')) { $this->markTestSkipped('Test case requires PHP 7.1.'); @@ -55,9 +52,8 @@ public function testValidCode($code, $assertions = [], $error_levels = [], $scop } $context = new Context(); - foreach ($scope_vars as $var => $value) { - $context->vars_in_scope[$var] = $value; - } + + $this->project_analyzer->setPhpVersion($php_version); $file_path = self::$src_dir_path . 'somefile.php'; diff --git a/tests/TryCatchTest.php b/tests/TryCatchTest.php index 74c60292cfd..f626f7a78e8 100644 --- a/tests/TryCatchTest.php +++ b/tests/TryCatchTest.php @@ -12,7 +12,7 @@ class TryCatchTest extends TestCase public function providerValidCodeParse() { return [ - 'PHP7-addThrowableInterfaceType' => [ + 'addThrowableInterfaceType' => [ 'getMessage(); }', ], - 'PHP7-rethrowInterfaceExceptionWithoutInvalidThrow' => [ + 'rethrowInterfaceExceptionWithoutInvalidThrow' => [ '> - */ - public function providerTestValidCallMapType() - { - return \Psalm\Internal\Codebase\CallMap::getCallMap(); + if ($param_type_4 && $param_type_4 !== 'mixed') { + \Psalm\Type::parseString($param_type_4); + } + } } } diff --git a/tests/TypeReconciliationTest.php b/tests/TypeReconciliationTest.php index 6473dcd0bac..2ddb064949c 100644 --- a/tests/TypeReconciliationTest.php +++ b/tests/TypeReconciliationTest.php @@ -319,6 +319,8 @@ class A { } class B extends A { } + $a = new A(); + $out = null; if ($a instanceof B) { @@ -330,10 +332,6 @@ class B extends A { } 'assertions' => [ '$out' => 'null|A', ], - 'error_levels' => [], - 'scope_vars' => [ - '$a' => Type::parseString('A'), - ], ], 'notInstanceOfProperty' => [ ' 'null|B', ], 'error_levels' => [], - 'scope_vars' => [ - '$a' => Type::parseString('A'), - ], ], 'notInstanceOfPropertyElseif' => [ 'foo)) { @@ -394,9 +391,6 @@ class A { '$out' => 'null|B', ], 'error_levels' => [], - 'scope_vars' => [ - '$a' => Type::parseString('A'), - ], ], 'typeArguments' => [ 'getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'SKIPPED-') !== false) { + if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 5604f8b8480..1a8d0927600 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -42,13 +42,7 @@ public function setUp() public function testValidCode($code, array $error_levels = []) { $test_name = $this->getTestName(); - if (strpos($test_name, 'PHP7-') !== false) { - if (version_compare(PHP_VERSION, '7.0.0dev', '<')) { - $this->markTestSkipped('Test case requires PHP 7.'); - - return; - } - } elseif (strpos($test_name, 'SKIPPED-') !== false) { + if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); }