diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 13219480f64..81030f8654d 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -1,4 +1,4 @@ -name: Pull Request Labels +name: Pull Request Labels (to be added by maintainers) on: pull_request: types: [opened, reopened, labeled, unlabeled, synchronize] diff --git a/composer.json b/composer.json index c0b00c5b68a..d8a80df72df 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,7 @@ "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.16", - "sebastian/diff": "^4.0 || ^5.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", "spatie/array-to-xml": "^2.17.0 || ^3.0", "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" @@ -94,12 +94,6 @@ "Psalm\\Tests\\": "tests/" } }, - "repositories": [ - { - "type": "path", - "url": "examples/plugins/composer-based/echo-checker" - } - ], "minimum-stability": "dev", "prefer-stable": true, "bin": [ diff --git a/config.xsd b/config.xsd index 0f3e88916c8..c15a74e6080 100644 --- a/config.xsd +++ b/config.xsd @@ -42,6 +42,7 @@ + @@ -283,6 +284,7 @@ + @@ -318,6 +320,7 @@ + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index cec0545f126..191c7a6a54a 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1590,7 +1590,7 @@ 'DirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'DirectoryIterator::getPathname' => ['string'], 'DirectoryIterator::getPerms' => ['int'], -'DirectoryIterator::getRealPath' => ['string'], +'DirectoryIterator::getRealPath' => ['non-falsy-string'], 'DirectoryIterator::getSize' => ['int'], 'DirectoryIterator::getType' => ['string'], 'DirectoryIterator::isDir' => ['bool'], @@ -2792,7 +2792,7 @@ 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'length='=>'?int'], -'file_put_contents' => ['int|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], +'file_put_contents' => ['int<0, max>|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], 'fileatime' => ['int|false', 'filename'=>'string'], 'filectime' => ['int|false', 'filename'=>'string'], 'filegroup' => ['int|false', 'filename'=>'string'], @@ -2827,7 +2827,7 @@ 'FilesystemIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'FilesystemIterator::getPathname' => ['string'], 'FilesystemIterator::getPerms' => ['int'], -'FilesystemIterator::getRealPath' => ['string'], +'FilesystemIterator::getRealPath' => ['non-falsy-string'], 'FilesystemIterator::getSize' => ['int'], 'FilesystemIterator::getType' => ['string'], 'FilesystemIterator::isDir' => ['bool'], @@ -3296,7 +3296,7 @@ 'get_resource_type' => ['string', 'resource'=>'resource'], 'get_resources' => ['array', 'type='=>'?string'], 'getallheaders' => ['array|false'], -'getcwd' => ['string|false'], +'getcwd' => ['non-falsy-string|false'], 'getdate' => ['array{seconds: int<0, 59>, minutes: int<0, 59>, hours: int<0, 23>, mday: int<1, 31>, wday: int<0, 6>, mon: int<1, 12>, year: int, yday: int<0, 365>, weekday: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday", month: "January"|"February"|"March"|"April"|"May"|"June"|"July"|"August"|"September"|"October"|"November"|"December", 0: int}', 'timestamp='=>'?int'], 'getenv' => ['string|false', 'name'=>'string', 'local_only='=>'bool'], 'getenv\'1' => ['array'], @@ -3343,7 +3343,7 @@ 'GlobIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'GlobIterator::getPathname' => ['string'], 'GlobIterator::getPerms' => ['int'], -'GlobIterator::getRealPath' => ['string|false'], +'GlobIterator::getRealPath' => ['non-falsy-string|false'], 'GlobIterator::getSize' => ['int'], 'GlobIterator::getType' => ['string|false'], 'GlobIterator::isDir' => ['bool'], @@ -5700,10 +5700,10 @@ 'ini_restore' => ['void', 'option'=>'string'], 'ini_parse_quantity' => ['int', 'shorthand'=>'non-empty-string'], 'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string|int|float|bool|null'], -'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], +'inotify_add_watch' => ['int|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], -'inotify_read' => ['array|false', 'inotify_instance'=>'resource'], +'inotify_read' => ['array{wd: int, mask: int, cookie: int, name: string}[]|false', 'inotify_instance'=>'resource'], 'inotify_rm_watch' => ['bool', 'inotify_instance'=>'resource', 'watch_descriptor'=>'int'], 'intdiv' => ['int', 'num1'=>'int', 'num2'=>'int'], 'interface_exists' => ['bool', 'interface'=>'string', 'autoload='=>'bool'], @@ -9722,8 +9722,8 @@ 'readline_read_history' => ['bool', 'filename='=>'?string'], 'readline_redisplay' => ['void'], 'readline_write_history' => ['bool', 'filename='=>'?string'], -'readlink' => ['string|false', 'path'=>'string'], -'realpath' => ['string|false', 'path'=>'string'], +'readlink' => ['non-falsy-string|false', 'path'=>'string'], +'realpath' => ['non-falsy-string|false', 'path'=>'string'], 'realpath_cache_get' => ['array'], 'realpath_cache_size' => ['int'], 'recode' => ['string', 'request'=>'string', 'string'=>'string'], @@ -9811,7 +9811,7 @@ 'RecursiveDirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'?class-string'], 'RecursiveDirectoryIterator::getPathname' => ['string'], 'RecursiveDirectoryIterator::getPerms' => ['int'], -'RecursiveDirectoryIterator::getRealPath' => ['string'], +'RecursiveDirectoryIterator::getRealPath' => ['non-falsy-string'], 'RecursiveDirectoryIterator::getSize' => ['int'], 'RecursiveDirectoryIterator::getSubPath' => ['string'], 'RecursiveDirectoryIterator::getSubPathname' => ['string'], @@ -12238,7 +12238,7 @@ 'SplFileInfo::getPathInfo' => ['SplFileInfo|null', 'class='=>'?class-string'], 'SplFileInfo::getPathname' => ['string'], 'SplFileInfo::getPerms' => ['int|false'], -'SplFileInfo::getRealPath' => ['string|false'], +'SplFileInfo::getRealPath' => ['non-falsy-string|false'], 'SplFileInfo::getSize' => ['int|false'], 'SplFileInfo::getType' => ['string|false'], 'SplFileInfo::isDir' => ['bool'], @@ -12288,7 +12288,7 @@ 'SplFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'?class-string'], 'SplFileObject::getPathname' => ['string'], 'SplFileObject::getPerms' => ['int|false'], -'SplFileObject::getRealPath' => ['false|string'], +'SplFileObject::getRealPath' => ['false|non-falsy-string'], 'SplFileObject::getSize' => ['int|false'], 'SplFileObject::getType' => ['string|false'], 'SplFileObject::hasChildren' => ['false'], @@ -12475,7 +12475,7 @@ 'SplTempFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'?class-string'], 'SplTempFileObject::getPathname' => ['string'], 'SplTempFileObject::getPerms' => ['int|false'], -'SplTempFileObject::getRealPath' => ['false|string'], +'SplTempFileObject::getRealPath' => ['false|non-falsy-string'], 'SplTempFileObject::getSize' => ['int|false'], 'SplTempFileObject::getType' => ['string|false'], 'SplTempFileObject::hasChildren' => ['false'], @@ -12690,8 +12690,8 @@ 'ssh2_sftp_chmod' => ['bool', 'sftp'=>'resource', 'filename'=>'string', 'mode'=>'int'], 'ssh2_sftp_lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_mkdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string', 'mode='=>'int', 'recursive='=>'bool'], -'ssh2_sftp_readlink' => ['string|false', 'sftp'=>'resource', 'link'=>'string'], -'ssh2_sftp_realpath' => ['string|false', 'sftp'=>'resource', 'filename'=>'string'], +'ssh2_sftp_readlink' => ['non-falsy-string|false', 'sftp'=>'resource', 'link'=>'string'], +'ssh2_sftp_realpath' => ['non-falsy-string|false', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_sftp_rename' => ['bool', 'sftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ssh2_sftp_rmdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string'], 'ssh2_sftp_stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 61e3db09d55..7a18de3c4f2 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -877,7 +877,7 @@ 'DirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'DirectoryIterator::getPathname' => ['string'], 'DirectoryIterator::getPerms' => ['int'], - 'DirectoryIterator::getRealPath' => ['string'], + 'DirectoryIterator::getRealPath' => ['non-falsy-string'], 'DirectoryIterator::getSize' => ['int'], 'DirectoryIterator::getType' => ['string'], 'DirectoryIterator::isDir' => ['bool'], @@ -1507,7 +1507,7 @@ 'FilesystemIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'FilesystemIterator::getPathname' => ['string'], 'FilesystemIterator::getPerms' => ['int'], - 'FilesystemIterator::getRealPath' => ['string'], + 'FilesystemIterator::getRealPath' => ['non-falsy-string'], 'FilesystemIterator::getSize' => ['int'], 'FilesystemIterator::getType' => ['string'], 'FilesystemIterator::isDir' => ['bool'], @@ -1762,7 +1762,7 @@ 'GlobIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'GlobIterator::getPathname' => ['string'], 'GlobIterator::getPerms' => ['int'], - 'GlobIterator::getRealPath' => ['string|false'], + 'GlobIterator::getRealPath' => ['non-falsy-string|false'], 'GlobIterator::getSize' => ['int'], 'GlobIterator::getType' => ['string|false'], 'GlobIterator::isDir' => ['bool'], @@ -5155,7 +5155,7 @@ 'RecursiveDirectoryIterator::getPathInfo' => ['?SplFileInfo', 'class='=>'class-string'], 'RecursiveDirectoryIterator::getPathname' => ['string'], 'RecursiveDirectoryIterator::getPerms' => ['int'], - 'RecursiveDirectoryIterator::getRealPath' => ['string'], + 'RecursiveDirectoryIterator::getRealPath' => ['non-falsy-string'], 'RecursiveDirectoryIterator::getSize' => ['int'], 'RecursiveDirectoryIterator::getSubPath' => ['string'], 'RecursiveDirectoryIterator::getSubPathname' => ['string'], @@ -7481,7 +7481,7 @@ 'SplFileInfo::getPathInfo' => ['SplFileInfo|null', 'class='=>'class-string'], 'SplFileInfo::getPathname' => ['string'], 'SplFileInfo::getPerms' => ['int|false'], - 'SplFileInfo::getRealPath' => ['string|false'], + 'SplFileInfo::getRealPath' => ['non-falsy-string|false'], 'SplFileInfo::getSize' => ['int|false'], 'SplFileInfo::getType' => ['string|false'], 'SplFileInfo::isDir' => ['bool'], @@ -7532,7 +7532,7 @@ 'SplFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'class-string'], 'SplFileObject::getPathname' => ['string'], 'SplFileObject::getPerms' => ['int|false'], - 'SplFileObject::getRealPath' => ['false|string'], + 'SplFileObject::getRealPath' => ['false|non-falsy-string'], 'SplFileObject::getSize' => ['int|false'], 'SplFileObject::getType' => ['string|false'], 'SplFileObject::hasChildren' => ['false'], @@ -7723,7 +7723,7 @@ 'SplTempFileObject::getPathInfo' => ['SplFileInfo|null', 'class='=>'class-string'], 'SplTempFileObject::getPathname' => ['string'], 'SplTempFileObject::getPerms' => ['int|false'], - 'SplTempFileObject::getRealPath' => ['false|string'], + 'SplTempFileObject::getRealPath' => ['false|non-falsy-string'], 'SplTempFileObject::getSize' => ['int|false'], 'SplTempFileObject::getType' => ['string|false'], 'SplTempFileObject::hasChildren' => ['false'], @@ -10417,7 +10417,7 @@ 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], 'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'length='=>'int'], - 'file_put_contents' => ['int|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], + 'file_put_contents' => ['int<0, max>|false', 'filename'=>'string', 'data'=>'string|resource|array', 'flags='=>'int', 'context='=>'resource'], 'fileatime' => ['int|false', 'filename'=>'string'], 'filectime' => ['int|false', 'filename'=>'string'], 'filegroup' => ['int|false', 'filename'=>'string'], @@ -10661,7 +10661,7 @@ 'get_resource_type' => ['string', 'resource'=>'resource'], 'get_resources' => ['array', 'type='=>'string'], 'getallheaders' => ['array|false'], - 'getcwd' => ['string|false'], + 'getcwd' => ['non-falsy-string|false'], 'getdate' => ['array{seconds: int<0, 59>, minutes: int<0, 59>, hours: int<0, 23>, mday: int<1, 31>, wday: int<0, 6>, mon: int<1, 12>, year: int, yday: int<0, 365>, weekday: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday", month: "January"|"February"|"March"|"April"|"May"|"June"|"July"|"August"|"September"|"October"|"November"|"December", 0: int}', 'timestamp='=>'int'], 'getenv' => ['string|false', 'name'=>'string', 'local_only='=>'bool'], 'gethostbyaddr' => ['string|false', 'ip'=>'string'], @@ -11832,10 +11832,10 @@ 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'option'=>'string'], 'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string'], - 'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], + 'inotify_add_watch' => ['int|false', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], - 'inotify_read' => ['array|false', 'inotify_instance'=>'resource'], + 'inotify_read' => ['array{wd: int, mask: int, cookie: int, name: string}[]|false', 'inotify_instance'=>'resource'], 'inotify_rm_watch' => ['bool', 'inotify_instance'=>'resource', 'watch_descriptor'=>'int'], 'intdiv' => ['int', 'num1'=>'int', 'num2'=>'int'], 'interface_exists' => ['bool', 'interface'=>'string', 'autoload='=>'bool'], @@ -13718,8 +13718,8 @@ 'readline_read_history' => ['bool', 'filename='=>'string'], 'readline_redisplay' => ['void'], 'readline_write_history' => ['bool', 'filename='=>'string'], - 'readlink' => ['string|false', 'path'=>'string'], - 'realpath' => ['string|false', 'path'=>'string'], + 'readlink' => ['non-falsy-string|false', 'path'=>'string'], + 'realpath' => ['non-falsy-string|false', 'path'=>'string'], 'realpath_cache_get' => ['array'], 'realpath_cache_size' => ['int'], 'recode' => ['string', 'request'=>'string', 'string'=>'string'], @@ -14120,8 +14120,8 @@ 'ssh2_sftp_chmod' => ['bool', 'sftp'=>'resource', 'filename'=>'string', 'mode'=>'int'], 'ssh2_sftp_lstat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], 'ssh2_sftp_mkdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string', 'mode='=>'int', 'recursive='=>'bool'], - 'ssh2_sftp_readlink' => ['string|false', 'sftp'=>'resource', 'link'=>'string'], - 'ssh2_sftp_realpath' => ['string|false', 'sftp'=>'resource', 'filename'=>'string'], + 'ssh2_sftp_readlink' => ['non-falsy-string|false', 'sftp'=>'resource', 'link'=>'string'], + 'ssh2_sftp_realpath' => ['non-falsy-string|false', 'sftp'=>'resource', 'filename'=>'string'], 'ssh2_sftp_rename' => ['bool', 'sftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ssh2_sftp_rmdir' => ['bool', 'sftp'=>'resource', 'dirname'=>'string'], 'ssh2_sftp_stat' => ['array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', 'sftp'=>'resource', 'path'=>'string'], diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php index d3a3f7ce0a3..25f94ef5298 100644 --- a/dictionaries/ImpureFunctionsList.php +++ b/dictionaries/ImpureFunctionsList.php @@ -85,6 +85,7 @@ 'ob_end_clean' => true, 'ob_get_clean' => true, 'readfile' => true, + 'readgzfile' => true, 'printf' => true, 'var_dump' => true, 'phpinfo' => true, diff --git a/dictionaries/PropertyMap.php b/dictionaries/PropertyMap.php index c5755235e91..521a1e7e34f 100644 --- a/dictionaries/PropertyMap.php +++ b/dictionaries/PropertyMap.php @@ -113,6 +113,7 @@ 'formatOutput' => 'bool', 'implementation' => 'DOMImplementation', 'lastElementChild' => 'DOMElement|null', + 'ownerDocument' => 'null', 'preserveWhiteSpace' => 'bool', 'recover' => 'bool', 'resolveExternals' => 'bool', @@ -173,7 +174,7 @@ 'nodeName' => 'string', 'nodeType' => 'int', 'nodeValue' => 'string|null', - 'ownerDocument' => 'DOMDocument|null', + 'ownerDocument' => 'DOMDocument', 'parentNode' => 'DOMNode|null', 'prefix' => 'string', 'previousSibling' => 'DOMNode|null', diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index f4976aaad83..d00baae88d3 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -273,6 +273,14 @@ When `true`, Psalm will complain when referencing an explicit string offset on a ``` When `true`, Psalm will complain when referencing an explicit integer offset on an array e.g. `$arr[7]` without a user first asserting that it exists (either via an `isset` check or via an object-like array). Defaults to `false`. +#### ensureOverrideAttribute +```xml + +``` +When `true`, Psalm will report class and interface methods that override a method on a parent, but do not have an `Override` attribute. Defaults to `false`. + #### phpVersion ```xml - - - - mapper]]> - - - - - $items - - + - - $returnType - attrGroups]]> - byRef]]> - expr]]> - params]]> - returnType]]> - static]]> - - - - $returnType - attrGroups]]> - byRef]]> - params]]> - returnType]]> - static]]> - stmts]]> - uses]]> - - - - - $items - - - - - $parts - - - - - $conds - - - - - $parts - $parts - $parts - - - - - $stmts - - - - - $stmts - - - - - $returnType - attrGroups]]> - byRef]]> - flags]]> - params]]> - returnType]]> - stmts]]> - - - - - attrGroups]]> - extends]]> - flags]]> - implements]]> - stmts]]> - - - - - $stmts - - - - - $stmts - - - - - $stmts - - - - - $stmts - - - - - cond]]> - init]]> - loop]]> - stmts]]> - - - - - byRef]]> - keyVar]]> - stmts]]> - - - - - $returnType - attrGroups]]> - byRef]]> - params]]> - returnType]]> - stmts]]> - - - - - else]]> - elseifs]]> - stmts]]> - - - - - attrGroups]]> - extends]]> - stmts]]> - - - - - $stmts - - - - - attrGroups]]> - stmts]]> - - - - - $stmts - - - - - $stmts - - - - - static::getDefaultDescription() - static::getDefaultDescription() - static::getDefaultDescription() - static::getDefaultName() - static::getDefaultName() - static::getDefaultName() - - - $name - - tags['variablesfrom'][0]]]> - $matches[1] + tags['variablesfrom'][0]]]> - $matches[1] + @@ -200,7 +24,7 @@ - !$appearing_method_id + @@ -214,19 +38,19 @@ - $const_name - $const_name - $symbol_name - $symbol_parts[1] + + + + - !$function_name + namespace]]> namespace]]> namespace]]> namespace_first_stmt_start]]> uses_end]]> - $file_path + insertText]]> symbol, '()')]]> symbol, '()')]]> @@ -247,16 +71,16 @@ - !$composer_json - !$config_path - !$file_path + + + - $cwd - $dir + + function_id]]> - $issue_handler_children - $parent_issue_type + + composer_class_loader->findFile($pluginClassName)]]> autoloader]]> localName, $offset)]]> @@ -265,7 +89,7 @@ - $suggested_dir + file_path, 'stub')]]> file_path, 'vendor')]]> @@ -275,10 +99,10 @@ - !$directory_path - !$file_path - !$glob_directory_path - !$glob_file_path + + + + directory]]> file]]> referencedClass]]> @@ -287,8 +111,8 @@ referencedMethod]]> referencedProperty]]> referencedVariable]]> - glob($parts[0], GLOB_NOSORT) - glob($parts[0], GLOB_ONLYDIR | GLOB_NOSORT) + + @@ -299,15 +123,15 @@ - $matches[1] - $matches[2] - $matches[3] + + + - $creating_conditional_id - $creating_conditional_id + + @@ -317,23 +141,23 @@ - $comments[0] - $property_name + + props[0]]]> - $uninitialized_variables[0] + - !$declaring_property_class - !$fq_class_name + + self]]> self]]> self]]> self]]> template_extended_params]]> template_types]]> - $class_template_params + initialized_class]]> - $parent_fq_class_name + getStmts()]]> getStmts()]]> template_extended_params]]> @@ -349,15 +173,15 @@ - $property_name + - !$appearing_property_class + self]]> - !$declaring_property_class + self]]> template_types]]> - $resolved_name + template_covariants]]> template_extended_params]]> template_types]]> @@ -373,13 +197,13 @@ - !$original_type + description]]> var_id]]> - !$var_type_tokens - $brackets - $template_type_map - $type_aliases + + + + line_number]]> type_end]]> type_start]]> @@ -387,27 +211,27 @@ - $namespace_name - $namespace_name + + root_file_name]]> root_file_path]]> - $namespace - $namespace + + getNamespace()]]> getStmts()]]> - $class_template_params + self]]> self]]> - $fq_class_name - $self_fq_class_name + + @@ -418,24 +242,24 @@ template_types]]> template_types]]> - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id + + + + + self]]> self]]> self]]> self]]> self]]> - $context_self - $hash - $namespace - $parent_fqcln - $parent_fqcln + + + + + cased_name]]> template_types]]> - $template_types + function->getStmts()]]> source->getTemplateTypeMap()]]> storage->template_types]]> @@ -444,12 +268,12 @@ - !$calling_method_id + self]]> - $appearing_method_class - $appearing_method_class + + self]]> - $context_self + @@ -466,16 +290,16 @@ - $destination_parts[1] - $destination_parts[1] - $destination_parts[1] - $php_minor_version - $source_parts[1] + + + + + self]]> - $potential_file_path + @@ -494,21 +318,21 @@ - if (AtomicTypeComparator::isContainedBy( - if (AtomicTypeComparator::isContainedBy( + + var_id]]> var_id]]> - $calling_type_params + branch_point]]> template_types]]> getTemplateTypeMap()]]> line_number]]> type_end]]> type_start]]> - $var_id - $var_id + + @@ -544,13 +368,13 @@ assigned_var_ids += $switch_scope->new_assigned_var_ids]]> - !$switch_var_id + new_assigned_var_ids]]> new_vars_in_scope]]> possibly_redefined_vars]]> possibly_redefined_vars]]> redefined_vars]]> - $switch_var_id + @@ -561,11 +385,11 @@ branch_point]]> - $nested_or_options - $switch_var_id - $switch_var_id - $switch_var_id - $type_statements + + + + + @@ -580,7 +404,7 @@ - $var_id + @@ -615,135 +439,135 @@ getArgs()[0]]]> - !$var_name - !$var_type + + ')]]> - $array_root - $count_equality_position - $count_equality_position - $count_equality_position - $count_inequality_position - $count_inequality_position - $count_inequality_position - $false_position - $false_position - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name - $first_var_name_in_array_argument - $get_debug_type_position - $get_debug_type_position - $getclass_position - $getclass_position - $gettype_position - $gettype_position - $if_false_assertions - $if_true_assertions - $inferior_value_position - $other_var_name - $superior_value_position - $this_class_name - $this_class_name - $this_class_name - $true_position - $true_position - $typed_value_position - $typed_value_position - $var_id - $var_id - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name - $var_name_left - $var_name_right - $var_type - $var_type - $var_type - self::hasReconcilableNonEmptyCountEqualityCheck($conditional) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - !$parent_var_id - $object_id - $parent_var_id - $parent_var_id - $root_var_id - $root_var_id - $root_var_id - $root_var_id - $root_var_id - $var_id - $var_var_id + + + + + + + + + + + self]]> - !$var_id - $appearing_property_class - $class_template_params - $class_template_params + + + + calling_method_id]]> calling_method_id]]> self]]> self]]> self]]> - $declaring_property_class + getter_method]]> - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_property_id - $var_property_id + + + + + + + + + + + calling_method_id, '::__clone')]]> calling_method_id, '::__construct')]]> calling_method_id, '::__unserialize')]]> @@ -752,12 +576,12 @@ - $new_property_name + calling_method_id]]> - $var_id - $var_id + + @@ -766,30 +590,30 @@ ')]]> ')]]> - $assign_value_id + calling_method_id]]> - $extended_var_id - $extended_var_id - $extended_var_id - $extended_var_id - $extended_var_id - $list_var_id - $list_var_id - $list_var_id - $prop_name - $root_var_id + + + + + + + + + + line_number]]> type_end]]> type_start]]> - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id + + + + + + + + + vars_in_scope[$lhs_var_id] = &$context->vars_in_scope[$rhs_var_id]]]> @@ -802,39 +626,39 @@ - $invalid_left_messages[0] - $invalid_right_messages[0] + + branch_point]]> - $var_id + - verifyType + - $method_name - $parts[1] + + - !$container_class - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $class_generic_params + + + + + + + calling_function_id]]> calling_function_id]]> calling_method_id]]> - $self_fq_class_name - $static_fq_class_name - $var_id + + + value, '::')]]> value, '::')]]> @@ -842,12 +666,12 @@ self]]> - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id - $cased_method_id + + + + + + calling_method_id]]> calling_method_id]]> calling_method_id]]> @@ -856,48 +680,48 @@ calling_method_id]]> calling_method_id]]> sinks]]> - $function_params - $function_params - $function_params + + + template_types]]> - $method_id - $method_id - $method_id - $method_id - $var_id - $var_id - $var_id + + + + + + + getFQCLN())]]> - $args[0] - $args[0] - $args[1] - $method_name + + + + - !$container_class + calling_method_id]]> - $var_id + - !$template_types - !$template_types + + template_types]]> - $method_name - $overridden_template_types + + template_extended_params]]> template_types]]> - $function_name - $function_name + + getArgs()[0]->value]]> @@ -906,7 +730,7 @@ getArgs()[0]]]> - $parts[1] + function_id]]> @@ -921,7 +745,7 @@ - $method + self]]> @@ -940,23 +764,23 @@ calling_method_id]]> calling_method_id]]> self]]> - $lhs_var_id - $mixin_class_template_params + + - $class_template_params + calling_method_id]]> calling_method_id]]> - $lhs_var_id + template_types]]> template_types]]> - $caller_identifier + @@ -967,26 +791,26 @@ specialization_key]]> - $var_id + self]]> self]]> - $appearing_method_name + - $found_generic_params - $found_generic_params - $found_generic_params - $found_generic_params - $found_generic_params - $found_generic_params - $intersection_method_id - $intersection_method_id + + + + + + + + @@ -1000,16 +824,16 @@ getFQCLN()]]> - $lhs_var_id - $lhs_var_id - $lhs_var_id + + + getFQCLN()]]> - $path_to_file - $var_id + + ')]]> @@ -1018,8 +842,8 @@ calling_method_id]]> self]]> - $fq_class_name - $fq_class_name + + getFullyQualifiedFunctionMethodOrNamespaceName()]]> template_extended_params]]> template_types]]> @@ -1030,7 +854,7 @@ parent_class]]> - $child_fq_class_name + calling_method_id]]> self]]> self]]> @@ -1039,7 +863,7 @@ self]]> - !$fq_class_name + mixin_declaring_fqcln]]> parent_class]]> parent_class]]> @@ -1050,15 +874,15 @@ - $new_method_name + self]]> self]]> self]]> self]]> - $found_generic_params - $found_generic_params + + template_extended_params]]> @@ -1068,9 +892,9 @@ items[1]]]> - !$arg_var_id - $arg_var_id - $assertion_var_id + + + template_extended_params]]> self]]> self]]> @@ -1081,8 +905,8 @@ - $new_const_name - $new_const_name + + self]]> @@ -1096,101 +920,101 @@ - !$lhs_var_name - !$object_id - !$object_id - !$this_class_name - $object_id - $property_root - $resolved_name - $resolved_name - $root_var_id - $this_class_name + + + + + + + + + + - $stmt_type - $stmt_type - $stmt_type + + + - $dim_var_id - $dim_var_id - $extended_var_id - $extended_var_id - $keyed_array_var_id - $keyed_array_var_id - $keyed_array_var_id - $keyed_array_var_id + + + + + + + + - $stmt_type + self]]> self]]> - $declaring_property_class - $declaring_property_class + + template_types]]> template_types]]> - $var_id - $var_id - $var_property_id - $var_property_id + + + + - $invalid_fetch_types[0] + - !$prop_name + calling_method_id]]> calling_method_id]]> - $declaring_property_class - $stmt_var_id - $var_id - $var_id + + + + - $new_property_name + - !$prop_name + calling_method_id]]> calling_method_id]]> calling_method_id]]> self]]> - $string_type - $var_id - $var_id + + + - $branch_point - $branch_point + + - $var_id + - !$evaled_path - !$var_id - $include_path - $left_string - $path_to_file - $right_string - $var_id + + + + + + + @@ -1200,13 +1024,13 @@ - !$switch_var_id - $switch_var_id + + - $fq_classlike_name + @@ -1222,7 +1046,7 @@ var_id]]> - $class_template_params + declaring_yield_fqcn]]> self]]> line_number]]> @@ -1232,7 +1056,7 @@ - $method_name + calling_function_id]]> @@ -1240,7 +1064,7 @@ var_id]]> calling_function_id]]> self]]> - $found_generic_params + line_number]]> type_end]]> type_start]]> @@ -1248,20 +1072,20 @@ - $root_var_id - $var_id + + - $token_list[$iter] + - $token_list[$iter] - $token_list[$iter] - $token_list[$iter] - $token_list[$iter] - $token_list[0] + + + + + @@ -1269,23 +1093,17 @@ expr->getArgs()[0]]]> - $branch_point - $new_issues + + getNamespace()]]> - $possible_traced_variable_names + fake_this_class]]> vars_to_initialize]]> - - - UndefinedFunction - UndefinedFunction - - - !$root_path + @@ -1295,45 +1113,45 @@ error_baseline]]> - !$paths_to_check - !$root_path + + - $baseline_file_path - $cache_directory + + threads]]> - $find_references_to - empty($baselineFile) + + - !$root_path - $paths_to_check + + - $identifier_name + - !$last_arg - !$last_arg - !$last_arg - !$root_path + + + + - !$config_file - !$end_psalm_open_tag - !$path_to_check + + + error_baseline]]> - $f_paths - $path_to_config - $stdin = fgets(STDIN) + + + getPHPVersionFromComposerJson()]]> getPhpVersionFromConfig()]]> @@ -1341,7 +1159,7 @@ - $trait + @@ -1349,39 +1167,39 @@ - $destination_name - $destination_name - $destination_name - $source_const_name - $stub + + + + + - !$calling_fq_class_name - !$insert_pos - !$insert_pos - !$insert_pos - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_fq_class_name - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $file_path - $file_path - $file_path - $file_path - $file_path - $migrated_source_fqcln - $migrated_source_fqcln + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1391,50 +1209,50 @@ - $stub + - !$checked_file_path - !$root_file_path - $args + + + cased_name]]> - $namespace + - !$return_type_string + - !$calling_class_name - !$extends - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $found_generic_params - $old_method_id - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path - $source_file_path + + + + + + + + + + + + + + + + + + + + + - $mapped_name + template_extended_params]]> template_extended_params]]> template_extended_params]]> @@ -1444,12 +1262,12 @@ - $property_name - $property_name - $property_name - $property_name - $property_name - $property_name + + + + + + calling_method_id]]> @@ -1459,7 +1277,7 @@ - $composer_file_path + cased_name]]> cased_name]]> @@ -1481,17 +1299,17 @@ - $specialization_key + props[0]]]> stmts[0]]]> - $a_stmt_comments[0] + props[0]]]> stmts[0]]]> - $b_stmt_comments[0] + stmts]]> @@ -1500,7 +1318,7 @@ - $b[$y] + @@ -1510,21 +1328,21 @@ - $exploded[1] - $url + + - $var_end - $var_start + + new_php_return_type]]> - $last_arg_position + new_php_return_type]]> new_phpdoc_return_type]]> return_typehint_colon_start]]> @@ -1532,7 +1350,7 @@ return_typehint_end]]> return_typehint_start]]> return_typehint_start]]> - $php_type + new_phpdoc_return_type]]> new_psalm_return_type]]> return_type_description]]> @@ -1551,7 +1369,7 @@ typehint_end]]> typehint_start]]> typehint_start]]> - $preceding_semicolon_pos + new_phpdoc_type]]> new_psalm_type]]> type_description]]> @@ -1559,7 +1377,7 @@ - !$sockets + @@ -1569,7 +1387,7 @@ - empty($message) + @@ -1577,39 +1395,39 @@ TCPServerAddress]]> TCPServerAddress]]> onchangeLineLimit]]> - empty($additional_info) + - $method_id_parts[1] + - $arg_var_id - $arg_var_id - $left_var_id - $left_var_id - $right_var_id - $right_var_id - $var_id - $var_id + + + + + + + + - $cs[0] - $match[0] - $match[1] - $match[2] + + + + stmts[0]]]> - $replacement_stmts[0] - $replacement_stmts[0] - $replacement_stmts[0] + + + - !$method_contents + parser->parse( $hacky_class_fix, $error_handler, @@ -1622,25 +1440,25 @@ - $doc_line_parts[1] - $matches[0] + + children[0]]]> children[1]]]> - !$method_entry + - $l[4] - $r[4] + + - !$var_line_parts + newModifier]]> - $class_name + description]]> inheritors]]> yield]]> @@ -1660,10 +1478,10 @@ - $fq_classlike_name - $string_value - $string_value - $string_value + + + + @@ -1672,15 +1490,15 @@ getArgs()[1]]]> - !$skip_if_descendants - !$skip_if_descendants - $include_path - $path_to_file + + + + - $since_parts[1] + 0]]> @@ -1688,7 +1506,7 @@ - $source_param_string + namespace]]> @@ -1703,9 +1521,9 @@ template_types]]> template_types]]> template_types]]> - $template_types - $template_types - $template_types + + + @@ -1717,10 +1535,10 @@ aliases->namespace]]> aliases->namespace]]> template_types]]> - $fq_classlike_name - $function_id - $function_id - $method_name_lc + + + + stmts]]> stmts]]> stmts]]> @@ -1731,7 +1549,7 @@ - $type_string + @@ -1752,17 +1570,17 @@ - $cs[0] + - $offset_map + end_change]]> start_change]]> - $config_file_path !== null + getArgument('pluginName')]]> @@ -1771,7 +1589,7 @@ - $config_file_path !== null + getArgument('pluginName')]]> @@ -1780,7 +1598,7 @@ - $config_file_path !== null + getOption('config')]]> @@ -1788,8 +1606,8 @@ - !$path - $explicit_path + + psalm_header]]> psalm_tag_end_pos]]> @@ -1801,17 +1619,17 @@ - !$root_cache_directory - $file_contents - $file_path + + + - !$cache_directory - !$cache_directory - !$cache_directory - $cache_directory + + + + @@ -1821,88 +1639,88 @@ - !$root_cache_directory + - $result + - $called_method_name + - $extended_var_id + - !$cache_directory - !$root_cache_directory - !$root_cache_directory - !$root_cache_directory + + + + - !$cache_directory - !$cache_directory + + composer_lock_hash]]> - $cache_directory + - !$key_column_name + - $callable_extended_var_id + getTemplateTypeMap()]]> getTemplateTypeMap()]]> - $callable_method_name + - $class_strings ?: null + - $method_name + - $fetch_class_name + - !$call_args + - $existing_file_contents - $existing_file_contents - $existing_file_contents - $existing_statements - $existing_statements - $existing_statements - $existing_statements - $file_changes - $file_path + + + + + + + + + parse($file_contents, $error_handler)]]> parse($file_contents, $error_handler)]]> @@ -1910,14 +1728,14 @@ - $first_line_padding + - !$resolved_name - $mapped_type = $map[$offset_arg_value] ?? null - $mapped_type = $map[$offset_arg_value] ?? null + + + @@ -1939,19 +1757,19 @@ - $key - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id - $var_id + + + + + + + + - isContainedBy + properties[0]]]> @@ -1960,106 +1778,106 @@ - $callable + - TCallable|TClosure|null + - !$class_name - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id - $calling_method_id + + + + + + params]]> - $file_name - $file_name - $input_variadic_param_idx - $member_id + + + + - !($container_type_params_covariant[$i] ?? false) + - $intersection_container_type_lower + - $key - $key - $key + + + properties[0]]]> - $properties[0] - $properties[0] - $properties[0] + + + - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $var_id - $var_id - $var_id - $var_id + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - !$count - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $key - $var_id - $var_id - $var_id - $var_id + + + + + + + + + + + + + + + + + + + + + + @@ -2069,12 +1887,12 @@ - getClassTemplateTypes + - $input_template_types + template_extended_params[$container_class])]]> template_extended_params[$base_type->as_type->value])]]> template_extended_params[$base_type->value])]]> @@ -2113,60 +1931,60 @@ value_types['string'] instanceof TNonFalsyString ? $type->value : $type->value !== '']]> - $shared_classlikes + - $fallback_params + template_types]]> - $params - $parent_class - $self_class - $self_class - $self_class - $self_class - $self_class - $self_class - $static_class_type + + + + + + + + + - $const_name - $const_name + + children[0]]]> condition->children[0]]]> - array_keys($offset_template_data)[0] - array_keys($template_type_map[$array_param_name])[0] - array_keys($template_type_map[$class_name])[0] - array_keys($template_type_map[$fq_classlike_name])[0] - array_keys($template_type_map[$template_param_name])[0] + + + + + - $extra_params + value, '::')]]> value, '::')]]> - $type_tokens[$i - 1] - $type_tokens[$i - 1] - $type_tokens[$i - 1] - $type_tokens[$i - 1] + + + + - $parent_fqcln - $self_fqcln + + - !$fq_classlike_name + template_types]]> template_types]]> calling_method_id]]> @@ -2174,23 +1992,23 @@ - $function_id + - $function_id + - $function_id + output_path]]> - $parent_issue_type + @@ -2214,47 +2032,47 @@ - CustomMetadataTrait + - traverse - traverse - traverse - traverse + + + + - $this_var_id + - !$namespace - $namespace - $namespace + + + - classOrInterfaceExists - classOrInterfaceExists - classOrInterfaceExists - getMappedGenericTypeParams - interfaceExtends - interfaceExtends - interfaceExtends - traverse - traverse + + + + + + + + + - array_keys($template_type_map[$value])[0] + - $value + @@ -2262,54 +2080,54 @@ - replace - replace - replace - replace + + + + - $params - $params + + - getMappedGenericTypeParams - replace - replace + + + type_params[1]]]> - !($container_type_params_covariant[$offset] ?? true) + - getMostSpecificTypeFromBounds + - TNonEmptyList + - replace + - !$namespace - $namespace + + - getString - getString - replace - replace + + + + value_param]]> @@ -2317,51 +2135,51 @@ - !$intersection - !$intersection + + - replace + - __construct + - !$intersection - !$intersection + + - !$intersection + - TList + getGenericValueType())]]> getGenericValueType())]]> - combine - combine - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - combineUnionTypes - replace - replace - replace - replace + + + + + + + + + + + + + possibly_undefined]]> @@ -2371,13 +2189,13 @@ properties[0]]]> - getList + - replace - replace + + type_param]]> @@ -2385,52 +2203,52 @@ - !$namespace - $namespace + + - !$intersection - $intersection + + - TList + - setCount + - replace - replace + + - !$intersection - !$intersection + + - replace + - !$intersection + - replace + - replace + @@ -2440,13 +2258,13 @@ - $allow_mutations - $by_ref - $failed_reconciliation - $from_template_default - $has_mutations - $initialized_class - $reference_free + + + + + + + @@ -2454,13 +2272,13 @@ - $const_name + - $array_key_offset - $failed_reconciliation + + ')]]> @@ -2469,40 +2287,40 @@ - $node + - visit + - $ignore_isset + - traverse - traverse - traverseArray - traverseArray + + + + - TArray|TKeyedArray|TClassStringMap + types['array']]]> - allFloatLiterals - allFloatLiterals - hasLowercaseString - hasLowercaseString + + + + - !$php_type + exact_id]]> id]]> exact_id]]> @@ -2519,8 +2337,8 @@ - $level - $php_version + + @@ -2531,11 +2349,11 @@ - $param_type_1 - $param_type_2 - $param_type_3 - $param_type_4 - $return_type + + + + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 816cdc02e87..0843b86bcc9 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -173,4 +173,8 @@ + + + + diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index 344a5981972..1d80ef71912 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -157,7 +157,7 @@ public function __construct( $this->preview_start = $this->docblock_start ?: $this->file_start; /** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */ - $this->raw_line_number = $stmt->getLine(); + $this->raw_line_number = $stmt->getStartLine(); $this->docblock_line_number = $comment_line; } diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 993958e607a..a02f61403f6 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -2039,6 +2039,8 @@ public function getCompletionItemsForClassishThing( /** * @param list $items * @return list + * @deprecated to be removed in Psalm 6 + * @api fix deprecation problem "PossiblyUnusedMethod: Cannot find any calls to method" */ public function filterCompletionItemsByBeginLiteralPart(array $items, string $literal_part): array { @@ -2404,7 +2406,6 @@ public function getKeyValueParamsForTraversableObject(Atomic $type): array /** * @param array $phantom_classes - * @psalm-suppress PossiblyUnusedMethod part of the public API */ public function queueClassLikeForScanning( string $fq_classlike_name, diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 49e6efdc396..96dd1a2cb67 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -245,7 +245,7 @@ class Config protected $extra_files; /** - * The base directory of this config file + * The base directory of this config file without trailing slash * * @var string */ @@ -446,6 +446,11 @@ class Config */ public $ensure_array_int_offsets_exist = false; + /** + * @var bool + */ + public $ensure_override_attribute = false; + /** * @var array */ @@ -1081,6 +1086,7 @@ private static function fromXmlAndPaths( 'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline', 'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist', 'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist', + 'ensureOverrideAttribute' => 'ensure_override_attribute', 'reportMixedIssues' => 'show_mixed_issues', 'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes', 'sealAllMethods' => 'seal_all_methods', @@ -1445,7 +1451,7 @@ private static function fromXmlAndPaths( if (!$file_path) { throw new ConfigException( 'Cannot resolve stubfile path ' - . rtrim($config->base_dir, DIRECTORY_SEPARATOR) + . $config->base_dir . DIRECTORY_SEPARATOR . $stub_file['name'], ); @@ -1582,11 +1588,11 @@ public function safeSetCustomErrorLevel(string $issue_key, string $error_level): private function loadFileExtensions(SimpleXMLElement $extensions): void { foreach ($extensions as $extension) { - $extension_name = preg_replace('/^\.?/', '', (string)$extension['name'], 1); + $extension_name = preg_replace('/^\.?/', '', (string) $extension['name'], 1); $this->file_extensions[] = $extension_name; if (isset($extension['scanner'])) { - $path = $this->base_dir . (string)$extension['scanner']; + $path = $this->base_dir . DIRECTORY_SEPARATOR . (string) $extension['scanner']; if (!file_exists($path)) { throw new ConfigException('Error parsing config: cannot find file ' . $path); @@ -1596,7 +1602,7 @@ private function loadFileExtensions(SimpleXMLElement $extensions): void } if (isset($extension['checker'])) { - $path = $this->base_dir . (string)$extension['checker']; + $path = $this->base_dir . DIRECTORY_SEPARATOR . (string) $extension['checker']; if (!file_exists($path)) { throw new ConfigException('Error parsing config: cannot find file ' . $path); @@ -1817,7 +1823,13 @@ private function getPluginClassForPath(Codebase $codebase, string $path, string public function shortenFileName(string $to): string { if (!is_file($to)) { - return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to, 1); + // if cwd is the root directory it will be just the directory separator - trim it off first + return preg_replace( + '/^' . preg_quote(rtrim($this->base_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, '/') . '/', + '', + $to, + 1, + ); } $from = $this->base_dir; diff --git a/src/Psalm/ErrorBaseline.php b/src/Psalm/ErrorBaseline.php index 9a83b0a2899..783ef4a8c84 100644 --- a/src/Psalm/ErrorBaseline.php +++ b/src/Psalm/ErrorBaseline.php @@ -16,7 +16,6 @@ use function array_reduce; use function array_values; use function get_loaded_extensions; -use function htmlspecialchars; use function implode; use function ksort; use function min; @@ -268,11 +267,7 @@ private static function writeToFile( foreach ($existingIssueType['s'] as $selection) { $codeNode = $baselineDoc->createElement('code'); $textContent = trim($selection); - if ($textContent !== htmlspecialchars($textContent)) { - $codeNode->appendChild($baselineDoc->createCDATASection($textContent)); - } else { - $codeNode->textContent = trim($textContent); - } + $codeNode->appendChild($baselineDoc->createCDATASection($textContent)); $issueNode->appendChild($codeNode); } $fileNode->appendChild($issueNode); diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 4e6ef467bcb..0db77ba83e2 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1127,8 +1127,11 @@ private function checkPropertyInitialization( $uninitialized_variables[] = '$this->' . $property_name; $uninitialized_properties[$property_class_name . '::$' . $property_name] = $property; - if ($property->type && !$property->type->isMixed()) { - $uninitialized_typed_properties[$property_class_name . '::$' . $property_name] = $property; + if ($property->type) { + // Complain about all natively typed properties and all non-mixed docblock typed properties + if (!$property->type->from_docblock || !$property->type->isMixed()) { + $uninitialized_typed_properties[$property_class_name . '::$' . $property_name] = $property; + } } } @@ -1228,7 +1231,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $fake_stmt = new VirtualClassMethod( new VirtualIdentifier('__construct'), [ - 'type' => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + 'flags' => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, 'params' => $fake_constructor_params, 'stmts' => $fake_constructor_stmts, ], @@ -1646,7 +1649,11 @@ private static function addOrUpdatePropertyType( $allow_native_type = !$docblock_only && $codebase->analysis_php_version_id >= 7_04_00 - && $codebase->allow_backwards_incompatible_changes; + && $codebase->allow_backwards_incompatible_changes + // PHP does not support callable properties, but does allow Closure properties + // hasCallableType() treats Closure as a callable, but getCallableTypes() does not + && $inferred_type->getCallableTypes() === [] + ; $manipulator->setType( $allow_native_type @@ -2483,7 +2490,8 @@ private function checkEnum(): void $seen_values = []; foreach ($storage->enum_cases as $case_storage) { - if ($case_storage->value !== null && $storage->enum_type === null) { + $case_value = $case_storage->getValue($this->getCodebase()->classlikes); + if ($case_value !== null && $storage->enum_type === null) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( 'Case of a non-backed enum should not have a value', @@ -2491,7 +2499,7 @@ private function checkEnum(): void $storage->name, ), ); - } elseif ($case_storage->value === null && $storage->enum_type !== null) { + } elseif ($case_value === null && $storage->enum_type !== null) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( 'Case of a backed enum should have a value', @@ -2499,9 +2507,9 @@ private function checkEnum(): void $storage->name, ), ); - } elseif ($case_storage->value !== null) { - if ((is_int($case_storage->value) && $storage->enum_type === 'string') - || (is_string($case_storage->value) && $storage->enum_type === 'int') + } elseif ($case_value !== null) { + if ((is_int($case_value) && $storage->enum_type === 'string') + || (is_string($case_value) && $storage->enum_type === 'int') ) { IssueBuffer::maybeAdd( new InvalidEnumCaseValue( @@ -2513,8 +2521,8 @@ private function checkEnum(): void } } - if ($case_storage->value !== null) { - if (in_array($case_storage->value, $seen_values, true)) { + if ($case_value !== null) { + if (in_array($case_value, $seen_values, true)) { IssueBuffer::maybeAdd( new DuplicateEnumCaseValue( 'Enum case values should be unique', @@ -2523,7 +2531,7 @@ private function checkEnum(): void ), ); } else { - $seen_values[] = $case_storage->value; + $seen_values[] = $case_value; } } } diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index d9879558922..80db22ed9d1 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -237,7 +237,7 @@ public function analyze( $this->suppressed_issues, new ClassLikeNameOptions( true, - false, + true, true, true, true, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index bf4378d9158..70786016bf7 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -31,11 +31,13 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\InvalidDocblockParamName; +use Psalm\Issue\InvalidOverride; use Psalm\Issue\InvalidParamDefault; use Psalm\Issue\InvalidThrow; use Psalm\Issue\MethodSignatureMismatch; use Psalm\Issue\MismatchingDocblockParamType; use Psalm\Issue\MissingClosureParamType; +use Psalm\Issue\MissingOverrideAttribute; use Psalm\Issue\MissingParamType; use Psalm\Issue\MissingThrowsDocblock; use Psalm\Issue\ReferenceConstraintViolation; @@ -48,6 +50,7 @@ use Psalm\Node\Expr\VirtualVariable; use Psalm\Node\Stmt\VirtualWhile; use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent; +use Psalm\Storage\AttributeStorage; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\FunctionLikeStorage; @@ -65,6 +68,7 @@ use function array_combine; use function array_diff_key; +use function array_filter; use function array_key_exists; use function array_keys; use function array_merge; @@ -1970,6 +1974,39 @@ private function getFunctionInformation( true, ); + if ($codebase->analysis_php_version_id >= 8_03_00) { + $has_override_attribute = array_filter( + $storage->attributes, + static fn(AttributeStorage $s): bool => $s->fq_class_name === 'Override', + ); + + if ($has_override_attribute + && (!$overridden_method_ids || $storage->cased_name === '__construct') + ) { + IssueBuffer::maybeAdd( + new InvalidOverride( + 'Method ' . $storage->cased_name . ' does not match any parent method', + $codeLocation, + ), + $this->getSuppressedIssues(), + ); + } + + if (!$has_override_attribute + && $codebase->config->ensure_override_attribute + && $overridden_method_ids + && $storage->cased_name !== '__construct' + ) { + IssueBuffer::maybeAdd( + new MissingOverrideAttribute( + 'Method ' . $storage->cased_name . ' should have the "Override" attribute', + $codeLocation, + ), + $this->getSuppressedIssues(), + ); + } + } + if ($overridden_method_ids && !$context->collect_initializations && !$context->collect_mutations diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 8c6aaa03ce3..4cf8e778ee6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -2055,7 +2055,9 @@ protected static function hasNonEmptyCountCheck(PhpParser\Node\Expr\FuncCall $st protected static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool { - return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'array_key_exists'; + return $stmt->name instanceof PhpParser\Node\Name + && (strtolower($stmt->name->getFirst()) === 'array_key_exists' + || strtolower($stmt->name->getFirst()) === 'key_exists'); } /** diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index a1df71add81..ec72396f28d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -1355,6 +1355,7 @@ private static function verifyExplicitParam( } else { if (!$param_type->hasString() && !$param_type->hasArray() + && $context->check_functions && CallAnalyzer::checkFunctionExists( $statements_analyzer, $function_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 003355bfcd4..81843d56a15 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -191,7 +191,7 @@ public static function analyze( $toggled_class_exists = false; - if ($method_id === 'class_exists' + if (in_array($method_id, ['class_exists', 'interface_exists', 'enum_exists', 'trait_exists'], true) && $argument_offset === 0 && !$context->inside_class_exists ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index d9ec74f47bf..92025d0bb2f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -206,7 +206,25 @@ public static function analyzeFetch( } if (!$stmt->name instanceof PhpParser\Node\Identifier) { - return true; + if ($codebase->analysis_php_version_id < 8_03_00) { + IssueBuffer::maybeAdd( + new ParseError( + 'Dynamically fetching class constants and enums requires PHP 8.3', + new CodeLocation($statements_analyzer->getSource(), $stmt), + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + $was_inside_general_use = $context->inside_general_use; + + $context->inside_general_use = true; + + $ret = ExpressionAnalyzer::analyze($statements_analyzer, $stmt->name, $context); + + $context->inside_general_use = $was_inside_general_use; + + return $ret; } $const_id = $fq_class_name . '::' . $stmt->name; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index 563d58b1a58..63c815ff521 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -46,9 +46,12 @@ public static function analyze( return false; } - $part_type = $statements_analyzer->node_data->getType($part); - - if ($part_type !== null) { + if ($part instanceof EncapsedStringPart) { + if ($literal_string !== null) { + $literal_string .= $part->value; + } + $non_empty = $non_empty || $part->value !== ""; + } elseif ($part_type = $statements_analyzer->node_data->getType($part)) { $casted_part_type = CastAnalyzer::castStringAttempt( $statements_analyzer, $context, @@ -110,11 +113,6 @@ public static function analyze( } } } - } elseif ($part instanceof EncapsedStringPart) { - if ($literal_string !== null) { - $literal_string .= $part->value; - } - $non_empty = $non_empty || $part->value !== ""; } else { $all_literals = false; $literal_string = null; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 0fdc76b40ce..6ad6983b76e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -1035,10 +1035,11 @@ private static function handleEnumValue( $case_values = []; foreach ($enum_cases as $enum_case) { - if (is_string($enum_case->value)) { - $case_values[] = Type::getAtomicStringFromLiteral($enum_case->value); - } elseif (is_int($enum_case->value)) { - $case_values[] = new TLiteralInt($enum_case->value); + $case_value = $enum_case->getValue($statements_analyzer->getCodebase()->classlikes); + if (is_string($case_value)) { + $case_values[] = Type::getAtomicStringFromLiteral($case_value); + } elseif (is_int($case_value)) { + $case_values[] = new TLiteralInt($case_value); } else { // this should never happen $case_values[] = new TMixed(); @@ -1141,6 +1142,8 @@ private static function handleNonExistentClass( $override_property_visibility = $interface_storage->override_property_visibility; + $intersects_with_enum = false; + foreach ($intersection_types as $intersection_type) { if ($intersection_type instanceof TNamedObject && $codebase->classExists($intersection_type->value) @@ -1149,12 +1152,19 @@ private static function handleNonExistentClass( $class_exists = true; return; } + if ($intersection_type instanceof TNamedObject + && (in_array($intersection_type->value, ['UnitEnum', 'BackedEnum'], true) + || in_array('UnitEnum', $codebase->getParentInterfaces($intersection_type->value))) + ) { + $intersects_with_enum = true; + } } if (!$class_exists && //interfaces can't have properties. Except when they do... In PHP Core, they can !in_array($fq_class_name, ['UnitEnum', 'BackedEnum'], true) && - !in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name)) + !in_array('UnitEnum', $codebase->getParentInterfaces($fq_class_name)) && + !$intersects_with_enum ) { if (IssueBuffer::accepts( new NoInterfaceProperties( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index 94771ed2e17..790b36b30e7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -134,12 +134,26 @@ public static function analyze( if ($stmt->name instanceof PhpParser\Node\VarLikeIdentifier) { $prop_name = $stmt->name->name; - } elseif (($stmt_name_type = $statements_analyzer->node_data->getType($stmt->name)) - && $stmt_name_type->isSingleStringLiteral() - ) { - $prop_name = $stmt_name_type->getSingleStringLiteral()->value; } else { - $prop_name = null; + $was_inside_general_use = $context->inside_general_use; + + $context->inside_general_use = true; + + if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->name, $context) === false) { + $context->inside_general_use = $was_inside_general_use; + + return false; + } + + $context->inside_general_use = $was_inside_general_use; + + if (($stmt_name_type = $statements_analyzer->node_data->getType($stmt->name)) + && $stmt_name_type->isSingleStringLiteral() + ) { + $prop_name = $stmt_name_type->getSingleStringLiteral()->value; + } else { + $prop_name = null; + } } if (!$prop_name) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index b8f67edb619..3d60782d1b9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -283,23 +283,28 @@ public static function infer( } if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) { - $name = strtolower($stmt->name->getFirst()); - if ($name === 'false') { + $name = $stmt->name->getFirst(); + $name_lowercase = strtolower($name); + if ($name_lowercase === 'false') { return Type::getFalse(); } - if ($name === 'true') { + if ($name_lowercase === 'true') { return Type::getTrue(); } - if ($name === 'null') { + if ($name_lowercase === 'null') { return Type::getNull(); } - if ($stmt->name->getFirst() === '__NAMESPACE__') { + if ($name === '__NAMESPACE__') { return Type::getString($aliases->namespace); } + if ($type = ConstFetchAnalyzer::getGlobalConstType($codebase, $name, $name)) { + return $type; + } + return null; } diff --git a/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php b/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php index d3aaa7050a7..a82a3fabb13 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnusedAssignmentRemover.php @@ -65,7 +65,7 @@ public function findUnusedAssignment( $traverser->addVisitor($visitor); $traverser->traverse([$rhs_exp]); - $rhs_exp_trivial = (count($visitor->getNonTrivialExpr()) === 0); + $rhs_exp_trivial = !$visitor->hasNonTrivialExpr(); if ($rhs_exp_trivial) { $treat_as_expr = false; diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index cb3c9b49d94..4f89f5de1d8 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -45,6 +45,8 @@ use Psalm\Internal\ReferenceConstraint; use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TypeParser; +use Psalm\Internal\Type\TypeTokenizer; use Psalm\Issue\CheckType; use Psalm\Issue\ComplexFunction; use Psalm\Issue\ComplexMethod; @@ -678,11 +680,23 @@ private static function analyzeStatement( } else { try { $checked_type = $context->vars_in_scope[$checked_var_id]; - $fq_check_type_string = Type::getFQCLNFromString( + + $path = $statements_analyzer->getRootFilePath(); + $file_storage = $codebase->file_storage_provider->get($path); + + $check_tokens = TypeTokenizer::getFullyQualifiedTokens( $check_type_string, $statements_analyzer->getAliases(), + $statements_analyzer->getTemplateTypeMap(), + $file_storage->type_aliases, + ); + $check_type = TypeParser::parseTokens( + $check_tokens, + null, + $statements_analyzer->getTemplateTypeMap() ?? [], + $file_storage->type_aliases, + true, ); - $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/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 1dc16fbe5bf..0fa174eff1f 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -258,12 +258,12 @@ static function (string $arg) use ($valid_long_options): void { $options['r'] = $options['root']; } - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { + if ($root_path === false) { fwrite( STDERR, 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, @@ -271,7 +271,7 @@ static function (string $arg) use ($valid_long_options): void { exit(1); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } $vendor_dir = CliUtils::getVendorDir($current_dir); diff --git a/src/Psalm/Internal/Cli/Plugin.php b/src/Psalm/Internal/Cli/Plugin.php index 2388238262d..c89cbeed54c 100644 --- a/src/Psalm/Internal/Cli/Plugin.php +++ b/src/Psalm/Internal/Cli/Plugin.php @@ -12,8 +12,6 @@ use function dirname; use function getcwd; -use const DIRECTORY_SEPARATOR; - // phpcs:disable PSR1.Files.SideEffects require_once __DIR__ . '/../CliUtils.php'; @@ -27,13 +25,13 @@ final class Plugin public static function run(): void { CliUtils::checkRuntimeRequirements(); - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $vendor_dir = CliUtils::getVendorDir($current_dir); CliUtils::requireAutoloaders($current_dir, false, $vendor_dir); $app = new Application('psalm-plugin', PSALM_VERSION); - $psalm_root = dirname(__DIR__, 4) . DIRECTORY_SEPARATOR; + $psalm_root = dirname(__DIR__, 4); $plugin_list_factory = new PluginListFactory($current_dir, $psalm_root); diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index fdacd14eef3..a7d22017644 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -275,7 +275,8 @@ public static function run(array $argv): void if (isset($options['set-baseline'])) { if (is_array($options['set-baseline'])) { - die('Only one baseline file can be created at a time' . PHP_EOL); + fwrite(STDERR, 'Only one baseline file can be created at a time' . PHP_EOL); + exit(1); } } @@ -484,8 +485,9 @@ static function (string $arg): void { */ private static function generateConfig(string $current_dir, array &$args): void { - if (file_exists($current_dir . 'psalm.xml')) { - die('A config file already exists in the current directory' . PHP_EOL); + if (file_exists($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml')) { + fwrite(STDERR, 'A config file already exists in the current directory' . PHP_EOL); + exit(1); } $args = array_values(array_filter( @@ -506,12 +508,14 @@ private static function generateConfig(string $current_dir, array &$args): void $init_source_dir = null; if (count($args)) { if (count($args) > 2) { - die('Too many arguments provided for psalm --init' . PHP_EOL); + fwrite(STDERR, 'Too many arguments provided for psalm --init' . PHP_EOL); + exit(1); } if (isset($args[1])) { if (!preg_match('/^[1-8]$/', $args[1])) { - die('Config strictness must be a number between 1 and 8 inclusive' . PHP_EOL); + fwrite(STDERR, 'Config strictness must be a number between 1 and 8 inclusive' . PHP_EOL); + exit(1); } $init_level = (int)$args[1]; @@ -531,11 +535,13 @@ private static function generateConfig(string $current_dir, array &$args): void $vendor_dir, ); } catch (ConfigCreationException $e) { - die($e->getMessage() . PHP_EOL); + fwrite(STDERR, $e->getMessage() . PHP_EOL); + exit(1); } - if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) { - die('Could not write to psalm.xml' . PHP_EOL); + if (file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents) === false) { + fwrite(STDERR, 'Could not write to psalm.xml' . PHP_EOL); + exit(1); } exit('Config file created successfully. Please re-run psalm.' . PHP_EOL); @@ -685,7 +691,8 @@ private static function updateBaseline(array $options, Config $config): array $baselineFile = $config->error_baseline; if (empty($baselineFile)) { - die('Cannot update baseline, because no baseline file is configured.' . PHP_EOL); + fwrite(STDERR, 'Cannot update baseline, because no baseline file is configured.' . PHP_EOL); + exit(1); } try { @@ -780,11 +787,13 @@ private static function autoGenerateConfig( $vendor_dir, ); } catch (ConfigCreationException $e) { - die($e->getMessage() . PHP_EOL); + fwrite(STDERR, $e->getMessage() . PHP_EOL); + exit(1); } - if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) { - die('Could not write to psalm.xml' . PHP_EOL); + if (file_put_contents($current_dir . DIRECTORY_SEPARATOR . 'psalm.xml', $template_contents) === false) { + fwrite(STDERR, 'Could not write to psalm.xml' . PHP_EOL); + exit(1); } exit('Config file created successfully. Please re-run psalm.' . PHP_EOL); @@ -844,12 +853,12 @@ private static function getCurrentDir(array $options): string exit(1); } - $current_dir = $cwd . DIRECTORY_SEPARATOR; + $current_dir = $cwd; if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { + if ($root_path === false) { fwrite( STDERR, 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, @@ -857,7 +866,7 @@ private static function getCurrentDir(array $options): string exit(1); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } return $current_dir; @@ -1071,7 +1080,8 @@ private static function initBaseline( if ($paths_to_check !== null) { $filtered_issue_baseline = []; foreach ($paths_to_check as $path_to_check) { - $path_to_check = substr($path_to_check, strlen($config->base_dir)); + // +1 to remove the initial slash from $path_to_check + $path_to_check = substr($path_to_check, strlen($config->base_dir) + 1); if (isset($issue_baseline[$path_to_check])) { $filtered_issue_baseline[$path_to_check] = $issue_baseline[$path_to_check]; } diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 9dd8eaf47d0..d42a1f10843 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -112,7 +112,8 @@ public static function run(array $argv): void self::syncShortOptions($options); if (isset($options['c']) && is_array($options['c'])) { - die('Too many config files provided' . PHP_EOL); + fwrite(STDERR, 'Too many config files provided' . PHP_EOL); + exit(1); } if (array_key_exists('h', $options)) { @@ -194,16 +195,20 @@ public static function run(array $argv): void exit(1); } - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { - die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL); + if ($root_path === false) { + fwrite( + STDERR, + 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, + ); + exit(1); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } $vendor_dir = CliUtils::getVendorDir($current_dir); @@ -304,7 +309,8 @@ public static function run(array $argv): void if (array_key_exists('issues', $options)) { if (!is_string($options['issues']) || !$options['issues']) { - die('Expecting a comma-separated list of issues' . PHP_EOL); + fwrite(STDERR, 'Expecting a comma-separated list of issues' . PHP_EOL); + exit(1); } $issues = explode(',', $options['issues']); @@ -339,7 +345,11 @@ public static function run(array $argv): void ); if ($allow_backwards_incompatible_changes === null) { - die('--allow-backwards-incompatible-changes expects a boolean value [true|false|1|0]' . PHP_EOL); + fwrite( + STDERR, + '--allow-backwards-incompatible-changes expects a boolean value [true|false|1|0]' . PHP_EOL, + ); + exit(1); } $project_analyzer->getCodebase()->allow_backwards_incompatible_changes @@ -354,7 +364,11 @@ public static function run(array $argv): void ); if ($doc_block_add_new_line_before_return === null) { - die('--add-newline-between-docblock-annotations expects a boolean value [true|false|1|0]' . PHP_EOL); + fwrite( + STDERR, + '--add-newline-between-docblock-annotations expects a boolean value [true|false|1|0]' . PHP_EOL, + ); + exit(1); } ParsedDocblock::addNewLineBetweenAnnotations($doc_block_add_new_line_before_return); @@ -505,7 +519,8 @@ private static function loadCodeowners(Providers $providers): array } elseif (file_exists('docs/CODEOWNERS')) { $codeowners_file_path = realpath('docs/CODEOWNERS'); } else { - die('Cannot use --codeowner without a CODEOWNERS file' . PHP_EOL); + fwrite(STDERR, 'Cannot use --codeowner without a CODEOWNERS file' . PHP_EOL); + exit(1); } $codeowners_file = file_get_contents($codeowners_file_path); @@ -555,7 +570,8 @@ static function (string $line): bool { } if (!$codeowner_files) { - die('Could not find any available entries in CODEOWNERS' . PHP_EOL); + fwrite(STDERR, 'Could not find any available entries in CODEOWNERS' . PHP_EOL); + exit(1); } return $codeowner_files; @@ -571,11 +587,13 @@ private static function loadCodeownersFiles(array $desired_codeowners, array $co /** @psalm-suppress MixedAssignment */ foreach ($desired_codeowners as $desired_codeowner) { if (!is_string($desired_codeowner)) { - die('Invalid --codeowner ' . (string)$desired_codeowner . PHP_EOL); + fwrite(STDERR, 'Invalid --codeowner ' . (string) $desired_codeowner . PHP_EOL); + exit(1); } if ($desired_codeowner[0] !== '@') { - die('--codeowner option must start with @' . PHP_EOL); + fwrite(STDERR, '--codeowner option must start with @' . PHP_EOL); + exit(1); } $matched_file = false; @@ -588,7 +606,8 @@ private static function loadCodeownersFiles(array $desired_codeowners, array $co } if (!$matched_file) { - die('User/group ' . $desired_codeowner . ' does not own any PHP files' . PHP_EOL); + fwrite(STDERR, 'User/group ' . $desired_codeowner . ' does not own any PHP files' . PHP_EOL); + exit(1); } } diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 0fca3ab46f2..b23bcb863c2 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -121,7 +121,8 @@ static function (string $arg) use ($valid_long_options): void { } if (isset($options['c']) && is_array($options['c'])) { - die('Too many config files provided' . PHP_EOL); + fwrite(STDERR, 'Too many config files provided' . PHP_EOL); + exit(1); } if (array_key_exists('h', $options)) { @@ -165,16 +166,20 @@ static function (string $arg) use ($valid_long_options): void { $options['r'] = $options['root']; } - $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); - if (!$root_path) { - die('Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL); + if ($root_path === false) { + fwrite( + STDERR, + 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL, + ); + exit(1); } - $current_dir = $root_path . DIRECTORY_SEPARATOR; + $current_dir = $root_path; } $vendor_dir = CliUtils::getVendorDir($current_dir); @@ -210,7 +215,8 @@ static function (string $arg) use ($valid_long_options): void { if ($arg === '--into') { if ($operation !== 'move' || !$last_arg) { - die('--into is not expected here' . PHP_EOL); + fwrite(STDERR, '--into is not expected here' . PHP_EOL); + exit(1); } $operation = 'move_into'; @@ -224,7 +230,8 @@ static function (string $arg) use ($valid_long_options): void { if ($arg === '--to') { if ($operation !== 'rename' || !$last_arg) { - die('--to is not expected here' . PHP_EOL); + fwrite(STDERR, '--to is not expected here' . PHP_EOL); + exit(1); } $operation = 'rename_to'; @@ -239,7 +246,8 @@ static function (string $arg) use ($valid_long_options): void { if ($operation === 'move_into' || $operation === 'rename_to') { if (!$last_arg) { - die('Expecting a previous argument' . PHP_EOL); + fwrite(STDERR, 'Expecting a previous argument' . PHP_EOL); + exit(1); } if ($operation === 'move_into') { @@ -273,11 +281,13 @@ static function (string $arg) use ($valid_long_options): void { continue; } - die('Unexpected argument "' . $arg . '"' . PHP_EOL); + fwrite(STDERR, 'Unexpected argument "' . $arg . '"' . PHP_EOL); + exit(1); } if (!$to_refactor) { - die('No --move or --rename arguments supplied' . PHP_EOL); + fwrite(STDERR, 'No --move or --rename arguments supplied' . PHP_EOL); + exit(1); } $config = CliUtils::initializeConfig( diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index e90f73e4e5d..8f0f1fbf9cb 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -483,7 +483,8 @@ public static function initPhpVersion(array $options, Config $config, ProjectAna if (isset($options['php-version'])) { if (!is_string($options['php-version'])) { - die('Expecting a version number in the format x.y' . PHP_EOL); + fwrite(STDERR, 'Expecting a version number in the format x.y' . PHP_EOL); + exit(1); } $version = $options['php-version']; $source = 'cli'; diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index b57bbe076da..552fab265d5 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -356,6 +356,7 @@ public function hasFullyQualifiedClassName( } } + // fixme: this looks like a crazy caching hack if (!isset($this->existing_classes_lc[$fq_class_name_lc]) || !$this->existing_classes_lc[$fq_class_name_lc] || !$this->classlike_storage_provider->has($fq_class_name_lc) @@ -396,13 +397,14 @@ public function hasFullyQualifiedInterfaceName( ): bool { $fq_class_name_lc = strtolower($this->getUnAliasedName($fq_class_name)); + // fixme: this looks like a crazy caching hack if (!isset($this->existing_interfaces_lc[$fq_class_name_lc]) || !$this->existing_interfaces_lc[$fq_class_name_lc] || !$this->classlike_storage_provider->has($fq_class_name_lc) ) { if (( - !isset($this->existing_classes_lc[$fq_class_name_lc]) - || $this->existing_classes_lc[$fq_class_name_lc] + !isset($this->existing_interfaces_lc[$fq_class_name_lc]) + || $this->existing_interfaces_lc[$fq_class_name_lc] ) && !$this->classlike_storage_provider->has($fq_class_name_lc) ) { @@ -463,13 +465,14 @@ public function hasFullyQualifiedEnumName( ): bool { $fq_class_name_lc = strtolower($this->getUnAliasedName($fq_class_name)); + // fixme: this looks like a crazy caching hack if (!isset($this->existing_enums_lc[$fq_class_name_lc]) || !$this->existing_enums_lc[$fq_class_name_lc] || !$this->classlike_storage_provider->has($fq_class_name_lc) ) { if (( - !isset($this->existing_classes_lc[$fq_class_name_lc]) - || $this->existing_classes_lc[$fq_class_name_lc] + !isset($this->existing_enums_lc[$fq_class_name_lc]) + || $this->existing_enums_lc[$fq_class_name_lc] ) && !$this->classlike_storage_provider->has($fq_class_name_lc) ) { diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 4bc718e3e49..fc6940b1cfb 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -344,6 +344,13 @@ public static function resolve( return Type::getString($value)->getSingleAtomic(); } elseif (is_int($value)) { return Type::getInt(false, $value)->getSingleAtomic(); + } elseif ($value instanceof UnresolvedConstantComponent) { + return self::resolve( + $classlikes, + $value, + $statements_analyzer, + $visited_constant_ids + [$c_id => true], + ); } } elseif ($c instanceof EnumNameFetch) { return Type::getString($c->case)->getSingleAtomic(); diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 9648729c473..ad97dfbc65e 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -628,11 +628,13 @@ public function getMethodReturnType( ) { $types = []; foreach ($original_class_storage->enum_cases as $case_name => $case_storage) { + $case_value = $case_storage->getValue($this->classlikes); + if (UnionTypeComparator::isContainedBy( $source_analyzer->getCodebase(), - is_int($case_storage->value) ? - Type::getInt(false, $case_storage->value) : - Type::getString($case_storage->value), + is_int($case_value) ? + Type::getInt(false, $case_value) : + Type::getString($case_value), $first_arg_type, )) { $types[] = new TEnumCase($original_fq_class_name, $case_name); diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php index 53de9ec014c..af4d83776b8 100644 --- a/src/Psalm/Internal/Fork/PsalmRestarter.php +++ b/src/Psalm/Internal/Fork/PsalmRestarter.php @@ -84,6 +84,11 @@ protected function requiresRestart($default): bool } } + // opcache.save_comments is required for json mapper (used in language server) to work + if ($opcache_loaded && in_array(ini_get('opcache.save_comments'), ['0', 'false', 0, false])) { + return true; + } + return $default || $this->required; } @@ -152,6 +157,10 @@ protected function restart($command): void ]; } + if ($opcache_loaded) { + $additional_options[] = '-dopcache.save_comments=1'; + } + array_splice( $command, 1, diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 7062885e790..54d15a4aa7c 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -721,7 +721,7 @@ public function emitVersionedIssues(array $files, ?int $version = null): void $diagnostics = array_map( function (IssueData $issue_data): Diagnostic { //$check_name = $issue->check_name; - $description = $issue_data->message; + $description = '[' . $issue_data->type . '] ' . $issue_data->message; $severity = $issue_data->severity; $start_line = max($issue_data->line_from, 1); diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php index 451da44e938..a4af46cacec 100644 --- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php @@ -297,7 +297,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit try { $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); - $literal_part = $this->codebase->getBeginedLiteralPart($file_path, $position); if ($completion_data) { [$recent_type, $gap, $offset] = $completion_data; @@ -306,8 +305,6 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit ->textDocument->completion->completionItem->snippetSupport ?? false; $completion_items = $this->codebase->getCompletionItemsForClassishThing($recent_type, $gap, $snippetSupport); - $completion_items = - $this->codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); } elseif ($gap === '[') { $completion_items = $this->codebase->getCompletionItemsForArrayKeys($recent_type); } else { diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php index 4fe4afe5269..4179ac0d0e6 100644 --- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php @@ -9,10 +9,7 @@ */ final class CheckTrivialExprVisitor extends PhpParser\NodeVisitorAbstract { - /** - * @var array - */ - protected array $non_trivial_expr = []; + private bool $has_non_trivial_expr = false; private function checkNonTrivialExpr(PhpParser\Node\Expr $node): bool { @@ -55,7 +52,7 @@ public function enterNode(PhpParser\Node $node): ?int if ($node instanceof PhpParser\Node\Expr) { // Check for Non-Trivial Expression first if ($this->checkNonTrivialExpr($node)) { - $this->non_trivial_expr[] = $node; + $this->has_non_trivial_expr = true; return PhpParser\NodeTraverser::STOP_TRAVERSAL; } @@ -70,11 +67,8 @@ public function enterNode(PhpParser\Node $node): ?int return null; } - /** - * @return array - */ - public function getNonTrivialExpr(): array + public function hasNonTrivialExpr(): bool { - return $this->non_trivial_expr; + return $this->has_non_trivial_expr; } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 87da361b2e1..999f95df554 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -32,6 +32,7 @@ use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Internal\Scanner\ClassLikeDocblockComment; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\ClassTypeAlias; use Psalm\Internal\Type\TypeAlias\InlineTypeAlias; @@ -65,7 +66,6 @@ use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; -use RuntimeException; use UnexpectedValueException; use function array_merge; @@ -81,7 +81,6 @@ use function preg_match; use function preg_replace; use function preg_split; -use function str_replace; use function strtolower; use function trim; use function usort; @@ -753,6 +752,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $values_types[] = Type::getAtomicStringFromLiteral($enumCaseStorage->value); } elseif (is_int($enumCaseStorage->value)) { $values_types[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value); + } elseif ($enumCaseStorage->value instanceof UnresolvedConstantComponent) { + // backed enum with a type yet unknown + $values_types[] = new Type\Atomic\TMixed; } } } @@ -1463,7 +1465,12 @@ private function visitEnumDeclaration( ); } } else { - throw new RuntimeException('Failed to infer case value for ' . $stmt->name->name); + $enum_value = ExpressionResolver::getUnresolvedClassConstExpr( + $stmt->expr, + $this->aliases, + $fq_classlike_name, + $storage->parent_class, + ); } } @@ -1913,10 +1920,6 @@ private static function getTypeAliasesFromCommentLines( continue; } - $var_line = preg_replace('/[ \t]+/', ' ', preg_replace('@^[ \t]*\*@m', '', $var_line)); - $var_line = preg_replace('/,\n\s+\}/', '}', $var_line); - $var_line = str_replace("\n", '', $var_line); - $var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$var_line_parts) { @@ -1949,7 +1952,7 @@ private static function getTypeAliasesFromCommentLines( array_shift($var_line_parts); } - $type_string = str_replace("\n", '', implode('', $var_line_parts)); + $type_string = implode('', $var_line_parts); try { $type_string = CommentAnalyzer::splitDocLine($type_string)[0]; } catch (DocblockParseException $e) { diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index c8a05ea6d65..188e073153e 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -546,7 +546,7 @@ public function leaveNode(PhpParser\Node $node) } throw new UnexpectedValueException( - 'There should be function storages for line ' . $this->file_path . ':' . $node->getLine(), + 'There should be function storages for line ' . $this->file_path . ':' . $node->getStartLine(), ); } diff --git a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php index af7b4bb90d9..7c1e6b9a27a 100644 --- a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php @@ -16,8 +16,6 @@ use function getcwd; use function is_string; -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -50,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $current_dir = (string) getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $config_file_path = $input->getOption('config'); if ($config_file_path !== null && !is_string($config_file_path)) { diff --git a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php index 6278b7018f2..0a8df8d1dfe 100644 --- a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php @@ -16,8 +16,6 @@ use function getcwd; use function is_string; -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -50,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $current_dir = (string) getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $config_file_path = $input->getOption('config'); if ($config_file_path !== null && !is_string($config_file_path)) { diff --git a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php index a8e78a732c4..ecc24712ce4 100644 --- a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php +++ b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php @@ -17,8 +17,6 @@ use function getcwd; use function is_string; -use const DIRECTORY_SEPARATOR; - /** * @internal */ @@ -44,7 +42,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $current_dir = (string) getcwd() . DIRECTORY_SEPARATOR; + $current_dir = (string) getcwd(); $config_file_path = $input->getOption('config'); if ($config_file_path !== null && !is_string($config_file_path)) { diff --git a/src/Psalm/Internal/PluginManager/PluginListFactory.php b/src/Psalm/Internal/PluginManager/PluginListFactory.php index 950b6dd24a6..927d0f8c32e 100644 --- a/src/Psalm/Internal/PluginManager/PluginListFactory.php +++ b/src/Psalm/Internal/PluginManager/PluginListFactory.php @@ -7,10 +7,8 @@ use function array_filter; use function json_encode; -use function rtrim; use function urlencode; -use const DIRECTORY_SEPARATOR; use const JSON_THROW_ON_ERROR; /** @@ -53,13 +51,13 @@ private function findLockFiles(): array if ($this->psalm_root === $this->project_root) { // managing plugins for psalm itself $composer_lock_filenames = [ - Composer::getLockFilePath(rtrim($this->psalm_root, DIRECTORY_SEPARATOR)), + Composer::getLockFilePath($this->psalm_root), ]; } else { $composer_lock_filenames = [ - Composer::getLockFilePath(rtrim($this->project_root, DIRECTORY_SEPARATOR)), - Composer::getLockFilePath(rtrim($this->psalm_root, DIRECTORY_SEPARATOR) . '/../../..'), - Composer::getLockFilePath(rtrim($this->psalm_root, DIRECTORY_SEPARATOR)), + Composer::getLockFilePath($this->project_root), + Composer::getLockFilePath($this->psalm_root . '/../../..'), + Composer::getLockFilePath($this->psalm_root), ]; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php index e0de55bba04..5c32126de0e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php @@ -33,6 +33,7 @@ use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; +use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; @@ -42,6 +43,7 @@ use function array_keys; use function array_merge; use function filter_var; +use function get_class; use function implode; use function in_array; use function preg_match; @@ -919,7 +921,11 @@ public static function getReturnType( $filter_types[] = new TFloat(); } - if ($atomic_type instanceof TMixed) { + // only these specific classes, not any class that extends either + // to avoid matching already better handled cases from above, e.g. float is numeric and scalar + if ($atomic_type instanceof TMixed + || get_class($atomic_type) === TNumeric::class + || get_class($atomic_type) === TScalar::class) { $filter_types[] = new TFloat(); } @@ -967,7 +973,9 @@ public static function getReturnType( if ($atomic_type instanceof TMixed || $atomic_type instanceof TString || $atomic_type instanceof TInt - || $atomic_type instanceof TFloat) { + || $atomic_type instanceof TFloat + || $atomic_type instanceof TNumeric + || $atomic_type instanceof TScalar) { $filter_types[] = new TBool(); } @@ -994,6 +1002,7 @@ public static function getReturnType( } else { $int_type = new TInt(); } + foreach ($input_type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TLiteralInt) { if ($min_range !== null && $min_range > $atomic_type->value) { @@ -1108,7 +1117,9 @@ public static function getReturnType( $filter_types[] = $int_type; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed + || get_class($atomic_type) === TNumeric::class + || get_class($atomic_type) === TScalar::class) { $filter_types[] = $int_type; } @@ -1129,9 +1140,7 @@ public static function getReturnType( $filter_types[] = $atomic_type; } elseif ($atomic_type instanceof TString) { $filter_types[] = new TNonFalsyString(); - } - - if ($atomic_type instanceof TMixed) { + } elseif ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TNonFalsyString(); } @@ -1159,6 +1168,7 @@ public static function getReturnType( || $atomic_type instanceof TInt || $atomic_type instanceof TFloat || $atomic_type instanceof TNumeric + || $atomic_type instanceof TScalar || $atomic_type instanceof TMixed) { $filter_types[] = new TString(); } @@ -1183,11 +1193,10 @@ public static function getReturnType( } else { $filter_types[] = $atomic_type; } - } - - if ($atomic_type instanceof TMixed + } elseif ($atomic_type instanceof TMixed || $atomic_type instanceof TInt - || $atomic_type instanceof TFloat) { + || $atomic_type instanceof TFloat + || $atomic_type instanceof TScalar) { $filter_types[] = $string_type; } @@ -1230,7 +1239,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TString(); } @@ -1310,7 +1319,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TString(); } @@ -1329,7 +1338,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TString(); } @@ -1386,7 +1395,7 @@ public static function getReturnType( continue; } - if ($atomic_type instanceof TMixed) { + if ($atomic_type instanceof TMixed || $atomic_type instanceof TScalar) { $filter_types[] = new TNumericString(); $filter_types[] = Type::getAtomicStringFromLiteral(''); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php index 55e4f38bd42..910300497fc 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php @@ -63,10 +63,11 @@ public static function getGetObjectVarsReturnType( return new TKeyedArray($properties); } $enum_case_storage = $enum_classlike_storage->enum_cases[$object_type->case_name]; - if (is_int($enum_case_storage->value)) { - $properties['value'] = new Union([new Atomic\TLiteralInt($enum_case_storage->value)]); - } elseif (is_string($enum_case_storage->value)) { - $properties['value'] = new Union([Type::getAtomicStringFromLiteral($enum_case_storage->value)]); + $case_value = $enum_case_storage->getValue($statements_source->getCodebase()->classlikes); + if (is_int($case_value)) { + $properties['value'] = new Union([new Atomic\TLiteralInt($case_value)]); + } elseif (is_string($case_value)) { + $properties['value'] = new Union([Type::getAtomicStringFromLiteral($case_value)]); } return new TKeyedArray($properties); } diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 28807b052b7..66cfc2f9e96 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -2971,11 +2971,12 @@ private static function reconcileValueOf( // For value-of, the assertion is meant to return *ANY* value of *ANY* enum case if ($enum_case_to_assert === null) { foreach ($class_storage->enum_cases as $enum_case) { + $enum_value = $enum_case->getValue($codebase->classlikes); assert( - $enum_case->value !== null, + $enum_value !== null, 'Verified enum type above, value can not contain `null` anymore.', ); - $reconciled_types[] = Type::getLiteral($enum_case->value); + $reconciled_types[] = Type::getLiteral($enum_value); } continue; @@ -2986,8 +2987,10 @@ private static function reconcileValueOf( return null; } - assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.'); - $reconciled_types[] = Type::getLiteral($enum_case->value); + $enum_value = $enum_case->getValue($codebase->classlikes); + + assert($enum_value !== null, 'Verified enum type above, value can not contain `null` anymore.'); + $reconciled_types[] = Type::getLiteral($enum_value); } if ($reconciled_types === []) { diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 7ce88ebb54c..7384d3bce88 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1254,7 +1254,6 @@ public static function getMappedGenericTypeParams( Atomic $container_type_part, ?array &$container_type_params_covariant = null ): array { - $_ = null; if ($input_type_part instanceof TGenericObject || $input_type_part instanceof TIterable) { $input_type_params = $input_type_part->type_params; } elseif ($codebase->classlike_storage_provider->has($input_type_part->value)) { @@ -1290,7 +1289,6 @@ public static function getMappedGenericTypeParams( $replacement_templates = []; if ($input_template_types - && (!$input_type_part instanceof TGenericObject || !$input_type_part->remapped_params) && (!$container_type_part instanceof TGenericObject || !$container_type_part->remapped_params) ) { foreach ($input_template_types as $template_name => $_) { diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 795b5ad9a8a..0855a1ab732 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -283,7 +283,9 @@ public static function expandAtomic( $declaring_fq_classlike_name = $self_class; } - if (!($evaluate_class_constants && $codebase->classOrInterfaceExists($declaring_fq_classlike_name))) { + if (!($evaluate_class_constants + && $codebase->classlikes->doesClassLikeExist(strtolower($declaring_fq_classlike_name)) + )) { return [$return_type]; } diff --git a/src/Psalm/Issue/InvalidOverride.php b/src/Psalm/Issue/InvalidOverride.php new file mode 100644 index 00000000000..bea55e8d0ad --- /dev/null +++ b/src/Psalm/Issue/InvalidOverride.php @@ -0,0 +1,9 @@ +value = $value; $this->stmt_location = $location; } + + /** @return int|string|null */ + public function getValue(ClassLikes $classlikes) + { + $case_value = $this->value; + + if ($case_value instanceof UnresolvedConstantComponent) { + $case_value = ConstantTypeResolver::resolve( + $classlikes, + $case_value, + ); + + if ($case_value instanceof TLiteralString) { + $case_value = $case_value->value; + } elseif ($case_value instanceof TLiteralInt) { + $case_value = $case_value->value; + } else { + throw new UnexpectedValueException('Failed to infer case value'); + } + } + + return $case_value; + } } diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index 94ee9446d44..07d6740cbec 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -50,7 +50,8 @@ public function __construct( public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { - return false; + // it can, if it's just 'Closure' + return $this->params === null && $this->return_type === null && $this->is_pure === null; } /** diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 59941bc61e5..eb8df8ce03a 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -32,20 +32,24 @@ public function __construct(Union $type, bool $from_docblock = false) /** * @param non-empty-array $cases */ - private static function getValueTypeForNamedObject(array $cases, TNamedObject $atomic_type): Union - { + private static function getValueTypeForNamedObject( + array $cases, + TNamedObject $atomic_type, + Codebase $codebase + ): Union { if ($atomic_type instanceof TEnumCase) { assert(isset($cases[$atomic_type->case_name]), 'Should\'ve been verified in TValueOf#getValueType'); - $value = $cases[$atomic_type->case_name]->value; + $value = $cases[$atomic_type->case_name]->getValue($codebase->classlikes); assert($value !== null, 'Backed enum must have a value.'); return new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]); } return new Union(array_map( - static function (EnumCaseStorage $case): Atomic { - assert($case->value !== null); + static function (EnumCaseStorage $case) use ($codebase): Atomic { + $case_value = $case->getValue($codebase->classlikes); + assert($case_value !== null); // Backed enum must have a value - return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value); + return ConstantTypeResolver::getLiteralTypeFromScalarValue($case_value); }, array_values($cases), )); @@ -141,7 +145,7 @@ public static function getValueType( continue; } - $value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type); + $value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type, $codebase); } else { continue; } diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index 2520a479902..b240a8d143d 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -154,7 +154,7 @@ class DOMNode */ public ?DOMNamedNodeMap $attributes; /** @readonly */ - public ?DOMDocument $ownerDocument; + public DOMDocument $ownerDocument; /** @readonly */ public ?string $namespaceURI; public string $prefix; @@ -242,7 +242,7 @@ class DOMNameSpaceNode /** @readonly */ public ?string $namespaceURI; /** @readonly */ - public ?DOMDocument $ownerDocument; + public DOMDocument $ownerDocument; /** @readonly */ public ?DOMNode $parentNode; } @@ -281,6 +281,8 @@ class DOMDocument extends DOMNode implements DOMParentNode public DOMImplementation $implementation; /** @readonly */ public ?DOMElement $documentElement; + /** @readonly */ + public null $ownerDocument; /** * @deprecated diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 5b619971f8e..83945999d87 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -2255,7 +2255,36 @@ function takesSomeIntFromEnum(int $foo): IntEnum function isNonEmptyString($_str): bool { return true; - }', + } + ', + ], + 'assertStringIsNonEmptyStringInNamespace' => [ + 'code' => ' [ 'code' => ' [], 'php_version' => '8.2', ], - 'override' => [ - 'code' => ' [], - 'ignored_issues' => [], - 'php_version' => '8.3', - ], 'sensitiveParameter' => [ 'code' => ' $file_contents) { - $file_path = $config->base_dir . str_replace('/', DIRECTORY_SEPARATOR, $file_path); + $file_path = $config->base_dir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file_path); if ($file_contents === null) { $file_provider->deleteFile($file_path); } else { @@ -126,7 +126,7 @@ public static function provideCacheInteractions(): iterable [ [ 'files' => [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/B.php' => null, + 'src/B.php' => null, ], 'issues' => [ - '/src/A.php' => [ + 'src/A.php' => [ 'UndefinedClass: Class, interface or enum named B does not exist', ], ], @@ -163,7 +163,7 @@ public function do(): void [ [ 'files' => [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/A.php' => [ + 'src/A.php' => [ "NullableReturnStatement: The declared return type 'int' for A::foo is not nullable, but the function returns 'int|null'", "InvalidNullableReturnType: The declared return type 'int' for A::foo is not nullable, but 'int|null' contains null", ], @@ -188,7 +188,7 @@ class B { ], [ 'files' => [ - '/src/B.php' => <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' <<<'PHP' + 'src/B.php' => <<<'PHP' [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' [ - '/src/A.php' => [ + 'src/A.php' => [ "UndefinedDocblockClass: Docblock-defined class, interface or enum named T does not exist", ], - '/src/B.php' => [ + 'src/B.php' => [ "InvalidArgument: Argument 1 of A::foo expects T, but 1 provided", ], ], @@ -266,7 +266,7 @@ public function foo($baz): void [ [ 'files' => [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' [ - '/src/A.php' => <<<'PHP' + 'src/A.php' => <<<'PHP' [ - '/src/A.php' => [ + 'src/A.php' => [ "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", "MixedReturnStatement: Could not infer a return type", "MixedInferredReturnType: Could not verify return type 'string' for A::bar", diff --git a/tests/CheckTypeTest.php b/tests/CheckTypeTest.php index 457c496db83..64b65e3fc32 100644 --- a/tests/CheckTypeTest.php +++ b/tests/CheckTypeTest.php @@ -38,6 +38,15 @@ final class A {} $_a = new stdClass(); /** @psalm-check-type-exact $_a = \stdClass */', ]; + yield 'allowType' => [ + 'code' => ' [ 'code' => ' 'InvalidArrayOffset', ], + 'unsupportedDynamicFetch' => [ + 'code' => ' 'ParseError', + 'error_levels' => [], + 'php_version' => '8.2', + ], ]; } } diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index b0189897090..142be4a3e8a 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -424,6 +424,20 @@ function takesList(array $list): void {} $globBrace = glob('abc', GLOB_BRACE); PHP, ]; + yield "ownerDocument's type is non-nullable DOMDocument and always null on DOMDocument itself" => [ + 'code' => 'ownerDocument; + $b = (new DOMNode())->ownerDocument; + $c = (new DOMElement("p"))->ownerDocument; + $d = (new DOMNameSpaceNode())->ownerDocument; + ', + 'assertions' => [ + '$a===' => 'null', + '$b===' => 'DOMDocument', + '$c===' => 'DOMDocument', + '$d===' => 'DOMDocument', + ], + ]; } public function providerInvalidCodeParse(): iterable diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 26b734c803a..74c0cb6eae6 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -221,6 +221,8 @@ public function testInvalidCode(string $code, string $error_message, array $igno $this->project_analyzer->getConfig()->ensure_array_string_offsets_exist = $is_array_offset_test; $this->project_analyzer->getConfig()->ensure_array_int_offsets_exist = $is_array_offset_test; + $this->project_analyzer->getConfig()->ensure_override_attribute = $error_message === 'MissingOverrideAttribute'; + foreach ($ignored_issues as $error_level) { $this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS); } @@ -311,6 +313,11 @@ public function providerInvalidCodeParse(): array case 'InvalidInterfaceImplementation': $php_version = '8.1'; break; + + case 'InvalidOverride': + case 'MissingOverrideAttribute': + $php_version = '8.3'; + break; } $invalid_code_data[$issue_name] = [ diff --git a/tests/EndToEnd/PsalmEndToEndTest.php b/tests/EndToEnd/PsalmEndToEndTest.php index b02660cecd2..bdbb2cb1a0e 100644 --- a/tests/EndToEnd/PsalmEndToEndTest.php +++ b/tests/EndToEnd/PsalmEndToEndTest.php @@ -24,6 +24,8 @@ use function tempnam; use function unlink; +use const DIRECTORY_SEPARATOR; + /** * Tests some of the most important use cases of the psalm and psalter commands, by launching a new * process as if invoked by a real user. @@ -47,8 +49,6 @@ public static function setUpBeforeClass(): void throw new Exception('Couldn\'t get working directory'); } - mkdir(self::$tmpDir . '/src'); - copy(__DIR__ . '/../fixtures/DummyProjectWithErrors/composer.json', self::$tmpDir . '/composer.json'); $process = new Process(['composer', 'install', '--no-plugins'], self::$tmpDir, null, null, 120); @@ -63,7 +63,8 @@ public static function tearDownAfterClass(): void public function setUp(): void { - @unlink(self::$tmpDir . '/psalm.xml'); + mkdir(self::$tmpDir . '/src'); + copy( __DIR__ . '/../fixtures/DummyProjectWithErrors/src/FileWithErrors.php', self::$tmpDir . '/src/FileWithErrors.php', @@ -73,9 +74,16 @@ public function setUp(): void public function tearDown(): void { + @unlink(self::$tmpDir . '/psalm.xml'); + if (file_exists(self::$tmpDir . '/cache')) { self::recursiveRemoveDirectory(self::$tmpDir . '/cache'); } + + if (file_exists(self::$tmpDir . '/src')) { + self::recursiveRemoveDirectory(self::$tmpDir . '/src'); + } + parent::tearDown(); } @@ -275,7 +283,7 @@ private static function recursiveRemoveDirectory(string $src): void $dir = opendir($src); while (false !== ($file = readdir($dir))) { if (($file !== '.') && ($file !== '..')) { - $full = $src . '/' . $file; + $full = $src . DIRECTORY_SEPARATOR . $file; if (is_dir($full)) { self::recursiveRemoveDirectory($full); } else { diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 12322c66698..f66cdae3889 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -630,6 +630,89 @@ enum BarEnum: int { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'allowPropertiesOnIntersectionsWithEnumInterfaces' => [ + 'code' => <<<'PHP' + name; + } + if ($i instanceof UnitEnum) { + echo $i->name; + } + if ($i instanceof UE) { + echo $i->name; + } + if ($i instanceof BE) { + echo $i->name; + } + } + PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'stringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'enumWithCasesReferencingClassConstantsWhereClassIsDefinedAfterTheEnum' => [ + 'code' => <<<'PHP' + value; + PHP, + 'assertions' => [ + '$a===' => "'foo'", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'enumWithCasesReferencingAnotherEnumCase' => [ + 'code' => <<<'PHP' + value; + } + enum Foo: string { + case FOO = "foo"; + } + $a = Bar::BAR->value; + PHP, + 'assertions' => [ + '$a===' => "'foo'", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1080,6 +1163,50 @@ enum Bar: int 'ignored_issues' => [], 'php_version' => '8.1', ], + 'invalidStringBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidStringBackedEnumCaseValueFromIntGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'invalidIntBackedEnumCaseValueFromStringGlobalConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } diff --git a/tests/FileManipulation/MissingPropertyTypeTest.php b/tests/FileManipulation/MissingPropertyTypeTest.php index afeb644ee88..f32d8c2562c 100644 --- a/tests/FileManipulation/MissingPropertyTypeTest.php +++ b/tests/FileManipulation/MissingPropertyTypeTest.php @@ -294,6 +294,65 @@ public function bar() { 'issues_to_fix' => ['MissingPropertyType'], 'safe_types' => true, ], + 'doNotAddCallablePropertyTypes' => [ + 'input' => <<<'PHP' + u = $u; + $this->v = $v; + } + } + PHP, + 'output' => <<<'PHP' + u = $u; + $this->v = $v; + } + } + PHP, + 'php_version' => '7.4', + 'issues_to_fix' => ['MissingPropertyType'], + 'safe_types' => true, + ], + 'addClosurePropertyType' => [ + 'input' => <<<'PHP' + u = $u; + } + } + PHP, + 'output' => <<<'PHP' + u = $u; + } + } + PHP, + 'php_version' => '7.4', + 'issues_to_fix' => ['MissingPropertyType'], + 'safe_types' => true, + ], ]; } } diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index 5acae2112f9..861c4548470 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -603,7 +603,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -613,7 +613,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -657,7 +657,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], @@ -667,7 +667,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], @@ -707,7 +707,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -717,7 +717,7 @@ public function foo() : void { class A { public function foo() : void { - throw new Error("bad", 5); + throw new Error("bad", []); } }', ], @@ -755,7 +755,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], @@ -765,7 +765,7 @@ public function foo() : void { class A { public function foo() : void { - throw new E("bad", 5); + throw new E("bad", []); } }', ], diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 4d997070211..c2ca6040367 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -777,8 +777,31 @@ function exploder(string $d, string $s) : array { }', ], 'allowPossiblyUndefinedClassInClassExists' => [ - 'code' => ' <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.1', ], 'allowConstructorAfterClassExists' => [ 'code' => ' ["default" => 5.0]]); + } + + /** + * @param mixed $c + * @return int<1, 100>|stdClass|array + */ + function filterNumericIntWithDefault($c) { + if (is_numeric($c)) { + return filter_var($c, FILTER_VALIDATE_INT, [ + "options" => [ + "default" => new stdClass(), + "min_range" => 1, + "max_range" => 100, + ], + ]); + } + + return array(); }', ], 'callVariableVar' => [ @@ -1647,6 +1688,14 @@ function in_array($a, $b) { } }', ], + 'callableArgumentWithFunctionExists' => [ + 'code' => <<<'PHP' + [ 'code' => ' ['8.1', '8.2', '8.3'], 'appenditerator::getiteratorindex' => ['8.1', '8.2', '8.3'], - 'arrayobject::getiterator' => ['8.1', '8.2', '8.3'], 'cachingiterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'callbackfilteriterator::getinneriterator' => ['8.1', '8.2', '8.3'], 'curl_multi_getcontent', @@ -631,6 +631,8 @@ private function assertTypeValidity(ReflectionType $reflected, string $specified } catch (InvalidArgumentException $e) { if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches) && !class_exists($matches[1]) + && !interface_exists($matches[1]) + && !enum_exists($matches[1]) ) { $this->fail("Class used in CallMap does not exist: {$matches[1]}"); } diff --git a/tests/LanguageServer/CompletionTest.php b/tests/LanguageServer/CompletionTest.php index 268f399821e..656951253d5 100644 --- a/tests/LanguageServer/CompletionTest.php +++ b/tests/LanguageServer/CompletionTest.php @@ -15,7 +15,6 @@ use Psalm\Tests\TestConfig; use Psalm\Type; -use function array_map; use function count; class CompletionTest extends TestCase @@ -726,201 +725,6 @@ public function baz() {} $this->assertSame('baz()', $completion_items[1]->insertText); } - public function testObjectPropertyOnAppendToEnd(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'aPr - } - }', - ); - - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A&static', '->', 223], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['aProp'], $completion_item_texts); - } - - public function testObjectPropertyOnReplaceEndPart(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'aProp2; - } - }', - ); - - $codebase->file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A&static', '->', 225], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['aProp1', 'aProp2'], $completion_item_texts); - } - - public function testSelfPropertyOnAppendToEnd(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A', '::', 237], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['$aProp'], $completion_item_texts); - } - - public function testStaticPropertyOnAppendToEnd(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 36); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A', '::', 239], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['$aProp'], $completion_item_texts); - } - - public function testStaticPropertyOnReplaceEndPart(): void - { - $codebase = $this->codebase; - $config = $codebase->config; - $config->throw_exception = false; - - $this->addFile( - 'somefile.php', - 'file_provider->openFile('somefile.php'); - $codebase->scanFiles(); - - $this->analyzeFile('somefile.php', new Context()); - - $position = new Position(8, 34); - $completion_data = $codebase->getCompletionDataAtPosition('somefile.php', $position); - $literal_part = $codebase->getBeginedLiteralPart('somefile.php', $position); - - $this->assertSame(['B\A', '::', 239], $completion_data); - - $completion_items = $codebase->getCompletionItemsForClassishThing($completion_data[0], $completion_data[1], true); - $completion_items = $codebase->filterCompletionItemsByBeginLiteralPart($completion_items, $literal_part); - $completion_item_texts = array_map(fn($item) => $item->insertText, $completion_items); - - $this->assertSame(['$aProp1', '$aProp2'], $completion_item_texts); - } - public function testCompletionOnNewExceptionWithoutNamespace(): void { $codebase = $this->codebase; diff --git a/tests/OverrideTest.php b/tests/OverrideTest.php new file mode 100644 index 00000000000..ab808a06df9 --- /dev/null +++ b/tests/OverrideTest.php @@ -0,0 +1,178 @@ +ensure_override_attribute = true; + return $config; + } + + public function providerValidCodeParse(): iterable + { + return [ + 'constructor' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'overrideClass' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'overrideInterface' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + ]; + } + + public function providerInvalidCodeParse(): iterable + { + return [ + 'noParent' => [ + 'code' => ' 'InvalidOverride', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'classMissingAttribute' => [ + 'code' => ' 'MissingOverrideAttribute', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'classUsingTrait' => [ + 'code' => ' 'MissingOverrideAttribute', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'constructor' => [ + 'code' => ' 'InvalidOverride', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'interfaceMissingAttribute' => [ + 'code' => ' 'MissingOverrideAttribute', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'privateMethod' => [ + 'code' => ' 'InvalidOverride', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'interfaceWithNoParent' => [ + 'code' => ' 'InvalidOverride', + 'error_levels' => [], + 'php_version' => '8.3', + ], + ]; + } +} diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 2e1fb412e58..3d066210444 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -718,7 +718,7 @@ class Foo { $a = new DOMElement("foo"); $owner = $a->ownerDocument;', 'assertions' => [ - '$owner' => 'DOMDocument|null', + '$owner' => 'DOMDocument', ], ], 'propertyMapHydration' => [ @@ -1166,7 +1166,7 @@ class Finally_ extends Node\Stmt * Constructs a finally node. * * @param list $stmts Statements - * @param array $attributes Additional attributes + * @param array $attributes Additional attributes */ public function __construct(array $stmts = array(), array $attributes = array()) { parent::__construct($attributes); @@ -3827,6 +3827,15 @@ class A { ', 'error_message' => 'UndefinedPropertyAssignment', ], + 'nativeMixedPropertyWithNoConstructor' => [ + 'code' => <<< 'PHP' + 'MissingConstructor', + ], ]; } } diff --git a/tests/StubTest.php b/tests/StubTest.php index f12fb943ed8..1e297daf976 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -864,7 +864,7 @@ function_exists("fooBar"); public function testNoStubFunction(): void { - $this->expectExceptionMessage('UndefinedFunction - /src/somefile.php:2:22 - Function barBar does not exist'); + $this->expectExceptionMessage('UndefinedFunction'); $this->expectException(CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index 800e2956cd7..01b58efb671 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -14,6 +14,68 @@ class FunctionTemplateTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'extractTypeParameterValue' => [ + 'code' => ' + */ + final readonly class IntType implements Type {} + + /** + * @template T + * @implements Type> + */ + final readonly class ListType implements Type + { + /** + * @param Type $type + */ + public function __construct( + public Type $type, + ) { + } + } + + /** + * @template T + * @param Type $type + * @return T + */ + function extractType(Type $type): mixed + { + throw new \RuntimeException("Should never be called at runtime"); + } + + /** + * @template T + * @param Type $t + * @return ListType + */ + function listType(Type $t): ListType + { + return new ListType($t); + } + + function intType(): IntType + { + return new IntType(); + } + + $listType = listType(intType()); + $list = extractType($listType); + ', + 'assertions' => [ + '$listType===' => 'ListType', + '$list' => 'list', + ], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], 'validTemplatedType' => [ 'code' => 'level = 1; $this->cache_directory = null; - $this->base_dir = getcwd() . DIRECTORY_SEPARATOR; + $this->base_dir = getcwd(); if (!self::$cached_project_files) { self::$cached_project_files = ProjectFileFilter::loadFromXMLElement( diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 29ee8f6581f..0e101137c84 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -865,6 +865,57 @@ public function doesNotWork($_doesNotWork): void { } }', ], + 'importFromEnum' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'importFromTrait' => [ + 'code' => <<<'PHP' + [ + 'code' => <<<'PHP' + [], 'php_version' => '8.0', ], + 'keyExistsAsAliasForArrayKeyExists' => [ + 'code' => <<<'PHP' + $arr + */ + function foo(array $arr): void { + if (key_exists("a", $arr)) { + echo $arr["a"]; + } + } + PHP, + ], ]; } diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index a3b722ff3fd..91f958425eb 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -61,6 +61,7 @@ class A {} class B {} interface SomeInterface {} '); + $this->project_analyzer->getCodebase()->queueClassLikeForScanning('Countable'); $this->project_analyzer->getCodebase()->scanFiles(); } diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 8518dd5107f..15bf8b182a3 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -1012,7 +1012,43 @@ function foo() : void { A::$method(); }', ], - 'usedAsStaticPropertyName' => [ + 'usedAsClassConstFetch' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'usedAsEnumFetch' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'usedAsStaticPropertyAssign' => [ 'code' => ' [ + 'code' => ' [ 'code' => '