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' => '