diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 90e3705cf4c..28480bab993 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -135,6 +135,7 @@ jobs:
CHUNK_COUNT: "${{ matrix.count }}"
CHUNK_NUMBER: "${{ matrix.chunk }}"
PARALLEL_PROCESSES: 5
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
steps:
- name: Set up PHP
diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml
index 3314de45861..c5bd292b267 100644
--- a/.github/workflows/windows-ci.yml
+++ b/.github/workflows/windows-ci.yml
@@ -55,10 +55,12 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
- ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M
+ #ini-values: zend.assertions=1, assert.exception=1, opcache.enable_cli=1, opcache.jit=function, opcache.jit_buffer_size=512M
+ ini-values: zend.assertions=1, assert.exception=1
tools: composer:v2
coverage: none
- extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter
+ #extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter
+ extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter
env:
fail-fast: true
diff --git a/UPGRADING.md b/UPGRADING.md
index 9b3665d4dcf..767e293871e 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -17,7 +17,7 @@
- [BC] Class `Psalm\Issue\MixedInferredReturnType` was removed
-- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well.
+- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_EXTRACT`, `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well.
- [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint`
diff --git a/bin/test-with-real-projects.sh b/bin/test-with-real-projects.sh
index 923312bbea6..d22dc0c2d0a 100755
--- a/bin/test-with-real-projects.sh
+++ b/bin/test-with-real-projects.sh
@@ -26,6 +26,9 @@ collections)
git clone --depth=1 git@github.com:psalm/endtoend-test-collections.git
cd endtoend-test-collections
composer install
+ rm vendor/amphp/amp/lib/functions.php; touch vendor/amphp/amp/lib/functions.php;
+ rm vendor/amphp/amp/lib/Internal/functions.php; touch vendor/amphp/amp/lib/Internal/functions.php
+ rm vendor/amphp/byte-stream/lib/functions.php; touch vendor/amphp/byte-stream/lib/functions.php
"$PSALM" --monochrome --show-info=false
;;
@@ -36,7 +39,7 @@ psl)
git clone git@github.com:psalm/endtoend-test-psl.git
cd endtoend-test-psl
- git checkout 2.3.x
+ git checkout 2.3.x_master
composer install
# Avoid conflicts with old psalm when running phar tests
rm -rf vendor/vimeo/psalm
diff --git a/bin/tests-github-actions.sh b/bin/tests-github-actions.sh
index 4296f70591a..2c0ef4b787b 100755
--- a/bin/tests-github-actions.sh
+++ b/bin/tests-github-actions.sh
@@ -3,13 +3,15 @@
set -eu
function get_seeded_random() {
- openssl enc -aes-256-ctr -pass pass:"vimeo/psalm" -nosalt /dev/null
+ local -r branch_name="$1"
+ openssl enc -aes-256-ctr -pass pass:"$branch_name" -nosalt /dev/null
}
function run {
local -r chunk_count="$1"
local -r chunk_number="$2"
local -r parallel_processes="$3"
+ local -r branch_name="$4"
local -r phpunit_cmd='
echo "::group::{}";
@@ -23,7 +25,7 @@ exit "$exit_code"'
mkdir -p build/parallel/ build/phpunit/logs/
- find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random) > build/tests_all
+ find tests -name '*Test.php' | shuf --random-source=<(get_seeded_random "$branch_name") > build/tests_all
# split incorrectly splits the lines by byte size, which means that the number of tests per file are as evenly distributed as possible
#split --number="l/$chunk_number/$chunk_count" build/tests_all > build/tests_split
local -r lines=$(wc -l
+
@@ -428,11 +429,13 @@
+
+
diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php
index 4e7166ec105..cec0545f126 100644
--- a/dictionaries/CallMap.php
+++ b/dictionaries/CallMap.php
@@ -1552,7 +1552,7 @@
'decbin' => ['string', 'num'=>'int'],
'dechex' => ['string', 'num'=>'int'],
'decoct' => ['string', 'num'=>'int'],
-'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'],
+'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'],
'define_syslog_variables' => ['void'],
'defined' => ['bool', 'constant_name'=>'string'],
'deflate_add' => ['string|false', 'context'=>'DeflateContext', 'data'=>'string', 'flush_mode='=>'int'],
@@ -2847,11 +2847,11 @@
'FilesystemIterator::setInfoClass' => ['void', 'class='=>'class-string'],
'FilesystemIterator::valid' => ['bool'],
'filetype' => ['string|false', 'filename'=>'string'],
-'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'],
+'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'],
'filter_id' => ['int|false', 'name'=>'string'],
-'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
-'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'],
-'filter_list' => ['array'],
+'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
+'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'],
+'filter_list' => ['non-empty-list'],
'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'],
'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'],
'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'],
@@ -3315,7 +3315,7 @@
'getopt' => ['array>|false', 'short_options'=>'string', 'long_options='=>'array', '&w_rest_index='=>'int'],
'getprotobyname' => ['int|false', 'protocol'=>'string'],
'getprotobynumber' => ['string', 'protocol'=>'int'],
-'getrandmax' => ['int'],
+'getrandmax' => ['int<1, max>'],
'getrusage' => ['array', 'mode='=>'int'],
'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'],
'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'],
@@ -6625,7 +6625,7 @@
'mb_ereg_search_setpos' => ['bool', 'offset'=>'int'],
'mb_eregi' => ['bool', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'],
'mb_eregi_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'],
-'mb_get_info' => ['array|string|int|false', 'type='=>'string'],
+'mb_get_info' => ['array|string|int|false|null', 'type='=>'string'],
'mb_http_input' => ['array|string|false', 'type='=>'string|null'],
'mb_http_output' => ['string|bool', 'encoding='=>'string|null'],
'mb_internal_encoding' => ['string|bool', 'encoding='=>'string|null'],
@@ -7646,7 +7646,7 @@
'msql_query' => ['resource', 'query'=>'string', 'link_identifier='=>'?resource'],
'msql_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field='=>'mixed'],
'msql_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'?resource'],
-'mt_getrandmax' => ['int'],
+'mt_getrandmax' => ['int<1, max>'],
'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'],
'mt_rand\'1' => ['int'],
'mt_srand' => ['void', 'seed='=>'?int', 'mode='=>'int'],
@@ -11141,8 +11141,8 @@
'SessionIdInterface::create_sid' => ['string'],
'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'],
-'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'value'=>'string'],
-'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'],
+'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
+'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'id'=>'string'],
'set_error_handler' => ['null|callable(int,string,string=,int=,array=):bool', 'callback'=>'null|callable(int,string,string=,int=,array=):bool', 'error_levels='=>'int'],
'set_exception_handler' => ['null|callable(Throwable):void', 'callback'=>'null|callable(Throwable):void'],
'set_file_buffer' => ['int', 'stream'=>'resource', 'size'=>'int'],
@@ -12923,8 +12923,8 @@
'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'],
'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'?int'],
'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'],
-'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'],
-'strtok\'1' => ['string|false', 'string'=>'string'],
+'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'],
+'strtok\'1' => ['non-empty-string|false', 'string'=>'string'],
'strtolower' => ['lowercase-string', 'string'=>'string'],
'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'?int'],
'strtoupper' => ['string', 'string'=>'string'],
@@ -14165,7 +14165,7 @@
'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'],
'unpack' => ['array|false', 'format'=>'string', 'string'=>'string', 'offset='=>'int'],
'unregister_tick_function' => ['void', 'callback'=>'callable'],
-'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:string[]|bool}'],
+'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:class-string[]|bool}'],
'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'],
'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'],
'uopz_allow_exit' => ['void', 'allow'=>'bool'],
diff --git a/dictionaries/CallMap_73_delta.php b/dictionaries/CallMap_73_delta.php
index d79026c3805..6352fa06292 100644
--- a/dictionaries/CallMap_73_delta.php
+++ b/dictionaries/CallMap_73_delta.php
@@ -64,6 +64,10 @@
'old' => ['int', 'scale'=>'int'],
'new' => ['int', 'scale='=>'int'],
],
+ 'define' => [
+ 'old' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'],
+ 'new' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'false'],
+ ],
'ldap_compare' => [
'old' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string'],
'new' => ['bool|int', 'ldap'=>'resource', 'dn'=>'string', 'attribute'=>'string', 'value'=>'string', 'controls='=>'array'],
diff --git a/dictionaries/CallMap_82_delta.php b/dictionaries/CallMap_82_delta.php
index 47ded4fbb19..3064f54ff52 100644
--- a/dictionaries/CallMap_82_delta.php
+++ b/dictionaries/CallMap_82_delta.php
@@ -53,6 +53,10 @@
'old' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'],
'new' => ['list', 'string'=>'string', 'length='=>'positive-int'],
],
+ 'mb_get_info' => [
+ 'old' => ['array|string|int|false', 'type='=>'string'],
+ 'new' => ['array|string|int|false|null', 'type='=>'string'],
+ ],
],
'removed' => [
diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php
index afb21ba72df..61e3db09d55 100644
--- a/dictionaries/CallMap_historical.php
+++ b/dictionaries/CallMap_historical.php
@@ -6583,8 +6583,8 @@
'SessionIdInterface::create_sid' => ['string'],
'SessionUpdateTimestampHandler::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'],
- 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'value'=>'string'],
- 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'],
+ 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'id'=>'string', 'data'=>'string'],
+ 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'id'=>'string'],
'SimpleXMLElement::__construct' => ['void', 'data'=>'string', 'options='=>'int', 'dataIsURL='=>'bool', 'namespaceOrPrefix='=>'string', 'isPrefix='=>'bool'],
'SimpleXMLElement::__get' => ['SimpleXMLElement', 'name'=>'string'],
'SimpleXMLElement::__toString' => ['string'],
@@ -9937,7 +9937,7 @@
'decbin' => ['string', 'num'=>'int'],
'dechex' => ['string', 'num'=>'int'],
'decoct' => ['string', 'num'=>'int'],
- 'define' => ['bool', 'constant_name'=>'string', 'value'=>'mixed', 'case_insensitive='=>'bool'],
+ 'define' => ['bool', 'constant_name'=>'string', 'value'=>'array|scalar|null', 'case_insensitive='=>'bool'],
'define_syslog_variables' => ['void'],
'defined' => ['bool', 'constant_name'=>'string'],
'deflate_add' => ['string|false', 'context'=>'resource', 'data'=>'string', 'flush_mode='=>'int'],
@@ -10434,11 +10434,11 @@
'filepro_rowcount' => ['int'],
'filesize' => ['int|false', 'filename'=>'string'],
'filetype' => ['string|false', 'filename'=>'string'],
- 'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'],
+ 'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'],
'filter_id' => ['int|false', 'name'=>'string'],
- 'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
- 'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'],
- 'filter_list' => ['array'],
+ 'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
+ 'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'],
+ 'filter_list' => ['non-empty-list'],
'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'],
'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'],
'finfo::__construct' => ['void', 'flags='=>'int', 'magic_database='=>'string'],
@@ -10679,7 +10679,7 @@
'getopt' => ['array>|false', 'short_options'=>'string', 'long_options='=>'array'],
'getprotobyname' => ['int|false', 'protocol'=>'string'],
'getprotobynumber' => ['string', 'protocol'=>'int'],
- 'getrandmax' => ['int'],
+ 'getrandmax' => ['int<1, max>'],
'getrusage' => ['array', 'mode='=>'int'],
'getservbyname' => ['int|false', 'service'=>'string', 'protocol'=>'string'],
'getservbyport' => ['string|false', 'port'=>'int', 'protocol'=>'string'],
@@ -12488,7 +12488,7 @@
'msql_query' => ['resource', 'query'=>'string', 'link_identifier='=>'?resource'],
'msql_result' => ['string', 'result'=>'resource', 'row'=>'int', 'field='=>'mixed'],
'msql_select_db' => ['bool', 'database_name'=>'string', 'link_identifier='=>'?resource'],
- 'mt_getrandmax' => ['int'],
+ 'mt_getrandmax' => ['int<1, max>'],
'mt_rand' => ['int', 'min'=>'int', 'max'=>'int'],
'mt_rand\'1' => ['int'],
'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'],
@@ -14333,8 +14333,8 @@
'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'],
'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'],
'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'],
- 'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'],
- 'strtok\'1' => ['string|false', 'string'=>'string'],
+ 'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'],
+ 'strtok\'1' => ['non-empty-string|false', 'string'=>'string'],
'strtolower' => ['lowercase-string', 'string'=>'string'],
'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'int'],
'strtoupper' => ['string', 'string'=>'string'],
@@ -15197,7 +15197,7 @@
'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'],
'unpack' => ['array', 'format'=>'string', 'string'=>'string'],
'unregister_tick_function' => ['void', 'callback'=>'callable'],
- 'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:string[]|bool}'],
+ 'unserialize' => ['mixed', 'data'=>'string', 'options='=>'array{allowed_classes?:class-string[]|bool}'],
'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'],
'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'],
'uopz_allow_exit' => ['void', 'allow'=>'bool'],
diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md
index ba14e7d67c9..070f3ddd682 100644
--- a/docs/annotating_code/supported_annotations.md
+++ b/docs/annotating_code/supported_annotations.md
@@ -263,9 +263,9 @@ is not within the given namespace.
array('default' => 'world.com'), 'flags' => FILTER_NULL_ON_FAILURE));
+```
diff --git a/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md b/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md
new file mode 100644
index 00000000000..8d60969633e
--- /dev/null
+++ b/docs/running_psalm/issues/RiskyTruthyFalsyComparison.md
@@ -0,0 +1,29 @@
+# RiskyTruthyFalsyComparison
+
+Emitted when comparing a value with multiple types that can both contain truthy and falsy values.
+
+```php
+ $stmts
*/
- protected function checkWithViewClass(Context $context, array $stmts): void
+ private function checkWithViewClass(Context $context, array $stmts): void
{
$pseudo_method_stmts = [];
diff --git a/examples/TemplateScanner.php b/examples/TemplateScanner.php
index 70c37470ca0..254b06dc338 100644
--- a/examples/TemplateScanner.php
+++ b/examples/TemplateScanner.php
@@ -16,7 +16,7 @@
final class TemplateScanner extends Psalm\Internal\Scanner\FileScanner
{
- const VIEW_CLASS = 'Your\\View\\Class';
+ final public const VIEW_CLASS = 'Your\\View\\Class';
public function scan(
Codebase $codebase,
diff --git a/examples/plugins/ClassUnqualifier.php b/examples/plugins/ClassUnqualifier.php
index 2c84f051ef1..44104376292 100644
--- a/examples/plugins/ClassUnqualifier.php
+++ b/examples/plugins/ClassUnqualifier.php
@@ -29,7 +29,7 @@ public static function afterClassLikeExistenceCheck(
return;
}
- if (strpos($candidate_type, '\\' . $fq_class_name) !== false) {
+ if (str_contains($candidate_type, '\\' . $fq_class_name)) {
$type_tokens = TypeTokenizer::tokenize($candidate_type, false);
foreach ($type_tokens as &$type_token) {
diff --git a/examples/plugins/FunctionCasingChecker.php b/examples/plugins/FunctionCasingChecker.php
index f4147ab3f3f..56ec8983203 100644
--- a/examples/plugins/FunctionCasingChecker.php
+++ b/examples/plugins/FunctionCasingChecker.php
@@ -55,7 +55,7 @@ public static function afterMethodCallAnalysis(AfterMethodCallAnalysisEvent $eve
$statements_source->getSuppressedIssues(),
);
}
- } catch (Exception $e) {
+ } catch (Exception) {
// can throw if storage is missing
}
}
@@ -93,7 +93,7 @@ public static function afterFunctionCallAnalysis(AfterFunctionCallAnalysisEvent
$statements_source->getSuppressedIssues(),
);
}
- } catch (Exception $e) {
+ } catch (Exception) {
// can throw if storage is missing
}
}
diff --git a/examples/plugins/InternalChecker.php b/examples/plugins/InternalChecker.php
index 802edccf87d..0b2ea575187 100644
--- a/examples/plugins/InternalChecker.php
+++ b/examples/plugins/InternalChecker.php
@@ -19,7 +19,7 @@ public static function afterStatementAnalysis(AfterClassLikeAnalysisEvent $event
{
$storage = $event->getClasslikeStorage();
if (!$storage->internal
- && strpos($storage->name, 'Psalm\\Internal') === 0
+ && str_starts_with($storage->name, 'Psalm\\Internal')
&& $storage->location
) {
IssueBuffer::maybeAdd(
diff --git a/examples/plugins/StringChecker.php b/examples/plugins/StringChecker.php
index c16f108a0d0..1ce7a814086 100644
--- a/examples/plugins/StringChecker.php
+++ b/examples/plugins/StringChecker.php
@@ -31,8 +31,8 @@ public static function afterExpressionAnalysis(AfterExpressionAnalysisEvent $eve
if ($expr instanceof PhpParser\Node\Scalar\String_) {
$class_or_class_method = '/^\\\?Psalm(\\\[A-Z][A-Za-z0-9]+)+(::[A-Za-z0-9]+)?$/';
- if (strpos($statements_source->getFileName(), 'base/DefinitionManager.php') === false
- && strpos($expr->value, 'TestController') === false
+ if (!str_contains($statements_source->getFileName(), 'base/DefinitionManager.php')
+ && !str_contains($expr->value, 'TestController')
&& preg_match($class_or_class_method, $expr->value)
) {
/** @psalm-suppress PossiblyInvalidArrayAccess */
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 9728c570c20..813c55f46c5 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -1,5 +1,5 @@
-
+
tags['variablesfrom'][0]]]>
@@ -12,6 +12,45 @@
$matches[1]
+
+
+ cased_name]]>
+
+
+
+
+ !$appearing_method_id
+
+
+
+
+ docblock_line_number]]>
+ docblock_line_number]]>
+ docblock_start]]>
+ docblock_start_line_number]]>
+ text]]>
+
+
+
+
+ !$function_name
+ namespace]]>
+ namespace]]>
+ namespace]]>
+ namespace_first_stmt_start]]>
+ uses_end]]>
+ $file_path
+ insertText]]>
+ symbol, '()')]]>
+ symbol, '()')]]>
+ symbol, '()')]]>
+ symbol, '()')]]>
+ symbol, '::')]]>
+ symbol, '::')]]>
+ symbol, '\\')]]>
+
+
+
$deprecated_element_xml
@@ -22,11 +61,59 @@
$this
+
+ !$composer_json
+ !$config_path
+ !$file_path
+
+ $cwd
+ $dir
+ function_id]]>
+
+ $issue_handler_children
+ $parent_issue_type
+ $phpstorm_meta_files
+ composer_class_loader->findFile($pluginClassName)]]>
+ autoloader]]>
+ localName, $offset)]]>
+ name, $offset - strlen($file_contents))]]>
+
+
+
+
+ $suggested_dir
+
+
+
+ file_path, 'stub')]]>
+ file_path, 'vendor')]]>
+
+
+ !$directory_path
+ !$glob_directory_path
+ !$glob_file_path
+ directory]]>
+ file]]>
+ referencedClass]]>
+ referencedConstant]]>
+ referencedFunction]]>
+ referencedMethod]]>
+ referencedProperty]]>
+ referencedVariable]]>
+ glob($parts[0], GLOB_NOSORT)
+ glob($parts[0], GLOB_ONLYDIR | GLOB_NOSORT)
+
+
+
+
+
+
+
@@ -35,6 +122,17 @@
$matches[3]
+
+
+ $creating_conditional_id
+ $creating_conditional_id
+
+
+
+
+ name->name ?? null !== "name"]]>
+
+
$comments[0]
@@ -42,11 +140,151 @@
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]]>
+ template_types]]>
+ classlike_storage_provider->get($original_fq_classlike_name),
+ strtolower($stmt->name->name),
+ $this_object_type,
+ )]]>
+
$property_name
+
+ !$appearing_property_class
+ self]]>
+ !$declaring_property_class
+ self]]>
+ template_types]]>
+ $resolved_name
+ template_covariants]]>
+ template_extended_params]]>
+ template_types]]>
+ template_types]]>
+
+
+
+
+ self]]>
+ self]]>
+ self]]>
+
+
+
+
+ !$original_type
+ description]]>
+ var_id]]>
+ !$var_type_tokens
+ $template_type_map
+ $type_aliases
+ line_number]]>
+ type_end]]>
+ type_start]]>
+
+
+
+
+ $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
+
+
+
+
+ calling_method_id]]>
+ cased_name]]>
+ cased_name]]>
+
+ 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]]>
+
+
+
+
+
+ !$calling_method_id
+ self]]>
+ $appearing_method_class
+ $appearing_method_class
+ self]]>
+ $context_self
+
+
+
+
+ template_types]]>
+ cased_name]]>
+ cased_name]]>
+ cased_name]]>
+ template_extended_params]]>
+ template_extended_params]]>
+ template_extended_params]]>
+ defining_fqcln]]>
+
+
+
+
+ $source
+
@@ -56,22 +294,86 @@
$php_minor_version
$source_parts[1]
+
+ self]]>
+
+ $potential_file_path
+
+
+
+
+
+ branch_point]]>
+
cond]]>
+
+ branch_point]]>
+
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
+
+
+
+
+ negatable_if_types]]>
+ getTemplateTypeMap()]]>
+
+
+
+
+ getTemplateTypeMap()]]>
+ getTemplateTypeMap()]]>
+
+
+
+
+ getTemplateTypeMap()]]>
+
+
+
+
+ branch_point]]>
+ branch_point]]>
+ branch_point]]>
+ assigned_var_ids]]>
+ new_vars]]>
+ redefined_vars]]>
+ getTemplateTypeMap()]]>
+
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
+
@@ -79,6 +381,29 @@
leftover_statements[0]]]>
traverse([$switch_condition])[0]]]>
+
+ branch_point]]>
+ $nested_or_options
+ $switch_var_id
+ $switch_var_id
+ $switch_var_id
+ $type_statements
+
+
+
+
+ branch_point]]>
+
+
+
+
+ branch_point]]>
+
+
+
+
+ $var_id
+
@@ -111,23 +436,204 @@
getArgs()[0]]]>
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')]]>
+ calling_method_id, '::unserialize')]]>
+
$new_property_name
+
+ calling_method_id]]>
+ $var_id
+ $var_id
+
+
+ var_id]]>
+ ')]]>
+ ')]]>
+
+ $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]]]>
+
+
+ getTemplateTypeMap()]]>
+
+
$invalid_left_messages[0]
$invalid_right_messages[0]
+
+
+ branch_point]]>
+ $var_id
+
+
verifyType
@@ -137,6 +643,54 @@
$parts[1]
+
+ !$container_class
+ !$var_id
+ $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
+ value, '::')]]>
+ value, '::')]]>
+
+
+
+
+ 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]]>
+ calling_method_id]]>
+ calling_method_id]]>
+ 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())]]>
+
@@ -145,19 +699,117 @@
$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]]>
+
getArgs()[0]]]>
$parts[1]
+
+ function_id]]>
+ function_id]]>
+ function_id]]>
+ function_id]]>
+ function_id]]>
+ function_id]]>
+ getTemplateTypeMap()]]>
+ value, '::')]]>
+
$method
+
+ self]]>
+ self]]>
+ self]]>
+ template_types]]>
+ template_types]]>
+
+
+
+
+ mixin_declaring_fqcln]]>
+ mixin_declaring_fqcln]]>
+ template_types]]>
+ template_types]]>
+ 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
+
+
+
+
+ this_property_mutations]]>
+
+
+
+
+ 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
+
@@ -168,23 +820,115 @@
non_existent_interface_method_ids[0]]]>
non_existent_magic_method_ids[0]]]>
+
+ getFQCLN()]]>
+ $lhs_var_id
+ $lhs_var_id
+ $lhs_var_id
+
+
+
+
+ getFQCLN()]]>
+ $path_to_file
+ $var_id
+ ')]]>
+
+
+
+
+
+ calling_method_id]]>
+ self]]>
+ $fq_class_name
+ $fq_class_name
+ getFullyQualifiedFunctionMethodOrNamespaceName()]]>
+ template_extended_params]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+
+
+
+
+ parent_class]]>
+ $child_fq_class_name
+ calling_method_id]]>
+ self]]>
+ self]]>
+
+
+
+
+ self]]>
+ !$fq_class_name
+ mixin_declaring_fqcln]]>
+ parent_class]]>
+ parent_class]]>
+ calling_method_id]]>
+ calling_method_id]]>
+ self]]>
+
$new_method_name
+
+ self]]>
+ self]]>
+ self]]>
+ self]]>
+ $found_generic_params
+ $found_generic_params
+ template_extended_params]]>
+
items[0]]]>
items[1]]]>
+
+ !$arg_var_id
+ $arg_var_id
+ $assertion_var_id
+ template_extended_params]]>
+ self]]>
+ self]]>
+ self]]>
+ template_types]]>
+ template_types]]>
+
$new_const_name
$new_const_name
+
+ self]]>
+ calling_method_id]]>
+ calling_method_id]]>
+ self]]>
+ self]]>
+ self]]>
+ self]]>
+
+
+
+
+ !$lhs_var_name
+ !$object_id
+ !$object_id
+ !$this_class_name
+ $object_id
+ $property_root
+ $resolved_name
+ $resolved_name
+ $root_var_id
+ $this_class_name
+
@@ -192,56 +936,232 @@
$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
+
+
+
+
+ self]]>
+
+
+
+
+ !$switch_var_id
+ $switch_var_id
+
+
+
+
+ $fq_classlike_name
+
+
+
+
+ branch_point]]>
+ getTemplateTypeMap()]]>
+ getTemplateTypeMap()]]>
+
type_params[2]]]>
+
+ var_id]]>
+ $class_template_params
+ declaring_yield_fqcn]]>
+ self]]>
+ line_number]]>
+ type_end]]>
+ type_start]]>
+
$method_name
+
+ calling_function_id]]>
+ calling_method_id]]>
+ var_id]]>
+ calling_function_id]]>
+ self]]>
+ $found_generic_params
+ line_number]]>
+ type_end]]>
+ type_start]]>
+
+
+
+
+ $root_var_id
+ $var_id
+
+
+ $token_list[$iter]
+
$token_list[$iter]
$token_list[$iter]
$token_list[$iter]
$token_list[$iter]
$token_list[0]
- $token_list[1]
expr->getArgs()[0]]]>
+
+ $branch_point
+ $new_issues
+ getNamespace()]]>
+ $possible_traced_variable_names
+ fake_this_class]]>
+ vars_to_initialize]]>
+
+
+
+
+ !$root_path
+
+
+
+
+
+
+
+
+ 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()]]>
+
+
$trait
+
+
+
@@ -251,21 +1171,98 @@
$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
+
+
+
+
+ value]]>
+
$stub
+
+ !$checked_file_path
+ !$root_file_path
+ $args
+ cased_name]]>
+ $namespace
+
+
+
+
+ !$return_type_string
+
methods[$declaring_method_name]->stubbed]]>
+
+ !$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
+
methods[$implementing_method_id->method_name]->abstract]]>
+
+
+ $mapped_name
+ template_extended_params]]>
+ template_extended_params]]>
+ template_extended_params]]>
+ template_types]]>
+ template_extended_params]]>
+
@@ -276,6 +1273,38 @@
$property_name
$property_name
+
+ calling_method_id]]>
+ calling_method_id]]>
+ calling_method_id]]>
+
+
+
+
+ $composer_file_path
+ cased_name]]>
+ cased_name]]>
+
+
+
+
+ specialization_key]]>
+ unspecialized_id]]>
+ escaped_taints]]>
+ unescaped_taints]]>
+ specialization_key]]>
+ path_types)]]>
+
+
+
+
+
+
+
+
+
+ $specialization_key
+
@@ -286,27 +1315,92 @@
stmts[0]]]>
$b_stmt_comments[0]
+
+ stmts]]>
+ stmts]]>
+
$b[$y]
+
+
+ readEnv['CI_PR_NUMBER']]]>
+
+
$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]]>
+ return_typehint_end]]>
+ return_typehint_end]]>
+ return_typehint_start]]>
+ return_typehint_start]]>
+ $php_type
+ new_phpdoc_return_type]]>
+ new_psalm_return_type]]>
+ return_type_description]]>
+ return_type_description]]>
+
+
props[0]]]>
+
+ new_php_type]]>
+ new_php_type]]>
+ new_phpdoc_type]]>
+ typehint_end]]>
+ typehint_end]]>
+ typehint_start]]>
+ typehint_start]]>
+ $preceding_semicolon_pos
+ new_phpdoc_type]]>
+ new_psalm_type]]>
+ type_description]]>
+
$buffer
+
+ !$sockets
+
+
+
+
+ tmpIni]]>
+
+
+
+
+ empty($message)
+
+
+
+
+ $findUnusedVariables
+
@@ -320,12 +1414,30 @@
$result
+
+ 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]
@@ -337,6 +1449,16 @@
$replacement_stmts[0]
$replacement_stmts[0]
+
+ parser->parse(
+ $hacky_class_fix,
+ $error_handler,
+ )]]>
+ parser->parse(
+ $fake_class,
+ $error_handler,
+ )]]>
+
@@ -345,21 +1467,66 @@
children[0]]]>
children[1]]]>
+
+
+
$l[4]
$r[4]
+
+ !$var_line_parts
+ newModifier]]>
+ $class_name
+ description]]>
+ inheritors]]>
+ yield]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ aliases->namespace]]>
+ aliases->namespace]]>
+ line_number]]>
+ type_end]]>
+ type_start]]>
+
+
+
+
+ $fq_classlike_name
+ $string_value
+ $string_value
+ $string_value
+
getArgs()[0]]]>
getArgs()[1]]]>
+
+ !$skip_if_descendants
+ !$skip_if_descendants
+ $include_path
+ $path_to_file
+
+
+ $line_parts
+
+
+ , string>]]>
+
+ $line_parts[0]
+ $line_parts[1]
$since_parts[1]
@@ -367,16 +1534,156 @@
$source_param_string
+
+ namespace]]>
+ template_types]]>
+ template_types]]>
+ description]]>
+ return_type_end]]>
+ return_type_line_number]]>
+ return_type_line_number]]>
+ return_type_start]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ template_types]]>
+ $template_types
+ $template_types
+ $template_types
+
stmts[0]]]>
+
+ stmts]]>
+ aliases->namespace]]>
+ aliases->namespace]]>
+ template_types]]>
+ $fq_classlike_name
+ $function_id
+ $function_id
+ $method_name_lc
+ stmts]]>
+ stmts]]>
+ stmts]]>
+ stmts]]>
+ aliases->namespace]]>
+ aliases->namespace]]>
+
+
+
+
+ $type_string
+
+
+
+
+ aliases->uses_start]]>
+ aliases->uses_start]]>
+ skip_if_descendants]]>
+ skip_if_descendants]]>
+ skip_if_descendants]]>
+ skip_if_descendants]]>
+ skip_if_descendants]]>
+ skip_if_descendants]]>
+ skip_if_descendants]]>
+ code_location->file_path, 'CoreGenericClasses.phpstub')]]>
+ code_location->file_path, 'CoreGenericFunctions.phpstub')]]>
+ file_path, 'CoreGenericIterators.phpstub')]]>
+
$cs[0]
+
+ $offset_map
+ end_change]]>
+ start_change]]>
+
+
+
+
+ !$path
+ $explicit_path
+ psalm_header]]>
+ psalm_tag_end_pos]]>
+
+
+
+
+ enabled_plugins]]>
+
+
+
+
+ !$root_cache_directory
+ $file_contents
+ $file_path
+
+
+
+
+ !$cache_directory
+ !$cache_directory
+ !$cache_directory
+ $cache_directory
+
+
+
+
+ cache->getFileMapCache()]]>
+
+
+
+
+ !$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()]]>
+
@@ -393,6 +1700,73 @@
$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)]]>
+
+
+
+
+
+ $first_line_padding
+
+
+
+
+ !$resolved_name
+ $mapped_type = $map[$offset_arg_value] ?? null
+ $mapped_type = $map[$offset_arg_value] ?? null
+
+
+
+
+
+
+ cased_name]]>
+ template_types]]>
+ parent_class]]>
+ template_types]]>
+
+
+
+
+ cased_name]]>
+ cased_name]]>
+ template_types]]>
+
+
+
+
+ $key
+ $var_id
+ $var_id
+ $var_id
+ $var_id
+ $var_id
+ $var_id
+ $var_id
+
+
properties[0]]]>
@@ -406,6 +1780,36 @@
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
+
@@ -414,11 +1818,83 @@
$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
+
+
+
+
+ template_extended_params]]>
+
getClassTemplateTypes
+
+
+
+ $input_template_types
+ template_extended_params[$container_class])]]>
+ template_extended_params[$base_type->as_type->value])]]>
+ template_extended_params[$base_type->value])]]>
+
+
@@ -429,11 +1905,48 @@
array_type_params[1]]]>
array_type_params[1]]]>
+
+ class_string_types]]>
+ floats]]>
+ ints]]>
+ named_object_types]]>
+ strings]]>
+ array_counts]]>
+ array_min_counts]]>
+ array_min_counts]]>
+ class_string_types]]>
+ class_string_types]]>
+ floats]]>
+ ints]]>
+ ints]]>
+ ints]]>
+ named_object_types]]>
+ strings]]>
+ strings]]>
+ strings]]>
+ strings]]>
+ 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
+
@@ -447,6 +1960,11 @@
array_keys($template_type_map[$fq_classlike_name])[0]
array_keys($template_type_map[$template_param_name])[0]
+
+ $extra_params
+ value, '::')]]>
+ value, '::')]]>
+
@@ -455,6 +1973,59 @@
$type_tokens[$i - 1]
$type_tokens[$i - 1]
+
+ $parent_fqcln
+ $self_fqcln
+
+
+
+
+
+ !$fq_classlike_name
+ template_types]]>
+ template_types]]>
+ calling_method_id]]>
+
+
+
+
+ $function_id
+
+
+
+
+ $function_id
+
+
+
+
+ $function_id
+
+
+
+
+ output_path]]>
+ $parent_issue_type
+
+
+
+
+ other_references]]>
+ taint_trace]]>
+
+
+
+
+
+ other_references]]>
+ taint_trace]]>
+
+
+
+
+
+ taint_trace]]>
+
@@ -469,6 +2040,19 @@
traverse
+
+
+ $this_var_id
+
+
+
+
+ !$namespace
+ $namespace
+ $namespace
+
+
+
classOrInterfaceExists
@@ -487,6 +2071,9 @@
$value
+
+
+
@@ -495,6 +2082,10 @@
replace
replace
+
+ $params
+ $params
+
@@ -505,6 +2096,9 @@
type_params[1]]]>
+
+ !($container_type_params_covariant[$offset] ?? true)
+
@@ -515,6 +2109,10 @@
replace
+
+ !$namespace
+ $namespace
+
@@ -527,11 +2125,28 @@
value_param]]>
+
+
+ !$intersection
+ !$intersection
+
+
replace
+
+
+ !$intersection
+ !$intersection
+
+
+
+
+ !$intersection
+
+
combine
@@ -556,17 +2171,38 @@
properties[0]]]>
+
+
+ !$namespace
+ $namespace
+
+
+
+
+ !$intersection
+ $intersection
+
+
replace
replace
+
+ !$intersection
+ !$intersection
+
replace
+
+
+ !$intersection
+
+
replace
@@ -587,11 +2223,24 @@
$initialized_class
$reference_free
+
+
+
$const_name
+
+
+
+ $array_key_offset
+ $failed_reconciliation
+
+
+ ')]]>
+
+
@@ -627,6 +2276,56 @@
hasLowercaseString
hasLowercaseString
+
+ !$php_type
+ exact_id]]>
+ id]]>
+ exact_id]]>
+ exact_id]]>
+ id]]>
+ id]]>
+
+
+
+
+
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ composer_lock]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+ config_file]]>
+
+
+
+
+
+
+
+
+
+ $level
+ $php_version
+
+
+
+
+
+
+
@@ -638,6 +2337,15 @@
public function getProjectDirectories(): array
+
+
+ $param_type_1
+ $param_type_2
+ $param_type_3
+ $param_type_4
+ $return_type
+
+
diff --git a/src/Psalm/Aliases.php b/src/Psalm/Aliases.php
index daa39b90a30..9471120d03e 100644
--- a/src/Psalm/Aliases.php
+++ b/src/Psalm/Aliases.php
@@ -4,39 +4,11 @@
namespace Psalm;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
+
final class Aliases
{
- /**
- * @var array
- */
- public array $uses;
-
- /**
- * @var array
- */
- public array $uses_flipped;
-
- /**
- * @var array
- */
- public array $functions;
-
- /**
- * @var array
- */
- public array $functions_flipped;
-
- /**
- * @var array
- */
- public array $constants;
-
- /**
- * @var array
- */
- public array $constants_flipped;
-
- public ?string $namespace = null;
+ use UnserializeMemoryUsageSuppressionTrait;
public ?int $namespace_first_stmt_start = null;
@@ -55,20 +27,13 @@ final class Aliases
* @psalm-mutation-free
*/
public function __construct(
- ?string $namespace = null,
- array $uses = [],
- array $functions = [],
- array $constants = [],
- array $uses_flipped = [],
- array $functions_flipped = [],
- array $constants_flipped = [],
+ public ?string $namespace = null,
+ public array $uses = [],
+ public array $functions = [],
+ public array $constants = [],
+ public array $uses_flipped = [],
+ public array $functions_flipped = [],
+ public array $constants_flipped = [],
) {
- $this->namespace = $namespace;
- $this->uses = $uses;
- $this->functions = $functions;
- $this->constants = $constants;
- $this->uses_flipped = $uses_flipped;
- $this->functions_flipped = $functions_flipped;
- $this->constants_flipped = $constants_flipped;
}
}
diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php
index 9c35acfb340..aa787fe0ecd 100644
--- a/src/Psalm/CodeLocation.php
+++ b/src/Psalm/CodeLocation.php
@@ -90,6 +90,32 @@ class CodeLocation
public const CATCH_VAR = 6;
public const FUNCTION_PHPDOC_METHOD = 7;
+ private const PROPERTY_KEYS_FOR_UNSERIALIZE = [
+ 'file_path' => 'file_path',
+ 'file_name' => 'file_name',
+ 'raw_line_number' => 'raw_line_number',
+ "\0" . self::class . "\0" . 'end_line_number' => 'end_line_number',
+ 'raw_file_start' => 'raw_file_start',
+ 'raw_file_end' => 'raw_file_end',
+ "\0*\0" . 'file_start' => 'file_start',
+ "\0*\0" . 'file_end' => 'file_end',
+ "\0*\0" . 'single_line' => 'single_line',
+ "\0*\0" . 'preview_start' => 'preview_start',
+ "\0" . self::class . "\0" . 'preview_end' => 'preview_end',
+ "\0" . self::class . "\0" . 'selection_start' => 'selection_start',
+ "\0" . self::class . "\0" . 'selection_end' => 'selection_end',
+ "\0" . self::class . "\0" . 'column_from' => 'column_from',
+ "\0" . self::class . "\0" . 'column_to' => 'column_to',
+ "\0" . self::class . "\0" . 'snippet' => 'snippet',
+ "\0" . self::class . "\0" . 'text' => 'text',
+ 'docblock_start' => 'docblock_start',
+ "\0" . self::class . "\0" . 'docblock_start_line_number' => 'docblock_start_line_number',
+ "\0*\0" . 'docblock_line_number' => 'docblock_line_number',
+ "\0" . self::class . "\0" . 'regex_type' => 'regex_type',
+ "\0" . self::class . "\0" . 'have_recalculated' => 'have_recalculated',
+ 'previous_location' => 'previous_location',
+ ];
+
public function __construct(
FileSource $file_source,
PhpParser\Node $stmt,
@@ -126,6 +152,19 @@ public function __construct(
$this->docblock_line_number = $comment_line;
}
+ /**
+ * Suppresses memory usage when unserializing objects.
+ *
+ * @see \Psalm\Storage\UnserializeMemoryUsageSuppressionTrait
+ */
+ public function __unserialize(array $properties): void
+ {
+ foreach (self::PROPERTY_KEYS_FOR_UNSERIALIZE as $key => $property_name) {
+ /** @psalm-suppress PossiblyUndefinedStringArrayOffset */
+ $this->$property_name = $properties[$key];
+ }
+ }
+
/**
* @psalm-suppress PossiblyUnusedMethod Part of public API
* @return static
@@ -219,42 +258,17 @@ private function calculateRealLocation(): void
}
if ($this->regex_type !== null) {
- switch ($this->regex_type) {
- case self::VAR_TYPE:
- $regex = '/@(?:psalm-)?var[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
- break;
-
- case self::FUNCTION_RETURN_TYPE:
- $regex = '/\\:\s+(\\??\s*[A-Za-z0-9_\\\\\[\]]+)/';
- break;
-
- case self::FUNCTION_PARAM_TYPE:
- $regex = '/^(\\??\s*[A-Za-z0-9_\\\\\[\]]+)\s/';
- break;
-
- case self::FUNCTION_PHPDOC_RETURN_TYPE:
- $regex = '/@(?:psalm-)?return[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
- break;
-
- case self::FUNCTION_PHPDOC_METHOD:
- $regex = '/@(?:psalm-)?method[ \t]+(.*)/';
- break;
-
- case self::FUNCTION_PHPDOC_PARAM_TYPE:
- $regex = '/@(?:psalm-)?param[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
- break;
-
- case self::FUNCTION_PARAM_VAR:
- $regex = '/(\$[^ ]*)/';
- break;
-
- case self::CATCH_VAR:
- $regex = '/(\$[^ ^\)]*)/';
- break;
-
- default:
- throw new UnexpectedValueException('Unrecognised regex type ' . $this->regex_type);
- }
+ $regex = match ($this->regex_type) {
+ self::VAR_TYPE => '/@(?:psalm-)?var[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/',
+ self::FUNCTION_RETURN_TYPE => '/\\:\s+(\\??\s*[A-Za-z0-9_\\\\\[\]]+)/',
+ self::FUNCTION_PARAM_TYPE => '/^(\\??\s*[A-Za-z0-9_\\\\\[\]]+)\s/',
+ self::FUNCTION_PHPDOC_RETURN_TYPE => '/@(?:psalm-)?return[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/',
+ self::FUNCTION_PHPDOC_METHOD => '/@(?:psalm-)?method[ \t]+(.*)/',
+ self::FUNCTION_PHPDOC_PARAM_TYPE => '/@(?:psalm-)?param[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/',
+ self::FUNCTION_PARAM_VAR => '/(\$[^ ]*)/',
+ self::CATCH_VAR => '/(\$[^ ^\)]*)/',
+ default => throw new UnexpectedValueException('Unrecognised regex type ' . $this->regex_type),
+ };
$preview_snippet = mb_strcut(
$file_contents,
diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php
index e3f48f025ed..7bc14965228 100644
--- a/src/Psalm/Codebase.php
+++ b/src/Psalm/Codebase.php
@@ -89,7 +89,9 @@
use function ksort;
use function preg_match;
use function preg_replace;
+use function str_contains;
use function str_replace;
+use function str_starts_with;
use function strlen;
use function strpos;
use function strrpos;
@@ -99,10 +101,11 @@
use const PHP_VERSION_ID;
+/**
+ * @api
+ */
final class Codebase
{
- public Config $config;
-
/**
* A map of fully-qualified use declarations to the files
* that reference them (keyed by filename)
@@ -130,7 +133,7 @@ final class Codebase
public StatementsProvider $statements_provider;
- private Progress $progress;
+ private readonly Progress $progress;
/**
* @var array
@@ -244,15 +247,13 @@ final class Codebase
/** @internal */
public function __construct(
- Config $config,
+ public Config $config,
Providers $providers,
?Progress $progress = null,
) {
if ($progress === null) {
$progress = new VoidProgress();
}
-
- $this->config = $config;
$this->file_storage_provider = $providers->file_storage_provider;
$this->classlike_storage_provider = $providers->classlike_storage_provider;
$this->progress = $progress;
@@ -507,11 +508,11 @@ public function findReferencesToSymbol(string $symbol): array
throw new UnexpectedValueException('Should not be checking references');
}
- if (strpos($symbol, '::$') !== false) {
+ if (str_contains($symbol, '::$')) {
return $this->findReferencesToProperty($symbol);
}
- if (strpos($symbol, '::') !== false) {
+ if (str_contains($symbol, '::')) {
return $this->findReferencesToMethod($symbol);
}
@@ -849,7 +850,7 @@ public function invalidateInformationForFile(string $file_path): void
try {
$file_storage = $this->file_storage_provider->get($file_path);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return;
}
@@ -938,7 +939,7 @@ public function getMarkupContentForSymbolByReference(
[, $symbol_name] = explode('::', $reference->symbol);
//Class Property
- if (strpos($reference->symbol, '$') !== false) {
+ if (str_contains($reference->symbol, '$')) {
$property_id = (string) preg_replace('/^\\\\/', '', $reference->symbol);
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
[$fq_class_name, $property_name] = explode('::$', $property_id);
@@ -1034,7 +1035,7 @@ public function getMarkupContentForSymbolByReference(
}
//Procedural Variable
- if (strpos($reference->symbol, '$') === 0) {
+ if (str_starts_with($reference->symbol, '$')) {
$type = VariableFetchAnalyzer::getGlobalType($reference->symbol, $this->analysis_php_version_id);
if (!$type->isMixed()) {
return new PHPMarkdownContent(
@@ -1055,7 +1056,7 @@ public function getMarkupContentForSymbolByReference(
$storage->name,
$storage->description,
);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
//continue on as normal
}
@@ -1152,7 +1153,7 @@ public function getSymbolLocationByReference(Reference $reference): ?CodeLocatio
return $storage->location;
}
- if (strpos($reference->symbol, '$') !== false) {
+ if (str_contains($reference->symbol, '$')) {
$storage = $this->properties->getStorage(
$reference->symbol,
);
@@ -1204,13 +1205,12 @@ public function getSymbolLocationByReference(Reference $reference): ?CodeLocatio
error_log($e->getMessage());
return null;
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return null;
}
}
/**
- * @psalm-suppress PossiblyUnusedMethod
* @return array{0: string, 1: Range}|null
*/
public function getReferenceAtPosition(string $file_path, Position $position): ?array
@@ -1344,7 +1344,7 @@ public function getSignatureInformation(
): ?SignatureInformation {
$signature_label = '';
$signature_documentation = null;
- if (strpos($function_symbol, '::') !== false) {
+ if (str_contains($function_symbol, '::')) {
/** @psalm-suppress ArgumentTypeCoercion */
$method_id = new MethodIdentifier(...explode('::', $function_symbol));
@@ -1373,11 +1373,11 @@ public function getSignatureInformation(
$params = $function_storage->params;
$signature_label = $function_storage->cased_name;
$signature_documentation = $function_storage->description;
- } catch (Exception $exception) {
+ } catch (Exception) {
if (InternalCallMapHandler::inCallMap($function_symbol)) {
$callables = InternalCallMapHandler::getCallablesFromCallMap($function_symbol);
- if (!$callables || !$callables[0]->params) {
+ if (!$callables || !isset($callables[0]->params)) {
return null;
}
@@ -1527,7 +1527,7 @@ public function getBeginedLiteralPart(string $file_path, Position $position): st
$offset = $position->toOffset($file_contents);
preg_match('/\$?\w+$/', substr($file_contents, 0, $offset), $matches);
-
+
return $matches[0] ?? '';
}
@@ -1653,7 +1653,7 @@ public function getCompletionItemsForClassishThing(
str_replace('$', '', $property_name),
);
}
-
+
foreach ($class_storage->pseudo_property_set_types as $property_name => $type) {
$pseudo_property_types[$property_name] = new CompletionItem(
str_replace('$', '', $property_name),
@@ -1665,7 +1665,7 @@ public function getCompletionItemsForClassishThing(
str_replace('$', '', $property_name),
);
}
-
+
$completion_items = [...$completion_items, ...array_values($pseudo_property_types)];
}
@@ -1777,7 +1777,7 @@ public function getCompletionItemsForPartialSymbol(
foreach ($file_storage->classlikes_in_file as $fq_class_name => $_) {
try {
$class_storage = $this->classlike_storage_provider->get($fq_class_name);
- } catch (Exception $e) {
+ } catch (Exception) {
continue;
}
@@ -1852,7 +1852,7 @@ public function getCompletionItemsForPartialSymbol(
try {
$class_storage = $this->classlike_storage_provider->get($fq_class_name);
$description = $class_storage->description;
- } catch (Exception $e) {
+ } catch (Exception) {
$description = null;
}
@@ -1892,14 +1892,14 @@ public function getCompletionItemsForPartialSymbol(
}
$in_namespace_map = false;
foreach ($namespace_map as $namespace_name => $namespace_alias) {
- if (strpos($function_lowercase, $namespace_name . '\\') === 0) {
+ if (str_starts_with($function_lowercase, $namespace_name . '\\')) {
$function_name = $namespace_alias . '\\' . substr($function_name, strlen($namespace_name) + 1);
$in_namespace_map = true;
}
}
// If the function is not use'd, and it's not a global function
// prepend it with a backslash.
- if (!$in_namespace_map && strpos($function_name, '\\') !== false) {
+ if (!$in_namespace_map && str_contains($function_name, '\\')) {
$function_name = '\\' . $function_name;
}
$completion_items[] = new CompletionItem(
@@ -2112,7 +2112,6 @@ public function queueClassLikeForScanning(
/**
* @param array $taints
- * @psalm-suppress PossiblyUnusedMethod
*/
public function addTaintSource(
Union $expr_type,
@@ -2139,7 +2138,6 @@ public function addTaintSource(
/**
* @param array $taints
- * @psalm-suppress PossiblyUnusedMethod
*/
public function addTaintSink(
string $taint_id,
diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php
index 9c188d9e0f2..af4810c755c 100644
--- a/src/Psalm/Config.php
+++ b/src/Psalm/Config.php
@@ -636,7 +636,7 @@ public static function loadFromXMLFile(string $file_path, string $current_dir):
{
$file_contents = file_get_contents($file_path);
- $base_dir = dirname($file_path) . DIRECTORY_SEPARATOR;
+ $base_dir = dirname($file_path);
if ($file_contents === false) {
throw new InvalidArgumentException('Cannot open ' . $file_path);
@@ -1147,7 +1147,17 @@ private static function fromXmlAndPaths(
// any paths passed via CLI should be added to the projectFiles
// as they're getting analyzed like if they are part of the project
// ProjectAnalyzer::getInstance()->check_paths_files is not populated at this point in time
- $paths_to_check = CliUtils::getPathsToCheck(null);
+
+ $paths_to_check = null;
+
+ global $argv;
+
+ // Hack for Symfonys own argv resolution.
+ // @see https://github.com/vimeo/psalm/issues/10465
+ if (!isset($argv[0]) || basename($argv[0]) !== 'psalm-plugin') {
+ $paths_to_check = CliUtils::getPathsToCheck(null);
+ }
+
if ($paths_to_check !== null) {
$paths_to_add_to_project_files = array();
foreach ($paths_to_check as $path) {
@@ -1160,7 +1170,7 @@ private static function fromXmlAndPaths(
}
// we need an absolute path for checks
- if ($path[0] !== '/' && DIRECTORY_SEPARATOR === '/') {
+ if (Path::isRelative($path)) {
$prospective_path = $base_dir . DIRECTORY_SEPARATOR . $path;
} else {
$prospective_path = $path;
@@ -1307,7 +1317,7 @@ private static function fromXmlAndPaths(
$path = Path::isAbsolute($plugin_file_name)
? $plugin_file_name
- : $config->base_dir . $plugin_file_name;
+ : $config->base_dir . DIRECTORY_SEPARATOR . $plugin_file_name;
$config->addPluginPath($path);
}
diff --git a/src/Psalm/Config/Creator.php b/src/Psalm/Config/Creator.php
index aa52bee23e1..2f88ade1042 100644
--- a/src/Psalm/Config/Creator.php
+++ b/src/Psalm/Config/Creator.php
@@ -289,11 +289,11 @@ private static function guessPhpFileDirs(string $current_dir): array
$nodes = [];
/** @var string[] */
- $php_files = array_merge(
- glob($current_dir . DIRECTORY_SEPARATOR . '*.php', GLOB_NOSORT) ?: [],
- glob($current_dir . DIRECTORY_SEPARATOR . '**/*.php', GLOB_NOSORT) ?: [],
- glob($current_dir . DIRECTORY_SEPARATOR . '**/**/*.php', GLOB_NOSORT) ?: [],
- );
+ $php_files = [
+ ...glob($current_dir . DIRECTORY_SEPARATOR . '*.php', GLOB_NOSORT) ?: [],
+ ...glob($current_dir . DIRECTORY_SEPARATOR . '**/*.php', GLOB_NOSORT) ?: [],
+ ...glob($current_dir . DIRECTORY_SEPARATOR . '**/**/*.php', GLOB_NOSORT) ?: [],
+ ];
foreach ($php_files as $php_file) {
$php_file = str_replace($current_dir . DIRECTORY_SEPARATOR, '', $php_file);
diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php
index 4eb8c1a32f2..d2dac452471 100644
--- a/src/Psalm/Config/FileFilter.php
+++ b/src/Psalm/Config/FileFilter.php
@@ -9,6 +9,7 @@
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SimpleXMLElement;
+use Symfony\Component\Filesystem\Path;
use function array_filter;
use function array_map;
@@ -30,9 +31,10 @@
use function restore_error_handler;
use function rtrim;
use function set_error_handler;
+use function str_contains;
use function str_replace;
+use function str_starts_with;
use function stripos;
-use function strpos;
use function strtolower;
use const DIRECTORY_SEPARATOR;
@@ -90,8 +92,6 @@ class FileFilter
*/
protected array $files_lowercase = [];
- protected bool $inclusive;
-
/**
* @var array
*/
@@ -102,9 +102,8 @@ class FileFilter
*/
protected array $declare_strict_types = [];
- public function __construct(bool $inclusive)
+ public function __construct(protected bool $inclusive)
{
- $this->inclusive = $inclusive;
}
/**
@@ -127,14 +126,14 @@ public static function loadFromArray(
$resolve_symlinks = (bool) ($directory['resolveSymlinks'] ?? false);
$declare_strict_types = (bool) ($directory['useStrictTypes'] ?? false);
- if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
+ if (Path::isAbsolute($directory_path)) {
/** @var non-empty-string */
$prospective_directory_path = $directory_path;
} else {
$prospective_directory_path = $base_dir . DIRECTORY_SEPARATOR . $directory_path;
}
- if (strpos($prospective_directory_path, '*') !== false) {
+ if (str_contains($prospective_directory_path, '*')) {
// Strip meaningless trailing recursive wildcard like "path/**/" or "path/**"
$prospective_directory_path = (string) preg_replace(
'#(\/\*\*)+\/?$#',
@@ -250,14 +249,14 @@ public static function loadFromArray(
foreach ($config['file'] as $file) {
$file_path = (string) ($file['name'] ?? '');
- if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
+ if (Path::isAbsolute($file_path)) {
/** @var non-empty-string */
$prospective_file_path = $file_path;
} else {
$prospective_file_path = $base_dir . DIRECTORY_SEPARATOR . $file_path;
}
- if (strpos($prospective_file_path, '*') !== false) {
+ if (str_contains($prospective_file_path, '*')) {
// Split by /**/, allow duplicated wildcards like "path/**/**/path" and any leading dir separator.
/** @var non-empty-list $path_parts */
$path_parts = preg_split('#(\/|\\\)(\*\*\/)+#', $prospective_file_path);
@@ -311,7 +310,7 @@ public static function loadFromArray(
foreach ($config['referencedClass'] as $referenced_class) {
$class_name = strtolower((string) ($referenced_class['name'] ?? ''));
- if (strpos($class_name, '*') !== false) {
+ if (str_contains($class_name, '*')) {
$regex = '/' . str_replace('*', '.*', str_replace('\\', '\\\\', $class_name)) . '/i';
$filter->fq_classlike_patterns[] = $regex;
} else {
@@ -522,7 +521,7 @@ public function allows(string $file_name, bool $case_sensitive = false): bool
if ($this->inclusive) {
foreach ($this->directories as $include_dir) {
if ($case_sensitive) {
- if (strpos($file_name, $include_dir) === 0) {
+ if (str_starts_with($file_name, $include_dir)) {
return true;
}
} else {
@@ -548,7 +547,7 @@ public function allows(string $file_name, bool $case_sensitive = false): bool
// exclusive
foreach ($this->directories as $exclude_dir) {
if ($case_sensitive) {
- if (strpos($file_name, $exclude_dir) === 0) {
+ if (str_starts_with($file_name, $exclude_dir)) {
return false;
}
} else {
diff --git a/src/Psalm/Config/ProjectFileFilter.php b/src/Psalm/Config/ProjectFileFilter.php
index e3ffd4b20f0..2526413db04 100644
--- a/src/Psalm/Config/ProjectFileFilter.php
+++ b/src/Psalm/Config/ProjectFileFilter.php
@@ -7,8 +7,8 @@
use Psalm\Exception\ConfigException;
use SimpleXMLElement;
+use function str_starts_with;
use function stripos;
-use function strpos;
/** @internal */
final class ProjectFileFilter extends FileFilter
@@ -59,7 +59,7 @@ public function reportTypeStats(string $file_name, bool $case_sensitive = false)
{
foreach ($this->ignore_type_stats as $exclude_dir => $_) {
if ($case_sensitive) {
- if (strpos($file_name, $exclude_dir) === 0) {
+ if (str_starts_with($file_name, $exclude_dir)) {
return false;
}
} else {
@@ -76,7 +76,7 @@ public function useStrictTypes(string $file_name, bool $case_sensitive = false):
{
foreach ($this->declare_strict_types as $exclude_dir => $_) {
if ($case_sensitive) {
- if (strpos($file_name, $exclude_dir) === 0) {
+ if (str_starts_with($file_name, $exclude_dir)) {
return true;
}
} else {
diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php
index 084e0650b31..b09a704d2c2 100644
--- a/src/Psalm/Context.php
+++ b/src/Psalm/Context.php
@@ -30,6 +30,7 @@
use function preg_match;
use function preg_quote;
use function preg_replace;
+use function str_contains;
use function strpos;
use function strtolower;
@@ -148,12 +149,6 @@ final class Context
public ?CodeLocation $include_location = null;
- /**
- * @var string|null
- * The name of the current class. Null if outside a class.
- */
- public ?string $self = null;
-
public ?string $parent = null;
public bool $check_classes = true;
@@ -332,9 +327,13 @@ final class Context
public array $parent_remove_vars = [];
/** @internal */
- public function __construct(?string $self = null)
- {
- $this->self = $self;
+ public function __construct(
+ /**
+ * @var string|null
+ * The name of the current class. Null if outside a class.
+ */
+ public ?string $self = null,
+ ) {
}
public function __destruct()
@@ -696,7 +695,7 @@ public function removeMutableObjectVars(bool $methods_only = false): void
foreach ($this->vars_in_scope as $var_id => $type) {
if ($type->has_mutations
- && (strpos($var_id, '->') !== false || strpos($var_id, '::') !== false)
+ && (str_contains($var_id, '->') || str_contains($var_id, '::'))
&& (!$methods_only || strpos($var_id, '()'))
) {
$vars_to_remove[] = $var_id;
@@ -717,7 +716,7 @@ public function removeMutableObjectVars(bool $methods_only = false): void
$abandon_clause = false;
foreach (array_keys($clause->possibilities) as $key) {
- if ((strpos($key, '->') !== false || strpos($key, '::') !== false)
+ if ((str_contains($key, '->') || str_contains($key, '::'))
&& (!$methods_only || strpos($key, '()'))
) {
$abandon_clause = true;
diff --git a/src/Psalm/DocComment.php b/src/Psalm/DocComment.php
index 791f287066c..0a6b0cce668 100644
--- a/src/Psalm/DocComment.php
+++ b/src/Psalm/DocComment.php
@@ -12,8 +12,8 @@
use function explode;
use function in_array;
use function preg_match;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function strspn;
use function substr;
use function trim;
@@ -50,7 +50,7 @@ public static function parsePreservingLength(Doc $docblock): ParsedDocblock
);
foreach ($parsed_docblock->tags as $special_key => $_) {
- if (strpos($special_key, 'psalm-') === 0) {
+ if (str_starts_with($special_key, 'psalm-')) {
$special_key = substr($special_key, 6);
if (!in_array(
diff --git a/src/Psalm/Exception/UnresolvableConstantException.php b/src/Psalm/Exception/UnresolvableConstantException.php
index 175fe7b033e..1f18cf24842 100644
--- a/src/Psalm/Exception/UnresolvableConstantException.php
+++ b/src/Psalm/Exception/UnresolvableConstantException.php
@@ -8,13 +8,7 @@
final class UnresolvableConstantException extends Exception
{
- public string $class_name;
-
- public string $const_name;
-
- public function __construct(string $class_name, string $const_name)
+ public function __construct(public string $class_name, public string $const_name)
{
- $this->class_name = $class_name;
- $this->const_name = $const_name;
}
}
diff --git a/src/Psalm/FileBasedPluginAdapter.php b/src/Psalm/FileBasedPluginAdapter.php
index 882e1b7d85b..e6aef73fcc8 100644
--- a/src/Psalm/FileBasedPluginAdapter.php
+++ b/src/Psalm/FileBasedPluginAdapter.php
@@ -22,21 +22,18 @@
/** @internal */
final class FileBasedPluginAdapter implements PluginEntryPointInterface
{
- private string $path;
+ private readonly string $path;
- private Codebase $codebase;
-
- private Config $config;
-
- public function __construct(string $path, Config $config, Codebase $codebase)
- {
+ public function __construct(
+ string $path,
+ private readonly Config $config,
+ private Codebase $codebase,
+ ) {
if (!$path) {
throw new UnexpectedValueException('$path cannot be empty');
}
$this->path = $path;
- $this->config = $config;
- $this->codebase = $codebase;
}
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
diff --git a/src/Psalm/FileManipulation.php b/src/Psalm/FileManipulation.php
index 56acda2c230..f475b0b6855 100644
--- a/src/Psalm/FileManipulation.php
+++ b/src/Psalm/FileManipulation.php
@@ -12,28 +12,13 @@
final class FileManipulation
{
- public int $start;
-
- public int $end;
-
- public string $insertion_text;
-
- public bool $preserve_indentation;
-
- public bool $remove_trailing_newline;
-
public function __construct(
- int $start,
- int $end,
- string $insertion_text,
- bool $preserve_indentation = false,
- bool $remove_trailing_newline = false,
+ public int $start,
+ public int $end,
+ public string $insertion_text,
+ public bool $preserve_indentation = false,
+ public bool $remove_trailing_newline = false,
) {
- $this->start = $start;
- $this->end = $end;
- $this->insertion_text = $insertion_text;
- $this->preserve_indentation = $preserve_indentation;
- $this->remove_trailing_newline = $remove_trailing_newline;
}
public function getKey(): string
diff --git a/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php b/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php
index 71f22863d2d..1bc26151484 100644
--- a/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php
@@ -45,7 +45,7 @@ public static function checkForParadox(
): void {
try {
$negated_formula2 = Algebra::negateFormula($formula_2);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
return;
}
diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
index 9f3e94472f8..7ac69ca8a63 100644
--- a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
@@ -25,11 +25,11 @@
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Union;
+use function array_key_first;
use function array_shift;
use function array_values;
use function assert;
use function count;
-use function reset;
use function strtolower;
/**
@@ -264,7 +264,7 @@ private static function getAttributeClassFlags(
return self::TARGET_ALL; // Defaults to TARGET_ALL
}
- $first_arg = reset($attribute_attribute->args);
+ $first_arg = $attribute_attribute->args[array_key_first($attribute_attribute->args)];
$first_arg_type = $first_arg->type;
diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
index 57ba7d23cd5..65738925bba 100644
--- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
@@ -350,7 +350,7 @@ public function analyze(
null,
true,
),
- $this->getSuppressedIssues(),
+ $storage->getSuppressedIssuesForTemplateExtendParams() + $this->getSuppressedIssues(),
);
}
}
@@ -580,7 +580,7 @@ public function analyze(
try {
$trait_file_analyzer = $project_analyzer->getFileAnalyzerForClassLike($fq_trait_name);
- } catch (Exception $e) {
+ } catch (Exception) {
continue;
}
@@ -907,7 +907,7 @@ public static function addContextProperties(
try {
$docBlock = DocComment::parsePreservingLength($docComment);
$suppressed = $docBlock->tags['psalm-suppress'] ?? [];
- } catch (DocblockParseException $e) {
+ } catch (DocblockParseException) {
// do nothing to keep original behavior
}
}
@@ -2039,7 +2039,7 @@ private function checkImplementedInterfaces(
try {
$interface_storage = $classlike_storage_provider->get($fq_interface_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return false;
}
@@ -2073,7 +2073,7 @@ private function checkImplementedInterfaces(
foreach ($storage->class_implements as $fq_interface_name_lc => $fq_interface_name) {
try {
$interface_storage = $classlike_storage_provider->get($fq_interface_name_lc);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return false;
}
@@ -2436,7 +2436,7 @@ private function checkParentClass(
$code_location,
$storage->template_type_extends_count[$parent_fq_class_name] ?? 0,
);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
// do nothing
}
}
diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
index 2e89585d8f6..47e716f2225 100644
--- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
@@ -82,12 +82,8 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
'unknown type' => true,
];
- protected PhpParser\Node\Stmt\ClassLike $class;
-
public FileAnalyzer $file_analyzer;
- protected string $fq_class_name;
-
/**
* The parent class
*/
@@ -95,12 +91,13 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer
protected ClassLikeStorage $storage;
- public function __construct(PhpParser\Node\Stmt\ClassLike $class, SourceAnalyzer $source, string $fq_class_name)
- {
- $this->class = $class;
+ public function __construct(
+ protected PhpParser\Node\Stmt\ClassLike $class,
+ SourceAnalyzer $source,
+ protected string $fq_class_name,
+ ) {
$this->source = $source;
$this->file_analyzer = $source->getFileAnalyzer();
- $this->fq_class_name = $fq_class_name;
$codebase = $source->getCodebase();
$this->storage = $codebase->classlike_storage_provider->get($fq_class_name);
}
@@ -806,7 +803,7 @@ public static function getClassesForFile(Codebase $codebase, string $file_path):
{
try {
return $codebase->file_storage_provider->get($file_path)->classlikes_in_file;
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return [];
}
}
diff --git a/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php b/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php
index d1fb0c95218..64e9db31885 100644
--- a/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php
+++ b/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php
@@ -9,31 +9,13 @@
*/
final class ClassLikeNameOptions
{
- public bool $inferred;
-
- public bool $allow_trait;
-
- public bool $allow_interface;
-
- public bool $allow_enum;
-
- public bool $from_docblock;
-
- public bool $from_attribute;
-
public function __construct(
- bool $inferred = false,
- bool $allow_trait = false,
- bool $allow_interface = true,
- bool $allow_enum = true,
- bool $from_docblock = false,
- bool $from_attribute = false,
+ public bool $inferred = false,
+ public bool $allow_trait = false,
+ public bool $allow_interface = true,
+ public bool $allow_enum = true,
+ public bool $from_docblock = false,
+ public bool $from_attribute = false,
) {
- $this->inferred = $inferred;
- $this->allow_trait = $allow_trait;
- $this->allow_interface = $allow_interface;
- $this->allow_enum = $allow_enum;
- $this->from_docblock = $from_docblock;
- $this->from_attribute = $from_attribute;
}
}
diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php
index 34cd81bc0fb..0149e800c92 100644
--- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php
@@ -14,15 +14,15 @@
use Psalm\Issue\PossiblyUndefinedVariable;
use Psalm\Issue\UndefinedVariable;
use Psalm\IssueBuffer;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
use Psalm\Type;
-use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use function in_array;
use function is_string;
use function preg_match;
-use function strpos;
+use function str_starts_with;
use function strtolower;
/**
@@ -31,6 +31,7 @@
*/
final class ClosureAnalyzer extends FunctionLikeAnalyzer
{
+ use UnserializeMemoryUsageSuppressionTrait;
/**
* @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function
*/
@@ -104,7 +105,7 @@ public static function analyzeExpression(
}
foreach ($context->vars_in_scope as $var => $type) {
- if (strpos($var, '$this->') === 0) {
+ if (str_starts_with($var, '$this->')) {
$use_context->vars_in_scope[$var] = $type;
}
}
@@ -122,7 +123,7 @@ public static function analyzeExpression(
}
foreach ($context->vars_possibly_in_scope as $var => $_) {
- if (strpos($var, '$this->') === 0) {
+ if (str_starts_with($var, '$this->')) {
$use_context->vars_possibly_in_scope[$var] = true;
}
}
@@ -135,12 +136,6 @@ public static function analyzeExpression(
$use_var_id = '$' . $use->var->name;
- // insert the ref into the current context if passed by ref, as whatever we're passing
- // the closure to could execute it straight away.
- if ($use->byRef && !$context->hasVariable($use_var_id)) {
- $context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]);
- }
-
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
&& $context->hasVariable($use_var_id)
) {
@@ -156,7 +151,7 @@ public static function analyzeExpression(
}
$use_context->vars_in_scope[$use_var_id] =
- $context->hasVariable($use_var_id) && !$use->byRef
+ $context->hasVariable($use_var_id)
? $context->vars_in_scope[$use_var_id]
: Type::getMixed();
@@ -207,7 +202,12 @@ public static function analyzeExpression(
$use_context->calling_method_id = $context->calling_method_id;
$use_context->phantom_classes = $context->phantom_classes;
- $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false);
+ $byref_vars = [];
+ $closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false, $byref_vars);
+
+ foreach ($byref_vars as $key => $value) {
+ $context->vars_in_scope[$key] = $value;
+ }
if ($closure_analyzer->inferred_impure
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
@@ -231,7 +231,7 @@ public static function analyzeExpression(
/**
* @return false|null
*/
- public static function analyzeClosureUses(
+ private static function analyzeClosureUses(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\Closure $stmt,
Context $context,
@@ -270,21 +270,6 @@ public static function analyzeClosureUses(
continue;
}
- if ($use->byRef) {
- $context->vars_in_scope[$use_var_id] = Type::getMixed();
- $context->vars_possibly_in_scope[$use_var_id] = true;
-
- if (!$statements_analyzer->hasVariable($use_var_id)) {
- $statements_analyzer->registerVariable(
- $use_var_id,
- new CodeLocation($statements_analyzer, $use->var),
- null,
- );
- }
-
- return null;
- }
-
if (!isset($context->vars_possibly_in_scope[$use_var_id])) {
if ($context->check_variables) {
if (IssueBuffer::accepts(
@@ -331,14 +316,6 @@ public static function analyzeClosureUses(
continue;
}
- } elseif ($use->byRef) {
- $new_type = new Union([new TMixed()], [
- 'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes,
- ]);
-
- $context->remove($use_var_id);
-
- $context->vars_in_scope[$use_var_id] = $new_type;
}
}
diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
index ea50398ba48..572df0f5078 100644
--- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
@@ -138,7 +138,7 @@ public static function arrayToDocblocks(
$template_type_map,
$type_aliases,
);
- } catch (TypeParseTreeException $e) {
+ } catch (TypeParseTreeException) {
throw new DocblockParseException($line_parts[0] . ' is not a valid type');
}
diff --git a/src/Psalm/Internal/Analyzer/DataFlowNodeData.php b/src/Psalm/Internal/Analyzer/DataFlowNodeData.php
index 7f2e989a547..c5711d701ad 100644
--- a/src/Psalm/Internal/Analyzer/DataFlowNodeData.php
+++ b/src/Psalm/Internal/Analyzer/DataFlowNodeData.php
@@ -14,51 +14,18 @@ final class DataFlowNodeData
{
use ImmutableNonCloneableTrait;
- public int $line_from;
-
- public int $line_to;
-
- public string $label;
-
- public string $file_name;
-
- public string $file_path;
-
- public string $snippet;
-
- public int $from;
-
- public int $to;
-
- public int $snippet_from;
-
- public int $column_from;
-
- public int $column_to;
-
public function __construct(
- string $label,
- int $line_from,
- int $line_to,
- string $file_name,
- string $file_path,
- string $snippet,
- int $from,
- int $to,
- int $snippet_from,
- int $column_from,
- int $column_to,
+ public readonly string $label,
+ public readonly int $line_from,
+ public readonly int $line_to,
+ public readonly string $file_name,
+ public readonly string $file_path,
+ public readonly string $snippet,
+ public readonly int $from,
+ public readonly int $to,
+ public readonly int $snippet_from,
+ public readonly int $column_from,
+ public readonly int $column_to,
) {
- $this->label = $label;
- $this->line_from = $line_from;
- $this->line_to = $line_to;
- $this->file_name = $file_name;
- $this->file_path = $file_path;
- $this->snippet = $snippet;
- $this->from = $from;
- $this->to = $to;
- $this->snippet_from = $snippet_from;
- $this->column_from = $column_from;
- $this->column_to = $column_to;
}
}
diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php
index 832bfecb59e..bbbe7c41c1e 100644
--- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php
@@ -34,7 +34,7 @@
use function array_diff_key;
use function array_keys;
use function count;
-use function strpos;
+use function str_starts_with;
use function strtolower;
/**
@@ -45,13 +45,9 @@ class FileAnalyzer extends SourceAnalyzer
{
use CanAlias;
- protected string $file_name;
+ private ?string $root_file_path = null;
- protected string $file_path;
-
- protected ?string $root_file_path = null;
-
- protected ?string $root_file_name = null;
+ private ?string $root_file_name = null;
/**
* @var array
@@ -95,8 +91,6 @@ class FileAnalyzer extends SourceAnalyzer
public ?Context $context = null;
- public ProjectAnalyzer $project_analyzer;
-
public Codebase $codebase;
private int $first_statement_offset = -1;
@@ -105,12 +99,12 @@ class FileAnalyzer extends SourceAnalyzer
private ?Union $return_type = null;
- public function __construct(ProjectAnalyzer $project_analyzer, string $file_path, string $file_name)
- {
+ public function __construct(
+ public ProjectAnalyzer $project_analyzer,
+ protected string $file_path,
+ protected string $file_name,
+ ) {
$this->source = $this;
- $this->file_path = $file_path;
- $this->file_name = $file_name;
- $this->project_analyzer = $project_analyzer;
$this->codebase = $project_analyzer->getCodebase();
}
@@ -148,7 +142,7 @@ public function analyze(
try {
$stmts = $codebase->getStatementsForFile($this->file_path);
- } catch (PhpParser\Error $e) {
+ } catch (PhpParser\Error) {
return;
}
@@ -395,13 +389,13 @@ public function getMethodMutations(
$call_context->calling_method_id = $this_context->calling_method_id;
foreach ($this_context->vars_possibly_in_scope as $var => $_) {
- if (strpos($var, '$this->') === 0) {
+ if (str_starts_with($var, '$this->')) {
$call_context->vars_possibly_in_scope[$var] = true;
}
}
foreach ($this_context->vars_in_scope as $var => $type) {
- if (strpos($var, '$this->') === 0) {
+ if (str_starts_with($var, '$this->')) {
$call_context->vars_in_scope[$var] = $type;
}
}
diff --git a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php
index bcf1395909a..3c8a293b0be 100644
--- a/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php
@@ -7,6 +7,7 @@
use PhpParser;
use Psalm\Config;
use Psalm\Context;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
use UnexpectedValueException;
use function is_string;
@@ -18,6 +19,7 @@
*/
final class FunctionAnalyzer extends FunctionLikeAnalyzer
{
+ use UnserializeMemoryUsageSuppressionTrait;
public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source)
{
$codebase = $source->getCodebase();
diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
index ae369d1307f..65d38b25096 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
@@ -58,7 +58,7 @@
use function count;
use function implode;
use function in_array;
-use function strpos;
+use function str_starts_with;
use function strtolower;
/**
@@ -126,7 +126,7 @@ public static function verifyReturnType(
$is_to_string = $function instanceof ClassMethod && strtolower($function->name->name) === '__tostring';
if ($function instanceof ClassMethod
- && strpos($function->name->name, '__') === 0
+ && str_starts_with($function->name->name, '__')
&& !$is_to_string
&& !$return_type
) {
diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
index 80bf330b332..46aaddd004e 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
@@ -47,6 +47,8 @@
use Psalm\Issue\UnusedDocblockParam;
use Psalm\Issue\UnusedParam;
use Psalm\IssueBuffer;
+use Psalm\Node\Expr\VirtualVariable;
+use Psalm\Node\Stmt\VirtualWhile;
use Psalm\Plugin\EventHandler\Event\AfterFunctionLikeAnalysisEvent;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FunctionLikeParameter;
@@ -79,6 +81,7 @@
use function md5;
use function microtime;
use function reset;
+use function str_starts_with;
use function strpos;
use function strtolower;
use function substr;
@@ -91,11 +94,6 @@
*/
abstract class FunctionLikeAnalyzer extends SourceAnalyzer
{
- /**
- * @var TFunction
- */
- protected Closure|Function_|ClassMethod|ArrowFunction $function;
-
protected Codebase $codebase;
/**
@@ -135,30 +133,33 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
*/
public array $param_nodes = [];
- protected FunctionLikeStorage $storage;
-
/**
* @param TFunction $function
*/
- public function __construct($function, SourceAnalyzer $source, FunctionLikeStorage $storage)
- {
- $this->function = $function;
+ public function __construct(
+ protected Closure|Function_|ClassMethod|ArrowFunction $function,
+ SourceAnalyzer $source,
+ protected FunctionLikeStorage $storage,
+ ) {
$this->source = $source;
$this->suppressed_issues = $source->getSuppressedIssues();
$this->codebase = $source->getCodebase();
- $this->storage = $storage;
}
/**
* @param bool $add_mutations whether or not to add mutations to this method
+ * @param array $byref_vars
+ * @param-out array $byref_vars
* @return false|null
* @psalm-suppress PossiblyUnusedReturnValue unused but seems important
+ * @psalm-suppress ComplexMethod Unavoidably complex
*/
public function analyze(
Context $context,
NodeDataProvider $type_provider,
?Context $global_context = null,
bool $add_mutations = false,
+ array &$byref_vars = [],
): ?bool {
$storage = $this->storage;
@@ -188,7 +189,13 @@ public function analyze(
|| !in_array("UnusedPsalmSuppress", $storage->suppressed_issues)
) {
foreach ($storage->suppressed_issues as $offset => $issue_name) {
- IssueBuffer::addUnusedSuppression($this->getFilePath(), $offset, $issue_name);
+ IssueBuffer::addUnusedSuppression(
+ $storage->location !== null
+ ? $storage->location->file_path
+ : $this->getFilePath(),
+ $offset,
+ $issue_name,
+ );
}
}
}
@@ -231,9 +238,8 @@ public function analyze(
$statements_analyzer = new StatementsAnalyzer($this, $type_provider);
+ $byref_uses = [];
if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) {
- $byref_uses = [];
-
foreach ($this->function->uses as $use) {
if (!is_string($use->var->name)) {
continue;
@@ -348,6 +354,31 @@ public function analyze(
(bool) $template_types,
);
+ if ($byref_uses) {
+ $ref_context = clone $context;
+ $var = '$__tmp_byref_closure_if__' . (int) $this->function->getAttribute('startFilePos');
+
+ $ref_context->vars_in_scope[$var] = Type::getBool();
+
+ $var = new VirtualVariable(
+ substr($var, 1),
+ );
+ $virtual_while = new VirtualWhile(
+ $var,
+ $function_stmts,
+ );
+
+ $statements_analyzer->analyze(
+ [$virtual_while],
+ $ref_context,
+ );
+
+ foreach ($byref_uses as $var_id => $_) {
+ $byref_vars[$var_id] = $ref_context->vars_in_scope[$var_id];
+ $context->vars_in_scope[$var_id] = $ref_context->vars_in_scope[$var_id];
+ }
+ }
+
if ($storage->pure) {
$context->pure = true;
}
@@ -804,20 +835,20 @@ public function analyze(
}
if ($this->return_vars_possibly_in_scope !== null) {
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $this->return_vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$this->return_vars_possibly_in_scope,
+ ];
}
foreach ($context->vars_in_scope as $var => $_) {
- if (strpos($var, '$this->') !== 0 && $var !== '$this') {
+ if (!str_starts_with($var, '$this->') && $var !== '$this') {
$context->removePossibleReference($var);
}
}
foreach ($context->vars_possibly_in_scope as $var => $_) {
- if (strpos($var, '$this->') !== 0 && $var !== '$this') {
+ if (!str_starts_with($var, '$this->') && $var !== '$this') {
unset($context->vars_possibly_in_scope[$var]);
}
}
@@ -1545,10 +1576,10 @@ public function addReturnTypes(Context $context): void
}
if ($this->return_vars_possibly_in_scope !== null) {
- $this->return_vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $this->return_vars_possibly_in_scope,
- );
+ $this->return_vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$this->return_vars_possibly_in_scope,
+ ];
} else {
$this->return_vars_possibly_in_scope = $context->vars_possibly_in_scope;
}
@@ -1635,7 +1666,7 @@ public function getFunctionLikeStorage(?StatementsAnalyzer $statements_analyzer
try {
return $codebase_methods->getStorage($method_id);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
$declaring_method_id = $codebase_methods->getDeclaringMethodId($method_id);
if ($declaring_method_id === null) {
@@ -2168,6 +2199,6 @@ private function detectPreviousUnusedArgumentPosition(FunctionLikeStorage $funct
private function isIgnoredForUnusedParam(string $var_name): bool
{
- return strpos($var_name, '$_') === 0 || (strpos($var_name, '$unused') === 0 && $var_name !== '$unused');
+ return str_starts_with($var_name, '$_') || (str_starts_with($var_name, '$unused') && $var_name !== '$unused');
}
}
diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
index d7092e97b11..5e3f4151fb7 100644
--- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
@@ -74,7 +74,7 @@ public function analyze(): void
try {
$extended_interface_storage = $codebase->classlike_storage_provider->get($extended_interface_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
diff --git a/src/Psalm/Internal/Analyzer/IssueData.php b/src/Psalm/Internal/Analyzer/IssueData.php
index 9e90b2d9020..e948337ce45 100644
--- a/src/Psalm/Internal/Analyzer/IssueData.php
+++ b/src/Psalm/Internal/Analyzer/IssueData.php
@@ -16,89 +16,7 @@ final class IssueData
public const SEVERITY_INFO = 'info';
public const SEVERITY_ERROR = 'error';
- /**
- * @var self::SEVERITY_*
- */
- public string $severity;
-
- public int $line_from;
-
- public int $line_to;
-
- /**
- * @readonly
- */
- public string $type;
-
- /**
- * @readonly
- */
- public string $message;
-
- /**
- * @readonly
- */
- public string $file_name;
-
- /**
- * @readonly
- */
- public string $file_path;
-
- /**
- * @readonly
- */
- public string $snippet;
-
- /**
- * @readonly
- */
- public string $selected_text;
-
- public int $from;
-
- public int $to;
-
- public int $snippet_from;
-
- public int $snippet_to;
-
- /**
- * @readonly
- */
- public int $column_from;
-
- /**
- * @readonly
- */
- public int $column_to;
-
- public int $error_level;
-
- /**
- * @readonly
- */
- public int $shortcode;
-
- /**
- * @readonly
- */
- public string $link;
-
- /**
- * @var ?list
- */
- public ?array $taint_trace = null;
-
- /**
- * @var ?list
- */
- public ?array $other_references = null;
-
- /**
- * @readonly
- */
- public ?string $dupe_key = null;
+ public readonly string $link;
/**
* @param self::SEVERITY_* $severity
@@ -106,47 +24,27 @@ final class IssueData
* @param ?list $other_references
*/
public function __construct(
- string $severity,
- int $line_from,
- int $line_to,
- string $type,
- string $message,
- string $file_name,
- string $file_path,
- string $snippet,
- string $selected_text,
- int $from,
- int $to,
- int $snippet_from,
- int $snippet_to,
- int $column_from,
- int $column_to,
- int $shortcode = 0,
- int $error_level = -1,
- ?array $taint_trace = null,
- array $other_references = null,
- ?string $dupe_key = null,
+ public string $severity,
+ public int $line_from,
+ public int $line_to,
+ public readonly string $type,
+ public readonly string $message,
+ public readonly string $file_name,
+ public readonly string $file_path,
+ public readonly string $snippet,
+ public readonly string $selected_text,
+ public int $from,
+ public int $to,
+ public int $snippet_from,
+ public int $snippet_to,
+ public readonly int $column_from,
+ public readonly int $column_to,
+ public readonly int $shortcode = 0,
+ public int $error_level = -1,
+ public ?array $taint_trace = null,
+ public ?array $other_references = null,
+ public readonly ?string $dupe_key = null,
) {
- $this->severity = $severity;
- $this->line_from = $line_from;
- $this->line_to = $line_to;
- $this->type = $type;
- $this->message = $message;
- $this->file_name = $file_name;
- $this->file_path = $file_path;
- $this->snippet = $snippet;
- $this->selected_text = $selected_text;
- $this->from = $from;
- $this->to = $to;
- $this->snippet_from = $snippet_from;
- $this->snippet_to = $snippet_to;
- $this->column_from = $column_from;
- $this->column_to = $column_to;
- $this->shortcode = $shortcode;
- $this->error_level = $error_level;
$this->link = $shortcode ? 'https://psalm.dev/' . str_pad((string) $shortcode, 3, "0", STR_PAD_LEFT) : '';
- $this->taint_trace = $taint_trace;
- $this->other_references = $other_references;
- $this->dupe_key = $dupe_key;
}
}
diff --git a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php
index 3758cd06bef..1c68bee7a7b 100644
--- a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php
@@ -20,6 +20,7 @@
use Psalm\StatementsSource;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\MethodStorage;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
use UnexpectedValueException;
use function in_array;
@@ -31,6 +32,7 @@
*/
final class MethodAnalyzer extends FunctionLikeAnalyzer
{
+ use UnserializeMemoryUsageSuppressionTrait;
// https://github.com/php/php-src/blob/a83923044c48982c80804ae1b45e761c271966d3/Zend/zend_enum.c#L77-L95
private const FORBIDDEN_ENUM_METHODS = [
'__construct',
diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php
index 210a669f83d..913c31d5338 100644
--- a/src/Psalm/Internal/Analyzer/MethodComparator.php
+++ b/src/Psalm/Internal/Analyzer/MethodComparator.php
@@ -44,7 +44,7 @@
use function array_filter;
use function count;
use function in_array;
-use function strpos;
+use function str_starts_with;
use function strtolower;
/**
@@ -506,7 +506,7 @@ private static function compareMethodParams(
&& $implementer_classlike_storage->user_defined
&& $implementer_param->location
&& $guide_method_storage->cased_name
- && strpos($guide_method_storage->cased_name, '__') !== 0
+ && !str_starts_with($guide_method_storage->cased_name, '__')
&& $config->isInProjectDirs(
$implementer_param->location->file_path,
)
@@ -846,7 +846,7 @@ private static function compareMethodDocblockParams(
$builder = $implementer_method_storage_param_type->getBuilder();
foreach ($builder->getAtomicTypes() as $k => $t) {
if ($t instanceof TTemplateParam
- && strpos($t->defining_class, 'fn-') === 0
+ && str_starts_with($t->defining_class, 'fn-')
) {
$builder->removeType($k);
@@ -860,7 +860,7 @@ private static function compareMethodDocblockParams(
$builder = $guide_method_storage_param_type->getBuilder();
foreach ($builder->getAtomicTypes() as $k => $t) {
if ($t instanceof TTemplateParam
- && strpos($t->defining_class, 'fn-') === 0
+ && str_starts_with($t->defining_class, 'fn-')
) {
$builder->removeType($k);
diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php
index 6974a4c45e9..6abd248d7b7 100644
--- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php
@@ -28,27 +28,22 @@ final class NamespaceAnalyzer extends SourceAnalyzer
{
use CanAlias;
- /**
- * @var FileAnalyzer
- * @psalm-suppress NonInvariantDocblockPropertyType
- */
- protected SourceAnalyzer $source;
-
- private Namespace_ $namespace;
-
- private string $namespace_name;
+ private readonly string $namespace_name;
/**
* A lookup table for public namespace constants
*
* @var array>
*/
- protected static array $public_namespace_constants = [];
-
- public function __construct(Namespace_ $namespace, FileAnalyzer $source)
- {
- $this->source = $source;
- $this->namespace = $namespace;
+ private static array $public_namespace_constants = [];
+
+ public function __construct(
+ private readonly Namespace_ $namespace,
+ /**
+ * @var FileAnalyzer
+ */
+ protected SourceAnalyzer $source,
+ ) {
$this->namespace_name = $this->namespace->name ? $this->namespace->name->toString() : '';
}
diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
index be4ca33a212..40c85901cba 100644
--- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
@@ -82,6 +82,8 @@
use function preg_match;
use function rename;
use function sprintf;
+use function str_ends_with;
+use function str_starts_with;
use function strlen;
use function strpos;
use function strtolower;
@@ -100,7 +102,7 @@ final class ProjectAnalyzer
/**
* Cached config
*/
- private Config $config;
+ private readonly Config $config;
public static ProjectAnalyzer $instance;
@@ -109,15 +111,15 @@ final class ProjectAnalyzer
*/
private Codebase $codebase;
- private FileProvider $file_provider;
+ private readonly FileProvider $file_provider;
- private ClassLikeStorageProvider $classlike_storage_provider;
+ private readonly ClassLikeStorageProvider $classlike_storage_provider;
private ?ParserCacheProvider $parser_cache_provider = null;
public ?ProjectCacheProvider $project_cache_provider = null;
- private FileReferenceProvider $file_reference_provider;
+ private readonly FileReferenceProvider $file_reference_provider;
public Progress $progress;
@@ -127,8 +129,6 @@ final class ProjectAnalyzer
public bool $show_issues = true;
- public int $threads;
-
/**
* @var array
*/
@@ -164,13 +164,6 @@ final class ProjectAnalyzer
*/
private array $to_refactor = [];
- public ?ReportOptions $stdout_report_options = null;
-
- /**
- * @var array
- */
- public array $generated_report_options;
-
/**
* @var array>
*/
@@ -208,9 +201,9 @@ final class ProjectAnalyzer
public function __construct(
Config $config,
Providers $providers,
- ?ReportOptions $stdout_report_options = null,
- array $generated_report_options = [],
- int $threads = 1,
+ public ?ReportOptions $stdout_report_options = null,
+ public array $generated_report_options = [],
+ public int $threads = 1,
?Progress $progress = null,
?Codebase $codebase = null,
) {
@@ -233,16 +226,12 @@ public function __construct(
$this->file_reference_provider = $providers->file_reference_provider;
$this->progress = $progress;
- $this->threads = $threads;
$this->config = $config;
$this->clearCacheDirectoryIfConfigOrComposerLockfileChanged();
$this->codebase = $codebase;
- $this->stdout_report_options = $stdout_report_options;
- $this->generated_report_options = $generated_report_options;
-
$this->config->processPluginFileExtensions($this);
$file_extensions = $this->config->getFileExtensions();
@@ -250,7 +239,7 @@ public function __construct(
$file_paths = $this->file_provider->getFilesInDir(
$dir_name,
$file_extensions,
- [$this->config, 'isInProjectDirs'],
+ $this->config->isInProjectDirs(...),
);
foreach ($file_paths as $file_path) {
@@ -262,7 +251,7 @@ public function __construct(
$file_paths = $this->file_provider->getFilesInDir(
$dir_name,
$file_extensions,
- [$this->config, 'isInExtraDirs'],
+ $this->config->isInExtraDirs(...),
);
foreach ($file_paths as $file_path) {
@@ -349,7 +338,7 @@ public static function getFileReportOptions(array $report_file_paths, bool $show
foreach ($report_file_paths as $report_file_path) {
foreach ($mapping as $extension => $type) {
- if (substr($report_file_path, -strlen($extension)) === $extension) {
+ if (str_ends_with($report_file_path, $extension)) {
$o = new ReportOptions();
$o->format = $type;
@@ -600,7 +589,7 @@ public function interpretRefactors(): void
&& $destination_pos === (strlen($destination) - 1)
) {
foreach ($this->codebase->classlike_storage_provider->getAll() as $class_storage) {
- if (strpos($source, substr($class_storage->name, 0, $source_pos)) === 0) {
+ if (str_starts_with($source, substr($class_storage->name, 0, $source_pos))) {
$this->to_refactor[$class_storage->name]
= substr($destination, 0, -1) . substr($class_storage->name, $source_pos);
}
@@ -926,7 +915,7 @@ public function checkDir(string $dir_name): void
private function checkDirWithConfig(string $dir_name, Config $config, bool $allow_non_project_files = false): void
{
$file_extensions = $config->getFileExtensions();
- $filter = $allow_non_project_files ? null : [$this->config, 'isInProjectDirs'];
+ $filter = $allow_non_project_files ? null : $this->config->isInProjectDirs(...);
$file_paths = $this->file_provider->getFilesInDir(
$dir_name,
@@ -956,7 +945,7 @@ public function addExtraFile(string $file_path): void
/**
* @return list
*/
- protected function getDiffFiles(): array
+ private function getDiffFiles(): array
{
if (!$this->parser_cache_provider || !$this->project_cache_provider) {
throw new UnexpectedValueException('Parser cache provider cannot be null here');
@@ -1067,6 +1056,10 @@ public function checkPaths(array $paths_to_check): void
$this->config->visitStubFiles($this->codebase, $this->progress);
+ $event = new AfterCodebasePopulatedEvent($this->codebase);
+
+ $this->config->eventDispatcher->dispatchAfterCodebasePopulated($event);
+
$this->progress->startAnalyzingFiles();
$this->codebase->analyzer->analyzeFiles(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php
index 236b8a1f791..3fc0b4f3563 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php
@@ -20,7 +20,6 @@
use function array_diff;
use function array_filter;
use function array_keys;
-use function array_merge;
use function array_values;
use function in_array;
use function preg_match;
@@ -158,10 +157,10 @@ static function (Clause $c) use ($mixed_var_ids): bool {
$do_context->loop_scope = null;
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $do_context->vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$do_context->vars_possibly_in_scope,
+ ];
if ($context->collect_exceptions) {
$context->mergeExceptions($inner_loop_context);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php
index ad1b5a601de..b8e95b7d9e2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php
@@ -170,10 +170,10 @@ public static function analyze(
$for_context->loop_scope = null;
if ($can_leave_loop) {
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $for_context->vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$for_context->vars_possibly_in_scope,
+ ];
} elseif ($pre_context) {
$context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
index d248ba90b57..d71628cdfb9 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
@@ -63,7 +63,6 @@
use function array_keys;
use function array_map;
-use function array_merge;
use function array_search;
use function array_values;
use function assert;
@@ -386,10 +385,10 @@ public static function analyze(
$foreach_context->loop_scope = null;
- $context->vars_possibly_in_scope = array_merge(
- $foreach_context->vars_possibly_in_scope,
- $context->vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$foreach_context->vars_possibly_in_scope,
+ ...$context->vars_possibly_in_scope,
+ ];
if ($context->collect_exceptions) {
$context->mergeExceptions($foreach_context);
@@ -549,10 +548,10 @@ public static function checkIteratorType(
}
} elseif ($iterator_atomic_type instanceof TIterable) {
if ($iterator_atomic_type->extra_types) {
- $iterator_atomic_types = array_merge(
- [$iterator_atomic_type->setIntersectionTypes([])],
- $iterator_atomic_type->extra_types,
- );
+ $iterator_atomic_types = [
+ $iterator_atomic_type->setIntersectionTypes([]),
+ ...$iterator_atomic_type->extra_types,
+ ];
} else {
$iterator_atomic_types = [$iterator_atomic_type];
}
@@ -732,10 +731,10 @@ public static function handleIterable(
bool &$has_valid_iterator,
): void {
if ($iterator_atomic_type->extra_types) {
- $iterator_atomic_types = array_merge(
- [$iterator_atomic_type->setIntersectionTypes([])],
- $iterator_atomic_type->extra_types,
- );
+ $iterator_atomic_types = [
+ $iterator_atomic_type->setIntersectionTypes([]),
+ ...$iterator_atomic_type->extra_types,
+ ];
} else {
$iterator_atomic_types = [$iterator_atomic_type];
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php
index 843bbbce0de..8e0e23ec18d 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php
@@ -17,8 +17,10 @@
use Psalm\Issue\DocblockTypeContradiction;
use Psalm\Issue\RedundantCondition;
use Psalm\Issue\RedundantConditionGivenDocblockType;
+use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\Issue\TypeDoesNotContainType;
use Psalm\IssueBuffer;
+use Psalm\Type\Atomic\TBool;
use Psalm\Type\Reconciler;
use function array_diff_key;
@@ -219,7 +221,7 @@ public static function analyze(
// get all the var ids that were referenced in the conditional, but not assigned in it
$cond_referenced_var_ids = array_diff_key($cond_referenced_var_ids, $assigned_in_conditional_var_ids);
- $cond_referenced_var_ids = array_merge($newish_var_ids, $cond_referenced_var_ids);
+ $cond_referenced_var_ids = [...$newish_var_ids, ...$cond_referenced_var_ids];
return new IfConditionalScope(
$if_context,
@@ -368,6 +370,34 @@ public static function handleParadoxicalCondition(
$statements_analyzer->getSuppressedIssues(),
);
}
+ } elseif (!($stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical)
+ && !($stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical)
+ && !($stmt instanceof PhpParser\Node\Expr\BooleanNot)) {
+ if (count($type->getAtomicTypes()) > 1) {
+ $both_types = $type->getBuilder();
+ foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
+ if ($atomic_type->isTruthy()
+ || $atomic_type->isFalsy()
+ || $atomic_type instanceof TBool) {
+ $both_types->removeType($key);
+ }
+ }
+
+ if (count($both_types->getAtomicTypes()) > 0) {
+ $both_types = $both_types->freeze();
+ IssueBuffer::maybeAdd(
+ new RiskyTruthyFalsyComparison(
+ 'Operand of type ' . $type->getId() . ' contains ' .
+ 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
+ $both_types->getId() . ', which can be falsy and truthy. ' .
+ 'This can cause possibly unexpected behavior. Use strict comparison instead.',
+ new CodeLocation($statements_analyzer, $stmt),
+ $type->getId(),
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+ }
}
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php
index f33293ca9bc..568a19e1a88 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php
@@ -200,22 +200,22 @@ public static function analyze(
if ($has_leaving_statements) {
if ($else_context->loop_scope) {
if (!$has_continue_statement && !$has_break_statement) {
- $if_scope->new_vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $if_scope->new_vars_possibly_in_scope,
- );
+ $if_scope->new_vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$if_scope->new_vars_possibly_in_scope,
+ ];
}
- $else_context->loop_scope->vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $else_context->loop_scope->vars_possibly_in_scope,
- );
+ $else_context->loop_scope->vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$else_context->loop_scope->vars_possibly_in_scope,
+ ];
}
} else {
- $if_scope->new_vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $if_scope->new_vars_possibly_in_scope,
- );
+ $if_scope->new_vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$if_scope->new_vars_possibly_in_scope,
+ ];
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php
index 4a7769247b2..f4aa30a1982 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php
@@ -71,7 +71,7 @@ public static function analyze(
$elseif_context = $if_conditional_scope->if_context;
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
$assigned_in_conditional_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
- } catch (ScopeAnalysisException $e) {
+ } catch (ScopeAnalysisException) {
return false;
}
@@ -188,7 +188,7 @@ public static function analyze(
$negated_elseif_types = Algebra::getTruthsFromFormula(
Algebra::negateFormula($elseif_clauses),
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$reconcilable_elseif_types = [];
$negated_elseif_types = [];
}
@@ -373,25 +373,25 @@ public static function analyze(
if ($has_leaving_statements && $elseif_context->loop_scope) {
if (!$has_continue_statement && !$has_break_statement) {
- $if_scope->new_vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $if_scope->new_vars_possibly_in_scope,
- );
+ $if_scope->new_vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$if_scope->new_vars_possibly_in_scope,
+ ];
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids,
);
}
- $elseif_context->loop_scope->vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $elseif_context->loop_scope->vars_possibly_in_scope,
- );
+ $elseif_context->loop_scope->vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$elseif_context->loop_scope->vars_possibly_in_scope,
+ ];
} elseif (!$has_leaving_statements) {
- $if_scope->new_vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $if_scope->new_vars_possibly_in_scope,
- );
+ $if_scope->new_vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$if_scope->new_vars_possibly_in_scope,
+ ];
$if_scope->possibly_assigned_var_ids = array_merge(
$possibly_assigned_var_ids,
$if_scope->possibly_assigned_var_ids,
@@ -407,7 +407,7 @@ public static function analyze(
$if_scope->negated_clauses = Algebra::simplifyCNF(
[...$if_scope->negated_clauses, ...Algebra::negateFormula($elseif_clauses)],
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$if_scope->negated_clauses = [];
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php
index 2168ed3f956..3a23be749c1 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php
@@ -36,14 +36,11 @@
use function array_keys;
use function array_merge;
use function array_reduce;
-use function array_unique;
use function count;
use function in_array;
use function preg_match;
use function preg_quote;
use function spl_object_id;
-use function strpos;
-use function substr;
/**
* @internal
@@ -146,10 +143,10 @@ public static function analyze(
$if_context->reconciled_expression_clauses = [];
- $outer_context->vars_possibly_in_scope = array_merge(
- $if_context->vars_possibly_in_scope,
- $outer_context->vars_possibly_in_scope,
- );
+ $outer_context->vars_possibly_in_scope = [
+ ...$if_context->vars_possibly_in_scope,
+ ...$outer_context->vars_possibly_in_scope,
+ ];
$old_if_context = clone $if_context;
@@ -274,20 +271,6 @@ public static function analyze(
array_keys($if_scope->negated_types),
);
- $extra_vars_to_update = [];
-
- // if there's an object-like array in there, we also need to update the root array variable
- foreach ($vars_to_update as $var_id) {
- $bracked_pos = strpos($var_id, '[');
- if ($bracked_pos !== false) {
- $extra_vars_to_update[] = substr($var_id, 0, $bracked_pos);
- }
- }
-
- if ($extra_vars_to_update) {
- $vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update));
- }
-
$outer_context->update(
$old_if_context,
$if_context,
@@ -308,10 +291,10 @@ public static function analyze(
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
}
- $if_context->loop_scope->vars_possibly_in_scope = array_merge(
- $vars_possibly_in_scope,
- $if_context->loop_scope->vars_possibly_in_scope,
- );
+ $if_context->loop_scope->vars_possibly_in_scope = [
+ ...$vars_possibly_in_scope,
+ ...$if_context->loop_scope->vars_possibly_in_scope,
+ ];
} elseif (!$has_leaving_statements) {
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php
index 273842a3722..b14891ce0f9 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php
@@ -112,7 +112,7 @@ public static function analyze(
// this is the context for stuff that happens after the `if` block
$post_if_context = $if_conditional_scope->post_if_context;
$assigned_in_conditional_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
- } catch (ScopeAnalysisException $e) {
+ } catch (ScopeAnalysisException) {
return false;
}
@@ -202,7 +202,7 @@ public static function analyze(
try {
$if_scope->negated_clauses = Algebra::negateFormula($if_clauses);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
try {
$if_scope->negated_clauses = FormulaGenerator::getFormula(
$cond_object_id,
@@ -213,7 +213,7 @@ public static function analyze(
$codebase,
false,
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$if_scope->negated_clauses = [];
}
}
@@ -363,15 +363,15 @@ public static function analyze(
);
}
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $if_scope->new_vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$if_scope->new_vars_possibly_in_scope,
+ ];
- $context->possibly_assigned_var_ids = array_merge(
- $context->possibly_assigned_var_ids,
- $if_scope->possibly_assigned_var_ids ?: [],
- );
+ $context->possibly_assigned_var_ids = [
+ ...$context->possibly_assigned_var_ids,
+ ...$if_scope->possibly_assigned_var_ids ?: [],
+ ];
// vars can only be defined/redefined if there was an else (defined in every block)
$context->assigned_var_ids = array_merge(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php
index c3a7648aafc..60e3acaa964 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php
@@ -139,10 +139,10 @@ public static function analyze(
}
}
- $loop_parent_context->vars_possibly_in_scope = array_merge(
- $continue_context->vars_possibly_in_scope,
- $loop_parent_context->vars_possibly_in_scope,
- );
+ $loop_parent_context->vars_possibly_in_scope = [
+ ...$continue_context->vars_possibly_in_scope,
+ ...$loop_parent_context->vars_possibly_in_scope,
+ ];
} else {
$original_parent_context = clone $loop_parent_context;
@@ -270,10 +270,10 @@ public static function analyze(
$continue_context->has_returned = false;
- $loop_parent_context->vars_possibly_in_scope = array_merge(
- $continue_context->vars_possibly_in_scope,
- $loop_parent_context->vars_possibly_in_scope,
- );
+ $loop_parent_context->vars_possibly_in_scope = [
+ ...$continue_context->vars_possibly_in_scope,
+ ...$loop_parent_context->vars_possibly_in_scope,
+ ];
// if there are no changes to the types, no need to re-examine
if (!$has_changes) {
@@ -442,10 +442,10 @@ public static function analyze(
$loop_parent_context->removeVarFromConflictingClauses($var_id);
} else {
$loop_parent_context->vars_in_scope[$var_id] =
- $loop_parent_context->vars_in_scope[$var_id]->setParentNodes(array_merge(
- $loop_parent_context->vars_in_scope[$var_id]->parent_nodes,
- $continue_context->vars_in_scope[$var_id]->parent_nodes,
- ))
+ $loop_parent_context->vars_in_scope[$var_id]->setParentNodes([
+ ...$loop_parent_context->vars_in_scope[$var_id]->parent_nodes,
+ ...$continue_context->vars_in_scope[$var_id]->parent_nodes,
+ ])
;
}
}
@@ -457,7 +457,7 @@ public static function analyze(
try {
$negated_pre_condition_clauses = Algebra::negateFormula(array_merge(...$pre_condition_clauses));
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$negated_pre_condition_clauses = [];
}
@@ -556,10 +556,10 @@ private static function updateLoopScopeContexts(
}
// merge vars possibly in scope at the end of each loop
- $loop_context->vars_possibly_in_scope = array_merge(
- $loop_context->vars_possibly_in_scope,
- $loop_scope->vars_possibly_in_scope,
- );
+ $loop_context->vars_possibly_in_scope = [
+ ...$loop_context->vars_possibly_in_scope,
+ ...$loop_scope->vars_possibly_in_scope,
+ ];
}
/**
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php
index e5dfacd83d7..ee9d7cb1822 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php
@@ -219,10 +219,10 @@ public static function analyze(
$context->assigned_var_ids += $switch_scope->new_assigned_var_ids;
}
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $switch_scope->new_vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$switch_scope->new_vars_possibly_in_scope,
+ ];
//a switch can't return in all options without a default
$context->has_returned = $all_options_returned && $has_default;
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php
index 7e1dbdd3da0..a5a9f51e542 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php
@@ -49,7 +49,7 @@
use function in_array;
use function is_string;
use function spl_object_id;
-use function strpos;
+use function str_starts_with;
use function substr;
/**
@@ -92,7 +92,7 @@ public static function analyze(
$fake_switch_condition = false;
- if ($switch_var_id && strpos($switch_var_id, '$__tmp_switch__') === 0) {
+ if ($switch_var_id && str_starts_with($switch_var_id, '$__tmp_switch__')) {
$switch_condition = new VirtualVariable(
substr($switch_var_id, 1),
$stmt->cond->getAttributes(),
@@ -439,7 +439,7 @@ public static function analyze(
if ($case_clauses && $case_equality_expr) {
try {
$negated_case_clauses = Algebra::negateFormula($case_clauses);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$case_equality_expr_id = spl_object_id($case_equality_expr);
try {
@@ -453,7 +453,7 @@ public static function analyze(
false,
false,
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$negated_case_clauses = [];
}
}
@@ -636,13 +636,10 @@ private static function handleNonReturningCase(
}
}
- $switch_scope->new_vars_possibly_in_scope = array_merge(
- array_diff_key(
- $case_context->vars_possibly_in_scope,
- $context->vars_possibly_in_scope,
- ),
- $switch_scope->new_vars_possibly_in_scope,
- );
+ $switch_scope->new_vars_possibly_in_scope = [...array_diff_key(
+ $case_context->vars_possibly_in_scope,
+ $context->vars_possibly_in_scope,
+ ), ...$switch_scope->new_vars_possibly_in_scope];
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php
index f992eb7a489..fa2fd99e653 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php
@@ -12,7 +12,6 @@
use Psalm\Type;
use UnexpectedValueException;
-use function array_merge;
use function in_array;
/**
@@ -104,10 +103,10 @@ public static function analyze(
$while_context->loop_scope = null;
if ($can_leave_loop) {
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $while_context->vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$while_context->vars_possibly_in_scope,
+ ];
} elseif ($pre_context) {
$context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php
index 16ea3e83561..ee2746a3be5 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php
@@ -109,7 +109,9 @@
use function is_numeric;
use function is_string;
use function sprintf;
+use function str_ends_with;
use function str_replace;
+use function str_starts_with;
use function strpos;
use function strtolower;
use function substr;
@@ -543,9 +545,9 @@ private static function scrapeEqualityAssertions(
);
} else {
// both side of the Identical can be asserted to the intersection of both
- $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase);
+ $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase, false, false);
- if ($intersection_type !== null && $intersection_type->isSingle()) {
+ if ($intersection_type !== null) {
$if_types = [];
$var_name_left = ExpressionIdentifier::getExtendedVarId(
@@ -556,8 +558,13 @@ private static function scrapeEqualityAssertions(
$var_assertion_different = $var_type->getId() !== $intersection_type->getId();
+ $all_assertions = [];
+ foreach ($intersection_type->getAtomicTypes() as $atomic_type) {
+ $all_assertions[] = new IsIdentical($atomic_type);
+ }
+
if ($var_name_left && $var_assertion_different) {
- $if_types[$var_name_left] = [[new IsIdentical($intersection_type->getSingleAtomic())]];
+ $if_types[$var_name_left] = [$all_assertions];
}
$var_name_right = ExpressionIdentifier::getExtendedVarId(
@@ -569,7 +576,7 @@ private static function scrapeEqualityAssertions(
$other_assertion_different = $other_type->getId() !== $intersection_type->getId();
if ($var_name_right && $other_assertion_different) {
- $if_types[$var_name_right] = [[new IsIdentical($intersection_type->getSingleAtomic())]];
+ $if_types[$var_name_right] = [$all_assertions];
}
return $if_types ? [$if_types] : [];
@@ -926,7 +933,7 @@ private static function processIrreconcilableFunctionCall(
* @param PhpParser\Node\Expr\FuncCall|PhpParser\Node\Expr\MethodCall|PhpParser\Node\Expr\StaticCall $expr
* @return list>>>
*/
- protected static function processCustomAssertion(
+ private static function processCustomAssertion(
PhpParser\Node\Expr $expr,
?string $this_class_name,
FileSource $source,
@@ -1015,7 +1022,7 @@ protected static function processCustomAssertion(
$if_types[$var_id] = [[$assertion->rule[0]]];
}
} elseif (is_string($assertion->var_id)) {
- $is_function = substr($assertion->var_id, -2) === '()';
+ $is_function = str_ends_with($assertion->var_id, '()');
$exploded_id = explode('->', $assertion->var_id);
$var_id = $exploded_id[0] ?? null;
$property = $exploded_id[1] ?? null;
@@ -1071,7 +1078,7 @@ protected static function processCustomAssertion(
} elseif (!$expr instanceof PhpParser\Node\Expr\FuncCall) {
$assertion_var_id = $assertion->var_id;
- if (strpos($assertion_var_id, 'self::') === 0) {
+ if (str_starts_with($assertion_var_id, 'self::')) {
$assertion_var_id = $this_class_name.'::'.substr($assertion_var_id, 6);
}
} else {
@@ -1145,7 +1152,7 @@ protected static function processCustomAssertion(
$if_types[$var_id] = [[$assertion->rule[0]->getNegation()]];
}
} elseif (is_string($assertion->var_id)) {
- $is_function = substr($assertion->var_id, -2) === '()';
+ $is_function = str_ends_with($assertion->var_id, '()');
$exploded_id = explode('->', $assertion->var_id);
$var_id = $exploded_id[0] ?? null;
$property = $exploded_id[1] ?? null;
@@ -1202,7 +1209,7 @@ protected static function processCustomAssertion(
$if_types[$assertion_var_id] = [[$rule]];
} elseif (!$expr instanceof PhpParser\Node\Expr\FuncCall) {
$var_id = $assertion->var_id;
- if (strpos($var_id, 'self::') === 0) {
+ if (str_starts_with($var_id, 'self::')) {
$var_id = $this_class_name.'::'.substr($var_id, 6);
}
$if_types[$var_id] = [[$assertion->rule[0]->getNegation()]];
@@ -1228,7 +1235,7 @@ protected static function processCustomAssertion(
/**
* @return list
*/
- protected static function getInstanceOfAssertions(
+ private static function getInstanceOfAssertions(
PhpParser\Node\Expr\Instanceof_ $stmt,
?string $this_class_name,
FileSource $source,
@@ -1293,7 +1300,7 @@ protected static function getInstanceOfAssertions(
/**
* @param Identical|Equal|NotIdentical|NotEqual $conditional
*/
- protected static function hasNullVariable(
+ private static function hasNullVariable(
PhpParser\Node\Expr\BinaryOp $conditional,
FileSource $source,
): ?int {
@@ -1364,7 +1371,7 @@ public static function hasTrueVariable(
/**
* @param Identical|Equal|NotIdentical|NotEqual $conditional
*/
- protected static function hasEmptyArrayVariable(
+ private static function hasEmptyArrayVariable(
PhpParser\Node\Expr\BinaryOp $conditional,
): ?int {
if ($conditional->right instanceof PhpParser\Node\Expr\Array_
@@ -1384,10 +1391,11 @@ protected static function hasEmptyArrayVariable(
/**
* @param Identical|Equal|NotIdentical|NotEqual $conditional
+ * @return false|int
*/
- protected static function hasGetTypeCheck(
+ private static function hasGetTypeCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
- ): false|int {
+ ): bool|int {
if ($conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->getFirst()) === 'gettype'
@@ -1411,10 +1419,11 @@ protected static function hasGetTypeCheck(
/**
* @param Identical|Equal|NotIdentical|NotEqual $conditional
+ * @return false|int
*/
- protected static function hasGetDebugTypeCheck(
+ private static function hasGetDebugTypeCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
- ): false|int {
+ ): bool|int {
if ($conditional->right instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->right->name instanceof PhpParser\Node\Name
&& strtolower($conditional->right->name->getFirst()) === 'get_debug_type'
@@ -1440,11 +1449,12 @@ protected static function hasGetDebugTypeCheck(
/**
* @param Identical|Equal|NotIdentical|NotEqual $conditional
+ * @return false|int
*/
- protected static function hasGetClassCheck(
+ private static function hasGetClassCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
FileSource $source,
- ): false|int {
+ ): bool|int {
if (!$source instanceof StatementsAnalyzer) {
return false;
}
@@ -1460,7 +1470,7 @@ protected static function hasGetClassCheck(
&& strtolower($conditional->right->name->name) === 'class';
$right_variable_class_const = $conditional->right instanceof PhpParser\Node\Expr\ClassConstFetch
- && $conditional->right->class instanceof PhpParser\Node\Expr\Variable
+ && !$conditional->right->class instanceof PhpParser\Node\Name
&& $conditional->right->name instanceof PhpParser\Node\Identifier
&& strtolower($conditional->right->name->name) === 'class';
@@ -1469,15 +1479,22 @@ protected static function hasGetClassCheck(
&& $conditional->left->name instanceof PhpParser\Node\Identifier
&& strtolower($conditional->left->name->name) === 'class';
- $left_type = $source->node_data->getType($conditional->left);
+ $left_variable_class_const = $conditional->left instanceof PhpParser\Node\Expr\ClassConstFetch
+ && !$conditional->left->class instanceof PhpParser\Node\Name
+ && $conditional->left->name instanceof PhpParser\Node\Identifier
+ && strtolower($conditional->left->name->name) === 'class';
$left_class_string_t = false;
- if ($left_type && $left_type->isSingle()) {
- foreach ($left_type->getAtomicTypes() as $type_part) {
- if ($type_part instanceof TClassString) {
- $left_class_string_t = true;
- break;
+ if (!$left_variable_class_const) {
+ $left_type = $source->node_data->getType($conditional->left);
+
+ if ($left_type && $left_type->isSingle()) {
+ foreach ($left_type->getAtomicTypes() as $type_part) {
+ if ($type_part instanceof TClassString) {
+ $left_class_string_t = true;
+ break;
+ }
}
}
}
@@ -1498,29 +1515,26 @@ protected static function hasGetClassCheck(
&& $conditional->left->name instanceof PhpParser\Node\Identifier
&& strtolower($conditional->left->name->name) === 'class';
- $left_variable_class_const = $conditional->left instanceof PhpParser\Node\Expr\ClassConstFetch
- && $conditional->left->class instanceof PhpParser\Node\Expr\Variable
- && $conditional->left->name instanceof PhpParser\Node\Identifier
- && strtolower($conditional->left->name->name) === 'class';
-
$right_class_string = $conditional->right instanceof PhpParser\Node\Expr\ClassConstFetch
&& $conditional->right->class instanceof PhpParser\Node\Name
&& $conditional->right->name instanceof PhpParser\Node\Identifier
&& strtolower($conditional->right->name->name) === 'class';
- $right_type = $source->node_data->getType($conditional->right);
-
$right_class_string_t = false;
- if ($right_type && $right_type->isSingle()) {
- foreach ($right_type->getAtomicTypes() as $type_part) {
- if ($type_part instanceof TClassString) {
- $right_class_string_t = true;
- break;
+ if (!$right_variable_class_const) {
+ $right_type = $source->node_data->getType($conditional->right);
+
+ if ($right_type && $right_type->isSingle()) {
+ foreach ($right_type->getAtomicTypes() as $type_part) {
+ if ($type_part instanceof TClassString) {
+ $right_class_string_t = true;
+ break;
+ }
}
}
}
-
+
if (($left_get_class || $left_static_class || $left_variable_class_const)
&& ($right_class_string || $right_class_string_t)
) {
@@ -1532,11 +1546,12 @@ protected static function hasGetClassCheck(
/**
* @param Greater|GreaterOrEqual|Smaller|SmallerOrEqual $conditional
+ * @return false|int
*/
- protected static function hasNonEmptyCountEqualityCheck(
+ private static function hasNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count,
- ): false|int {
+ ): bool|int {
if ($conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& in_array(strtolower($conditional->left->name->getFirst()), ['count', 'sizeof'])
@@ -1573,11 +1588,12 @@ protected static function hasNonEmptyCountEqualityCheck(
/**
* @param Greater|GreaterOrEqual|Smaller|SmallerOrEqual $conditional
+ * @return false|int
*/
- protected static function hasLessThanCountEqualityCheck(
+ private static function hasLessThanCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$max_count,
- ): false|int {
+ ): bool|int {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& in_array(strtolower($conditional->left->name->getFirst()), ['count', 'sizeof'])
@@ -1621,11 +1637,12 @@ protected static function hasLessThanCountEqualityCheck(
/**
* @param Equal|Identical|NotEqual|NotIdentical $conditional
+ * @return false|int
*/
- protected static function hasCountEqualityCheck(
+ private static function hasCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$count,
- ): false|int {
+ ): bool|int {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& in_array(strtolower($conditional->left->name->getFirst()), ['count', 'sizeof'])
@@ -1653,12 +1670,13 @@ protected static function hasCountEqualityCheck(
/**
* @param PhpParser\Node\Expr\BinaryOp\Greater|PhpParser\Node\Expr\BinaryOp\GreaterOrEqual $conditional
+ * @return false|int
*/
- protected static function hasSuperiorNumberCheck(
+ private static function hasSuperiorNumberCheck(
FileSource $source,
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$literal_value_comparison,
- ): false|int {
+ ): bool|int {
$right_assignment = false;
$value_right = null;
if ($source instanceof StatementsAnalyzer
@@ -1712,12 +1730,13 @@ protected static function hasSuperiorNumberCheck(
/**
* @param PhpParser\Node\Expr\BinaryOp\Smaller|PhpParser\Node\Expr\BinaryOp\SmallerOrEqual $conditional
+ * @return false|int
*/
- protected static function hasInferiorNumberCheck(
+ private static function hasInferiorNumberCheck(
FileSource $source,
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$literal_value_comparison,
- ): false|int {
+ ): bool|int {
$right_assignment = false;
$value_right = null;
if ($source instanceof StatementsAnalyzer
@@ -1771,10 +1790,11 @@ protected static function hasInferiorNumberCheck(
/**
* @param PhpParser\Node\Expr\BinaryOp\Greater|PhpParser\Node\Expr\BinaryOp\GreaterOrEqual $conditional
+ * @return false|int
*/
- protected static function hasReconcilableNonEmptyCountEqualityCheck(
+ private static function hasReconcilableNonEmptyCountEqualityCheck(
PhpParser\Node\Expr\BinaryOp $conditional,
- ): false|int {
+ ): bool|int {
$left_count = $conditional->left instanceof PhpParser\Node\Expr\FuncCall
&& $conditional->left->name instanceof PhpParser\Node\Name
&& in_array(strtolower($conditional->left->name->getFirst()), ['count', 'sizeof']);
@@ -1792,11 +1812,12 @@ protected static function hasReconcilableNonEmptyCountEqualityCheck(
/**
* @param Identical|Equal|NotIdentical|NotEqual $conditional
+ * @return false|int
*/
- protected static function hasTypedValueComparison(
+ private static function hasTypedValueComparison(
PhpParser\Node\Expr\BinaryOp $conditional,
FileSource $source,
- ): false|int {
+ ): bool|int {
if (!$source instanceof StatementsAnalyzer) {
return false;
}
@@ -1827,7 +1848,7 @@ protected static function hasTypedValueComparison(
return false;
}
- protected static function hasIsACheck(
+ private static function hasIsACheck(
PhpParser\Node\Expr\FuncCall $stmt,
StatementsAnalyzer $source,
): bool {
@@ -1857,44 +1878,24 @@ protected static function hasIsACheck(
private static function getIsAssertion(string $function_name): ?Assertion
{
- switch ($function_name) {
- case 'is_string':
- return new IsType(new Atomic\TString());
- case 'is_int':
- case 'is_integer':
- case 'is_long':
- return new IsType(new Atomic\TInt());
- case 'is_float':
- case 'is_double':
- case 'is_real':
- return new IsType(new Atomic\TFloat());
- case 'is_scalar':
- return new IsType(new Atomic\TScalar());
- case 'is_bool':
- return new IsType(new Atomic\TBool());
- case 'is_resource':
- return new IsType(new Atomic\TResource());
- case 'is_object':
- return new IsType(new Atomic\TObject());
- case 'array_is_list':
- return new IsType(Type::getListAtomic(Type::getMixed()));
- case 'is_array':
- return new IsType(new Atomic\TArray([Type::getArrayKey(), Type::getMixed()]));
- case 'is_numeric':
- return new IsType(new Atomic\TNumeric());
- case 'is_null':
- return new IsType(new Atomic\TNull());
- case 'is_iterable':
- return new IsType(new Atomic\TIterable());
- case 'is_countable':
- return new IsCountable();
- case 'ctype_digit':
- return new IsType(new Atomic\TNumericString);
- case 'ctype_lower':
- return new IsType(new Atomic\TNonEmptyLowercaseString);
- }
-
- return null;
+ return match ($function_name) {
+ 'is_string' => new IsType(new Atomic\TString()),
+ 'is_int', 'is_integer', 'is_long' => new IsType(new Atomic\TInt()),
+ 'is_float', 'is_double', 'is_real' => new IsType(new Atomic\TFloat()),
+ 'is_scalar' => new IsType(new Atomic\TScalar()),
+ 'is_bool' => new IsType(new Atomic\TBool()),
+ 'is_resource' => new IsType(new Atomic\TResource()),
+ 'is_object' => new IsType(new Atomic\TObject()),
+ 'array_is_list' => new IsType(Type::getListAtomic(Type::getMixed())),
+ 'is_array' => new IsType(new Atomic\TArray([Type::getArrayKey(), Type::getMixed()])),
+ 'is_numeric' => new IsType(new Atomic\TNumeric()),
+ 'is_null' => new IsType(new Atomic\TNull()),
+ 'is_iterable' => new IsType(new Atomic\TIterable()),
+ 'is_countable' => new IsCountable(),
+ 'ctype_digit' => new IsType(new Atomic\TNumericString),
+ 'ctype_lower' => new IsType(new Atomic\TNonEmptyLowercaseString),
+ default => null,
+ };
}
/**
@@ -1943,7 +1944,7 @@ private static function handleIsTypeCheck(
return $if_types;
}
- protected static function hasCallableCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasCallableCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'is_callable';
}
@@ -1951,7 +1952,7 @@ protected static function hasCallableCheck(PhpParser\Node\Expr\FuncCall $stmt):
/**
* @return Reconciler::RECONCILIATION_*
*/
- protected static function hasClassExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): int
+ private static function hasClassExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): int
{
if ($stmt->name instanceof PhpParser\Node\Name
&& strtolower($stmt->name->getFirst()) === 'class_exists'
@@ -1977,7 +1978,7 @@ protected static function hasClassExistsCheck(PhpParser\Node\Expr\FuncCall $stmt
/**
* @return 0|1|2
*/
- protected static function hasTraitExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): int
+ private static function hasTraitExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): int
{
if ($stmt->name instanceof PhpParser\Node\Name
&& strtolower($stmt->name->getFirst()) === 'trait_exists'
@@ -2000,22 +2001,22 @@ protected static function hasTraitExistsCheck(PhpParser\Node\Expr\FuncCall $stmt
return 0;
}
- protected static function hasEnumExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasEnumExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'enum_exists';
}
- protected static function hasInterfaceExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasInterfaceExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'interface_exists';
}
- protected static function hasFunctionExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasFunctionExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'function_exists';
}
- protected static function hasInArrayCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasInArrayCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
if ($stmt->name instanceof PhpParser\Node\Name
&& strtolower($stmt->name->getFirst()) === 'in_array'
@@ -2033,13 +2034,13 @@ protected static function hasInArrayCheck(PhpParser\Node\Expr\FuncCall $stmt): b
return false;
}
- protected static function hasNonEmptyCountCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasNonEmptyCountCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name &&
in_array(strtolower($stmt->name->getFirst()), ['count', 'sizeof']);
}
- protected static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
+ private static function hasArrayKeyExistsCheck(PhpParser\Node\Expr\FuncCall $stmt): bool
{
return $stmt->name instanceof PhpParser\Node\Name && strtolower($stmt->name->getFirst()) === 'array_key_exists';
}
@@ -2531,7 +2532,7 @@ private static function getGettypeInequalityAssertions(
$if_types[$var_name] = [[new IsNotIdentical(new TObject())]];
} elseif ($var_type === 'resource (closed)') {
$if_types[$var_name] = [[new IsNotType(new TClosedResource())]];
- } elseif (strpos($var_type, 'resource (') === 0) {
+ } elseif (str_starts_with($var_type, 'resource (')) {
$if_types[$var_name] = [[new IsNotIdentical(new TResource())]];
} else {
$if_types[$var_name] = [[new IsNotType(Atomic::create($var_type))]];
@@ -2589,7 +2590,7 @@ private static function getGetdebugTypeInequalityAssertions(
$if_types[$var_name] = [[new IsNotIdentical(new TObject())]];
} elseif ($var_type === 'resource (closed)') {
$if_types[$var_name] = [[new IsNotType(new TClosedResource())]];
- } elseif (strpos($var_type, 'resource (') === 0) {
+ } elseif (str_starts_with($var_type, 'resource (')) {
$if_types[$var_name] = [[new IsNotIdentical(new TResource())]];
} else {
$if_types[$var_name] = [[new IsNotType(Atomic::create($var_type))]];
@@ -3244,7 +3245,7 @@ private static function getGettypeEqualityAssertions(
$if_types[$var_name] = [[new IsIdentical(new TObject())]];
} elseif ($var_type === 'resource (closed)') {
$if_types[$var_name] = [[new IsType(new TClosedResource())]];
- } elseif (strpos($var_type, 'resource (') === 0) {
+ } elseif (str_starts_with($var_type, 'resource (')) {
$if_types[$var_name] = [[new IsIdentical(new TResource())]];
} elseif ($var_type === 'integer') {
$if_types[$var_name] = [[new IsType(new Atomic\TInt())]];
@@ -3308,7 +3309,7 @@ private static function getGetdebugtypeEqualityAssertions(
$if_types[$var_name] = [[new IsIdentical(new TObject())]];
} elseif ($var_type === 'resource (closed)') {
$if_types[$var_name] = [[new IsType(new TClosedResource())]];
- } elseif (strpos($var_type, 'resource (') === 0) {
+ } elseif (str_starts_with($var_type, 'resource (')) {
$if_types[$var_name] = [[new IsIdentical(new TResource())]];
} elseif ($var_type === 'integer') {
$if_types[$var_name] = [[new IsType(new Atomic\TInt())]];
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php
index 3a47c43d894..4f49634c593 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php
@@ -49,8 +49,8 @@
use function in_array;
use function is_string;
use function preg_match;
+use function str_contains;
use function strlen;
-use function strpos;
/**
* @internal
@@ -346,29 +346,25 @@ private static function updateTypeWithKeyValues(
}
if (!$has_matching_objectlike_property && !$has_matching_string) {
- if (count($key_values) === 1) {
- $key_value = $key_values[0];
-
- $object_like = new TKeyedArray(
- [$key_value->value => $current_type],
- $key_value instanceof TLiteralClassString
- ? [$key_value->value => true]
- : null,
- );
-
- $array_assignment_type = new Union([
- $object_like,
- ]);
- } else {
- $array_assignment_literals = $key_values;
-
- $array_assignment_type = new Union([
- new TNonEmptyArray([
- new Union($array_assignment_literals),
- $current_type,
- ]),
- ]);
+ $properties = [];
+ $classStrings = [];
+ $current_type = $current_type->setPossiblyUndefined(
+ $current_type->possibly_undefined || count($key_values) > 1,
+ );
+ foreach ($key_values as $key_value) {
+ $properties[$key_value->value] = $current_type;
+ if ($key_value instanceof TLiteralClassString) {
+ $classStrings[$key_value->value] = true;
+ }
}
+ $object_like = new TKeyedArray(
+ $properties,
+ $classStrings ?: null,
+ );
+
+ $array_assignment_type = new Union([
+ $object_like,
+ ]);
return Type::combineUnionTypes(
$child_stmt_type,
@@ -510,7 +506,7 @@ private static function updateArrayAssignmentChildType(
}
if ($parent_var_id && ($parent_type = $context->vars_in_scope[$parent_var_id] ?? null)) {
- if ($offset_already_existed && $parent_type->hasList() && strpos($parent_var_id, '[') === false) {
+ if ($offset_already_existed && $parent_type->hasList() && !str_contains($parent_var_id, '[')) {
$array_atomic_type_list = $value_type;
} elseif ($parent_type->hasClassStringMap()
&& $key_type
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php
index 7a35ff2c069..2f55918ffec 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/AssignedProperty.php
@@ -11,19 +11,7 @@
*/
final class AssignedProperty
{
- public Union $property_type;
-
- public string $id;
-
- public Union $assignment_type;
-
- public function __construct(
- Union $property_type,
- string $id,
- Union $assignment_type,
- ) {
- $this->property_type = $property_type;
- $this->id = $id;
- $this->assignment_type = $assignment_type;
+ public function __construct(public Union $property_type, public string $id, public Union $assignment_type)
+ {
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php
index 88a2bf6b511..c11731e2758 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php
@@ -122,7 +122,7 @@ public static function analyze(
$statements_analyzer,
$context,
);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
// do nothing
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
index 43327ddb7bf..2e28081e8e5 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
@@ -92,6 +92,8 @@
use function is_string;
use function reset;
use function spl_object_id;
+use function str_contains;
+use function str_starts_with;
use function strpos;
use function strtolower;
@@ -112,7 +114,7 @@ public static function analyze(
?PhpParser\Comment\Doc $doc_comment,
array $not_ignored_docblock_var_ids = [],
?PhpParser\Node\Expr $assign_expr = null,
- ): false|Union {
+ ): ?Union {
$var_id = ExpressionIdentifier::getVarId(
$assign_var,
$statements_analyzer->getFQCLN(),
@@ -256,7 +258,7 @@ public static function analyze(
$context->vars_in_scope[$var_id] = $comment_type ?? Type::getMixed();
}
- return false;
+ return null;
}
$context->inside_general_use = $was_inside_general_use;
@@ -400,7 +402,7 @@ public static function analyze(
if (!$assign_var instanceof PhpParser\Node\Expr\PropertyFetch
&& !strpos($root_var_id ?? '', '->')
&& !$comment_type
- && strpos($var_id ?? '', '$_') !== 0
+ && !str_starts_with($var_id ?? '', '$_')
) {
$origin_locations = [];
@@ -478,7 +480,7 @@ public static function analyze(
),
$statements_analyzer->getSuppressedIssues(),
)) {
- return false;
+ return null;
}
if (isset($context->protected_var_ids[$var_id])
@@ -507,7 +509,7 @@ public static function analyze(
$removed_taints,
) === false
) {
- return false;
+ return null;
}
if ($var_id && isset($context->vars_in_scope[$var_id])) {
@@ -962,7 +964,7 @@ public static function analyzeAssignmentRef(
// Remove old reference parent node so previously referenced variable usage doesn't count as reference usage
$old_type = $context->vars_in_scope[$lhs_var_id];
foreach ($old_type->parent_nodes as $old_parent_node_id => $_) {
- if (strpos($old_parent_node_id, "$lhs_var_id-") === 0) {
+ if (str_starts_with($old_parent_node_id, "$lhs_var_id-")) {
unset($old_type->parent_nodes[$old_parent_node_id]);
}
}
@@ -975,12 +977,12 @@ public static function analyzeAssignmentRef(
$context->hasVariable($lhs_var_id);
$context->references_in_scope[$lhs_var_id] = $rhs_var_id;
$context->referenced_counts[$rhs_var_id] = ($context->referenced_counts[$rhs_var_id] ?? 0) + 1;
- if (strpos($rhs_var_id, '[') !== false) {
+ if (str_contains($rhs_var_id, '[')) {
// Reference to array item, we always consider array items to be an external scope for references
// TODO handle differently so it's detected as unused if the array is unused?
$context->references_to_external_scope[$lhs_var_id] = true;
}
- if (strpos($rhs_var_id, '->') !== false) {
+ if (str_contains($rhs_var_id, '->')) {
IssueBuffer::maybeAdd(
new UnsupportedPropertyReferenceUsage(
new CodeLocation($statements_analyzer->getSource(), $stmt),
@@ -991,7 +993,7 @@ public static function analyzeAssignmentRef(
// TODO handle differently so it's detected as unused if the object is unused?
$context->references_to_external_scope[$lhs_var_id] = true;
}
- if (strpos($rhs_var_id, '::') !== false) {
+ if (str_contains($rhs_var_id, '::')) {
IssueBuffer::maybeAdd(
new UnsupportedPropertyReferenceUsage(
new CodeLocation($statements_analyzer->getSource(), $stmt),
@@ -1370,7 +1372,7 @@ private static function analyzeDestructuringAssignment(
$already_in_scope = isset($context->vars_in_scope[$list_var_id]);
- if (strpos($list_var_id, '-') === false && strpos($list_var_id, '[') === false) {
+ if (!str_contains($list_var_id, '-') && !str_contains($list_var_id, '[')) {
$location = new CodeLocation($statements_analyzer, $var);
if (!$statements_analyzer->hasVariable($list_var_id)) {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php
index 911827f2c4a..5356280bd5e 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php
@@ -188,20 +188,20 @@ public static function analyze(
if ($context->if_body_context && !$context->inside_negation) {
$if_body_context = $context->if_body_context;
$context->vars_in_scope = $right_context->vars_in_scope;
- $if_body_context->vars_in_scope = array_merge(
- $if_body_context->vars_in_scope,
- $context->vars_in_scope,
- );
+ $if_body_context->vars_in_scope = [
+ ...$if_body_context->vars_in_scope,
+ ...$context->vars_in_scope,
+ ];
- $if_body_context->cond_referenced_var_ids = array_merge(
- $if_body_context->cond_referenced_var_ids,
- $context->cond_referenced_var_ids,
- );
+ $if_body_context->cond_referenced_var_ids = [
+ ...$if_body_context->cond_referenced_var_ids,
+ ...$context->cond_referenced_var_ids,
+ ];
- $if_body_context->assigned_var_ids = array_merge(
- $if_body_context->assigned_var_ids,
- $context->assigned_var_ids,
- );
+ $if_body_context->assigned_var_ids = [
+ ...$if_body_context->assigned_var_ids,
+ ...$context->assigned_var_ids,
+ ];
$if_body_context->reconciled_expression_clauses = [
...$if_body_context->reconciled_expression_clauses,
@@ -212,10 +212,10 @@ public static function analyze(
),
];
- $if_body_context->vars_possibly_in_scope = array_merge(
- $if_body_context->vars_possibly_in_scope,
- $context->vars_possibly_in_scope,
- );
+ $if_body_context->vars_possibly_in_scope = [
+ ...$if_body_context->vars_possibly_in_scope,
+ ...$context->vars_possibly_in_scope,
+ ];
$if_body_context->updateChecks($context);
} else {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
index 5d4e3e290aa..04e1c9e2e48 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
@@ -50,7 +50,6 @@
use function array_diff_key;
use function array_values;
use function count;
-use function get_class;
use function is_int;
use function is_numeric;
use function max;
@@ -323,7 +322,7 @@ private static function analyzeOperands(
// get_class is fine here because both classes are final.
if ($statements_source !== null
&& $config->strict_binary_operands
- && get_class($left_type_part) !== get_class($right_type_part)
+ && $left_type_part::class !== $right_type_part::class
) {
IssueBuffer::maybeAdd(
new InvalidOperand(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php
index 364a9ec7ef5..e50f0bb55e2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/CoalesceAnalyzer.php
@@ -41,6 +41,7 @@ public static function analyze(
|| $root_expr instanceof PhpParser\Node\Expr\MethodCall
|| $root_expr instanceof PhpParser\Node\Expr\StaticCall
|| $root_expr instanceof PhpParser\Node\Expr\Cast
+ || $root_expr instanceof PhpParser\Node\Expr\Match_
|| $root_expr instanceof PhpParser\Node\Expr\NullsafePropertyFetch
|| $root_expr instanceof PhpParser\Node\Expr\NullsafeMethodCall
|| $root_expr instanceof PhpParser\Node\Expr\Ternary
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
index b3f7a03e5ed..f06c4f8a1b6 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
@@ -257,6 +257,19 @@ public static function analyze(
$has_numeric_and_non_empty = $has_numeric_type && $has_non_empty;
+ $non_falsy_string = $numeric_type->getBuilder()->addType(new TNonFalsyString())->freeze();
+ $left_non_falsy = UnionTypeComparator::isContainedBy(
+ $codebase,
+ $left_type,
+ $non_falsy_string,
+ );
+
+ $right_non_falsy = UnionTypeComparator::isContainedBy(
+ $codebase,
+ $right_type,
+ $non_falsy_string,
+ );
+
$all_literals = $left_type->allLiterals() && $right_type->allLiterals();
if ($has_non_empty) {
@@ -264,9 +277,10 @@ public static function analyze(
$result_type = new Union([new TNonEmptyNonspecificLiteralString]);
} elseif ($all_lowercase) {
$result_type = Type::getNonEmptyLowercaseString();
+ } elseif ($all_non_empty || $has_numeric_and_non_empty || $left_non_falsy || $right_non_falsy) {
+ $result_type = Type::getNonFalsyString();
} else {
- $result_type = $all_non_empty || $has_numeric_and_non_empty ?
- Type::getNonFalsyString() : Type::getNonEmptyString();
+ $result_type = Type::getNonEmptyString();
}
} else {
if ($all_literals) {
@@ -425,7 +439,7 @@ private static function analyzeOperand(
)) {
try {
$storage = $codebase->methods->getStorage($to_string_method_id);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
continue;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php
index eaf715bfa37..e7134d07487 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php
@@ -92,7 +92,7 @@ public static function analyze(
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
$post_leaving_if_context = clone $context;
}
- } catch (ScopeAnalysisException $e) {
+ } catch (ScopeAnalysisException) {
return false;
}
} else {
@@ -132,10 +132,16 @@ public static function analyze(
}
$left_referenced_var_ids = $left_context->cond_referenced_var_ids;
- $left_context->cond_referenced_var_ids = array_merge($pre_referenced_var_ids, $left_referenced_var_ids);
+ $left_context->cond_referenced_var_ids = [
+ ...$pre_referenced_var_ids,
+ ...$left_referenced_var_ids,
+ ];
$left_assigned_var_ids = array_diff_key($left_context->assigned_var_ids, $pre_assigned_var_ids);
- $left_context->assigned_var_ids = array_merge($pre_assigned_var_ids, $left_context->assigned_var_ids);
+ $left_context->assigned_var_ids = [
+ ...$pre_assigned_var_ids,
+ ...$left_context->assigned_var_ids,
+ ];
$left_referenced_var_ids = array_diff_key($left_referenced_var_ids, $left_assigned_var_ids);
}
@@ -153,7 +159,7 @@ public static function analyze(
try {
$negated_left_clauses = Algebra::negateFormula($left_clauses);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
try {
$negated_left_clauses = FormulaGenerator::getFormula(
$left_cond_id,
@@ -164,7 +170,7 @@ public static function analyze(
$codebase,
false,
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
return false;
}
}
@@ -354,15 +360,12 @@ public static function analyze(
$context->updateChecks($right_context);
}
- $context->cond_referenced_var_ids = array_merge(
- $right_context->cond_referenced_var_ids,
- $context->cond_referenced_var_ids,
- );
+ $context->cond_referenced_var_ids = [
+ ...$right_context->cond_referenced_var_ids,
+ ...$context->cond_referenced_var_ids,
+ ];
- $context->assigned_var_ids = array_merge(
- $context->assigned_var_ids,
- $right_context->assigned_var_ids,
- );
+ $context->assigned_var_ids = [...$context->assigned_var_ids, ...$right_context->assigned_var_ids];
if ($context->if_body_context) {
$if_body_context = $context->if_body_context;
@@ -383,23 +386,23 @@ public static function analyze(
}
}
- $if_body_context->cond_referenced_var_ids = array_merge(
- $context->cond_referenced_var_ids,
- $if_body_context->cond_referenced_var_ids,
- );
+ $if_body_context->cond_referenced_var_ids = [
+ ...$context->cond_referenced_var_ids,
+ ...$if_body_context->cond_referenced_var_ids,
+ ];
- $if_body_context->assigned_var_ids = array_merge(
- $context->assigned_var_ids,
- $if_body_context->assigned_var_ids,
- );
+ $if_body_context->assigned_var_ids = [
+ ...$context->assigned_var_ids,
+ ...$if_body_context->assigned_var_ids,
+ ];
$if_body_context->updateChecks($context);
}
- $context->vars_possibly_in_scope = array_merge(
- $right_context->vars_possibly_in_scope,
- $context->vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$right_context->vars_possibly_in_scope,
+ ...$context->vars_possibly_in_scope,
+ ];
return true;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
index 3086a7ee031..4557e1cfebf 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
@@ -488,7 +488,7 @@ private static function checkForImpureEqualityComparison(
'__tostring',
),
);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
continue;
}
@@ -522,7 +522,7 @@ private static function checkForImpureEqualityComparison(
'__tostring',
),
);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
continue;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
index 09bfb35f877..1f36642ecab 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
@@ -5,15 +5,20 @@
namespace Psalm\Internal\Analyzer\Statements\Expression;
use PhpParser;
+use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
+use Psalm\Issue\RiskyTruthyFalsyComparison;
+use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TBool;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TTrue;
use Psalm\Type\Union;
+use function count;
+
/**
* @internal
*/
@@ -42,6 +47,32 @@ public static function analyze(
} elseif ($expr_type->isAlwaysFalsy()) {
$stmt_type = new TTrue($expr_type->from_docblock);
} else {
+ if (count($expr_type->getAtomicTypes()) > 1) {
+ $both_types = $expr_type->getBuilder();
+ foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
+ if ($atomic_type->isTruthy()
+ || $atomic_type->isFalsy()
+ || $atomic_type instanceof TBool) {
+ $both_types->removeType($key);
+ }
+ }
+
+ if (count($both_types->getAtomicTypes()) > 0) {
+ $both_types = $both_types->freeze();
+ IssueBuffer::maybeAdd(
+ new RiskyTruthyFalsyComparison(
+ 'Operand of type ' . $expr_type->getId() . ' contains ' .
+ 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
+ $both_types->getId() . ', which can be falsy and truthy. ' .
+ 'This can cause possibly unexpected behavior. Use strict comparison instead.',
+ new CodeLocation($statements_analyzer, $stmt),
+ $expr_type->getId(),
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+ }
+
$stmt_type = new TBool();
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
index 7e1c1a9758d..80107a525e4 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
@@ -69,6 +69,8 @@
use function ord;
use function preg_split;
use function reset;
+use function str_contains;
+use function str_starts_with;
use function strpos;
use function strtolower;
use function substr;
@@ -683,7 +685,7 @@ public static function verifyType(
&& !$param_type->from_docblock
&& !$param_type->had_template
&& $method_id
- && strpos($method_id->method_name, '__') !== 0
+ && !str_starts_with($method_id->method_name, '__')
) {
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
@@ -896,7 +898,7 @@ public static function verifyType(
$input_type,
$param_type,
true,
- true,
+ !isset($param_type->getAtomicTypes()['true']),
$union_comparison_results,
);
@@ -1268,7 +1270,7 @@ private static function verifyExplicitParam(
);
foreach ($function_ids as $function_id) {
- if (strpos($function_id, '::') !== false) {
+ if (str_contains($function_id, '::')) {
if ($function_id[0] === '$') {
$function_id = substr($function_id, 1);
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
index 87106622daa..a563b7495f2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
@@ -4,11 +4,13 @@
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
+use InvalidArgumentException;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\Internal\Analyzer\AttributesAnalyzer;
+use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
@@ -45,6 +47,7 @@
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLiteralString;
+use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
@@ -61,7 +64,7 @@
use function max;
use function min;
use function reset;
-use function strpos;
+use function str_contains;
use function strtolower;
/**
@@ -448,7 +451,7 @@ private static function handleClosureArg(
$statements_analyzer->getFilePath(),
$closure_id,
);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
return;
}
@@ -1135,7 +1138,7 @@ static function (
$by_ref_type,
$by_ref_out_type ?: $by_ref_type,
$context,
- $method_id && (strpos($method_id, '::') !== false || !InternalCallMapHandler::inCallMap($method_id)),
+ $method_id && (str_contains($method_id, '::') || !InternalCallMapHandler::inCallMap($method_id)),
$check_null_ref,
);
}
@@ -1252,6 +1255,42 @@ private static function evaluateArbitraryParam(
return null;
}
+ private static function handleByRefReadonlyArg(
+ StatementsAnalyzer $statements_analyzer,
+ Context $context,
+ PhpParser\Node\Expr\PropertyFetch $stmt,
+ string $fq_class_name,
+ string $prop_name,
+ ): void {
+ $property_id = $fq_class_name . '::$' . $prop_name;
+
+ $codebase = $statements_analyzer->getCodebase();
+ $declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
+ $property_id,
+ true,
+ $statements_analyzer,
+ );
+
+ try {
+ $declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
+ } catch (InvalidArgumentException $_) {
+ return;
+ }
+
+ if (isset($declaring_class_storage->properties[$prop_name])) {
+ $property_storage = $declaring_class_storage->properties[$prop_name];
+
+ InstancePropertyAssignmentAnalyzer::trackPropertyImpurity(
+ $statements_analyzer,
+ $stmt,
+ $property_id,
+ $property_storage,
+ $declaring_class_storage,
+ $context,
+ );
+ }
+ }
+
/**
* @return false|null
*/
@@ -1270,9 +1309,49 @@ private static function handleByRefFunctionArg(
$builtin_array_functions = [
'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort',
- 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift',
+ 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'extract',
];
+ if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch
+ && $arg->value->name instanceof PhpParser\Node\Identifier) {
+ $prop_name = $arg->value->name->name;
+ if (!empty($statements_analyzer->getFQCLN())) {
+ $fq_class_name = $statements_analyzer->getFQCLN();
+
+ self::handleByRefReadonlyArg(
+ $statements_analyzer,
+ $context,
+ $arg->value,
+ $fq_class_name,
+ $prop_name,
+ );
+ } else {
+ // @todo atm only works for simple fetch, $a->foo, not $a->foo->bar
+ // I guess there's a function to do this, but I couldn't locate it
+ $var_id = ExpressionIdentifier::getVarId(
+ $arg->value->var,
+ $statements_analyzer->getFQCLN(),
+ $statements_analyzer,
+ );
+
+ if ($var_id && isset($context->vars_in_scope[$var_id])) {
+ foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNamedObject) {
+ $fq_class_name = $atomic_type->value;
+
+ self::handleByRefReadonlyArg(
+ $statements_analyzer,
+ $context,
+ $arg->value,
+ $fq_class_name,
+ $prop_name,
+ );
+ }
+ }
+ }
+ }
+ }
+
if (($var_id && isset($context->vars_in_scope[$var_id]))
|| ($method_id
&& in_array(
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php
index f252f93bde6..a8465bf5656 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php
@@ -43,7 +43,6 @@
use UnexpectedValueException;
use function array_filter;
-use function array_merge;
use function array_pop;
use function array_shift;
use function array_unshift;
@@ -51,7 +50,7 @@
use function count;
use function explode;
use function is_numeric;
-use function strpos;
+use function str_contains;
use function strtolower;
use function substr;
@@ -460,12 +459,11 @@ public static function handleSplice(
$length_min = (int) $length_literal->value;
}
} else {
- $literals = array_merge(
- $length_arg_type->getLiteralStrings(),
- $length_arg_type->getLiteralInts(),
- $length_arg_type->getLiteralFloats(),
- );
- foreach ($literals as $literal) {
+ foreach ([
+ ...$length_arg_type->getLiteralStrings(),
+ ...$length_arg_type->getLiteralInts(),
+ ...$length_arg_type->getLiteralFloats(),
+ ] as $literal) {
if ($literal->isNumericType()
&& ($literal_val = (int) $literal->value)
&& ((isset($length_min) && $length_min> $literal_val) || !isset($length_min))) {
@@ -766,7 +764,7 @@ private static function checkClosureType(
foreach ($function_ids as $function_id) {
$function_id = strtolower($function_id);
- if (strpos($function_id, '::') !== false) {
+ if (str_contains($function_id, '::')) {
if ($function_id[0] === '$') {
$function_id = substr($function_id, 1);
}
@@ -804,7 +802,7 @@ private static function checkClosureType(
try {
$method_storage = $codebase->methods->getStorage($function_id_part);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
// the method may not exist, but we're suppressing that issue
continue;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php
index ba425e189d9..76501316200 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php
@@ -238,10 +238,7 @@ public static function analyze(
$function_call_info->function_id,
);
- $template_result->lower_bounds = array_merge(
- $template_result->lower_bounds,
- $already_inferred_lower_bounds,
- );
+ $template_result->lower_bounds = [...$template_result->lower_bounds, ...$already_inferred_lower_bounds];
if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) {
$stmt_type = FunctionCallReturnTypeFetcher::fetch(
@@ -405,13 +402,6 @@ public static function analyze(
}
}
- if ($function_call_info->byref_uses) {
- foreach ($function_call_info->byref_uses as $byref_use_var => $_) {
- $context->vars_in_scope['$' . $byref_use_var] = Type::getMixed();
- $context->vars_possibly_in_scope['$' . $byref_use_var] = true;
- }
- }
-
if ($function_name instanceof PhpParser\Node\Name && $function_call_info->function_id) {
NamedFunctionCallHandler::handle(
$statements_analyzer,
@@ -569,7 +559,7 @@ private static function handleNamedFunction(
$function_call_info->defined_constants = $function_storage->defined_constants;
$function_call_info->global_variables = $function_storage->global_variables;
}
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
$function_call_info->function_params = [
new FunctionLikeParameter('args', false, null, null, null, null, false, false, true),
];
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php
index 2d35a9c8168..b7de2fd223c 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php
@@ -47,10 +47,11 @@
use function count;
use function explode;
use function in_array;
+use function str_contains;
use function strlen;
-use function strpos;
use function strtolower;
use function substr;
+use function trim;
/**
* @internal
@@ -232,7 +233,7 @@ public static function fetch(
);
}
}
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
// this can happen when the function was defined in the Config startup script
$stmt_type = Type::getMixed();
}
@@ -278,7 +279,7 @@ public static function fetch(
$fake_call_factory = new BuilderFactory();
- if (strpos($proxy_call['fqn'], '::') !== false) {
+ if (str_contains($proxy_call['fqn'], '::')) {
[$fqcn, $method] = explode('::', $proxy_call['fqn']);
$fake_call = $fake_call_factory->staticCall($fqcn, $method, $fake_call_arguments);
} else {
@@ -631,17 +632,19 @@ private static function taintReturnType(
$first_arg_value = $first_stmt_type->getSingleStringLiteral()->value;
$pattern = substr($first_arg_value, 1, -1);
+ if (strlen(trim($pattern)) > 0) {
+ $pattern = trim($pattern);
+ if ($pattern[0] === '['
+ && $pattern[1] === '^'
+ && substr($pattern, -1) === ']'
+ ) {
+ $pattern = substr($pattern, 2, -1);
- if ($pattern[0] === '['
- && $pattern[1] === '^'
- && substr($pattern, -1) === ']'
- ) {
- $pattern = substr($pattern, 2, -1);
-
- if (self::simpleExclusion($pattern, $first_arg_value[0])) {
- $removed_taints[] = 'html';
- $removed_taints[] = 'has_quotes';
- $removed_taints[] = 'sql';
+ if (self::simpleExclusion($pattern, $first_arg_value[0])) {
+ $removed_taints[] = 'html';
+ $removed_taints[] = 'has_quotes';
+ $removed_taints[] = 'sql';
+ }
}
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php
index ff627e40f54..4822d894c99 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgHandler.php
@@ -21,7 +21,7 @@
use UnexpectedValueException;
use function is_string;
-use function strpos;
+use function str_contains;
use function strtolower;
/**
@@ -274,7 +274,7 @@ public static function getCallableArgInfo(
$class_storage,
);
}
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
return null;
}
@@ -288,7 +288,8 @@ private static function isSupported(FunctionLikeParameter $container_param): boo
}
foreach ($container_param->type->getAtomicTypes() as $a) {
- if (($a instanceof TClosure || $a instanceof TCallable) && !$a->params) {
+ // must check null explicitly, since no params (empty array) would not be handled correctly otherwise
+ if (($a instanceof TClosure || $a instanceof TCallable) && $a->params === null) {
return false;
}
@@ -316,7 +317,7 @@ private static function fromLiteralString(
return new HighOrderFunctionArgInfo(
HighOrderFunctionArgInfo::TYPE_STRING_CALLABLE,
- strpos($literal->value, '::') !== false
+ str_contains($literal->value, '::')
? $codebase->methods->getStorage(MethodIdentifier::wrap($literal->value))
: $codebase->functions->getStorage($statements_analyzer, strtolower($literal->value)),
);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php
index c7cb62a05a3..54921838c0c 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php
@@ -12,8 +12,6 @@
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Union;
-use function array_merge;
-
/**
* @internal
*/
@@ -24,31 +22,20 @@ final class HighOrderFunctionArgInfo
public const TYPE_STRING_CALLABLE = 'string-callable';
public const TYPE_CALLABLE = 'callable';
- /** @psalm-var HighOrderFunctionArgInfo::TYPE_* */
- private string $type;
- private FunctionLikeStorage $function_storage;
- private ?ClassLikeStorage $class_storage;
-
/**
* @psalm-param HighOrderFunctionArgInfo::TYPE_* $type
*/
public function __construct(
- string $type,
- FunctionLikeStorage $function_storage,
- ClassLikeStorage $class_storage = null,
+ private readonly string $type,
+ private functionLikeStorage $function_storage,
+ private readonly ?ClassLikeStorage $class_storage = null,
) {
- $this->type = $type;
- $this->function_storage = $function_storage;
- $this->class_storage = $class_storage;
}
public function getTemplates(): TemplateResult
{
$templates = $this->class_storage
- ? array_merge(
- $this->function_storage->template_types ?? [],
- $this->class_storage->template_types ?? [],
- )
+ ? [...$this->function_storage->template_types ?? [], ...$this->class_storage->template_types ?? []]
: $this->function_storage->template_types ?? [];
return new TemplateResult($templates, []);
@@ -61,30 +48,24 @@ public function getType(): string
public function getFunctionType(): Union
{
- switch ($this->type) {
- case self::TYPE_FIRST_CLASS_CALLABLE:
- return new Union([
- new TClosure(
- 'Closure',
- $this->function_storage->params,
- $this->function_storage->return_type,
- $this->function_storage->pure,
- ),
- ]);
-
- case self::TYPE_STRING_CALLABLE:
- case self::TYPE_CLASS_CALLABLE:
- return new Union([
- new TCallable(
- 'callable',
- $this->function_storage->params,
- $this->function_storage->return_type,
- $this->function_storage->pure,
- ),
- ]);
-
- default:
- return $this->function_storage->return_type ?? Type::getMixed();
- }
+ return match ($this->type) {
+ self::TYPE_FIRST_CLASS_CALLABLE => new Union([
+ new TClosure(
+ 'Closure',
+ $this->function_storage->params,
+ $this->function_storage->return_type,
+ $this->function_storage->pure,
+ ),
+ ]),
+ self::TYPE_STRING_CALLABLE, self::TYPE_CLASS_CALLABLE => new Union([
+ new TCallable(
+ 'callable',
+ $this->function_storage->params,
+ $this->function_storage->return_type,
+ $this->function_storage->pure,
+ ),
+ ]),
+ default => $this->function_storage->return_type ?? Type::getMixed(),
+ };
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php
index 131ca47c79e..419ab0ecbf6 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php
@@ -12,15 +12,8 @@
*/
final class AtomicCallContext
{
- public MethodIdentifier $method_id;
-
- /** @var list */
- public array $args;
-
/** @param list $args */
- public function __construct(MethodIdentifier $method_id, array $args)
+ public function __construct(public MethodIdentifier $method_id, public array $args)
{
- $this->method_id = $method_id;
- $this->args = $args;
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php
index 7dde48e6d9e..1e956a9039f 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php
@@ -51,7 +51,6 @@
use function array_shift;
use function array_values;
use function count;
-use function get_class;
use function reset;
use function strtolower;
@@ -620,7 +619,7 @@ private static function handleInvalidClass(
bool $is_intersection,
AtomicMethodCallAnalysisResult $result,
): void {
- switch (get_class($lhs_type_part)) {
+ switch ($lhs_type_part::class) {
case TNull::class:
case TFalse::class:
// handled above
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php
index b1d1d0226cb..248f5a48fc9 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php
@@ -51,7 +51,7 @@
use function explode;
use function in_array;
use function is_string;
-use function strpos;
+use function str_starts_with;
use function strtolower;
/**
@@ -206,7 +206,7 @@ public static function analyze(
try {
$method_storage = $codebase->methods->getStorage($declaring_method_id ?? $method_id);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
$method_storage = null;
}
@@ -447,7 +447,7 @@ public static function analyze(
$possibilities = array_filter(
$possibilities,
static fn(Possibilities $assertion): bool => !(is_string($assertion->var_id)
- && strpos($assertion->var_id, '$this->') === 0
+ && str_starts_with($assertion->var_id, '$this->')
)
);
}
@@ -470,7 +470,7 @@ public static function analyze(
$possibilities = array_filter(
$possibilities,
static fn(Possibilities $assertion): bool => !(is_string($assertion->var_id)
- && strpos($assertion->var_id, '$this->') === 0
+ && str_starts_with($assertion->var_id, '$this->')
)
);
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php
index a04104107b2..3d3b39e5b37 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php
@@ -577,7 +577,7 @@ public static function replaceTemplateTypes(
) {
if ($template_type->param_name === 'TFunctionArgCount') {
$template_result->lower_bounds[$template_type->param_name] = [
- 'fn-' . strtolower((string) $method_id) => [
+ 'fn-' . $method_id->method_name => [
new TemplateBound(
Type::getInt(false, $arg_count),
),
@@ -585,7 +585,7 @@ public static function replaceTemplateTypes(
];
} elseif ($template_type->param_name === 'TPhpMajorVersion') {
$template_result->lower_bounds[$template_type->param_name] = [
- 'fn-' . strtolower((string) $method_id) => [
+ 'fn-' . $method_id->method_name => [
new TemplateBound(
Type::getInt(false, $codebase->getMajorAnalysisPhpVersion()),
),
@@ -593,7 +593,7 @@ public static function replaceTemplateTypes(
];
} elseif ($template_type->param_name === 'TPhpVersionId') {
$template_result->lower_bounds[$template_type->param_name] = [
- 'fn-' . strtolower((string) $method_id) => [
+ 'fn-' . $method_id->method_name => [
new TemplateBound(
Type::getInt(
false,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php
index a4f0c47d864..80c780de50b 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php
@@ -37,6 +37,7 @@
use Psalm\Type\Atomic\TDependentGetType;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
+use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLowercaseString;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
@@ -49,12 +50,19 @@
use Psalm\Type\Union;
use function array_map;
+use function count;
use function extension_loaded;
use function in_array;
+use function is_numeric;
use function is_string;
+use function preg_match;
+use function str_starts_with;
use function strpos;
use function strtolower;
+use const EXTR_OVERWRITE;
+use const EXTR_SKIP;
+
/**
* @internal
*/
@@ -227,18 +235,130 @@ public static function handle(
}
if ($function_id === 'defined') {
- $context->check_consts = false;
+ if ($first_arg && !$context->inside_negation) {
+ $fq_const_name = ConstFetchAnalyzer::getConstName(
+ $first_arg->value,
+ $statements_analyzer->node_data,
+ $codebase,
+ $statements_analyzer->getAliases(),
+ );
+
+ if ($fq_const_name !== null) {
+ $const_type = ConstFetchAnalyzer::getConstType(
+ $statements_analyzer,
+ $fq_const_name,
+ true,
+ $context,
+ );
+
+ if (!$const_type) {
+ ConstFetchAnalyzer::setConstType(
+ $statements_analyzer,
+ $fq_const_name,
+ Type::getMixed(),
+ $context,
+ );
+
+ $context->check_consts = false;
+ }
+ } else {
+ $context->check_consts = false;
+ }
+ } else {
+ $context->check_consts = false;
+ }
return;
}
if ($function_id === 'extract') {
+ $flag_value = false;
+ if (!isset($stmt->args[1])) {
+ $flag_value = EXTR_OVERWRITE;
+ } elseif (isset($stmt->args[1]->value)
+ && $stmt->args[1]->value instanceof PhpParser\Node\Expr
+ && ($flags_type = $statements_analyzer->node_data->getType($stmt->args[1]->value))
+ && $flags_type->hasLiteralInt() && count($flags_type->getAtomicTypes()) === 1) {
+ $flag_type_value = $flags_type->getSingleIntLiteral()->value;
+ if ($flag_type_value === EXTR_SKIP) {
+ $flag_value = EXTR_SKIP;
+ } elseif ($flag_type_value === EXTR_OVERWRITE) {
+ $flag_value = EXTR_OVERWRITE;
+ }
+ // @todo add support for other flags
+ }
+
+ $is_unsealed = true;
+ $validated_var_ids = [];
+ if ($flag_value !== false && isset($stmt->args[0]->value)
+ && $stmt->args[0]->value instanceof PhpParser\Node\Expr
+ && ($array_type_union = $statements_analyzer->node_data->getType($stmt->args[0]->value))
+ && $array_type_union->isSingle()
+ ) {
+ foreach ($array_type_union->getAtomicTypes() as $array_type) {
+ if ($array_type instanceof TKeyedArray) {
+ foreach ($array_type->properties as $key => $type) {
+ // variables must start with letters or underscore
+ if ($key === '' || is_numeric($key) || preg_match('/^[A-Za-z_]/', $key) !== 1) {
+ continue;
+ }
+
+ $var_id = '$' . $key;
+ $validated_var_ids[] = $var_id;
+
+ if (isset($context->vars_in_scope[$var_id]) && $flag_value === EXTR_SKIP) {
+ continue;
+ }
+
+ if (!isset($context->vars_in_scope[$var_id]) && $type->possibly_undefined === true) {
+ $context->possibly_assigned_var_ids[$var_id] = true;
+ } elseif (isset($context->vars_in_scope[$var_id])
+ && $type->possibly_undefined === true
+ && $flag_value === EXTR_OVERWRITE) {
+ $type = Type::combineUnionTypes(
+ $context->vars_in_scope[$var_id],
+ $type,
+ $codebase,
+ false,
+ true,
+ 500,
+ false,
+ );
+ }
+
+ $context->vars_in_scope[$var_id] = $type;
+ $context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos');
+ }
+
+ if (!isset($array_type->fallback_params)) {
+ $is_unsealed = false;
+ }
+ }
+ }
+ }
+
+ if ($flag_value === EXTR_OVERWRITE && $is_unsealed === false) {
+ return;
+ }
+
+ if ($flag_value === EXTR_SKIP && $is_unsealed === false) {
+ return;
+ }
+
$context->check_variables = false;
+ if ($flag_value === EXTR_SKIP) {
+ return;
+ }
+
foreach ($context->vars_in_scope as $var_id => $_) {
if ($var_id === '$this' || strpos($var_id, '[') || strpos($var_id, '>')) {
continue;
}
+ if (in_array($var_id, $validated_var_ids, true)) {
+ continue;
+ }
+
$mixed_type = new Union([new TMixed()], [
'parent_nodes' => $context->vars_in_scope[$var_id]->parent_nodes,
]);
@@ -392,7 +512,7 @@ public static function handle(
if ($first_arg
&& $function_id
- && strpos($function_id, 'is_') === 0
+ && str_starts_with($function_id, 'is_')
&& $function_id !== 'is_a'
&& !$context->inside_negation
) {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php
index cfc1fedaacc..54b93dccbde 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php
@@ -568,12 +568,12 @@ private static function handleNamedCall(
true,
$context->insideUse(),
)) {
- $callstatic_appearing_id = $codebase->methods->getAppearingMethodId($callstatic_id);
- assert($callstatic_appearing_id !== null);
+ $callstatic_declaring_id = $codebase->methods->getDeclaringMethodId($callstatic_id);
+ assert($callstatic_declaring_id !== null);
$callstatic_pure = false;
$callstatic_mutation_free = false;
- if ($codebase->methods->hasStorage($callstatic_appearing_id)) {
- $callstatic_storage = $codebase->methods->getStorage($callstatic_appearing_id);
+ if ($codebase->methods->hasStorage($callstatic_declaring_id)) {
+ $callstatic_storage = $codebase->methods->getStorage($callstatic_declaring_id);
$callstatic_pure = $callstatic_storage->pure;
$callstatic_mutation_free = $callstatic_storage->mutation_free;
}
@@ -964,7 +964,7 @@ private static function checkPseudoMethod(
new CodeLocation($statements_analyzer, $stmt),
$context,
);
- } catch (Exception $e) {
+ } catch (Exception) {
// do nothing
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php
index c87fa0f8462..88646faa5b6 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php
@@ -44,6 +44,7 @@
use function explode;
use function in_array;
use function is_string;
+use function str_starts_with;
use function strlen;
use function strpos;
use function strtolower;
@@ -113,13 +114,13 @@ public static function analyze(
$local_vars_possibly_in_scope = [];
foreach ($context->vars_in_scope as $var => $_) {
- if (strpos($var, '$this->') !== 0 && $var !== '$this') {
+ if (!str_starts_with($var, '$this->') && $var !== '$this') {
$local_vars_in_scope[$var] = $context->vars_in_scope[$var];
}
}
foreach ($context->vars_possibly_in_scope as $var => $_) {
- if (strpos($var, '$this->') !== 0 && $var !== '$this') {
+ if (!str_starts_with($var, '$this->') && $var !== '$this') {
$local_vars_possibly_in_scope[$var] = $context->vars_possibly_in_scope[$var];
}
}
@@ -629,7 +630,7 @@ private static function resolveTemplateResultLowerBound(
): array {
if ($template_type->param_name === 'TFunctionArgCount') {
return [
- 'fn-' . strtolower((string)$method_id) => [
+ 'fn-' . $method_id->method_name => [
new TemplateBound(
Type::getInt(false, count($stmt->getArgs())),
),
@@ -639,7 +640,7 @@ private static function resolveTemplateResultLowerBound(
if ($template_type->param_name === 'TPhpMajorVersion') {
return [
- 'fn-' . strtolower((string)$method_id) => [
+ 'fn-' . $method_id->method_name => [
new TemplateBound(
Type::getInt(false, $codebase->getMajorAnalysisPhpVersion()),
),
@@ -649,7 +650,7 @@ private static function resolveTemplateResultLowerBound(
if ($template_type->param_name === 'TPhpVersionId') {
return [
- 'fn-' . strtolower((string) $method_id) => [
+ 'fn-' . $method_id->method_name => [
new TemplateBound(
Type::getInt(
false,
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
index a1bdb63638f..0e081a2c67c 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php
@@ -68,14 +68,15 @@
use function preg_match;
use function preg_replace;
use function spl_object_id;
+use function str_contains;
use function str_replace;
-use function strpos;
+use function str_starts_with;
use function strtolower;
/**
* @internal
*/
-class CallAnalyzer
+abstract class CallAnalyzer
{
public static function collectSpecialInformation(
FunctionLikeAnalyzer $source,
@@ -227,7 +228,7 @@ public static function collectSpecialInformation(
$local_vars_in_scope = [];
foreach ($context->vars_in_scope as $var_id => $type) {
- if (strpos($var_id, '$this->') === 0) {
+ if (str_starts_with($var_id, '$this->')) {
if ($type->initialized) {
$local_vars_in_scope[$var_id] = $context->vars_in_scope[$var_id];
@@ -670,16 +671,16 @@ public static function applyAssertionsToContext(
}
} elseif ($var_possibilities->var_id === '$this' && $thisName !== null) {
$assertion_var_id = $thisName;
- } elseif (strpos($var_possibilities->var_id, '$this->') === 0 && $thisName !== null) {
+ } elseif (str_starts_with($var_possibilities->var_id, '$this->') && $thisName !== null) {
$assertion_var_id = $thisName . str_replace('$this->', '->', $var_possibilities->var_id);
- } elseif (strpos($var_possibilities->var_id, 'self::') === 0 && $context->self) {
+ } elseif (str_starts_with($var_possibilities->var_id, 'self::') && $context->self) {
$assertion_var_id = $context->self . str_replace('self::', '::', $var_possibilities->var_id);
- } elseif (strpos($var_possibilities->var_id, '::$') !== false) {
+ } elseif (str_contains($var_possibilities->var_id, '::$')) {
// allow assertions to bring external static props into scope
$assertion_var_id = $var_possibilities->var_id;
} elseif (isset($context->vars_in_scope[$var_possibilities->var_id])) {
$assertion_var_id = $var_possibilities->var_id;
- } elseif (strpos($var_possibilities->var_id, '->') !== false) {
+ } elseif (str_contains($var_possibilities->var_id, '->')) {
$exploded = explode('->', $var_possibilities->var_id);
if (count($exploded) < 2) {
@@ -876,7 +877,7 @@ public static function applyAssertionsToContext(
$simplified_clauses,
);
- $type_assertions = array_merge($type_assertions, $assert_type_assertions);
+ $type_assertions = [...$type_assertions, ...$assert_type_assertions];
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php
index 2235b9501df..e2f2544f0ca 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php
@@ -53,7 +53,6 @@
use function array_merge;
use function array_pop;
use function array_values;
-use function get_class;
use function strtolower;
/**
@@ -294,7 +293,7 @@ public static function analyze(
IssueBuffer::maybeAdd(
new UnrecognizedExpression(
- 'Psalm does not understand the cast ' . get_class($stmt),
+ 'Psalm does not understand the cast ' . $stmt::class,
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
$statements_analyzer->getSuppressedIssues(),
@@ -385,7 +384,7 @@ public static function castIntAttempt(
$intersection_types = [$atomic_type];
if ($atomic_type->extra_types) {
- $intersection_types = array_merge($intersection_types, $atomic_type->extra_types);
+ $intersection_types = [...$intersection_types, ...$atomic_type->extra_types];
}
foreach ($intersection_types as $intersection_type) {
@@ -566,7 +565,7 @@ public static function castFloatAttempt(
$intersection_types = [$atomic_type];
if ($atomic_type->extra_types) {
- $intersection_types = array_merge($intersection_types, $atomic_type->extra_types);
+ $intersection_types = [...$intersection_types, ...$atomic_type->extra_types];
}
foreach ($intersection_types as $intersection_type) {
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php
index 48d2d5b71c7..1bd5b47ddf2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php
@@ -263,9 +263,9 @@ public static function analyzeFetch(
[],
$stmt->class->getFirst() === "static",
);
- } catch (InvalidArgumentException $_) {
+ } catch (InvalidArgumentException) {
return true;
- } catch (CircularReferenceException $e) {
+ } catch (CircularReferenceException) {
IssueBuffer::maybeAdd(
new CircularReference(
'Constant ' . $const_id . ' contains a circular reference',
@@ -567,9 +567,9 @@ public static function analyzeFetch(
$class_visibility,
$statements_analyzer,
);
- } catch (InvalidArgumentException $_) {
+ } catch (InvalidArgumentException) {
return true;
- } catch (CircularReferenceException $e) {
+ } catch (CircularReferenceException) {
IssueBuffer::maybeAdd(
new CircularReference(
'Constant ' . $const_id . ' contains a circular reference',
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php
index 8b3e6990a9e..1fbd8f8c86f 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EmptyAnalyzer.php
@@ -10,8 +10,15 @@
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\InvalidArgument;
+use Psalm\Issue\RiskyTruthyFalsyComparison;
use Psalm\IssueBuffer;
use Psalm\Type;
+use Psalm\Type\Atomic\TBool;
+use Psalm\Type\Atomic\TFalse;
+use Psalm\Type\Atomic\TTrue;
+use Psalm\Type\Union;
+
+use function count;
/**
* @internal
@@ -37,21 +44,64 @@ public static function analyze(
);
}
- if (($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr))
- && $stmt_expr_type->hasBool()
- && $stmt_expr_type->isSingle()
- && !$stmt_expr_type->from_docblock
- ) {
- IssueBuffer::maybeAdd(
- new InvalidArgument(
- 'Calling empty on a boolean value is almost certainly unintended',
- new CodeLocation($statements_analyzer->getSource(), $stmt->expr),
- 'empty',
- ),
- $statements_analyzer->getSuppressedIssues(),
- );
+ $expr_type = $statements_analyzer->node_data->getType($stmt->expr);
+
+ if ($expr_type) {
+ if ($expr_type->hasBool()
+ && $expr_type->isSingle()
+ && !$expr_type->from_docblock
+ ) {
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'Calling empty on a boolean value is almost certainly unintended',
+ new CodeLocation($statements_analyzer->getSource(), $stmt->expr),
+ 'empty',
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if ($expr_type->isAlwaysTruthy() && $expr_type->possibly_undefined === false) {
+ $stmt_type = new TFalse($expr_type->from_docblock);
+ } elseif ($expr_type->isAlwaysFalsy()) {
+ $stmt_type = new TTrue($expr_type->from_docblock);
+ } else {
+ if (count($expr_type->getAtomicTypes()) > 1) {
+ $both_types = $expr_type->getBuilder();
+ foreach ($both_types->getAtomicTypes() as $key => $atomic_type) {
+ if ($atomic_type->isTruthy()
+ || $atomic_type->isFalsy()
+ || $atomic_type instanceof TBool) {
+ $both_types->removeType($key);
+ }
+ }
+
+ if (count($both_types->getAtomicTypes()) > 0) {
+ $both_types = $both_types->freeze();
+ IssueBuffer::maybeAdd(
+ new RiskyTruthyFalsyComparison(
+ 'Operand of type ' . $expr_type->getId() . ' contains ' .
+ 'type' . (count($both_types->getAtomicTypes()) > 1 ? 's' : '') . ' ' .
+ $both_types->getId() . ', which can be falsy and truthy. ' .
+ 'This can cause possibly unexpected behavior. Use strict comparison instead.',
+ new CodeLocation($statements_analyzer, $stmt),
+ $expr_type->getId(),
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+ }
+
+ $stmt_type = new TBool();
+ }
+
+ $stmt_type = new Union([$stmt_type], [
+ 'parent_nodes' => $expr_type->parent_nodes,
+ ]);
+ } else {
+ $stmt_type = Type::getBool();
}
- $statements_analyzer->node_data->setType($stmt, Type::getBool());
+ $statements_analyzer->node_data->setType($stmt, $stmt_type);
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
index a57510665e4..70bf0c77412 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php
@@ -314,14 +314,18 @@ public static function analyze(
&& !$context->inside_unset
&& ($stmt_var_type && !$stmt_var_type->hasMixed())
) {
- IssueBuffer::maybeAdd(
+ if (IssueBuffer::accepts(
new PossiblyUndefinedArrayOffset(
'Possibly undefined array key ' . $keyed_array_var_id
. ' on ' . $stmt_var_type->getId(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
$statements_analyzer->getSuppressedIssues(),
- );
+ )) {
+ $stmt_type = $stmt_type->getBuilder()->addType(new TNull())->freeze();
+ }
+ } elseif ($stmt_type->possibly_undefined) {
+ $stmt_type = $stmt_type->getBuilder()->addType(new TNull())->freeze();
}
$stmt_type = $stmt_type->setPossiblyUndefined(false);
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php
index b9001192c98..95d86401724 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php
@@ -269,7 +269,7 @@ public static function analyze(
try {
$new_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$new_class_storage = null;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php
index b690b0af42a..b7766aa2617 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php
@@ -575,11 +575,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_
$var_id = '$_FILES full path';
}
- if (isset(self::$globalCache[$var_id])) {
- return self::$globalCache[$var_id];
- }
-
- return Type::getMixed();
+ return self::$globalCache[$var_id] ?? Type::getMixed();
}
/**
@@ -640,7 +636,7 @@ private static function getGlobalTypeInner(string $var_id, bool $files_full_path
return new Union([$type]);
}
- if (in_array($var_id, array('$_GET', '$_POST', '$_REQUEST'), true)) {
+ if (in_array($var_id, ['$_GET', '$_POST', '$_REQUEST'], true)) {
$array_key = new Union([new TNonEmptyString(), new TInt()]);
$array = new TNonEmptyArray(
[
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php
index 83123189023..f79479cc530 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php
@@ -22,6 +22,7 @@
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type\TaintKind;
+use Symfony\Component\Filesystem\Path;
use function constant;
use function defined;
@@ -95,13 +96,7 @@ public static function analyze(
$include_path = self::resolveIncludePath($path_to_file, dirname($statements_analyzer->getFilePath()));
$path_to_file = $include_path ?: $path_to_file;
- if (DIRECTORY_SEPARATOR === '/') {
- $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR;
- } else {
- $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file);
- }
-
- if ($is_path_relative) {
+ if (Path::isRelative($path_to_file)) {
$path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file;
}
} else {
@@ -209,7 +204,7 @@ public static function analyze(
$context,
$global_context,
);
- } catch (UnpreparedAnalysisException $e) {
+ } catch (UnpreparedAnalysisException) {
if ($config->skip_checks_on_unresolvable_includes) {
$context->check_classes = false;
$context->check_variables = false;
@@ -287,13 +282,7 @@ public static function getPathTo(
string $file_name,
Config $config,
): ?string {
- if (DIRECTORY_SEPARATOR === '/') {
- $is_path_relative = $file_name[0] !== DIRECTORY_SEPARATOR;
- } else {
- $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $file_name);
- }
-
- if ($is_path_relative) {
+ if (Path::isRelative($file_name)) {
$file_name = $config->base_dir . DIRECTORY_SEPARATOR . $file_name;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
index a22924ed21d..cba0730a470 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
@@ -383,7 +383,7 @@ public static function infer(
}
return null;
- } catch (InvalidArgumentException | CircularReferenceException $e) {
+ } catch (InvalidArgumentException | CircularReferenceException) {
return null;
}
}
@@ -511,11 +511,7 @@ public static function infer(
foreach ($array_type->getAtomicTypes() as $array_atomic_type) {
if ($array_atomic_type instanceof TKeyedArray) {
- if (isset($array_atomic_type->properties[$dim_value])) {
- return $array_atomic_type->properties[$dim_value];
- }
-
- return null;
+ return $array_atomic_type->properties[$dim_value] ?? null;
}
}
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php
index 814568e4fdc..9c574e7a0f8 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/TernaryAnalyzer.php
@@ -29,7 +29,6 @@
use function array_intersect_key;
use function array_keys;
use function array_map;
-use function array_merge;
use function array_values;
use function count;
use function in_array;
@@ -66,7 +65,7 @@ public static function analyze(
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
$assigned_in_conditional_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
- } catch (ScopeAnalysisException $e) {
+ } catch (ScopeAnalysisException) {
return false;
}
@@ -156,7 +155,7 @@ static function (Clause $c) use ($mixed_var_ids, $cond_object_id): Clause {
try {
$if_scope->negated_clauses = Algebra::negateFormula($if_clauses);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
try {
$if_scope->negated_clauses = FormulaGenerator::getFormula(
$cond_object_id,
@@ -167,7 +166,7 @@ static function (Clause $c) use ($mixed_var_ids, $cond_object_id): Clause {
$codebase,
false,
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$if_scope->negated_clauses = [];
}
}
@@ -211,10 +210,10 @@ static function (Clause $c) use ($mixed_var_ids, $cond_object_id): Clause {
return false;
}
- $context->cond_referenced_var_ids = array_merge(
- $context->cond_referenced_var_ids,
- $if_context->cond_referenced_var_ids,
- );
+ $context->cond_referenced_var_ids = [
+ ...$context->cond_referenced_var_ids,
+ ...$if_context->cond_referenced_var_ids,
+ ];
}
$t_else_context->clauses = Algebra::simplifyCNF(
@@ -290,16 +289,16 @@ static function (Clause $c) use ($mixed_var_ids, $cond_object_id): Clause {
}
}
- $context->vars_possibly_in_scope = array_merge(
- $context->vars_possibly_in_scope,
- $if_context->vars_possibly_in_scope,
- $t_else_context->vars_possibly_in_scope,
- );
+ $context->vars_possibly_in_scope = [
+ ...$context->vars_possibly_in_scope,
+ ...$if_context->vars_possibly_in_scope,
+ ...$t_else_context->vars_possibly_in_scope,
+ ];
- $context->cond_referenced_var_ids = array_merge(
- $context->cond_referenced_var_ids,
- $t_else_context->cond_referenced_var_ids,
- );
+ $context->cond_referenced_var_ids = [
+ ...$context->cond_referenced_var_ids,
+ ...$t_else_context->cond_referenced_var_ids,
+ ];
$lhs_type = null;
$stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond);
diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
index 7e6f30505bf..e82c928ddbd 100644
--- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
@@ -57,7 +57,6 @@
use Psalm\Plugin\EventHandler\Event\BeforeExpressionAnalysisEvent;
use Psalm\Type;
-use function get_class;
use function in_array;
use function strtolower;
@@ -439,7 +438,7 @@ private static function handleExpression(
IssueBuffer::maybeAdd(
new UnrecognizedExpression(
- 'Psalm does not understand ' . get_class($stmt) . ' for PHP ' .
+ 'Psalm does not understand ' . $stmt::class . ' for PHP ' .
$codebase->getMajorAnalysisPhpVersion() . '.' . $codebase->getMinorAnalysisPhpVersion(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
@@ -474,7 +473,7 @@ private static function analyzeAssignment(
!$from_stmt ? $stmt : null,
);
- if ($assignment_type === false) {
+ if ($assignment_type === null) {
return false;
}
diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php
index 02b411bc3d4..1cdd6a8b8a2 100644
--- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php
@@ -89,7 +89,7 @@ public static function analyze(
}
if ($context->check_variables) {
- $context->vars_in_scope[$var_id] = $comment_type ? $comment_type : Type::getMixed();
+ $context->vars_in_scope[$var_id] = $comment_type ?: Type::getMixed();
$context->vars_possibly_in_scope[$var_id] = true;
$context->assigned_var_ids[$var_id] = (int) $stmt->getAttribute('startFilePos');
$statements_analyzer->byref_uses[$var_id] = true;
diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
index 6187083430d..0ccadac662e 100644
--- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
@@ -76,14 +76,13 @@
use function count;
use function explode;
use function fwrite;
-use function get_class;
use function in_array;
use function is_string;
use function preg_split;
use function reset;
use function round;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function strrpos;
use function strtolower;
use function substr;
@@ -97,11 +96,9 @@
*/
final class StatementsAnalyzer extends SourceAnalyzer
{
- protected SourceAnalyzer $source;
+ private readonly FileAnalyzer $file_analyzer;
- protected FileAnalyzer $file_analyzer;
-
- protected Codebase $codebase;
+ private readonly Codebase $codebase;
/**
* @var array
@@ -139,8 +136,6 @@ final class StatementsAnalyzer extends SourceAnalyzer
private ?string $fake_this_class = null;
- public NodeDataProvider $node_data;
-
public ?DataFlowGraph $data_flow_graph = null;
/**
@@ -153,12 +148,10 @@ final class StatementsAnalyzer extends SourceAnalyzer
*/
public array $foreach_var_locations = [];
- public function __construct(SourceAnalyzer $source, NodeDataProvider $node_data)
+ public function __construct(protected SourceAnalyzer $source, public NodeDataProvider $node_data)
{
- $this->source = $source;
$this->file_analyzer = $source->getFileAnalyzer();
$this->codebase = $source->getCodebase();
- $this->node_data = $node_data;
if ($this->codebase->taint_flow_graph) {
$this->data_flow_graph = new TaintFlowGraph();
@@ -271,7 +264,7 @@ private function hoistFunctions(array $stmts, Context $context): void
try {
$function_analyzer = new FunctionAnalyzer($stmt, $this->source);
$this->function_analyzers[$fq_function_name] = $function_analyzer;
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
// do nothing
}
}
@@ -587,7 +580,7 @@ private static function analyzeStatement(
);
$class_analyzer->analyze(null, $global_context);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
// disregard this exception, we'll likely see it elsewhere in the form
// of an issue
}
@@ -606,7 +599,7 @@ private static function analyzeStatement(
} else {
if (IssueBuffer::accepts(
new UnrecognizedStatement(
- 'Psalm does not understand ' . get_class($stmt),
+ 'Psalm does not understand ' . $stmt::class,
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
@@ -867,7 +860,7 @@ public function checkUnreferencedVars(array $stmts, Context $context): void
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
- if (strpos($var_id, '$_') === 0) {
+ if (str_starts_with($var_id, '$_')) {
continue;
}
@@ -1098,7 +1091,7 @@ public function getUncaughtThrows(Context $context): array
$is_expected = true;
break;
}
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$is_expected = true;
break;
}
diff --git a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php
index 6f8d4b86c3a..10de0e5eff5 100644
--- a/src/Psalm/Internal/Analyzer/TraitAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/TraitAnalyzer.php
@@ -16,13 +16,11 @@
*/
final class TraitAnalyzer extends ClassLikeAnalyzer
{
- private Aliases $aliases;
-
public function __construct(
Trait_ $class,
SourceAnalyzer $source,
string $fq_class_name,
- Aliases $aliases,
+ private Aliases $aliases,
) {
$this->source = $source;
$this->file_analyzer = $source->getFileAnalyzer();
@@ -31,7 +29,6 @@ public function __construct(
$this->fq_class_name = $fq_class_name;
$codebase = $source->getCodebase();
$this->storage = $codebase->classlike_storage_provider->get($fq_class_name);
- $this->aliases = $aliases;
}
/** @psalm-mutation-free */
diff --git a/src/Psalm/Internal/Cache.php b/src/Psalm/Internal/Cache.php
index 27a6a84c260..86cf1ebf9bd 100644
--- a/src/Psalm/Internal/Cache.php
+++ b/src/Psalm/Internal/Cache.php
@@ -27,13 +27,11 @@
*/
final class Cache
{
- private Config $config;
-
public bool $use_igbinary;
- public function __construct(Config $config)
- {
- $this->config = $config;
+ public function __construct(
+ private readonly Config $config,
+ ) {
$this->use_igbinary = $config->use_igbinary;
}
diff --git a/src/Psalm/Internal/Clause.php b/src/Psalm/Internal/Clause.php
index 1482bbe736b..31c8e2804b0 100644
--- a/src/Psalm/Internal/Clause.php
+++ b/src/Psalm/Internal/Clause.php
@@ -11,6 +11,7 @@
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
+use Stringable;
use function array_diff;
use function array_keys;
@@ -23,18 +24,14 @@
use function serialize;
use function substr;
-use const PHP_VERSION_ID;
-
/**
* @internal
* @psalm-immutable
*/
-final class Clause
+final class Clause implements Stringable
{
use ImmutableNonCloneableTrait;
- public int $creating_conditional_id;
-
public int $creating_object_id;
/**
@@ -74,11 +71,6 @@ final class Clause
public bool $reconcilable;
- public bool $generated = false;
-
- /** @var array */
- public array $redefined_vars = [];
-
public string $hash;
/**
@@ -87,12 +79,12 @@ final class Clause
*/
public function __construct(
array $possibilities,
- int $creating_conditional_id,
+ public int $creating_conditional_id,
int $creating_object_id,
bool $wedge = false,
bool $reconcilable = true,
- bool $generated = false,
- array $redefined_vars = [],
+ public bool $generated = false,
+ public array $redefined_vars = [],
) {
if ($wedge || !$reconcilable) {
$this->hash = ($wedge ? 'w' : '') . $creating_object_id;
@@ -110,15 +102,12 @@ public function __construct(
/** @psalm-suppress ImpureFunctionCall */
$data = serialize($possibility_strings);
- $this->hash = PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data);
+ $this->hash = hash('xxh128', $data);
}
$this->possibilities = $possibilities;
$this->wedge = $wedge;
$this->reconcilable = $reconcilable;
- $this->generated = $generated;
- $this->redefined_vars = $redefined_vars;
- $this->creating_conditional_id = $creating_conditional_id;
$this->creating_object_id = $creating_object_id;
}
diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php
index 837e23c82b9..46550f8286c 100644
--- a/src/Psalm/Internal/Cli/LanguageServer.php
+++ b/src/Psalm/Internal/Cli/LanguageServer.php
@@ -35,8 +35,9 @@
use function preg_replace;
use function realpath;
use function setlocale;
+use function str_contains;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function strtolower;
use function substr;
@@ -111,7 +112,7 @@ public static function run(array $argv): void
array_map(
static function (string $arg) use ($valid_long_options): void {
- if (strpos($arg, '--') === 0 && $arg !== '--') {
+ if (str_starts_with($arg, '--') && $arg !== '--') {
$arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1);
if (!in_array($arg_name, $valid_long_options, true)
@@ -317,11 +318,9 @@ static function (string $arg) use ($valid_long_options): void {
$path_to_config = CliUtils::getPathToConfig($options);
- if (isset($options['tcp'])) {
- if (!is_string($options['tcp'])) {
- fwrite(STDERR, 'tcp url should be a string' . PHP_EOL);
- exit(1);
- }
+ if (isset($options['tcp']) && !is_string($options['tcp'])) {
+ fwrite(STDERR, 'tcp url should be a string' . PHP_EOL);
+ exit(1);
}
$config = CliUtils::initializeConfig(
@@ -437,7 +436,7 @@ private static function createPathMapper(array $options, string $server_start_di
}
if (is_string($map_folder)) {
- if (strpos($map_folder, ':') === false) {
+ if (!str_contains($map_folder, ':')) {
fwrite(
STDERR,
'invalid format for --map-folder option' . PHP_EOL,
diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php
index bcce5e34e60..8ca086c47fd 100644
--- a/src/Psalm/Internal/Cli/Psalm.php
+++ b/src/Psalm/Internal/Cli/Psalm.php
@@ -70,8 +70,8 @@
use function realpath;
use function setlocale;
use function str_repeat;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function substr;
use const DIRECTORY_SEPARATOR;
@@ -416,7 +416,7 @@ private static function initOutputFormat(array $options): string
private static function findDefaultOutputFormat(): string
{
$emulator = getenv('TERMINAL_EMULATOR');
- if (is_string($emulator) && substr($emulator, 0, 9) === 'JetBrains') {
+ if (is_string($emulator) && str_starts_with($emulator, 'JetBrains')) {
return Report::TYPE_PHP_STORM;
}
@@ -448,7 +448,7 @@ private static function validateCliArguments(array $args): void
{
array_map(
static function (string $arg): void {
- if (strpos($arg, '--') === 0 && $arg !== '--') {
+ if (str_starts_with($arg, '--') && $arg !== '--') {
$arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1);
if (!in_array($arg_name, self::LONG_OPTIONS)
@@ -462,7 +462,7 @@ static function (string $arg): void {
);
exit(1);
}
- } elseif (strpos($arg, '-') === 0 && $arg !== '-' && $arg !== '--') {
+ } elseif (str_starts_with($arg, '-') && $arg !== '-' && $arg !== '--') {
$arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 1));
if (!in_array($arg_name, self::SHORT_OPTIONS)
@@ -499,9 +499,9 @@ private static function generateConfig(string $current_dir, array &$args): void
&& $arg !== '--debug'
&& $arg !== '--debug-by-line'
&& $arg !== '--debug-emitted-issues'
- && strpos($arg, '--disable-extension=') !== 0
- && strpos($arg, '--root=') !== 0
- && strpos($arg, '--r=') !== 0
+ && !str_starts_with($arg, '--disable-extension=')
+ && !str_starts_with($arg, '--root=')
+ && !str_starts_with($arg, '--r=')
));
$init_level = null;
@@ -650,7 +650,7 @@ private static function generateBaseline(
new FileProvider,
$options['set-baseline'],
);
- } catch (ConfigException $e) {
+ } catch (ConfigException) {
$issue_baseline = [];
}
@@ -1094,7 +1094,7 @@ private static function storeFlowGraph(array $options, ProjectAnalyzer $project_
}
/** @return false|'always'|'auto' */
- private static function shouldFindUnusedCode(array $options, Config $config): false|string
+ private static function shouldFindUnusedCode(array $options, Config $config): bool|string
{
$find_unused_code = false;
if (isset($options['find-dead-code'])) {
diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php
index 242a27a967b..9e44e0a069f 100644
--- a/src/Psalm/Internal/Cli/Psalter.php
+++ b/src/Psalm/Internal/Cli/Psalter.php
@@ -56,7 +56,8 @@
use function preg_replace;
use function preg_split;
use function realpath;
-use function strpos;
+use function str_contains;
+use function str_starts_with;
use function strtolower;
use function substr;
use function trim;
@@ -385,7 +386,7 @@ public static function run(array $argv): void
foreach ($keyed_issues as $issue_name => $_) {
// MissingParamType requires the scanning of all files to inform possible params
- if (strpos($issue_name, 'Unused') !== false
+ if (str_contains($issue_name, 'Unused')
|| $issue_name === 'MissingParamType'
|| $issue_name === 'UnnecessaryVarAnnotation'
|| $issue_name === 'all'
@@ -453,7 +454,7 @@ private static function validateCliArguments(array $args): void
{
array_map(
static function (string $arg): void {
- if (strpos($arg, '--') === 0 && $arg !== '--') {
+ if (str_starts_with($arg, '--') && $arg !== '--') {
$arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1);
if ($arg_name === 'alter') {
@@ -534,7 +535,7 @@ static function (string $line): bool {
// currently we don’t match wildcard files or files that could appear anywhere
// in the repo
- return $line && $line[0] === '/' && strpos($line, '*') === false;
+ return $line && $line[0] === '/' && !str_contains($line, '*');
},
),
);
diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php
index b00848ddcb6..fecbb416057 100644
--- a/src/Psalm/Internal/Cli/Refactor.php
+++ b/src/Psalm/Internal/Cli/Refactor.php
@@ -46,6 +46,7 @@
use function preg_replace;
use function preg_split;
use function realpath;
+use function str_starts_with;
use function strpos;
use function substr;
@@ -93,7 +94,7 @@ public static function run(array $argv): void
array_map(
static function (string $arg) use ($valid_long_options): void {
- if (strpos($arg, '--') === 0 && $arg !== '--') {
+ if (str_starts_with($arg, '--') && $arg !== '--') {
$arg_name = (string) preg_replace('/=.*$/', '', substr($arg, 2), 1);
if ($arg_name === 'refactor') {
diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php
index b1b8cc8b4c7..28d0e215632 100644
--- a/src/Psalm/Internal/CliUtils.php
+++ b/src/Psalm/Internal/CliUtils.php
@@ -41,6 +41,7 @@
use function preg_replace;
use function preg_split;
use function realpath;
+use function str_starts_with;
use function stream_get_meta_data;
use function stream_set_blocking;
use function strlen;
@@ -236,7 +237,7 @@ public static function getArguments(): array
}
if ($input_path[0] === '-' && strlen($input_path) === 2) {
- if ($input_path[1] === 'c' || $input_path[1] === 'f') {
+ if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') {
++$i;
}
continue;
@@ -275,7 +276,7 @@ public static function getPathsToCheck(string|array|false|null $f_paths): ?array
$input_path = $input_paths[$i];
if ($input_path[0] === '-' && strlen($input_path) === 2) {
- if ($input_path[1] === 'c' || $input_path[1] === 'f') {
+ if ($input_path[1] === 'c' || $input_path[1] === 'f' || $input_path[1] === 'r') {
++$i;
}
continue;
@@ -285,12 +286,13 @@ public static function getPathsToCheck(string|array|false|null $f_paths): ?array
continue;
}
- if (strpos($input_path, '--') === 0 && strlen($input_path) > 2) {
+ if (str_starts_with($input_path, '--') && strlen($input_path) > 2) {
// ignore --config psalm.xml
// ignore common phpunit args that accept a class instead of a path, as this can cause issues on Windows
$ignored_arguments = array(
'config',
'printer',
+ 'root',
);
if (in_array(substr($input_path, 2), $ignored_arguments, true)) {
diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php
index 461aae3e153..80cdba04ddc 100644
--- a/src/Psalm/Internal/Codebase/Analyzer.php
+++ b/src/Psalm/Internal/Codebase/Analyzer.php
@@ -4,7 +4,6 @@
namespace Psalm\Internal\Codebase;
-use Closure;
use InvalidArgumentException;
use PhpParser;
use Psalm\CodeLocation;
@@ -41,6 +40,8 @@
use function number_format;
use function pathinfo;
use function preg_replace;
+use function str_ends_with;
+use function str_starts_with;
use function strlen;
use function strpos;
use function strtolower;
@@ -100,14 +101,6 @@
*/
final class Analyzer
{
- private Config $config;
-
- private FileProvider $file_provider;
-
- private FileStorageProvider $file_storage_provider;
-
- private Progress $progress;
-
/**
* Used to store counts of mixed vs non-mixed variables
*
@@ -189,15 +182,11 @@ final class Analyzer
public array $mutable_classes = [];
public function __construct(
- Config $config,
- FileProvider $file_provider,
- FileStorageProvider $file_storage_provider,
- Progress $progress,
+ private readonly Config $config,
+ private readonly FileProvider $file_provider,
+ private readonly FileStorageProvider $file_storage_provider,
+ private readonly Progress $progress,
) {
- $this->config = $config;
- $this->file_provider = $file_provider;
- $this->file_storage_provider = $file_storage_provider;
- $this->progress = $progress;
}
/**
@@ -269,7 +258,7 @@ public function analyzeFiles(
$this->files_to_analyze = array_filter(
$this->files_to_analyze,
- [$this->file_provider, 'fileExists'],
+ $this->file_provider->fileExists(...),
);
$this->doAnalysis($project_analyzer, $pool_size);
@@ -332,9 +321,9 @@ private function doAnalysis(ProjectAnalyzer $project_analyzer, int $pool_size):
$codebase = $project_analyzer->getCodebase();
- $analysis_worker = Closure::fromCallable([$this, 'analysisWorker']);
+ $analysis_worker = $this->analysisWorker(...);
- $task_done_closure = Closure::fromCallable([$this, 'taskDoneClosure']);
+ $task_done_closure = $this->taskDoneClosure(...);
if ($pool_size > 1 && count($this->files_to_analyze) > $pool_size) {
$shuffle_count = $pool_size + 1;
@@ -396,7 +385,7 @@ static function (): void {
$file_reference_provider->setMethodParamUses([]);
},
$analysis_worker,
- Closure::fromCallable([$this, 'getWorkerData']),
+ $this->getWorkerData(...),
$task_done_closure,
);
@@ -607,7 +596,7 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void
[$base_class, $trait] = explode('&', $changed_member);
foreach ($all_referencing_methods as $member_id => $_) {
- if (strpos($member_id, $base_class . '::') !== 0) {
+ if (!str_starts_with($member_id, $base_class . '::')) {
continue;
}
@@ -629,7 +618,7 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void
// also check for things that might invalidate constructor property initialisation
if (isset($all_referencing_methods[$unchanged_signature_member_id])) {
foreach ($all_referencing_methods[$unchanged_signature_member_id] as $referencing_method_id => $_) {
- if (substr($referencing_method_id, -13) === '::__construct') {
+ if (str_ends_with($referencing_method_id, '::__construct')) {
$referencing_base_classlike = explode('::', $referencing_method_id)[0];
$unchanged_signature_classlike = explode('::', $unchanged_signature_member_id)[0];
@@ -640,7 +629,7 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void
$referencing_storage = $codebase->classlike_storage_provider->get(
$referencing_base_classlike,
);
- } catch (InvalidArgumentException $_) {
+ } catch (InvalidArgumentException) {
// Workaround for #3671
$newly_invalidated_methods[$referencing_method_id] = true;
$referencing_storage = null;
diff --git a/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php b/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php
index 397633c5e19..594768af845 100644
--- a/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php
+++ b/src/Psalm/Internal/Codebase/AssertionsFromInheritanceResolver.php
@@ -10,7 +10,6 @@
use Psalm\Storage\Possibilities;
use function array_filter;
-use function array_merge;
use function array_values;
use function strtolower;
@@ -19,12 +18,9 @@
*/
final class AssertionsFromInheritanceResolver
{
- private Codebase $codebase;
-
public function __construct(
- Codebase $codebase,
+ private readonly Codebase $codebase,
) {
- $this->codebase = $codebase;
}
/**
@@ -37,10 +33,10 @@ public function resolve(
$method_name_lc = strtolower($method_storage->cased_name ?? '');
$assertions = $method_storage->assertions;
- $inherited_classes_and_interfaces = array_values(array_filter(array_merge(
- $called_class->parent_classes,
- $called_class->class_implements,
- ), fn(string $classOrInterface) => $this->codebase->classOrInterfaceOrEnumExists($classOrInterface)));
+ $inherited_classes_and_interfaces = array_values(array_filter([
+ ...$called_class->parent_classes,
+ ...$called_class->class_implements,
+ ], fn(string $classOrInterface) => $this->codebase->classOrInterfaceOrEnumExists($classOrInterface)));
foreach ($inherited_classes_and_interfaces as $potential_assertion_providing_class) {
$potential_assertion_providing_classlike_storage = $this->codebase->classlike_storage_provider->get(
diff --git a/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php
index 0e6ef6f8a33..edfe9717c29 100644
--- a/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php
+++ b/src/Psalm/Internal/Codebase/ClassConstantByWildcardResolver.php
@@ -15,13 +15,12 @@
*/
final class ClassConstantByWildcardResolver
{
- private StorageByPatternResolver $resolver;
- private Codebase $codebase;
+ private readonly StorageByPatternResolver $resolver;
- public function __construct(Codebase $codebase)
- {
+ public function __construct(
+ private readonly Codebase $codebase,
+ ) {
$this->resolver = new StorageByPatternResolver();
- $this->codebase = $codebase;
}
/**
diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php
index e7af2f4a19b..b829e026fac 100644
--- a/src/Psalm/Internal/Codebase/ClassLikes.php
+++ b/src/Psalm/Internal/Codebase/ClassLikes.php
@@ -75,10 +75,6 @@
*/
final class ClassLikes
{
- private ClassLikeStorageProvider $classlike_storage_provider;
-
- public FileReferenceProvider $file_reference_provider;
-
/**
* @var array
*/
@@ -143,25 +139,13 @@ final class ClassLikes
public bool $collect_locations = false;
- private StatementsProvider $statements_provider;
-
- private Config $config;
-
- private Scanner $scanner;
-
public function __construct(
- Config $config,
- ClassLikeStorageProvider $storage_provider,
- FileReferenceProvider $file_reference_provider,
- StatementsProvider $statements_provider,
- Scanner $scanner,
+ private readonly Config $config,
+ private readonly ClassLikeStorageProvider $classlike_storage_provider,
+ public FileReferenceProvider $file_reference_provider,
+ private readonly StatementsProvider $statements_provider,
+ private readonly Scanner $scanner,
) {
- $this->config = $config;
- $this->classlike_storage_provider = $storage_provider;
- $this->file_reference_provider = $file_reference_provider;
- $this->statements_provider = $statements_provider;
- $this->scanner = $scanner;
-
$this->collectPredefinedClassLikes();
}
@@ -848,7 +832,7 @@ public function consolidateAnalyzedData(Methods $methods, ?Progress $progress, b
foreach ($this->existing_classlikes_lc as $fq_class_name_lc => $_) {
try {
$classlike_storage = $this->classlike_storage_provider->get($fq_class_name_lc);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -955,7 +939,7 @@ public function moveMethods(Methods $methods, ?Progress $progress = null): void
$source_method_storage = $methods->getStorage(
new MethodIdentifier(...$source_parts),
);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -963,7 +947,7 @@ public function moveMethods(Methods $methods, ?Progress $progress = null): void
try {
$classlike_storage = $this->classlike_storage_provider->get($destination_fq_class_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -1032,7 +1016,7 @@ public function moveProperties(Properties $properties, ?Progress $progress = nul
foreach ($codebase->properties_to_move as $source => $destination) {
try {
$source_property_storage = $properties->getStorage($source);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -1692,7 +1676,7 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
try {
$declaring_classlike_storage = $this->classlike_storage_provider->get($declaring_fq_classlike_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -1792,7 +1776,7 @@ private function checkMethodReferences(ClassLikeStorage $classlike_storage, Meth
foreach ($classlike_storage->class_implements as $fq_interface_name_lc => $_) {
try {
$interface_storage = $this->classlike_storage_provider->get($fq_interface_name_lc);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -1950,7 +1934,7 @@ private function checkMethodParamReferences(ClassLikeStorage $classlike_storage)
try {
$declaring_classlike_storage = $this->classlike_storage_provider->get($declaring_fq_classlike_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -2019,7 +2003,7 @@ private function findPossibleMethodParamTypes(ClassLikeStorage $classlike_storag
try {
$declaring_classlike_storage = $this->classlike_storage_provider->get($declaring_fq_classlike_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -2385,7 +2369,7 @@ public function getStorageFor(string $fq_class_name): ?ClassLikeStorage
try {
return $this->classlike_storage_provider->get($fq_class_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return null;
}
}
diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
index 2d826dc0891..d088c06eac6 100644
--- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
+++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php
@@ -218,8 +218,8 @@ public static function resolve(
return new TArray([Type::getArrayKey(), Type::getMixed()]);
}
- foreach ($spread_array->properties as $spread_array_type) {
- $properties[$auto_key++] = $spread_array_type;
+ foreach ($spread_array->properties as $k => $spread_array_type) {
+ $properties[is_string($k) ? $k : $auto_key++] = $spread_array_type;
}
continue;
}
diff --git a/src/Psalm/Internal/Codebase/DataFlowGraph.php b/src/Psalm/Internal/Codebase/DataFlowGraph.php
index e07a73d53f5..f94d6f74a83 100644
--- a/src/Psalm/Internal/Codebase/DataFlowGraph.php
+++ b/src/Psalm/Internal/Codebase/DataFlowGraph.php
@@ -12,8 +12,8 @@
use function array_reverse;
use function array_sum;
use function count;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function substr;
/**
@@ -71,7 +71,7 @@ protected static function shouldIgnoreFetch(
// arraykey-fetch requires a matching arraykey-assignment at the same level
// otherwise the tainting is not valid
- if (strpos($path_type, $expression_type . '-fetch-') === 0
+ if (str_starts_with($path_type, $expression_type . '-fetch-')
|| ($path_type === 'arraykey-fetch' && $expression_type === 'arrayvalue')
) {
$fetch_nesting = 0;
@@ -87,11 +87,11 @@ protected static function shouldIgnoreFetch(
$fetch_nesting--;
}
- if (strpos($previous_path_type, $expression_type . '-fetch') === 0) {
+ if (str_starts_with($previous_path_type, $expression_type . '-fetch')) {
$fetch_nesting++;
}
- if (strpos($previous_path_type, $expression_type . '-assignment-') === 0) {
+ if (str_starts_with($previous_path_type, $expression_type . '-assignment-')) {
if ($fetch_nesting > 0) {
$fetch_nesting--;
continue;
diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php
index 2cb3ffefd88..07f5025cc9c 100644
--- a/src/Psalm/Internal/Codebase/Functions.php
+++ b/src/Psalm/Internal/Codebase/Functions.php
@@ -30,7 +30,9 @@
use function in_array;
use function is_bool;
use function rtrim;
-use function strpos;
+use function str_contains;
+use function str_ends_with;
+use function str_starts_with;
use function strtolower;
use function substr;
@@ -39,8 +41,6 @@
*/
final class Functions
{
- private FileStorageProvider $file_storage_provider;
-
/**
* @var array
*/
@@ -54,12 +54,10 @@ final class Functions
public DynamicFunctionStorageProvider $dynamic_storage_provider;
- private Reflection $reflection;
-
- public function __construct(FileStorageProvider $storage_provider, Reflection $reflection)
- {
- $this->file_storage_provider = $storage_provider;
- $this->reflection = $reflection;
+ public function __construct(
+ private readonly FileStorageProvider $file_storage_provider,
+ private readonly Reflection $reflection,
+ ) {
$this->return_type_provider = new FunctionReturnTypeProvider();
$this->existence_provider = new FunctionExistenceProvider();
$this->params_provider = new FunctionParamsProvider();
@@ -238,7 +236,7 @@ public function getFullyQualifiedFunctionNameFromString(string $function_name, S
$imported_function_namespaces = $aliases->functions;
$imported_namespaces = $aliases->uses;
- if (strpos($function_name, '\\') !== false) {
+ if (str_contains($function_name, '\\')) {
$function_name_parts = explode('\\', $function_name);
$first_namespace = array_shift($function_name_parts);
$first_namespace_lcase = strtolower($first_namespace);
@@ -311,10 +309,10 @@ public function getMatchingFunctionNames(
if ($current_namespace_aliases) {
foreach ($current_namespace_aliases->functions as $alias_name => $function_name) {
- if (strpos($alias_name, $stub) === 0) {
+ if (str_starts_with($alias_name, $stub)) {
try {
$match_function_patterns[] = $function_name;
- } catch (Exception $e) {
+ } catch (Exception) {
}
}
}
@@ -335,8 +333,8 @@ public function getMatchingFunctionNames(
foreach ($match_function_patterns as $pattern) {
$pattern_lc = strtolower($pattern);
- if (substr($pattern, -1, 1) === '*') {
- if (strpos($function_name, rtrim($pattern_lc, '*')) !== 0) {
+ if (str_ends_with($pattern, '*')) {
+ if (!str_starts_with($function_name, rtrim($pattern_lc, '*'))) {
continue;
}
} elseif ($function_name !== $pattern) {
@@ -406,11 +404,11 @@ public function isCallMapFunctionPure(
}
}
- if (strpos($function_id, 'image') === 0) {
+ if (str_starts_with($function_id, 'image')) {
return false;
}
- if (strpos($function_id, 'readline') === 0) {
+ if (str_starts_with($function_id, 'readline')) {
return false;
}
@@ -440,7 +438,7 @@ public function isCallMapFunctionPure(
try {
return $codebase->methods->getStorage($count_method_id)->mutation_free;
- } catch (Exception $e) {
+ } catch (Exception) {
// do nothing
}
}
@@ -455,7 +453,7 @@ public function isCallMapFunctionPure(
null,
);
- if (!$function_callable->params
+ if (!isset($function_callable->params)
|| ($args !== null && count($args) === 0)
|| ($function_callable->return_type && $function_callable->return_type->isVoid())
) {
diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php
index 7bf56f42fbc..70a435b814d 100644
--- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php
+++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php
@@ -24,8 +24,9 @@
use function count;
use function dirname;
use function file_exists;
+use function str_ends_with;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function strtolower;
use function substr;
use function version_compare;
@@ -271,12 +272,12 @@ public static function getCallablesFromCallMap(string $function_id): ?array
$by_reference = true;
}
- if (substr($arg_name, -1) === '=') {
+ if (str_ends_with($arg_name, '=')) {
$arg_name = substr($arg_name, 0, -1);
$optional = true;
}
- if (strpos($arg_name, '...') === 0) {
+ if (str_starts_with($arg_name, '...')) {
$arg_name = substr($arg_name, 3);
$variadic = true;
}
diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php
index 34bc56181a1..c93735ab0ae 100644
--- a/src/Psalm/Internal/Codebase/Methods.php
+++ b/src/Psalm/Internal/Codebase/Methods.php
@@ -57,14 +57,8 @@
*/
final class Methods
{
- private ClassLikeStorageProvider $classlike_storage_provider;
-
public bool $collect_locations = false;
- public FileReferenceProvider $file_reference_provider;
-
- private ClassLikes $classlikes;
-
public MethodReturnTypeProvider $return_type_provider;
public MethodParamsProvider $params_provider;
@@ -74,13 +68,10 @@ final class Methods
public MethodVisibilityProvider $visibility_provider;
public function __construct(
- ClassLikeStorageProvider $storage_provider,
- FileReferenceProvider $file_reference_provider,
- ClassLikes $classlikes,
+ private readonly ClassLikeStorageProvider $classlike_storage_provider,
+ public FileReferenceProvider $file_reference_provider,
+ private readonly ClassLikes $classlikes,
) {
- $this->classlike_storage_provider = $storage_provider;
- $this->file_reference_provider = $file_reference_provider;
- $this->classlikes = $classlikes;
$this->return_type_provider = new MethodReturnTypeProvider();
$this->existence_provider = new MethodExistenceProvider();
$this->visibility_provider = new MethodVisibilityProvider();
@@ -125,7 +116,7 @@ public function methodExists(
try {
$class_storage = $this->classlike_storage_provider->get($fq_class_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return false;
}
@@ -817,7 +808,7 @@ public function getMethodReturnType(
$overridden_storage_return_type,
$source_analyzer->getCodebase(),
);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
// TODO: fix
}
} else {
@@ -1167,7 +1158,7 @@ public function hasStorage(MethodIdentifier $method_id): bool
{
try {
$class_storage = $this->classlike_storage_provider->get($method_id->fq_class_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return false;
}
diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php
index 65078689a12..4093c263db5 100644
--- a/src/Psalm/Internal/Codebase/Populator.php
+++ b/src/Psalm/Internal/Codebase/Populator.php
@@ -46,33 +46,18 @@
*/
final class Populator
{
- private ClassLikeStorageProvider $classlike_storage_provider;
-
- private FileStorageProvider $file_storage_provider;
-
/**
* @var array>
*/
private array $invalid_class_storages = [];
- private Progress $progress;
-
- private ClassLikes $classlikes;
-
- private FileReferenceProvider $file_reference_provider;
-
public function __construct(
- ClassLikeStorageProvider $classlike_storage_provider,
- FileStorageProvider $file_storage_provider,
- ClassLikes $classlikes,
- FileReferenceProvider $file_reference_provider,
- Progress $progress,
+ private ClassLikeStorageProvider $classlike_storage_provider,
+ private readonly FileStorageProvider $file_storage_provider,
+ private readonly ClassLikes $classlikes,
+ private readonly FileReferenceProvider $file_reference_provider,
+ private readonly Progress $progress,
) {
- $this->classlike_storage_provider = $classlike_storage_provider;
- $this->file_storage_provider = $file_storage_provider;
- $this->classlikes = $classlikes;
- $this->progress = $progress;
- $this->file_reference_provider = $file_reference_provider;
}
public function populateCodebase(): void
@@ -97,7 +82,7 @@ public function populateCodebase(): void
foreach ($class_storage->dependent_classlikes as $dependent_classlike_lc => $_) {
try {
$dependee_storage = $this->classlike_storage_provider->get($dependent_classlike_lc);
- } catch (InvalidArgumentException $exception) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -286,7 +271,7 @@ private function populateOverriddenMethods(
),
);
$implemented_interface_storage = $storage_provider->get($implemented_interface);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -439,7 +424,7 @@ private function populateDataFromTrait(
),
);
$trait_storage = $storage_provider->get($used_trait_lc);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return;
}
@@ -505,7 +490,7 @@ private function populateDataFromParentClass(
try {
$parent_storage = $storage_provider->get($parent_storage_class);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n");
$storage->invalid_dependencies[$parent_storage_class] = true;
@@ -517,32 +502,26 @@ private function populateDataFromParentClass(
$this->populateClassLikeStorage($parent_storage, $dependent_classlikes);
- $storage->parent_classes = array_merge($storage->parent_classes, $parent_storage->parent_classes);
+ $storage->parent_classes = [...$storage->parent_classes, ...$parent_storage->parent_classes];
self::extendTemplateParams($storage, $parent_storage, true);
$this->inheritMethodsFromParent($storage, $parent_storage);
$this->inheritPropertiesFromParent($storage, $parent_storage);
- $storage->class_implements = array_merge($storage->class_implements, $parent_storage->class_implements);
- $storage->invalid_dependencies = array_merge(
- $storage->invalid_dependencies,
- $parent_storage->invalid_dependencies,
- );
+ $storage->class_implements = [...$storage->class_implements, ...$parent_storage->class_implements];
+ $storage->invalid_dependencies = [...$storage->invalid_dependencies, ...$parent_storage->invalid_dependencies];
if ($parent_storage->has_visitor_issues) {
$storage->has_visitor_issues = true;
}
- $storage->constants = array_merge(
- array_filter(
- $parent_storage->constants,
- static fn(ClassConstantStorage $constant): bool
- => $constant->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
- || $constant->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED,
- ),
- $storage->constants,
- );
+ $storage->constants = [...array_filter(
+ $parent_storage->constants,
+ static fn(ClassConstantStorage $constant): bool
+ => $constant->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC
+ || $constant->visibility === ClassLikeAnalyzer::VISIBILITY_PROTECTED,
+ ), ...$storage->constants];
if ($parent_storage->preserve_constructor_signature) {
$storage->preserve_constructor_signature = true;
@@ -592,19 +571,16 @@ private function populateInterfaceData(
$this->populateClassLikeStorage($interface_storage, $dependent_classlikes);
// copy over any constants
- $storage->constants = array_merge(
- array_filter(
- $interface_storage->constants,
- static fn(ClassConstantStorage $constant): bool
- => $constant->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC,
- ),
- $storage->constants,
- );
+ $storage->constants = [...array_filter(
+ $interface_storage->constants,
+ static fn(ClassConstantStorage $constant): bool
+ => $constant->visibility === ClassLikeAnalyzer::VISIBILITY_PUBLIC,
+ ), ...$storage->constants];
- $storage->invalid_dependencies = array_merge(
- $storage->invalid_dependencies,
- $interface_storage->invalid_dependencies,
- );
+ $storage->invalid_dependencies = [
+ ...$storage->invalid_dependencies,
+ ...$interface_storage->invalid_dependencies,
+ ];
self::extendTemplateParams($storage, $interface_storage, false);
@@ -618,7 +594,7 @@ private function populateInterfaceData(
),
);
$new_parent_interface_storage = $storage_provider->get($new_parent);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -698,7 +674,7 @@ private function populateInterfaceDataFromParentInterface(
),
);
$parent_interface_storage = $storage_provider->get($parent_interface_lc);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n");
$storage->invalid_dependencies[$parent_interface_lc] = true;
@@ -712,10 +688,7 @@ private function populateInterfaceDataFromParentInterface(
$storage->pseudo_methods += $parent_interface_storage->pseudo_methods;
$storage->declaring_pseudo_method_ids += $parent_interface_storage->declaring_pseudo_method_ids;
- $storage->parent_interfaces = array_merge(
- $parent_interface_storage->parent_interfaces,
- $storage->parent_interfaces,
- );
+ $storage->parent_interfaces = [...$parent_interface_storage->parent_interfaces, ...$storage->parent_interfaces];
if (isset($storage->parent_interfaces[strtolower(UnitEnum::class)])) {
$storage->declaring_property_ids['name'] = $storage->name;
@@ -744,7 +717,7 @@ private function populateDataFromImplementedInterface(
),
);
$implemented_interface_storage = $storage_provider->get($implemented_interface_lc);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n");
$storage->invalid_dependencies[$implemented_interface_lc] = true;
@@ -758,10 +731,10 @@ private function populateDataFromImplementedInterface(
$dependent_classlikes,
);
- $storage->class_implements = array_merge(
- $storage->class_implements,
- $implemented_interface_storage->parent_interfaces,
- );
+ $storage->class_implements = [
+ ...$storage->class_implements,
+ ...$implemented_interface_storage->parent_interfaces,
+ ];
}
/**
@@ -786,7 +759,7 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
foreach ($storage->required_file_paths as $included_file_path => $_) {
try {
$included_file_storage = $this->file_storage_provider->get($included_file_path);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -798,25 +771,25 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
foreach ($all_required_file_paths as $included_file_path => $_) {
try {
$included_file_storage = $this->file_storage_provider->get($included_file_path);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
- $storage->declaring_function_ids = array_merge(
- $included_file_storage->declaring_function_ids,
- $storage->declaring_function_ids,
- );
+ $storage->declaring_function_ids = [
+ ...$included_file_storage->declaring_function_ids,
+ ...$storage->declaring_function_ids,
+ ];
- $storage->declaring_constants = array_merge(
- $included_file_storage->declaring_constants,
- $storage->declaring_constants,
- );
+ $storage->declaring_constants = [
+ ...$included_file_storage->declaring_constants,
+ ...$storage->declaring_constants,
+ ];
}
foreach ($storage->referenced_classlikes as $fq_class_name) {
try {
$classlike_storage = $this->classlike_storage_provider->get($fq_class_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -826,14 +799,14 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
try {
$included_file_storage = $this->file_storage_provider->get($classlike_storage->location->file_path);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
foreach ($classlike_storage->used_traits as $used_trait) {
try {
$trait_storage = $this->classlike_storage_provider->get($used_trait);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -845,20 +818,20 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
$included_trait_file_storage = $this->file_storage_provider->get(
$trait_storage->location->file_path,
);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
- $storage->declaring_function_ids = array_merge(
- $included_trait_file_storage->declaring_function_ids,
- $storage->declaring_function_ids,
- );
+ $storage->declaring_function_ids = [
+ ...$included_trait_file_storage->declaring_function_ids,
+ ...$storage->declaring_function_ids,
+ ];
}
- $storage->declaring_function_ids = array_merge(
- $included_file_storage->declaring_function_ids,
- $storage->declaring_function_ids,
- );
+ $storage->declaring_function_ids = [
+ ...$included_file_storage->declaring_function_ids,
+ ...$storage->declaring_function_ids,
+ ];
}
$storage->required_file_paths = $all_required_file_paths;
@@ -866,7 +839,7 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
foreach ($all_required_file_paths as $required_file_path) {
try {
$required_file_storage = $this->file_storage_provider->get($required_file_path);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -876,7 +849,7 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
foreach ($storage->required_classes as $required_classlike) {
try {
$classlike_storage = $this->classlike_storage_provider->get($required_classlike);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -886,7 +859,7 @@ private function populateFileStorage(FileStorage $storage, array $dependent_file
try {
$required_file_storage = $this->file_storage_provider->get($classlike_storage->location->file_path);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
continue;
}
@@ -932,7 +905,7 @@ private function inheritConstantsFromTrait(
}
}
- protected function inheritMethodsFromParent(
+ private function inheritMethodsFromParent(
ClassLikeStorage $storage,
ClassLikeStorage $parent_storage,
): void {
diff --git a/src/Psalm/Internal/Codebase/Properties.php b/src/Psalm/Internal/Codebase/Properties.php
index 4547721fe1b..e8ed2cf2dc4 100644
--- a/src/Psalm/Internal/Codebase/Properties.php
+++ b/src/Psalm/Internal/Codebase/Properties.php
@@ -27,14 +27,8 @@
*/
final class Properties
{
- private ClassLikeStorageProvider $classlike_storage_provider;
-
- private ClassLikes $classlikes;
-
public bool $collect_locations = false;
- public FileReferenceProvider $file_reference_provider;
-
public PropertyExistenceProvider $property_existence_provider;
public PropertyTypeProvider $property_type_provider;
@@ -43,16 +37,13 @@ final class Properties
public function __construct(
- ClassLikeStorageProvider $storage_provider,
- FileReferenceProvider $file_reference_provider,
- ClassLikes $classlikes,
+ private readonly ClassLikeStorageProvider $classlike_storage_provider,
+ public FileReferenceProvider $file_reference_provider,
+ private readonly ClassLikes $classlikes,
) {
- $this->classlike_storage_provider = $storage_provider;
- $this->file_reference_provider = $file_reference_provider;
$this->property_existence_provider = new PropertyExistenceProvider();
$this->property_visibility_provider = new PropertyVisibilityProvider();
$this->property_type_provider = new PropertyTypeProvider();
- $this->classlikes = $classlikes;
}
/**
diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php
index d4c083641f5..9d8d50becb5 100644
--- a/src/Psalm/Internal/Codebase/Reflection.php
+++ b/src/Psalm/Internal/Codebase/Reflection.php
@@ -30,7 +30,6 @@
use function array_map;
use function array_merge;
-use function get_class;
use function implode;
use function strtolower;
@@ -41,19 +40,15 @@
*/
final class Reflection
{
- private ClassLikeStorageProvider $storage_provider;
-
- private Codebase $codebase;
-
/**
* @var array
*/
private static array $builtin_functions = [];
- public function __construct(ClassLikeStorageProvider $storage_provider, Codebase $codebase)
- {
- $this->storage_provider = $storage_provider;
- $this->codebase = $codebase;
+ public function __construct(
+ private readonly ClassLikeStorageProvider $storage_provider,
+ private readonly Codebase $codebase,
+ ) {
self::$builtin_functions = [];
}
@@ -71,7 +66,7 @@ public function registerClass(ReflectionClass $reflected_class): void
$this->storage_provider->get($class_name_lower);
return;
- } catch (Exception $e) {
+ } catch (Exception) {
// this is fine
}
@@ -411,7 +406,7 @@ public function registerFunction(string $function_id): ?bool
}
$storage->cased_name = $reflection_function->getName();
- } catch (ReflectionException $e) {
+ } catch (ReflectionException) {
return false;
}
@@ -436,7 +431,7 @@ public static function getPsalmTypeFromReflectionType(?ReflectionType $reflectio
),
);
} else {
- throw new LogicException('Unexpected reflection class ' . get_class($reflection_type) . ' found.');
+ throw new LogicException('Unexpected reflection class ' . $reflection_type::class . ' found.');
}
if ($reflection_type->allowsNull()) {
diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php
index 262f0ad4ed8..eabb6673e90 100644
--- a/src/Psalm/Internal/Codebase/Scanner.php
+++ b/src/Psalm/Internal/Codebase/Scanner.php
@@ -4,7 +4,6 @@
namespace Psalm\Internal\Codebase;
-use Closure;
use Psalm\Codebase;
use Psalm\Config;
use Psalm\Internal\Analyzer\IssueData;
@@ -35,6 +34,7 @@
use function file_exists;
use function min;
use function realpath;
+use function str_ends_with;
use function strtolower;
use function substr;
@@ -87,8 +87,6 @@
*/
final class Scanner
{
- private Codebase $codebase;
-
/**
* @var array
*/
@@ -134,36 +132,17 @@ final class Scanner
*/
private array $reflected_classlikes_lc = [];
- private Reflection $reflection;
-
- private Config $config;
-
- private Progress $progress;
-
- private FileStorageProvider $file_storage_provider;
-
- private FileProvider $file_provider;
-
- private FileReferenceProvider $file_reference_provider;
-
private bool $is_forked = false;
public function __construct(
- Codebase $codebase,
- Config $config,
- FileStorageProvider $file_storage_provider,
- FileProvider $file_provider,
- Reflection $reflection,
- FileReferenceProvider $file_reference_provider,
- Progress $progress,
+ private readonly Codebase $codebase,
+ private readonly Config $config,
+ private readonly FileStorageProvider $file_storage_provider,
+ private readonly FileProvider $file_provider,
+ private readonly Reflection $reflection,
+ private readonly FileReferenceProvider $file_reference_provider,
+ private readonly Progress $progress,
) {
- $this->codebase = $codebase;
- $this->reflection = $reflection;
- $this->file_provider = $file_provider;
- $this->progress = $progress;
- $this->file_storage_provider = $file_storage_provider;
- $this->config = $config;
- $this->file_reference_provider = $file_reference_provider;
}
/**
@@ -241,7 +220,7 @@ public function queueClassLikeForScanning(
}
// avoid checking classes that we know will just end in failure
- if ($fq_classlike_name_lc === 'null' || substr($fq_classlike_name_lc, -5) === '\null') {
+ if ($fq_classlike_name_lc === 'null' || str_ends_with($fq_classlike_name_lc, '\null')) {
return;
}
@@ -302,7 +281,7 @@ private function scanFilePaths(int $pool_size): bool
{
$files_to_scan = array_filter(
$this->files_to_scan,
- [$this, 'shouldScan'],
+ $this->shouldScan(...),
);
$this->files_to_scan = [];
@@ -350,7 +329,7 @@ function (): void {
$this->progress->debug('Have initialised forked process for scanning' . PHP_EOL);
},
- Closure::fromCallable([$this, 'scanAPath']),
+ $this->scanAPath(...),
/**
* @return PoolData
*/
@@ -686,7 +665,7 @@ function () use ($fq_class_name): ?ReflectionClass {
/** @psalm-suppress ArgumentTypeCoercion */
return new ReflectionClass($fq_class_name);
- } catch (Throwable $e) {
+ } catch (Throwable) {
// do not cache any results here (as case-sensitive filenames can screw things up)
return null;
diff --git a/src/Psalm/Internal/Codebase/StorageByPatternResolver.php b/src/Psalm/Internal/Codebase/StorageByPatternResolver.php
index f075fd81895..67a70a72003 100644
--- a/src/Psalm/Internal/Codebase/StorageByPatternResolver.php
+++ b/src/Psalm/Internal/Codebase/StorageByPatternResolver.php
@@ -10,8 +10,8 @@
use function preg_match;
use function sprintf;
+use function str_contains;
use function str_replace;
-use function strpos;
/**
* @internal
@@ -30,7 +30,7 @@ public function resolveConstants(
): array {
$constants = $class_like_storage->constants;
- if (strpos($pattern, '*') === false) {
+ if (!str_contains($pattern, '*')) {
if (isset($constants[$pattern])) {
return [$pattern => $constants[$pattern]];
}
@@ -62,7 +62,7 @@ public function resolveEnums(
string $pattern,
): array {
$enum_cases = $class_like_storage->enum_cases;
- if (strpos($pattern, '*') === false) {
+ if (!str_contains($pattern, '*')) {
if (isset($enum_cases[$pattern])) {
return [$pattern => $enum_cases[$pattern]];
}
diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php
index 5c5f72173eb..ddd366476ff 100644
--- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php
+++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php
@@ -14,6 +14,7 @@
use Psalm\Issue\TaintedCookie;
use Psalm\Issue\TaintedCustom;
use Psalm\Issue\TaintedEval;
+use Psalm\Issue\TaintedExtract;
use Psalm\Issue\TaintedFile;
use Psalm\Issue\TaintedHeader;
use Psalm\Issue\TaintedHtml;
@@ -224,15 +225,12 @@ public function connectSinksAndSources(): void
$generated_sources = $this->getSpecializedSources($source);
foreach ($generated_sources as $generated_source) {
- $new_sources = array_merge(
- $new_sources,
- $this->getChildNodes(
- $generated_source,
- $source_taints,
- $sinks,
- $visited_source_ids,
- ),
- );
+ $new_sources = [...$new_sources, ...$this->getChildNodes(
+ $generated_source,
+ $source_taints,
+ $sinks,
+ $visited_source_ids,
+ )];
}
}
@@ -471,6 +469,15 @@ private function getChildNodes(
);
break;
+ case TaintKind::INPUT_EXTRACT:
+ $issue = new TaintedExtract(
+ 'Detected tainted extract',
+ $issue_location,
+ $issue_trace,
+ $path,
+ );
+ break;
+
default:
$issue = new TaintedCustom(
'Detected tainted ' . $matching_taint,
@@ -543,7 +550,7 @@ private function getSpecializedSources(DataFlowNode $source): array
return array_filter(
$generated_sources,
- [$this, 'doesForwardEdgeExist'],
+ $this->doesForwardEdgeExist(...),
);
}
diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php
index 711dc6882d3..615f7482ee9 100644
--- a/src/Psalm/Internal/Codebase/VariableUseGraph.php
+++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php
@@ -9,7 +9,6 @@
use Psalm\Internal\DataFlow\Path;
use function abs;
-use function array_merge;
use function count;
/**
@@ -18,7 +17,7 @@
final class VariableUseGraph extends DataFlowGraph
{
/** @var array> */
- protected array $backward_edges = [];
+ private array $backward_edges = [];
/** @var array */
private array $nodes = [];
@@ -85,10 +84,7 @@ public function isVariableUsed(DataFlowNode $assignment_node): bool
return true;
}
- $new_child_nodes = array_merge(
- $new_child_nodes,
- $child_nodes,
- );
+ $new_child_nodes = [...$new_child_nodes, ...$child_nodes];
}
$sources = $new_child_nodes;
diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php
index 024cc000c73..c538d3518b8 100644
--- a/src/Psalm/Internal/DataFlow/DataFlowNode.php
+++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php
@@ -5,6 +5,7 @@
namespace Psalm\Internal\DataFlow;
use Psalm\CodeLocation;
+use Stringable;
use function strtolower;
@@ -12,21 +13,12 @@
* @psalm-consistent-constructor
* @internal
*/
-class DataFlowNode
+class DataFlowNode implements Stringable
{
- public string $id;
-
public ?string $unspecialized_id = null;
- public string $label;
-
- public ?CodeLocation $code_location = null;
-
public ?string $specialization_key = null;
- /** @var array */
- public array $taints;
-
/** @var ?self */
public ?DataFlowNode $previous = null;
@@ -42,23 +34,17 @@ class DataFlowNode
* @param array $taints
*/
public function __construct(
- string $id,
- string $label,
- ?CodeLocation $code_location,
+ public string $id,
+ public string $label,
+ public ?CodeLocation $code_location,
?string $specialization_key = null,
- array $taints = [],
+ public array $taints = [],
) {
- $this->id = $id;
-
if ($specialization_key) {
$this->unspecialized_id = $id;
$this->id .= '-' . $specialization_key;
}
-
- $this->label = $label;
- $this->code_location = $code_location;
$this->specialization_key = $specialization_key;
- $this->taints = $taints;
}
/**
diff --git a/src/Psalm/Internal/DataFlow/Path.php b/src/Psalm/Internal/DataFlow/Path.php
index 6e233f66e0d..785bb8e511a 100644
--- a/src/Psalm/Internal/DataFlow/Path.php
+++ b/src/Psalm/Internal/DataFlow/Path.php
@@ -14,29 +14,15 @@ final class Path
{
use ImmutableNonCloneableTrait;
- public string $type;
-
- /** @var ?array */
- public ?array $unescaped_taints = null;
-
- /** @var ?array */
- public ?array $escaped_taints = null;
-
- public int $length;
-
/**
* @param ?array $unescaped_taints
* @param ?array $escaped_taints
*/
public function __construct(
- string $type,
- int $length,
- ?array $unescaped_taints = null,
- ?array $escaped_taints = null,
+ public readonly string $type,
+ public readonly int $length,
+ public readonly ?array $unescaped_taints = null,
+ public readonly ?array $escaped_taints = null,
) {
- $this->type = $type;
- $this->length = $length;
- $this->unescaped_taints = $unescaped_taints;
- $this->escaped_taints = $escaped_taints;
}
}
diff --git a/src/Psalm/Internal/Diff/AstDiffer.php b/src/Psalm/Internal/Diff/AstDiffer.php
index fcc2bd25edc..1e436c0dbf4 100644
--- a/src/Psalm/Internal/Diff/AstDiffer.php
+++ b/src/Psalm/Internal/Diff/AstDiffer.php
@@ -21,7 +21,7 @@
*
* @internal
*/
-class AstDiffer
+abstract class AstDiffer
{
/**
* @param Closure(Stmt, Stmt, string, string, bool=): bool $is_equal
diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php
index e672e5109b1..129ee983681 100644
--- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php
+++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php
@@ -8,9 +8,8 @@
use UnexpectedValueException;
use function count;
-use function get_class;
use function is_string;
-use function strpos;
+use function str_contains;
use function strtolower;
use function substr;
use function trim;
@@ -45,7 +44,7 @@ static function (
string $b_code,
bool &$body_change = false,
) use (&$diff_map): bool {
- if (get_class($a) !== get_class($b)) {
+ if ($a::class !== $b::class) {
return false;
}
@@ -91,7 +90,6 @@ static function (
$start_diff = $b_start - $a_start;
$line_diff = $b->getLine() - $a->getLine();
- /** @psalm-suppress MixedArrayAssignment */
$diff_map[] = [$a_start, $a_end, $start_diff, $line_diff];
return true;
@@ -146,8 +144,8 @@ static function (
$a_signature = trim($a_signature);
$b_signature = trim($b_signature);
- if (strpos($a_signature, $b_signature) === false
- && strpos($b_signature, $a_signature) === false
+ if (!str_contains($a_signature, $b_signature)
+ && !str_contains($b_signature, $a_signature)
) {
$signature_change = true;
}
@@ -185,7 +183,6 @@ static function (
}
if (!$signature_change && !$body_change) {
- /** @psalm-suppress MixedArrayAssignment */
$diff_map[] = [$a_start, $a_end, $b_start - $a_start, $b->getLine() - $a->getLine()];
}
diff --git a/src/Psalm/Internal/Diff/DiffElem.php b/src/Psalm/Internal/Diff/DiffElem.php
index 74a993ace22..270c00aa332 100644
--- a/src/Psalm/Internal/Diff/DiffElem.php
+++ b/src/Psalm/Internal/Diff/DiffElem.php
@@ -20,17 +20,13 @@ final class DiffElem
public const TYPE_REPLACE = 3;
public const TYPE_KEEP_SIGNATURE = 4;
- /** @var int One of the TYPE_* constants */
- public int $type;
- /** @var mixed Is null for add operations */
- public mixed $old;
- /** @var mixed Is null for remove operations */
- public mixed $new;
-
- public function __construct(int $type, mixed $old, mixed $new)
- {
- $this->type = $type;
- $this->old = $old;
- $this->new = $new;
+ public function __construct(
+ /** @var int One of the TYPE_* constants */
+ public readonly int $type,
+ /** @var mixed Is null for add operations */
+ public readonly mixed $old,
+ /** @var mixed Is null for remove operations */
+ public readonly mixed $new,
+ ) {
}
}
diff --git a/src/Psalm/Internal/Diff/FileStatementsDiffer.php b/src/Psalm/Internal/Diff/FileStatementsDiffer.php
index cd0b0c66c97..b00f044207e 100644
--- a/src/Psalm/Internal/Diff/FileStatementsDiffer.php
+++ b/src/Psalm/Internal/Diff/FileStatementsDiffer.php
@@ -8,7 +8,6 @@
use function assert;
use function end;
-use function get_class;
use function substr;
/**
@@ -38,7 +37,7 @@ static function (
string $a_code,
string $b_code,
): bool {
- if (get_class($a) !== get_class($b)) {
+ if ($a::class !== $b::class) {
return false;
}
diff --git a/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php b/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php
index 2203ef76bfc..d16926d526a 100644
--- a/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php
+++ b/src/Psalm/Internal/Diff/NamespaceStatementsDiffer.php
@@ -8,7 +8,6 @@
use function assert;
use function end;
-use function get_class;
use function substr;
/**
@@ -38,7 +37,7 @@ static function (
string $a_code,
string $b_code,
): bool {
- if (get_class($a) !== get_class($b)) {
+ if ($a::class !== $b::class) {
return false;
}
diff --git a/src/Psalm/Internal/ErrorHandler.php b/src/Psalm/Internal/ErrorHandler.php
index a37a78d1671..9a41edf165d 100644
--- a/src/Psalm/Internal/ErrorHandler.php
+++ b/src/Psalm/Internal/ErrorHandler.php
@@ -31,7 +31,7 @@ final class ErrorHandler
/**
* @param array $argv
*/
- public static function install(array $argv = array()): void
+ public static function install(array $argv = []): void
{
self::$args = implode(' ', $argv);
self::setErrorReporting();
@@ -93,7 +93,7 @@ private static function installExceptionHandler(): void
* then print more of the backtrace than is done by default to stderr,
* then exit with a non-zero exit code to indicate failure.
*/
- set_exception_handler(static function (Throwable $throwable): void {
+ set_exception_handler(static function (Throwable $throwable): never {
fwrite(STDERR, "Uncaught $throwable\n");
$version = defined('PSALM_VERSION') ? PSALM_VERSION : '(unknown version)';
fwrite(STDERR, "(Psalm $version crashed due to an uncaught Throwable)\n");
diff --git a/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php b/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php
index cc820d0ec9e..0da2ea1dc62 100644
--- a/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php
+++ b/src/Psalm/Internal/ExecutionEnvironment/BuildInfoCollector.php
@@ -11,8 +11,8 @@
use function explode;
use function file_get_contents;
use function json_decode;
+use function str_contains;
use function str_replace;
-use function strpos;
use function strtotime;
use const JSON_THROW_ON_ERROR;
@@ -25,21 +25,19 @@
*/
final class BuildInfoCollector
{
- /**
- * Environment variables.
- *
- * Overwritten through collection process.
- */
- protected array $env;
-
/**
* Read environment variables.
*/
- protected array $readEnv = [];
-
- public function __construct(array $env)
- {
- $this->env = $env;
+ private array $readEnv = [];
+
+ public function __construct(
+ /**
+ * Environment variables.
+ *
+ * Overwritten through collection process.
+ */
+ protected array $env,
+ ) {
}
// API
@@ -72,7 +70,7 @@ public function collect(): array
* @return $this
* @psalm-suppress PossiblyUndefinedStringArrayOffset
*/
- protected function fillTravisCi(): self
+ private function fillTravisCi(): self
{
if (isset($this->env['TRAVIS']) && $this->env['TRAVIS'] && isset($this->env['TRAVIS_JOB_ID'])) {
$this->readEnv['CI_JOB_ID'] = $this->env['TRAVIS_JOB_ID'];
@@ -115,7 +113,7 @@ protected function fillTravisCi(): self
*
* @return $this
*/
- protected function fillCircleCi(): self
+ private function fillCircleCi(): self
{
if (isset($this->env['CIRCLECI']) && $this->env['CIRCLECI'] && isset($this->env['CIRCLE_BUILD_NUM'])) {
$this->env['CI_BUILD_NUMBER'] = $this->env['CIRCLE_BUILD_NUM'];
@@ -148,7 +146,7 @@ protected function fillCircleCi(): self
* @psalm-suppress PossiblyUndefinedStringArrayOffset
* @return $this
*/
- protected function fillAppVeyor(): self
+ private function fillAppVeyor(): self
{
if (isset($this->env['APPVEYOR']) && $this->env['APPVEYOR'] && isset($this->env['APPVEYOR_BUILD_NUMBER'])) {
$this->readEnv['CI_BUILD_NUMBER'] = $this->env['APPVEYOR_BUILD_NUMBER'];
@@ -195,7 +193,7 @@ protected function fillAppVeyor(): self
*
* @return $this
*/
- protected function fillJenkins(): self
+ private function fillJenkins(): self
{
if (isset($this->env['JENKINS_URL']) && isset($this->env['BUILD_NUMBER'])) {
$this->readEnv['CI_BUILD_NUMBER'] = $this->env['BUILD_NUMBER'];
@@ -219,7 +217,7 @@ protected function fillJenkins(): self
* @psalm-suppress PossiblyUndefinedStringArrayOffset
* @return $this
*/
- protected function fillScrutinizer(): self
+ private function fillScrutinizer(): self
{
if (isset($this->env['SCRUTINIZER']) && $this->env['SCRUTINIZER']) {
$this->readEnv['CI_JOB_ID'] = $this->env['SCRUTINIZER_INSPECTION_UUID'];
@@ -253,16 +251,16 @@ protected function fillScrutinizer(): self
* @return $this
* @psalm-suppress PossiblyUndefinedStringArrayOffset
*/
- protected function fillGithubActions(): BuildInfoCollector
+ private function fillGithubActions(): BuildInfoCollector
{
if (isset($this->env['GITHUB_ACTIONS'])) {
$this->env['CI_NAME'] = 'github-actions';
$this->env['CI_JOB_ID'] = $this->env['GITHUB_ACTIONS'];
$githubRef = (string) $this->env['GITHUB_REF'];
- if (strpos($githubRef, 'refs/heads/') !== false) {
+ if (str_contains($githubRef, 'refs/heads/')) {
$githubRef = str_replace('refs/heads/', '', $githubRef);
- } elseif (strpos($githubRef, 'refs/tags/') !== false) {
+ } elseif (str_contains($githubRef, 'refs/tags/')) {
$githubRef = str_replace('refs/tags/', '', $githubRef);
}
diff --git a/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php b/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php
index 5111b381d0b..f659fef639d 100644
--- a/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php
+++ b/src/Psalm/Internal/ExecutionEnvironment/GitInfoCollector.php
@@ -14,7 +14,8 @@
use function count;
use function explode;
use function range;
-use function strpos;
+use function str_contains;
+use function str_starts_with;
use function trim;
/**
@@ -28,7 +29,7 @@ final class GitInfoCollector
/**
* Git command.
*/
- protected SystemCommandExecutor $executor;
+ private readonly SystemCommandExecutor $executor;
/**
* Constructor.
@@ -57,12 +58,12 @@ public function collect(): GitInfo
*
* @throws RuntimeException
*/
- protected function collectBranch(): string
+ private function collectBranch(): string
{
$branchesResult = $this->executor->execute('git branch');
foreach ($branchesResult as $result) {
- if (strpos($result, '* ') === 0) {
+ if (str_starts_with($result, '* ')) {
$exploded = explode('* ', $result, 2);
return $exploded[1];
@@ -77,7 +78,7 @@ protected function collectBranch(): string
*
* @throws RuntimeException
*/
- protected function collectCommit(): CommitInfo
+ private function collectCommit(): CommitInfo
{
$commitResult = $this->executor->execute('git log -1 --pretty=format:%H%n%aN%n%ae%n%cN%n%ce%n%s%n%at');
@@ -103,7 +104,7 @@ protected function collectCommit(): CommitInfo
* @throws RuntimeException
* @return list
*/
- protected function collectRemotes(): array
+ private function collectRemotes(): array
{
$remotesResult = $this->executor->execute('git remote -v');
@@ -115,7 +116,7 @@ protected function collectRemotes(): array
$results = [];
foreach ($remotesResult as $result) {
- if (strpos($result, ' ') !== false) {
+ if (str_contains($result, ' ')) {
[$remote] = explode(' ', $result, 2);
$results[] = $remote;
@@ -129,7 +130,7 @@ protected function collectRemotes(): array
$remotes = [];
foreach ($results as $result) {
- if (strpos($result, "\t") !== false) {
+ if (str_contains($result, "\t")) {
[$name, $url] = explode("\t", $result, 2);
$remote = new RemoteInfo();
diff --git a/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php
index ec44a1c3575..be08809c224 100644
--- a/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php
+++ b/src/Psalm/Internal/FileManipulation/ClassDocblockManipulator.php
@@ -26,15 +26,13 @@ final class ClassDocblockManipulator
*/
private static array $manipulators = [];
- private Class_ $stmt;
+ private readonly int $docblock_start;
- private int $docblock_start;
-
- private int $docblock_end;
+ private readonly int $docblock_end;
private bool $immutable = false;
- private string $indentation;
+ private readonly string $indentation;
public static function getForClass(
ProjectAnalyzer $project_analyzer,
@@ -54,10 +52,9 @@ public static function getForClass(
private function __construct(
ProjectAnalyzer $project_analyzer,
- Class_ $stmt,
+ private readonly Class_ $stmt,
string $file_path,
) {
- $this->stmt = $stmt;
$docblock = $stmt->getDocComment();
$this->docblock_start = $docblock ? $docblock->getStartFilePos() : (int)$stmt->getAttribute('startFilePos');
$this->docblock_end = (int)$stmt->getAttribute('startFilePos');
diff --git a/src/Psalm/Internal/FileManipulation/CodeMigration.php b/src/Psalm/Internal/FileManipulation/CodeMigration.php
index 9c703dea506..980fa64b088 100644
--- a/src/Psalm/Internal/FileManipulation/CodeMigration.php
+++ b/src/Psalm/Internal/FileManipulation/CodeMigration.php
@@ -14,27 +14,12 @@ final class CodeMigration
{
use ImmutableNonCloneableTrait;
- public string $source_file_path;
-
- public int $source_start;
-
- public int $source_end;
-
- public string $destination_file_path;
-
- public int $destination_start;
-
public function __construct(
- string $source_file_path,
- int $source_start,
- int $source_end,
- string $destination_file_path,
- int $destination_start,
+ public readonly string $source_file_path,
+ public readonly int $source_start,
+ public readonly int $source_end,
+ public readonly string $destination_file_path,
+ public readonly int $destination_start,
) {
- $this->source_file_path = $source_file_path;
- $this->source_start = $source_start;
- $this->source_end = $source_end;
- $this->destination_file_path = $destination_file_path;
- $this->destination_start = $destination_start;
}
}
diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php
index 7448b3b8e0d..3004060d044 100644
--- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php
+++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php
@@ -17,7 +17,6 @@
use Psalm\Internal\Scanner\ParsedDocblock;
use function array_key_exists;
-use function array_merge;
use function array_reduce;
use function array_slice;
use function count;
@@ -45,13 +44,11 @@ final class FunctionDocblockManipulator
*/
private static array $manipulators = [];
- private Closure|Function_|ClassMethod|ArrowFunction $stmt;
+ private readonly int $docblock_start;
- private int $docblock_start;
+ private readonly int $docblock_end;
- private int $docblock_end;
-
- private int $return_typehint_area_start;
+ private readonly int $return_typehint_area_start;
private ?int $return_typehint_colon_start = null;
@@ -110,12 +107,11 @@ public static function getForFunction(
return $manipulator;
}
- /**
- * @param Closure|Function_|ClassMethod|ArrowFunction $stmt
- */
- private function __construct(string $file_path, FunctionLike $stmt, ProjectAnalyzer $project_analyzer)
- {
- $this->stmt = $stmt;
+ private function __construct(
+ string $file_path,
+ private readonly Closure|Function_|ClassMethod|ArrowFunction $stmt,
+ ProjectAnalyzer $project_analyzer,
+ ) {
$docblock = $stmt->getDocComment();
$this->docblock_start = $docblock ? $docblock->getStartFilePos() : (int)$stmt->getAttribute('startFilePos');
$this->docblock_end = $function_start = (int)$stmt->getAttribute('startFilePos');
@@ -578,7 +574,7 @@ public static function clearCache(): void
*/
public static function addManipulators(array $manipulators): void
{
- self::$manipulators = array_merge($manipulators, self::$manipulators);
+ self::$manipulators = [...$manipulators, ...self::$manipulators];
}
/**
diff --git a/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php
index 3f00530e5bb..6aa54dbed74 100644
--- a/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php
+++ b/src/Psalm/Internal/FileManipulation/PropertyDocblockManipulator.php
@@ -31,15 +31,13 @@ final class PropertyDocblockManipulator
*/
private static array $manipulators = [];
- private Property $stmt;
+ private readonly int $docblock_start;
- private int $docblock_start;
-
- private int $docblock_end;
+ private readonly int $docblock_end;
private ?int $typehint_start = null;
- private int $typehint_area_start;
+ private readonly int $typehint_area_start;
private ?int $typehint_end = null;
@@ -75,10 +73,9 @@ public static function getForProperty(
private function __construct(
ProjectAnalyzer $project_analyzer,
- Property $stmt,
+ private readonly Property $stmt,
string $file_path,
) {
- $this->stmt = $stmt;
$docblock = $stmt->getDocComment();
$this->docblock_start = $docblock ? $docblock->getStartFilePos() : (int)$stmt->getAttribute('startFilePos');
$this->docblock_end = (int)$stmt->getAttribute('startFilePos');
diff --git a/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php b/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php
index 233c2252411..0baea064884 100644
--- a/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php
+++ b/src/Psalm/Internal/Fork/ForkProcessDoneMessage.php
@@ -13,10 +13,8 @@
final class ForkProcessDoneMessage implements ForkMessage
{
use ImmutableNonCloneableTrait;
- public mixed $data;
- public function __construct(mixed $data)
+ public function __construct(public readonly mixed $data)
{
- $this->data = $data;
}
}
diff --git a/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php b/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php
index 1c59bac6793..e0ba5e0ba16 100644
--- a/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php
+++ b/src/Psalm/Internal/Fork/ForkProcessErrorMessage.php
@@ -14,10 +14,7 @@ final class ForkProcessErrorMessage implements ForkMessage
{
use ImmutableNonCloneableTrait;
- public string $message;
-
- public function __construct(string $message)
+ public function __construct(public readonly string $message)
{
- $this->message = $message;
}
}
diff --git a/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php b/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php
index 7cc1c01a0ac..6cd9ccd09f1 100644
--- a/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php
+++ b/src/Psalm/Internal/Fork/ForkTaskDoneMessage.php
@@ -14,10 +14,7 @@ final class ForkTaskDoneMessage implements ForkMessage
{
use ImmutableNonCloneableTrait;
- public mixed $data;
-
- public function __construct(mixed $data)
+ public function __construct(public readonly mixed $data)
{
- $this->data = $data;
}
}
diff --git a/src/Psalm/Internal/Fork/Pool.php b/src/Psalm/Internal/Fork/Pool.php
index 6be07fcac6d..64fc2ade52f 100644
--- a/src/Psalm/Internal/Fork/Pool.php
+++ b/src/Psalm/Internal/Fork/Pool.php
@@ -26,7 +26,6 @@
use function feof;
use function fread;
use function fwrite;
-use function get_class;
use function gettype;
use function igbinary_serialize;
use function igbinary_unserialize;
@@ -41,12 +40,12 @@
use function posix_kill;
use function posix_strerror;
use function serialize;
+use function str_contains;
use function stream_select;
use function stream_set_blocking;
use function stream_socket_pair;
use function stripos;
use function strlen;
-use function strpos;
use function substr;
use function unserialize;
use function usleep;
@@ -74,8 +73,6 @@ final class Pool
private const EXIT_SUCCESS = 0;
private const EXIT_FAILURE = 1;
- private Config $config;
-
/** @var int[] */
private array $child_pid_list = [];
@@ -84,9 +81,6 @@ final class Pool
private bool $did_have_error = false;
- /** @var ?Closure(mixed): void */
- private ?Closure $task_done_closure = null;
-
/**
* @param array> $process_task_data_iterator
* An array of task data items to be divided up among the
@@ -104,16 +98,14 @@ final class Pool
* @psalm-suppress MixedAssignment
*/
public function __construct(
- Config $config,
+ private readonly Config $config,
array $process_task_data_iterator,
Closure $startup_closure,
Closure $task_closure,
Closure $shutdown_closure,
- ?Closure $task_done_closure = null,
+ private readonly ?Closure $task_done_closure = null,
) {
$pool_size = count($process_task_data_iterator);
- $this->task_done_closure = $task_done_closure;
- $this->config = $config;
assert(
$pool_size > 1,
@@ -223,7 +215,7 @@ public function __construct(
// This can happen when developing Psalm from source without running `composer update`,
// or because of rare bugs in Psalm.
$process_done_message = new ForkProcessErrorMessage(
- get_class($t) . ' ' . $t->getMessage() . "\n" .
+ $t::class . ' ' . $t->getMessage() . "\n" .
"Emitted in " . $t->getFile() . ":" . $t->getLine() . "\n" .
"Stack trace in the forked worker:\n" .
$t->getTraceAsString(),
@@ -349,7 +341,7 @@ private function readResultsFromChildren(): array
$content[(int)$file] .= $buffer;
}
- if (strpos($buffer, "\n") !== false) {
+ if (str_contains($buffer, "\n")) {
$serialized_messages = explode("\n", $content[(int)$file]);
$content[(int)$file] = array_pop($serialized_messages);
diff --git a/src/Psalm/Internal/Fork/PsalmRestarter.php b/src/Psalm/Internal/Fork/PsalmRestarter.php
index 4350badbff6..e25f5627e78 100644
--- a/src/Psalm/Internal/Fork/PsalmRestarter.php
+++ b/src/Psalm/Internal/Fork/PsalmRestarter.php
@@ -16,21 +16,33 @@
use function implode;
use function in_array;
use function ini_get;
+use function is_int;
use function preg_replace;
use function strlen;
use function strtolower;
-use const PHP_VERSION_ID;
-
/**
* @internal
*/
final class PsalmRestarter extends XdebugHandler
{
private const REQUIRED_OPCACHE_SETTINGS = [
- 'enable_cli' => true,
+ 'enable_cli' => 1,
'jit' => 1205,
- 'jit_buffer_size' => 512 * 1024 * 1024,
+ 'validate_timestamps' => 0,
+ 'file_update_protection' => 0,
+ 'jit_buffer_size' => 128 * 1024 * 1024,
+ 'max_accelerated_files' => 1_000_000,
+ 'interned_strings_buffer' => 64,
+ 'jit_max_root_traces' => 1_000_000,
+ 'jit_max_side_traces' => 1_000_000,
+ 'jit_max_exit_counters' => 1_000_000,
+ 'jit_hot_loop' => 1,
+ 'jit_hot_func' => 1,
+ 'jit_hot_return' => 1,
+ 'jit_hot_side_exit' => 1,
+ 'jit_blacklist_root_trace' => 255,
+ 'jit_blacklist_side_trace' => 255,
'optimization_level' => '0x7FFEBFFF',
'preload' => '',
'log_verbosity_level' => 0,
@@ -69,19 +81,18 @@ protected function requiresRestart($default): bool
$opcache_loaded = extension_loaded('opcache') || extension_loaded('Zend OPcache');
- if (PHP_VERSION_ID >= 8_00_00 && $opcache_loaded) {
+ if ($opcache_loaded) {
// restart to enable JIT if it's not configured in the optimal way
- $opcache_settings = [
- 'enable_cli' => in_array(ini_get('opcache.enable_cli'), ['1', 'true', true, 1]),
- 'jit' => (int) ini_get('opcache.jit'),
- 'log_verbosity_level' => (int) ini_get('opcache.log_verbosity_level'),
- 'optimization_level' => (string) ini_get('opcache.optimization_level'),
- 'preload' => (string) ini_get('opcache.preload'),
- 'jit_buffer_size' => self::toBytes((string) ini_get('opcache.jit_buffer_size')),
- ];
-
foreach (self::REQUIRED_OPCACHE_SETTINGS as $ini_name => $required_value) {
- if ($opcache_settings[$ini_name] !== $required_value) {
+ $value = (string) ini_get("opcache.$ini_name");
+ if ($ini_name === 'jit_buffer_size') {
+ $value = self::toBytes($value);
+ } elseif ($ini_name === 'enable_cli') {
+ $value = in_array($value, ['1', 'true', true, 1]) ? 1 : 0;
+ } elseif (is_int($required_value)) {
+ $value = (int) $value;
+ }
+ if ($value !== $required_value) {
return true;
}
}
@@ -144,16 +155,11 @@ protected function restart($command): void
// executed in the parent process (before restart)
// if it wasn't loaded then we apparently don't have opcache installed and there's no point trying
// to tweak it
- // If we're running on 7.4 there's no JIT available
- if (PHP_VERSION_ID >= 8_00_00 && $opcache_loaded) {
- $additional_options = [
- '-dopcache.enable_cli=true',
- '-dopcache.jit_buffer_size=512M',
- '-dopcache.jit=1205',
- '-dopcache.optimization_level=0x7FFEBFFF',
- '-dopcache.preload=',
- '-dopcache.log_verbosity_level=0',
- ];
+ if ($opcache_loaded) {
+ $additional_options = [];
+ foreach (self::REQUIRED_OPCACHE_SETTINGS as $key => $value) {
+ $additional_options []= "-dopcache.{$key}={$value}";
+ }
}
array_splice(
diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php
index 5d44d2f598e..6350116a2b0 100644
--- a/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php
+++ b/src/Psalm/Internal/LanguageServer/Client/Progress/LegacyProgress.php
@@ -17,13 +17,11 @@ final class LegacyProgress implements ProgressInterface
private const STATUS_FINISHED = 'finished';
private string $status = self::STATUS_INACTIVE;
-
- private ClientHandler $handler;
private ?string $title = null;
- public function __construct(ClientHandler $handler)
- {
- $this->handler = $handler;
+ public function __construct(
+ private readonly ClientHandler $handler,
+ ) {
}
public function begin(string $title, ?string $message = null, ?int $percentage = null): void
diff --git a/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php
index 284d2bf76d1..865dcd33e45 100644
--- a/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php
+++ b/src/Psalm/Internal/LanguageServer/Client/Progress/Progress.php
@@ -15,15 +15,12 @@ final class Progress implements ProgressInterface
private const STATUS_FINISHED = 'finished';
private string $status = self::STATUS_INACTIVE;
-
- private ClientHandler $handler;
- private string $token;
private bool $withPercentage = false;
- public function __construct(ClientHandler $handler, string $token)
- {
- $this->handler = $handler;
- $this->token = $token;
+ public function __construct(
+ private readonly ClientHandler $handler,
+ private readonly string $token,
+ ) {
}
public function begin(
diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php
index 1e65565df3c..7d3e43f02db 100644
--- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php
+++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php
@@ -15,14 +15,10 @@
*/
final class TextDocument
{
- private ClientHandler $handler;
-
- private LanguageServer $server;
-
- public function __construct(ClientHandler $handler, LanguageServer $server)
- {
- $this->handler = $handler;
- $this->server = $server;
+ public function __construct(
+ private readonly ClientHandler $handler,
+ private readonly LanguageServer $server,
+ ) {
}
/**
diff --git a/src/Psalm/Internal/LanguageServer/Client/Workspace.php b/src/Psalm/Internal/LanguageServer/Client/Workspace.php
index e08e31192e0..164a3b83e67 100644
--- a/src/Psalm/Internal/LanguageServer/Client/Workspace.php
+++ b/src/Psalm/Internal/LanguageServer/Client/Workspace.php
@@ -4,7 +4,6 @@
namespace Psalm\Internal\LanguageServer\Client;
-use JsonMapper;
use Psalm\Internal\LanguageServer\ClientHandler;
use Psalm\Internal\LanguageServer\LanguageServer;
@@ -15,20 +14,10 @@
*/
final class Workspace
{
- private ClientHandler $handler;
-
- /**
- * @psalm-suppress UnusedProperty
- */
- private JsonMapper $mapper;
-
- private LanguageServer $server;
-
- public function __construct(ClientHandler $handler, JsonMapper $mapper, LanguageServer $server)
- {
- $this->handler = $handler;
- $this->mapper = $mapper;
- $this->server = $server;
+ public function __construct(
+ private readonly ClientHandler $handler,
+ private readonly LanguageServer $server,
+ ) {
}
/**
diff --git a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php
index a2ad6193eda..00d657c66fc 100644
--- a/src/Psalm/Internal/LanguageServer/ClientConfiguration.php
+++ b/src/Psalm/Internal/LanguageServer/ClientConfiguration.php
@@ -12,11 +12,6 @@
final class ClientConfiguration
{
- /**
- * Location of Baseline file
- */
- public ?string $baseline = null;
-
/**
* TCP Server Address
*/
@@ -27,68 +22,6 @@ final class ClientConfiguration
*/
public ?bool $TCPServerMode = null;
- /**
- * Hide Warnings or not
- */
- public ?bool $hideWarnings = null;
-
- /**
- * Provide Completion or not
- */
- public ?bool $provideCompletion = null;
-
- /**
- * Provide GoTo Definitions or not
- */
- public ?bool $provideDefinition = null;
-
- /**
- * Provide Hover Requests or not
- */
- public ?bool $provideHover = null;
-
- /**
- * Provide Signature Help or not
- */
- public ?bool $provideSignatureHelp = null;
-
- /**
- * Provide Code Actions or not
- */
- public ?bool $provideCodeActions = null;
-
- /**
- * Provide Diagnostics or not
- */
- public ?bool $provideDiagnostics = null;
-
- /**
- * Provide Completion or not
- *
- * @psalm-suppress PossiblyUnusedProperty
- */
- public ?bool $findUnusedVariables = null;
-
- /**
- * Look for dead code
- *
- * @var 'always'|'auto'|null
- */
- public ?string $findUnusedCode = null;
-
- /**
- * Log Level
- *
- * @see MessageType
- */
- public ?int $logLevel = null;
-
- /**
- * If added, the language server will not respond to onChange events.
- * You can also specify a line count over which Psalm will not run on-change events.
- */
- public ?int $onchangeLineLimit = null;
-
/**
* Debounce time in milliseconds for onChange events
*/
@@ -100,30 +33,59 @@ final class ClientConfiguration
* @param 'always'|'auto'|null $findUnusedCode
*/
public function __construct(
- bool $hideWarnings = true,
- ?bool $provideCompletion = null,
- ?bool $provideDefinition = null,
- ?bool $provideHover = null,
- ?bool $provideSignatureHelp = null,
- ?bool $provideCodeActions = null,
- ?bool $provideDiagnostics = null,
- ?bool $findUnusedVariables = null,
- ?string $findUnusedCode = null,
- ?int $logLevel = null,
- ?int $onchangeLineLimit = null,
- ?string $baseline = null,
+ /**
+ * Hide Warnings or not
+ */
+ public ?bool $hideWarnings = true,
+ /**
+ * Provide Completion or not
+ */
+ public ?bool $provideCompletion = null,
+ /**
+ * Provide GoTo Definitions or not
+ */
+ public ?bool $provideDefinition = null,
+ /**
+ * Provide Hover Requests or not
+ */
+ public ?bool $provideHover = null,
+ /**
+ * Provide Signature Help or not
+ */
+ public ?bool $provideSignatureHelp = null,
+ /**
+ * Provide Code Actions or not
+ */
+ public ?bool $provideCodeActions = null,
+ /**
+ * Provide Diagnostics or not
+ */
+ public ?bool $provideDiagnostics = null,
+ /**
+ * Provide Completion or not
+ *
+ * @psalm-suppress PossiblyUnusedProperty
+ */
+ public ?bool $findUnusedVariables = null,
+ /**
+ * Look for dead code
+ */
+ public ?string $findUnusedCode = null,
+ /**
+ * Log Level
+ *
+ * @see MessageType
+ */
+ public ?int $logLevel = null,
+ /**
+ * If added, the language server will not respond to onChange events.
+ * You can also specify a line count over which Psalm will not run on-change events.
+ */
+ public ?int $onchangeLineLimit = null,
+ /**
+ * Location of Baseline file
+ */
+ public ?string $baseline = null,
) {
- $this->hideWarnings = $hideWarnings;
- $this->provideCompletion = $provideCompletion;
- $this->provideDefinition = $provideDefinition;
- $this->provideHover = $provideHover;
- $this->provideSignatureHelp = $provideSignatureHelp;
- $this->provideCodeActions = $provideCodeActions;
- $this->provideDiagnostics = $provideDiagnostics;
- $this->findUnusedVariables = $findUnusedVariables;
- $this->findUnusedCode = $findUnusedCode;
- $this->logLevel = $logLevel;
- $this->onchangeLineLimit = $onchangeLineLimit;
- $this->baseline = $baseline;
}
}
diff --git a/src/Psalm/Internal/LanguageServer/ClientHandler.php b/src/Psalm/Internal/LanguageServer/ClientHandler.php
index 29d2cce6914..2b6bcc21aee 100644
--- a/src/Psalm/Internal/LanguageServer/ClientHandler.php
+++ b/src/Psalm/Internal/LanguageServer/ClientHandler.php
@@ -15,16 +15,10 @@
*/
final class ClientHandler
{
- public ProtocolReader $protocolReader;
-
- public ProtocolWriter $protocolWriter;
-
public IdGenerator $idGenerator;
- public function __construct(ProtocolReader $protocolReader, ProtocolWriter $protocolWriter)
+ public function __construct(public ProtocolReader $protocolReader, public ProtocolWriter $protocolWriter)
{
- $this->protocolReader = $protocolReader;
- $this->protocolWriter = $protocolWriter;
$this->idGenerator = new IdGenerator;
}
diff --git a/src/Psalm/Internal/LanguageServer/LanguageClient.php b/src/Psalm/Internal/LanguageServer/LanguageClient.php
index f23985592bd..12cdfc86c8b 100644
--- a/src/Psalm/Internal/LanguageServer/LanguageClient.php
+++ b/src/Psalm/Internal/LanguageServer/LanguageClient.php
@@ -4,7 +4,6 @@
namespace Psalm\Internal\LanguageServer;
-use JsonMapper;
use LanguageServerProtocol\LogMessage;
use LanguageServerProtocol\LogTrace;
use Psalm\Internal\LanguageServer\Client\Progress\LegacyProgress;
@@ -19,6 +18,8 @@
use function json_decode;
use function json_encode;
+use const JSON_THROW_ON_ERROR;
+
/**
* @internal
*/
@@ -37,30 +38,24 @@ final class LanguageClient
/**
* The client handler
*/
- private ClientHandler $handler;
-
- /**
- * The Language Server
- */
- private LanguageServer $server;
-
- /**
- * The Client Configuration
- */
- public ClientConfiguration $clientConfiguration;
+ private readonly ClientHandler $handler;
public function __construct(
ProtocolReader $reader,
ProtocolWriter $writer,
- LanguageServer $server,
- ClientConfiguration $clientConfiguration,
+ /**
+ * The Language Server
+ */
+ private readonly LanguageServer $server,
+ /**
+ * The Client Configuration
+ */
+ public ClientConfiguration $clientConfiguration,
) {
$this->handler = new ClientHandler($reader, $writer);
- $this->server = $server;
$this->textDocument = new ClientTextDocument($this->handler, $this->server);
- $this->workspace = new ClientWorkspace($this->handler, new JsonMapper, $this->server);
- $this->clientConfiguration = $clientConfiguration;
+ $this->workspace = new ClientWorkspace($this->handler, $this->server);
}
/**
@@ -147,8 +142,6 @@ public function makeProgress(string $token): ProgressInterface
/**
* Configuration Refreshed from Client
- *
- * @param array $config
*/
private function configurationRefreshed(array $config): void
{
@@ -159,7 +152,7 @@ private function configurationRefreshed(array $config): void
}
/** @var array */
- $array = json_decode((string) json_encode($config), true);
+ $array = json_decode(json_encode($config, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
if (isset($array['hideWarnings'])) {
$this->clientConfiguration->hideWarnings = (bool) $array['hideWarnings'];
diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php
index 8bc21c538bd..91d18a2bd10 100644
--- a/src/Psalm/Internal/LanguageServer/LanguageServer.php
+++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php
@@ -74,12 +74,13 @@
use function parse_url;
use function rawurlencode;
use function realpath;
+use function str_contains;
+use function str_ends_with;
use function str_replace;
use function stream_set_blocking;
use function stream_socket_accept;
use function stream_socket_client;
use function stream_socket_server;
-use function strpos;
use function substr;
use function trim;
use function uniqid;
@@ -108,27 +109,19 @@ final class LanguageServer extends Dispatcher
public ?ClientInfo $clientInfo = null;
- protected ProtocolReader $protocolReader;
-
- protected ProtocolWriter $protocolWriter;
-
public LanguageClient $client;
public ?ClientCapabilities $clientCapabilities = null;
public ?string $trace = null;
- protected ProjectAnalyzer $project_analyzer;
-
- protected Codebase $codebase;
-
/**
* The AMP Delay token
*/
- protected string $versionedAnalysisDelayToken = '';
+ private string $versionedAnalysisDelayToken = '';
/** @var array}>> */
- protected array $issue_baseline = [];
+ private array $issue_baseline = [];
/**
* This should actually be a private property on `parent`
@@ -137,33 +130,21 @@ final class LanguageServer extends Dispatcher
*/
protected JsonMapper $mapper;
- protected PathMapper $path_mapper;
-
public function __construct(
- ProtocolReader $reader,
- ProtocolWriter $writer,
- ProjectAnalyzer $project_analyzer,
- Codebase $codebase,
+ protected ProtocolReader $protocolReader,
+ protected ProtocolWriter $protocolWriter,
+ protected ProjectAnalyzer $project_analyzer,
+ protected Codebase $codebase,
ClientConfiguration $clientConfiguration,
Progress $progress,
- PathMapper $path_mapper,
+ protected PathMapper $path_mapper,
) {
parent::__construct($this, '/');
$progress->setServer($this);
-
- $this->project_analyzer = $project_analyzer;
-
- $this->codebase = $codebase;
-
- $this->path_mapper = $path_mapper;
-
- $this->protocolWriter = $writer;
-
- $this->protocolReader = $reader;
$this->protocolReader->on(
'close',
- function (): void {
+ function (): never {
$this->shutdown();
$this->exit();
},
@@ -225,7 +206,7 @@ static function (): void {
},
);
- $this->client = new LanguageClient($reader, $writer, $this, $clientConfiguration);
+ $this->client = new LanguageClient($protocolReader, $protocolWriter, $this, $clientConfiguration);
$this->logInfo("Psalm Language Server ".PSALM_VERSION." has started.");
@@ -728,14 +709,10 @@ function (IssueData $issue_data): Diagnostic {
new Position($start_line - 1, $start_column - 1),
new Position($end_line - 1, $end_column - 1),
);
- switch ($severity) {
- case IssueData::SEVERITY_INFO:
- $diagnostic_severity = DiagnosticSeverity::WARNING;
- break;
- default:
- $diagnostic_severity = DiagnosticSeverity::ERROR;
- break;
- }
+ $diagnostic_severity = match ($severity) {
+ IssueData::SEVERITY_INFO => DiagnosticSeverity::WARNING,
+ default => DiagnosticSeverity::ERROR,
+ };
$diagnostic = new Diagnostic(
$description,
$range,
@@ -772,11 +749,8 @@ function (IssueData $issue_data): Diagnostic {
//Process Baseline
$file = $issue_data->file_name;
$type = $issue_data->type;
- /** @psalm-suppress MixedArrayAccess */
if (isset($issue_baseline[$file][$type]) && $issue_baseline[$file][$type]['o'] > 0) {
- /** @psalm-suppress MixedArrayAccess, MixedArgument */
if ($issue_baseline[$file][$type]['o'] === count($issue_baseline[$file][$type]['s'])) {
- /** @psalm-suppress MixedArrayAccess, MixedAssignment */
$position = array_search(
str_replace("\r\n", "\n", trim($issue_data->selected_text)),
$issue_baseline[$file][$type]['s'],
@@ -785,16 +759,12 @@ function (IssueData $issue_data): Diagnostic {
if ($position !== false) {
$issue_data->severity = IssueData::SEVERITY_INFO;
- /** @psalm-suppress MixedArgument */
array_splice($issue_baseline[$file][$type]['s'], $position, 1);
- /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */
$issue_baseline[$file][$type]['o']--;
}
} else {
- /** @psalm-suppress MixedArrayAssignment */
$issue_baseline[$file][$type]['s'] = [];
$issue_data->severity = IssueData::SEVERITY_INFO;
- /** @psalm-suppress MixedArrayAssignment, MixedOperand, MixedAssignment */
$issue_baseline[$file][$type]['o']--;
}
}
@@ -842,7 +812,7 @@ public function shutdown(): void
* The server should exit with success code 0 if the shutdown request has been received before;
* otherwise with error code 1.
*/
- public function exit(): void
+ public function exit(): never
{
exit(0);
}
@@ -882,7 +852,7 @@ public function log(int $type, string $message, array $context = []): void
$message,
),
);
- } catch (Throwable $err) {
+ } catch (Throwable) {
// do nothing as we could potentially go into a loop here is not careful
//TODO: Investigate if we can use error_log instead
}
@@ -945,7 +915,7 @@ private function clientStatus(string $status, ?string $additional_info = null):
$status . (!empty($additional_info) ? ': ' . $additional_info : ''),
),
);
- } catch (Throwable $err) {
+ } catch (Throwable) {
// do nothing
}
}
@@ -964,7 +934,7 @@ public function pathToUri(string $filepath): string
$parts = explode('/', $filepath);
// Don't %-encode the colon after a Windows drive letter
$first = array_shift($parts);
- if (substr($first, -1) !== ':') {
+ if (!str_ends_with($first, ':')) {
$first = rawurlencode($first);
}
$parts = array_map('rawurlencode', $parts);
@@ -981,7 +951,7 @@ public function uriToPath(string $uri): string
{
$filepath = urldecode($this->getPathPart($uri));
- if (strpos($filepath, ':') !== false) {
+ if (str_contains($filepath, ':')) {
if ($filepath[0] === '/') {
$filepath = substr($filepath, 1);
}
diff --git a/src/Psalm/Internal/LanguageServer/Message.php b/src/Psalm/Internal/LanguageServer/Message.php
index 400cf9d0dbb..b0bae0c8655 100644
--- a/src/Psalm/Internal/LanguageServer/Message.php
+++ b/src/Psalm/Internal/LanguageServer/Message.php
@@ -5,6 +5,7 @@
namespace Psalm\Internal\LanguageServer;
use AdvancedJsonRpc\Message as MessageBody;
+use Stringable;
use function array_pop;
use function explode;
@@ -13,10 +14,8 @@
/**
* @internal
*/
-final class Message
+final class Message implements Stringable
{
- public ?MessageBody $body = null;
-
/**
* @var string[]
*/
@@ -45,9 +44,8 @@ public static function parse(string $msg): Message
/**
* @param string[] $headers
*/
- public function __construct(?MessageBody $body = null, array $headers = [])
+ public function __construct(public ?MessageBody $body = null, array $headers = [])
{
- $this->body = $body;
if (!isset($headers['Content-Type'])) {
$headers['Content-Type'] = 'application/vscode-jsonrpc; charset=utf8';
}
diff --git a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php
index a6141a98536..b0d6303227a 100644
--- a/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php
+++ b/src/Psalm/Internal/LanguageServer/PHPMarkdownContent.php
@@ -17,18 +17,8 @@
*/
final class PHPMarkdownContent extends MarkupContent implements JsonSerializable
{
- public string $code;
-
- public ?string $title = null;
-
- public ?string $description = null;
-
- public function __construct(string $code, ?string $title = null, ?string $description = null)
+ public function __construct(public string $code, public ?string $title = null, public ?string $description = null)
{
- $this->code = $code;
- $this->title = $title;
- $this->description = $description;
-
$markdown = '';
if ($title !== null) {
$markdown = "**$title**\n\n";
diff --git a/src/Psalm/Internal/LanguageServer/PathMapper.php b/src/Psalm/Internal/LanguageServer/PathMapper.php
index dbaaaf7018d..f0fd5abaffc 100644
--- a/src/Psalm/Internal/LanguageServer/PathMapper.php
+++ b/src/Psalm/Internal/LanguageServer/PathMapper.php
@@ -5,13 +5,14 @@
namespace Psalm\Internal\LanguageServer;
use function rtrim;
+use function str_starts_with;
use function strlen;
use function substr;
/** @internal */
final class PathMapper
{
- private string $server_root;
+ private readonly string $server_root;
private ?string $client_root;
public function __construct(string $server_root, ?string $client_root = null)
@@ -34,7 +35,7 @@ public function mapClientToServer(string $client_path): string
return $client_path;
}
- if (substr($client_path, 0, strlen($this->client_root)) === $this->client_root) {
+ if (str_starts_with($client_path, $this->client_root)) {
return $this->server_root . substr($client_path, strlen($this->client_root));
}
@@ -46,7 +47,7 @@ public function mapServerToClient(string $server_path): string
if ($this->client_root === null) {
return $server_path;
}
- if (substr($server_path, 0, strlen($this->server_root)) === $this->server_root) {
+ if (str_starts_with($server_path, $this->server_root)) {
return $this->client_root . substr($server_path, strlen($this->server_root));
}
return $server_path;
diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php
index 57422b52ee8..1e58dce1f9d 100644
--- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php
+++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php
@@ -10,8 +10,8 @@
use Revolt\EventLoop;
use function explode;
+use function str_ends_with;
use function strlen;
-use function substr;
use function trim;
/**
@@ -82,7 +82,7 @@ private function readMessages(string $buffer): int
$this->parsing_mode = self::PARSE_BODY;
$this->content_length = (int) ($this->headers['Content-Length'] ?? 0);
$this->buffer = '';
- } elseif (substr($this->buffer, -2) === "\r\n") {
+ } elseif (str_ends_with($this->buffer, "\r\n")) {
$parts = explode(':', $this->buffer);
if (isset($parts[1])) {
$this->headers[$parts[0]] = trim($parts[1]);
@@ -101,7 +101,7 @@ private function readMessages(string $buffer): int
// MessageBody::parse can throw an Error, maybe log an error?
try {
$msg = new Message(MessageBody::parse($this->buffer), $this->headers);
- } catch (Exception $_) {
+ } catch (Exception) {
$msg = null;
}
if ($msg) {
diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php
index 69405637577..2010816fd6a 100644
--- a/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php
+++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamWriter.php
@@ -11,7 +11,7 @@
*/
final class ProtocolStreamWriter implements ProtocolWriter
{
- private WritableResourceStream $output;
+ private readonly WritableResourceStream $output;
/**
* @param resource $output
diff --git a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php
index 47861abe470..6d988869a97 100644
--- a/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php
+++ b/src/Psalm/Internal/LanguageServer/Provider/ParserCacheProvider.php
@@ -58,11 +58,7 @@ public function loadStatementsFromCache(
*/
public function loadExistingStatementsFromCache(string $file_path): ?array
{
- if (isset($this->statements_cache[$file_path])) {
- return $this->statements_cache[$file_path];
- }
-
- return null;
+ return $this->statements_cache[$file_path] ?? null;
}
/**
@@ -81,11 +77,7 @@ public function saveStatementsToCache(
public function loadExistingFileContentsFromCache(string $file_path): ?string
{
- if (isset($this->file_contents_cache[$file_path])) {
- return $this->file_contents_cache[$file_path];
- }
-
- return null;
+ return $this->file_contents_cache[$file_path] ?? null;
}
public function cacheFileContents(string $file_path, string $file_contents): void
diff --git a/src/Psalm/Internal/LanguageServer/Reference.php b/src/Psalm/Internal/LanguageServer/Reference.php
index bedd931b589..fe376ef1d6b 100644
--- a/src/Psalm/Internal/LanguageServer/Reference.php
+++ b/src/Psalm/Internal/LanguageServer/Reference.php
@@ -11,14 +11,7 @@
*/
final class Reference
{
- public string $file_path;
- public string $symbol;
- public Range $range;
-
- public function __construct(string $file_path, string $symbol, Range $range)
+ public function __construct(public string $file_path, public string $symbol, public Range $range)
{
- $this->file_path = $file_path;
- $this->symbol = $symbol;
- $this->range = $range;
}
}
diff --git a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php
index 1830af2d93b..73a6781fa55 100644
--- a/src/Psalm/Internal/LanguageServer/Server/TextDocument.php
+++ b/src/Psalm/Internal/LanguageServer/Server/TextDocument.php
@@ -38,20 +38,11 @@
*/
final class TextDocument
{
- protected LanguageServer $server;
-
- protected Codebase $codebase;
-
- protected ProjectAnalyzer $project_analyzer;
-
public function __construct(
- LanguageServer $server,
- Codebase $codebase,
- ProjectAnalyzer $project_analyzer,
+ protected LanguageServer $server,
+ protected Codebase $codebase,
+ protected ProjectAnalyzer $project_analyzer,
) {
- $this->server = $server;
- $this->codebase = $codebase;
- $this->project_analyzer = $project_analyzer;
}
/**
@@ -312,10 +303,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit
}
return new CompletionList($completion_items, false);
}
- } catch (UnanalyzedFileException $e) {
- $this->server->logThrowable($e);
- return null;
- } catch (TypeParseTreeException $e) {
+ } catch (UnanalyzedFileException|TypeParseTreeException $e) {
$this->server->logThrowable($e);
return null;
}
@@ -326,10 +314,7 @@ public function completion(TextDocumentIdentifier $textDocument, Position $posit
$completion_items = $this->codebase->getCompletionItemsForType($type_context);
return new CompletionList($completion_items, false);
}
- } catch (UnexpectedValueException $e) {
- $this->server->logThrowable($e);
- return null;
- } catch (TypeParseTreeException $e) {
+ } catch (UnexpectedValueException|TypeParseTreeException $e) {
$this->server->logThrowable($e);
return null;
}
diff --git a/src/Psalm/Internal/LanguageServer/Server/Workspace.php b/src/Psalm/Internal/LanguageServer/Server/Workspace.php
index 2ce097eb0eb..a4f3aef6ca4 100644
--- a/src/Psalm/Internal/LanguageServer/Server/Workspace.php
+++ b/src/Psalm/Internal/LanguageServer/Server/Workspace.php
@@ -25,20 +25,11 @@
*/
final class Workspace
{
- protected LanguageServer $server;
-
- protected Codebase $codebase;
-
- protected ProjectAnalyzer $project_analyzer;
-
public function __construct(
- LanguageServer $server,
- Codebase $codebase,
- ProjectAnalyzer $project_analyzer,
+ protected LanguageServer $server,
+ protected Codebase $codebase,
+ protected ProjectAnalyzer $project_analyzer,
) {
- $this->server = $server;
- $this->codebase = $codebase;
- $this->project_analyzer = $project_analyzer;
}
/**
@@ -62,7 +53,7 @@ public function didChangeWatchedFiles(array $changes): void
array_map(function (FileEvent $change) {
try {
return $this->server->uriToPath($change->uri);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
return null;
}
}, $changes),
diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php
index c41a4b8ba9e..b50a9f9f316 100644
--- a/src/Psalm/Internal/MethodIdentifier.php
+++ b/src/Psalm/Internal/MethodIdentifier.php
@@ -6,32 +6,29 @@
use InvalidArgumentException;
use Psalm\Storage\ImmutableNonCloneableTrait;
+use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait;
+use Stringable;
use function explode;
use function is_string;
use function ltrim;
-use function strpos;
+use function str_contains;
use function strtolower;
/**
* @psalm-immutable
* @internal
*/
-final class MethodIdentifier
+final class MethodIdentifier implements Stringable
{
use ImmutableNonCloneableTrait;
-
- public string $fq_class_name;
- /** @var lowercase-string */
- public string $method_name;
+ use UnserializeMemoryUsageSuppressionTrait;
/**
* @param lowercase-string $method_name
*/
- public function __construct(string $fq_class_name, string $method_name)
+ public function __construct(public readonly string $fq_class_name, public readonly string $method_name)
{
- $this->fq_class_name = $fq_class_name;
- $this->method_name = $method_name;
}
/**
@@ -50,7 +47,7 @@ public static function wrap(string|MethodIdentifier $method_id): self
*/
public static function isValidMethodIdReference(string $method_id): bool
{
- return strpos($method_id, '::') !== false;
+ return str_contains($method_id, '::');
}
/**
diff --git a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php
index 609b098bdf2..de750effdf5 100644
--- a/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/AssignmentMapVisitor.php
@@ -20,13 +20,10 @@ final class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract
/**
* @var array>
*/
- protected array $assignment_map = [];
+ private array $assignment_map = [];
- protected ?string $this_class_name = null;
-
- public function __construct(?string $this_class_name)
+ public function __construct(protected ?string $this_class_name)
{
- $this->this_class_name = $this_class_name;
}
public function enterNode(PhpParser\Node $node): ?int
diff --git a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php
index 9c1bfa9de3f..1a3a14fdd2c 100644
--- a/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/CheckTrivialExprVisitor.php
@@ -14,7 +14,7 @@ final class CheckTrivialExprVisitor extends PhpParser\NodeVisitorAbstract
/**
* @var array
*/
- protected array $non_trivial_expr = [];
+ private array $non_trivial_expr = [];
private function checkNonTrivialExpr(PhpParser\Node\Expr $node): bool
{
diff --git a/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php b/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php
index c0c4d3e3f5a..02c3ec4cb24 100644
--- a/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/ConditionCloningVisitor.php
@@ -14,11 +14,9 @@
*/
final class ConditionCloningVisitor extends NodeVisitorAbstract
{
- private NodeDataProvider $type_provider;
-
- public function __construct(NodeDataProvider $old_type_provider)
- {
- $this->type_provider = $old_type_provider;
+ public function __construct(
+ private readonly NodeDataProvider $type_provider,
+ ) {
}
/**
diff --git a/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php b/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php
index fc60185ab27..936a58080a6 100644
--- a/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/NodeCleanerVisitor.php
@@ -12,11 +12,9 @@
*/
final class NodeCleanerVisitor extends PhpParser\NodeVisitorAbstract
{
- private NodeDataProvider $type_provider;
-
- public function __construct(NodeDataProvider $type_provider)
- {
- $this->type_provider = $type_provider;
+ public function __construct(
+ private readonly NodeDataProvider $type_provider,
+ ) {
}
public function enterNode(PhpParser\Node $node): ?int
diff --git a/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php b/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php
index 0a2d8c51e4b..35313a96c0e 100644
--- a/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/OffsetShifterVisitor.php
@@ -13,21 +13,14 @@
*/
final class OffsetShifterVisitor extends PhpParser\NodeVisitorAbstract
{
- private int $file_offset;
-
- private int $line_offset;
-
- /** @var array */
- private array $extra_offsets;
-
/**
* @param array $extra_offsets
*/
- public function __construct(int $offset, int $line_offset, array $extra_offsets)
- {
- $this->file_offset = $offset;
- $this->line_offset = $line_offset;
- $this->extra_offsets = $extra_offsets;
+ public function __construct(
+ private readonly int $file_offset,
+ private readonly int $line_offset,
+ private array $extra_offsets,
+ ) {
}
public function enterNode(PhpParser\Node $node): ?int
diff --git a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php
index d7cd254bed8..a10b264916f 100644
--- a/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/ParamReplacementVisitor.php
@@ -18,10 +18,6 @@
*/
final class ParamReplacementVisitor extends PhpParser\NodeVisitorAbstract
{
- private string $old_name;
-
- private string $new_name;
-
/** @var list */
private array $replacements = [];
@@ -29,10 +25,10 @@ final class ParamReplacementVisitor extends PhpParser\NodeVisitorAbstract
private bool $new_new_name_used = false;
- public function __construct(string $old_name, string $new_name)
- {
- $this->old_name = $old_name;
- $this->new_name = $new_name;
+ public function __construct(
+ private readonly string $old_name,
+ private readonly string $new_name,
+ ) {
}
public function enterNode(PhpParser\Node $node): ?int
diff --git a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php
index 16176468c44..19040e27a92 100644
--- a/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/PartialParserVisitor.php
@@ -33,37 +33,21 @@
*/
final class PartialParserVisitor extends PhpParser\NodeVisitorAbstract
{
- /** @var array */
- private array $offset_map;
-
private bool $must_rescan = false;
private int $non_method_changes;
- private string $a_file_contents;
-
- private string $b_file_contents;
-
- private int $a_file_contents_length;
-
- private Parser $parser;
-
- private Collecting $error_handler;
+ private readonly int $a_file_contents_length;
/** @param array $offset_map */
public function __construct(
- Parser $parser,
- Collecting $error_handler,
- array $offset_map,
- string $a_file_contents,
- string $b_file_contents,
+ private readonly Parser $parser,
+ private readonly Collecting $error_handler,
+ private readonly array $offset_map,
+ private readonly string $a_file_contents,
+ private readonly string $b_file_contents,
) {
- $this->parser = $parser;
- $this->error_handler = $error_handler;
- $this->offset_map = $offset_map;
- $this->a_file_contents = $a_file_contents;
$this->a_file_contents_length = strlen($a_file_contents);
- $this->b_file_contents = $b_file_contents;
$this->non_method_changes = count($offset_map);
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
index 9bc0a0cbf8a..be0ccb6102f 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
@@ -37,6 +37,7 @@
use function preg_replace;
use function preg_split;
use function reset;
+use function str_contains;
use function str_replace;
use function strlen;
use function strpos;
@@ -452,7 +453,7 @@ public static function parse(
$codebase->analysis_php_version_id,
$has_errors,
);
- } catch (Exception $e) {
+ } catch (Exception) {
throw new DocblockParseException('Badly-formatted @method string ' . $method_entry);
}
@@ -525,7 +526,7 @@ public static function parse(
* 'psalm-property-read'|'property-write'|'psalm-property-write' $property_tag
* @throws DocblockParseException
*/
- protected static function addMagicPropertyToInfo(
+ private static function addMagicPropertyToInfo(
Doc $comment,
ClassLikeDocblockComment $info,
array $specials,
@@ -591,7 +592,7 @@ private static function getMethodOffset(Doc $comment, string $method_entry): int
$method_offset = 0;
foreach ($lines as $i => $line) {
- if (strpos($line, $method_entry) !== false) {
+ if (str_contains($line, $method_entry)) {
$method_offset = $i;
break;
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index 21ab7445e88..17726f6f89a 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
@@ -77,7 +77,6 @@
use function array_values;
use function assert;
use function count;
-use function get_class;
use function implode;
use function preg_match;
use function preg_split;
@@ -94,16 +93,10 @@
*/
final class ClassLikeNodeScanner
{
- private FileScanner $file_scanner;
-
- private Codebase $codebase;
-
- private string $file_path;
+ private readonly string $file_path;
private Config $config;
- private FileStorage $file_storage;
-
/**
* @var array
*/
@@ -114,10 +107,6 @@ final class ClassLikeNodeScanner
*/
public array $class_template_types = [];
- private ?Name $namespace_name = null;
-
- private Aliases $aliases;
-
public ?ClassLikeStorage $storage = null;
/**
@@ -126,19 +115,14 @@ final class ClassLikeNodeScanner
public array $type_aliases = [];
public function __construct(
- Codebase $codebase,
- FileStorage $file_storage,
- FileScanner $file_scanner,
- Aliases $aliases,
- ?Name $namespace_name,
+ private readonly Codebase $codebase,
+ private readonly FileStorage $file_storage,
+ private readonly FileScanner $file_scanner,
+ private Aliases $aliases,
+ private readonly ?Name $namespace_name,
) {
- $this->codebase = $codebase;
- $this->file_storage = $file_storage;
- $this->file_scanner = $file_scanner;
$this->file_path = $file_storage->file_path;
- $this->aliases = $aliases;
$this->config = Config::getInstance();
- $this->namespace_name = $namespace_name;
}
/**
@@ -223,7 +207,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
foreach ($storage->dependent_classlikes as $dependent_name_lc => $_) {
try {
$dependent_storage = $this->codebase->classlike_storage_provider->get($dependent_name_lc);
- } catch (InvalidArgumentException $exception) {
+ } catch (InvalidArgumentException) {
continue;
}
$dependent_storage->populated = false;
@@ -512,7 +496,7 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
);
$storage->yield = $yield_type;
- } catch (TypeParseTreeException $e) {
+ } catch (TypeParseTreeException) {
// do nothing
}
}
@@ -876,7 +860,7 @@ public function finish(PhpParser\Node\Stmt\ClassLike $node): ClassLikeStorage
'@psalm-type ' . $key . ' contains invalid reference: ' . $e->getMessage(),
new CodeLocation($this->file_scanner, $node, null, true),
);
- } catch (Exception $e) {
+ } catch (Exception) {
$classlike_storage->docblock_issues[] = new InvalidDocblock(
'@psalm-type ' . $key . ' contains invalid references',
new CodeLocation($this->file_scanner, $node, null, true),
@@ -1366,7 +1350,7 @@ private function visitClassConstDeclaration(
&& !(
$const->value instanceof Concat
&& $inferred_type->isSingle()
- && get_class($inferred_type->getSingleAtomic()) === TString::class
+ && $inferred_type->getSingleAtomic()::class === TString::class
)
) {
$exists = true;
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php
index 0255eef98c7..bfcfc996a89 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionResolver.php
@@ -390,7 +390,7 @@ public static function enterConditional(
});
try {
return (bool) $evaluator->evaluateSilently($expr);
- } catch (ConstExprEvaluationException $e) {
+ } catch (ConstExprEvaluationException) {
return null;
}
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php
index bf5ac885de6..9ccbd8e6c61 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php
@@ -22,14 +22,14 @@
use Psalm\Storage\FileStorage;
use Psalm\Storage\FunctionLikeStorage;
use Psalm\Type;
+use Symfony\Component\Filesystem\Path;
use function assert;
use function defined;
use function dirname;
use function explode;
use function in_array;
-use function preg_match;
-use function strpos;
+use function str_contains;
use function strtolower;
use function substr;
@@ -170,7 +170,7 @@ private static function registerClassMapFunctionCall(
// only check the first @var comment
break;
}
- } catch (DocblockParseException $e) {
+ } catch (DocblockParseException) {
// do nothing
}
}
@@ -215,7 +215,7 @@ private static function registerClassMapFunctionCall(
}
foreach ($mapping_function_ids as $potential_method_id) {
- if (strpos($potential_method_id, '::') === false) {
+ if (!str_contains($potential_method_id, '::')) {
continue;
}
@@ -318,13 +318,7 @@ public static function visitInclude(
$include_path = IncludeAnalyzer::resolveIncludePath($path_to_file, dirname($file_storage->file_path));
$path_to_file = $include_path ?: $path_to_file;
- if (DIRECTORY_SEPARATOR === '/') {
- $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR;
- } else {
- $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file);
- }
-
- if ($is_path_relative) {
+ if (Path::isRelative($path_to_file)) {
$path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file;
}
} else {
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php
index 940a70e9b85..03a5d8c8d0f 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php
@@ -29,10 +29,12 @@
use function preg_replace;
use function preg_split;
use function reset;
+use function str_contains;
+use function str_ends_with;
use function str_replace;
+use function str_starts_with;
use function stripos;
use function strlen;
-use function strpos;
use function strtolower;
use function substr;
use function substr_count;
@@ -255,7 +257,7 @@ public static function parse(
if (count($param_parts) === 2) {
$taint_type = $param_parts[1];
- if (strpos($taint_type, 'exec_') === 0) {
+ if (str_starts_with($taint_type, 'exec_')) {
$taint_type = substr($taint_type, 5);
if ($taint_type === 'tainted') {
@@ -570,7 +572,7 @@ public static function parse(
*/
private static function sanitizeAssertionLineParts(array $line_parts): array
{
- if (count($line_parts) < 2 || strpos($line_parts[1], '$') === false) {
+ if (count($line_parts) < 2 || !str_contains($line_parts[1], '$')) {
throw new IncorrectDocblockException('Misplaced variable');
}
@@ -580,7 +582,7 @@ private static function sanitizeAssertionLineParts(array $line_parts): array
$param_name_parts = explode('->', $line_parts[1]);
foreach ($param_name_parts as $i => $param_name_part) {
- if (substr($param_name_part, -2) === '()') {
+ if (str_ends_with($param_name_part, '()')) {
$param_name_parts[$i] = strtolower($param_name_part);
}
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php
index 0d9bc183cba..fa9865718d9 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php
@@ -60,9 +60,11 @@
use function preg_match;
use function preg_replace;
use function preg_split;
+use function str_contains;
+use function str_ends_with;
use function str_replace;
+use function str_starts_with;
use function strlen;
-use function strpos;
use function strtolower;
use function substr;
use function substr_replace;
@@ -471,9 +473,7 @@ private static function getConditionalSanitizedTypeTokens(
= $storage->template_types[$template_name];
$param_type_mapping[$token_body] = $template_name;
} else {
- $template_as_type = $param_storage->type
- ? $param_storage->type
- : Type::getMixed();
+ $template_as_type = $param_storage->type ?: Type::getMixed();
$storage->template_types[$template_name] = [
$template_function_id => $template_as_type,
@@ -512,9 +512,8 @@ private static function getConditionalSanitizedTypeTokens(
if ($token_body === 'func_num_args()') {
$template_name = 'TFunctionArgCount';
-
$storage->template_types[$template_name] = [
- $template_function_id => Type::getInt(),
+ 'fn-' . strtolower($storage->cased_name ?? '') => Type::getInt(),
];
$function_template_types[$template_name]
@@ -527,7 +526,7 @@ private static function getConditionalSanitizedTypeTokens(
$template_name = 'TPhpMajorVersion';
$storage->template_types[$template_name] = [
- $template_function_id => Type::getInt(),
+ 'fn-' . strtolower($storage->cased_name ?? '') => Type::getInt(),
];
$function_template_types[$template_name]
@@ -540,7 +539,7 @@ private static function getConditionalSanitizedTypeTokens(
$template_name = 'TPhpVersionId';
$storage->template_types[$template_name] = [
- $template_function_id => Type::getInt(),
+ 'fn-' . strtolower($storage->cased_name ?? '') => Type::getInt(),
];
$function_template_types[$template_name]
@@ -721,7 +720,7 @@ private static function improveParamsFromDocblock(
$param_name = $docblock_param['name'];
$docblock_param_variadic = false;
- if (strpos($param_name, '...') === 0) {
+ if (str_starts_with($param_name, '...')) {
$docblock_param_variadic = true;
$param_name = substr($param_name, 3);
}
@@ -1095,7 +1094,7 @@ private static function handleTaintFlow(
if (isset($flow_parts[1]) && trim($flow_parts[1]) === 'return') {
$source_param_string = trim($flow_parts[0]);
- if ($source_param_string[0] === '(' && substr($source_param_string, -1) === ')') {
+ if ($source_param_string[0] === '(' && str_ends_with($source_param_string, ')')) {
$source_params = preg_split('/, ?/', substr($source_param_string, 1, -1));
if ($source_params === false) {
throw new AssertionError(preg_last_error_msg());
@@ -1113,7 +1112,7 @@ private static function handleTaintFlow(
}
}
- if (isset($flow_parts[0]) && strpos(trim($flow_parts[0]), 'proxy') === 0) {
+ if (isset($flow_parts[0]) && str_starts_with(trim($flow_parts[0]), 'proxy')) {
$proxy_call = trim(substr($flow_parts[0], strlen('proxy')));
[$fully_qualified_name, $source_param_string] = explode('(', $proxy_call, 2);
@@ -1250,7 +1249,7 @@ private static function handleAssertions(
continue 2;
}
- if (strpos($assertion['param_name'], $param->name.'->') === 0) {
+ if (str_starts_with($assertion['param_name'], $param->name.'->')) {
$storage->assertions[] = new Possibilities(
substr_replace($assertion['param_name'], (string) $i, 0, strlen($param->name)),
$assertion_type_parts,
@@ -1260,7 +1259,7 @@ private static function handleAssertions(
}
$storage->assertions[] = new Possibilities(
- (strpos($assertion['param_name'], '$') === false ? '$' : '') . $assertion['param_name'],
+ (!str_contains($assertion['param_name'], '$') ? '$' : '') . $assertion['param_name'],
$assertion_type_parts,
);
}
@@ -1297,7 +1296,7 @@ private static function handleAssertions(
continue 2;
}
- if (strpos($assertion['param_name'], $param->name.'->') === 0) {
+ if (str_starts_with($assertion['param_name'], $param->name.'->')) {
$storage->if_true_assertions[] = new Possibilities(
str_replace($param->name, (string) $i, $assertion['param_name']),
$assertion_type_parts,
@@ -1307,7 +1306,7 @@ private static function handleAssertions(
}
$storage->if_true_assertions[] = new Possibilities(
- (strpos($assertion['param_name'], '$') === false ? '$' : '') . $assertion['param_name'],
+ (!str_contains($assertion['param_name'], '$') ? '$' : '') . $assertion['param_name'],
$assertion_type_parts,
);
}
@@ -1344,7 +1343,7 @@ private static function handleAssertions(
continue 2;
}
- if (strpos($assertion['param_name'], $param->name.'->') === 0) {
+ if (str_starts_with($assertion['param_name'], $param->name.'->')) {
$storage->if_false_assertions[] = new Possibilities(
str_replace($param->name, (string) $i, $assertion['param_name']),
$assertion_type_parts,
@@ -1354,7 +1353,7 @@ private static function handleAssertions(
}
$storage->if_false_assertions[] = new Possibilities(
- (strpos($assertion['param_name'], '$') === false ? '$' : '') . $assertion['param_name'],
+ (!str_contains($assertion['param_name'], '$') ? '$' : '') . $assertion['param_name'],
$assertion_type_parts,
);
}
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
index e81f1ee47df..8ae03aa9b92 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
@@ -62,7 +62,8 @@
use function in_array;
use function is_string;
use function spl_object_id;
-use function strpos;
+use function str_contains;
+use function str_starts_with;
use function strtolower;
/**
@@ -70,29 +71,9 @@
*/
final class FunctionLikeNodeScanner
{
- private FileScanner $file_scanner;
+ private readonly string $file_path;
- private Codebase $codebase;
-
- private string $file_path;
-
- private Config $config;
-
- private FileStorage $file_storage;
-
- private ?ClassLikeStorage $classlike_storage = null;
-
- /**
- * @var array>
- */
- private array $existing_function_template_types;
-
- private Aliases $aliases;
-
- /**
- * @var array
- */
- private array $type_aliases;
+ private readonly Config $config;
public ?FunctionLikeStorage $storage = null;
@@ -101,23 +82,16 @@ final class FunctionLikeNodeScanner
* @param array $type_aliases
*/
public function __construct(
- Codebase $codebase,
- FileScanner $file_scanner,
- FileStorage $file_storage,
- Aliases $aliases,
- array $type_aliases,
- ?ClassLikeStorage $classlike_storage,
- array $existing_function_template_types,
+ private readonly Codebase $codebase,
+ private readonly FileScanner $file_scanner,
+ private readonly FileStorage $file_storage,
+ private readonly Aliases $aliases,
+ private readonly array $type_aliases,
+ private ?ClassLikeStorage $classlike_storage,
+ private readonly array $existing_function_template_types,
) {
- $this->codebase = $codebase;
- $this->file_storage = $file_storage;
- $this->file_scanner = $file_scanner;
$this->file_path = $file_storage->file_path;
- $this->aliases = $aliases;
- $this->type_aliases = $type_aliases;
$this->config = Config::getInstance();
- $this->classlike_storage = $classlike_storage;
- $this->existing_function_template_types = $existing_function_template_types;
}
/**
@@ -271,7 +245,7 @@ public function start(
$classlike_storage->properties[$property_name]->getter_method = strtolower($stmt->name->name);
}
- } elseif (strpos($stmt->name->name, 'assert') === 0
+ } elseif (str_starts_with($stmt->name->name, 'assert')
&& $stmt->stmts
) {
$var_assertions = [];
@@ -303,7 +277,7 @@ public function start(
try {
$negated_formula = Algebra::negateFormula($if_clauses);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$var_assertions = [];
break;
}
@@ -329,7 +303,7 @@ public function start(
$param_offset,
$rule_part,
);
- } elseif (strpos($var_id, '$this->') === 0) {
+ } elseif (str_starts_with($var_id, '$this->')) {
$var_assertions[] = new Possibilities(
$var_id,
$rule_part,
@@ -1045,7 +1019,7 @@ private function createStorageForFunctionLike(
$code_location,
$cased_function_id,
);
- } catch (IncorrectDocblockException|DocblockParseException $e) {
+ } catch (IncorrectDocblockException|DocblockParseException) {
}
if ($docblock_info) {
if ($docblock_info->since_php_major_version && !$this->aliases->namespace) {
@@ -1080,7 +1054,7 @@ private function createStorageForFunctionLike(
if ($method_name_lc === strtolower($class_name)
&& !isset($classlike_storage->methods['__construct'])
- && strpos($fq_classlike_name, '\\') === false
+ && !str_contains($fq_classlike_name, '\\')
&& $this->codebase->analysis_php_version_id <= 7_04_00
) {
$this->codebase->methods->setDeclaringMethodId(
diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php
index 29a3e07d25f..b8bde65c8ea 100644
--- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php
@@ -36,11 +36,9 @@
use Psalm\Type;
use UnexpectedValueException;
-use function array_merge;
use function array_pop;
use function end;
use function explode;
-use function get_class;
use function in_array;
use function is_string;
use function reset;
@@ -48,8 +46,6 @@
use function strpos;
use function strtolower;
-use const PHP_VERSION_ID;
-
/**
* @internal
*/
@@ -57,15 +53,9 @@ final class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements Fi
{
private Aliases $aliases;
- private FileScanner $file_scanner;
-
- private Codebase $codebase;
-
private string $file_path;
- private bool $scan_deep;
-
- private FileStorage $file_storage;
+ private readonly bool $scan_deep;
/**
* @var array
@@ -92,18 +82,15 @@ final class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements Fi
* @var array
*/
private array $bad_classes = [];
- private EventDispatcher $eventDispatcher;
+ private readonly EventDispatcher $eventDispatcher;
public function __construct(
- Codebase $codebase,
- FileScanner $file_scanner,
- FileStorage $file_storage,
+ private readonly Codebase $codebase,
+ private readonly FileScanner $file_scanner,
+ private readonly FileStorage $file_storage,
) {
- $this->codebase = $codebase;
- $this->file_scanner = $file_scanner;
$this->file_path = $file_scanner->file_path;
$this->scan_deep = $file_scanner->will_analyze;
- $this->file_storage = $file_storage;
$this->aliases = $this->file_storage->aliases = new Aliases();
$this->eventDispatcher = $this->codebase->config->eventDispatcher;
}
@@ -161,7 +148,7 @@ public function enterNode(PhpParser\Node $node): ?int
$this->classlike_node_scanners[] = $classlike_node_scanner;
- $this->type_aliases = array_merge($this->type_aliases, $classlike_node_scanner->type_aliases);
+ $this->type_aliases = [...$this->type_aliases, ...$classlike_node_scanner->type_aliases];
} elseif ($node instanceof PhpParser\Node\Stmt\TryCatch) {
foreach ($node->catches as $catch) {
foreach ($catch->types as $catch_type) {
@@ -221,9 +208,7 @@ public function enterNode(PhpParser\Node $node): ?int
$classlike_storage->class_implements['stringable'] = 'Stringable';
}
- if (PHP_VERSION_ID >= 8_00_00) {
- $this->codebase->scanner->queueClassLikeForScanning('Stringable');
- }
+ $this->codebase->scanner->queueClassLikeForScanning('Stringable');
}
if (!$this->scan_deep) {
@@ -351,7 +336,7 @@ public function enterNode(PhpParser\Node $node): ?int
$template_types,
$this->type_aliases,
);
- } catch (DocblockParseException $e) {
+ } catch (DocblockParseException) {
// do nothing
}
@@ -562,7 +547,7 @@ public function leaveNode(PhpParser\Node $node)
) {
$e = reset($functionlike_node_scanner->storage->docblock_issues);
- $fqcn_parts = explode('\\', get_class($e));
+ $fqcn_parts = explode('\\', $e::class);
$issue_type = array_pop($fqcn_parts);
$message = $e instanceof TaintedInput
diff --git a/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php b/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php
index 18137b22723..c10bc26ead5 100644
--- a/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/ShortClosureVisitor.php
@@ -16,7 +16,7 @@ final class ShortClosureVisitor extends PhpParser\NodeVisitorAbstract
/**
* @var array
*/
- protected array $used_variables = [];
+ private array $used_variables = [];
public function enterNode(PhpParser\Node $node): ?int
{
diff --git a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php
index 3b13e077bac..455a22696df 100644
--- a/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php
+++ b/src/Psalm/Internal/PhpVisitor/SimpleNameResolver.php
@@ -21,7 +21,7 @@
*/
final class SimpleNameResolver extends NodeVisitorAbstract
{
- private NameContext $nameContext;
+ private readonly NameContext $nameContext;
private ?int $start_change = null;
@@ -202,7 +202,7 @@ private function resolveType(?Node $node): ?Node
* @param Stmt\Use_::TYPE_* $type One of Stmt\Use_::TYPE_*
* @return Name Resolved name, or original name with attribute
*/
- protected function resolveName(Name $name, int $type): Name
+ private function resolveName(Name $name, int $type): Name
{
$resolvedName = $this->nameContext->getResolvedName($name, $type);
if (null !== $resolvedName) {
@@ -220,12 +220,12 @@ protected function resolveName(Name $name, int $type): Name
return $name;
}
- protected function resolveClassName(Name $name): Name
+ private function resolveClassName(Name $name): Name
{
return $this->resolveName($name, Stmt\Use_::TYPE_NORMAL);
}
- protected function addNamespacedName(Stmt\Class_ $node): void
+ private function addNamespacedName(Stmt\Class_ $node): void
{
$node->setAttribute('namespacedName', Name::concat(
$this->nameContext->getNamespace(),
@@ -233,7 +233,7 @@ protected function addNamespacedName(Stmt\Class_ $node): void
));
}
- protected function resolveAttrGroups(Stmt\Class_ $node): void
+ private function resolveAttrGroups(Stmt\Class_ $node): void
{
foreach ($node->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
@@ -242,7 +242,7 @@ protected function resolveAttrGroups(Stmt\Class_ $node): void
}
}
- protected function resolveTrait(Stmt\Trait_ $node): void
+ private function resolveTrait(Stmt\Trait_ $node): void
{
$resolvedName = Name::concat($this->nameContext->getNamespace(), (string) $node->name);
diff --git a/src/Psalm/Internal/PhpVisitor/TraitFinder.php b/src/Psalm/Internal/PhpVisitor/TraitFinder.php
index a79f8a5e21b..9decb8d8af1 100644
--- a/src/Psalm/Internal/PhpVisitor/TraitFinder.php
+++ b/src/Psalm/Internal/PhpVisitor/TraitFinder.php
@@ -24,11 +24,9 @@ final class TraitFinder extends PhpParser\NodeVisitorAbstract
/** @var list */
private array $matching_trait_nodes = [];
- private string $fq_trait_name;
-
- public function __construct(string $fq_trait_name)
- {
- $this->fq_trait_name = $fq_trait_name;
+ public function __construct(
+ private readonly string $fq_trait_name,
+ ) {
}
public function enterNode(PhpParser\Node $node, bool &$traverseChildren = true): ?int
@@ -73,7 +71,7 @@ public function getNode(): ?PhpParser\Node\Stmt\Trait_
try {
$reflection_trait = new ReflectionClass($this->fq_trait_name);
- } catch (Throwable $t) {
+ } catch (Throwable) {
return null;
}
diff --git a/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php b/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php
index 1236d8c0b47..5f9c6d7088c 100644
--- a/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php
+++ b/src/Psalm/Internal/PhpVisitor/TypeMappingVisitor.php
@@ -13,15 +13,10 @@
*/
final class TypeMappingVisitor extends NodeVisitorAbstract
{
- private NodeDataProvider $fake_type_provider;
- private NodeDataProvider $real_type_provider;
-
public function __construct(
- NodeDataProvider $fake_type_provider,
- NodeDataProvider $real_type_provider,
+ private readonly NodeDataProvider $fake_type_provider,
+ private readonly NodeDataProvider $real_type_provider,
) {
- $this->fake_type_provider = $fake_type_provider;
- $this->real_type_provider = $real_type_provider;
}
/**
diff --git a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
index ddb1360bd92..94de49e6619 100644
--- a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
+++ b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
@@ -23,11 +23,9 @@ final class YieldTypeCollector extends NodeVisitorAbstract
/** @var list */
private array $yield_types = [];
- private NodeDataProvider $nodes;
-
- public function __construct(NodeDataProvider $nodes)
- {
- $this->nodes = $nodes;
+ public function __construct(
+ private readonly NodeDataProvider $nodes,
+ ) {
}
public function enterNode(Node $node): ?int
@@ -45,7 +43,7 @@ public function enterNode(Node $node): ?int
$generator_type = new TGenericObject(
'Generator',
[
- $key_type ? $key_type : Type::getInt(),
+ $key_type ?: Type::getInt(),
$value_type,
Type::getMixed(),
Type::getMixed(),
diff --git a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php
index 9104dbaa904..a6cb1c505c8 100644
--- a/src/Psalm/Internal/PluginManager/Command/DisableCommand.php
+++ b/src/Psalm/Internal/PluginManager/Command/DisableCommand.php
@@ -25,11 +25,9 @@
*/
final class DisableCommand extends Command
{
- private PluginListFactory $plugin_list_factory;
-
- public function __construct(PluginListFactory $plugin_list_factory)
- {
- $this->plugin_list_factory = $plugin_list_factory;
+ public function __construct(
+ private readonly PluginListFactory $plugin_list_factory,
+ ) {
parent::__construct();
}
@@ -67,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
try {
$plugin_class = $plugin_list->resolvePluginClass($plugin_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$io->error('Unknown plugin class ' . $plugin_name);
return 2;
diff --git a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php
index 6a48504ddcb..71f520117f4 100644
--- a/src/Psalm/Internal/PluginManager/Command/EnableCommand.php
+++ b/src/Psalm/Internal/PluginManager/Command/EnableCommand.php
@@ -25,11 +25,9 @@
*/
final class EnableCommand extends Command
{
- private PluginListFactory $plugin_list_factory;
-
- public function __construct(PluginListFactory $plugin_list_factory)
- {
- $this->plugin_list_factory = $plugin_list_factory;
+ public function __construct(
+ private readonly PluginListFactory $plugin_list_factory,
+ ) {
parent::__construct();
}
@@ -67,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
try {
$plugin_class = $plugin_list->resolvePluginClass($plugin_name);
- } catch (InvalidArgumentException $e) {
+ } catch (InvalidArgumentException) {
$io->error('Unknown plugin class ' . $plugin_name);
return 2;
diff --git a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php
index 77a8499ec8e..e42b947f3aa 100644
--- a/src/Psalm/Internal/PluginManager/Command/ShowCommand.php
+++ b/src/Psalm/Internal/PluginManager/Command/ShowCommand.php
@@ -26,11 +26,9 @@
*/
final class ShowCommand extends Command
{
- private PluginListFactory $plugin_list_factory;
-
- public function __construct(PluginListFactory $plugin_list_factory)
- {
- $this->plugin_list_factory = $plugin_list_factory;
+ public function __construct(
+ private readonly PluginListFactory $plugin_list_factory,
+ ) {
parent::__construct();
}
diff --git a/src/Psalm/Internal/PluginManager/ComposerLock.php b/src/Psalm/Internal/PluginManager/ComposerLock.php
index 03c9733d95c..d806b09d81a 100644
--- a/src/Psalm/Internal/PluginManager/ComposerLock.php
+++ b/src/Psalm/Internal/PluginManager/ComposerLock.php
@@ -20,13 +20,10 @@
*/
final class ComposerLock
{
- /** @var string[] */
- private array $file_names;
-
/** @param string[] $file_names */
- public function __construct(array $file_names)
- {
- $this->file_names = $file_names;
+ public function __construct(
+ private readonly array $file_names,
+ ) {
}
/**
diff --git a/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php
index 0f94cd85825..bb9883c2808 100644
--- a/src/Psalm/Internal/PluginManager/ConfigFile.php
+++ b/src/Psalm/Internal/PluginManager/ConfigFile.php
@@ -23,16 +23,14 @@ final class ConfigFile
{
private string $path;
- private string $current_dir;
-
private ?string $psalm_header = null;
private ?int $psalm_tag_end_pos = null;
- public function __construct(string $current_dir, ?string $explicit_path)
- {
- $this->current_dir = $current_dir;
-
+ public function __construct(
+ private readonly string $current_dir,
+ ?string $explicit_path,
+ ) {
if ($explicit_path) {
$this->path = $explicit_path;
} else {
diff --git a/src/Psalm/Internal/PluginManager/PluginList.php b/src/Psalm/Internal/PluginManager/PluginList.php
index a00aad407bb..b25536ffe27 100644
--- a/src/Psalm/Internal/PluginManager/PluginList.php
+++ b/src/Psalm/Internal/PluginManager/PluginList.php
@@ -11,27 +11,23 @@
use function array_flip;
use function array_key_exists;
use function array_search;
-use function strpos;
+use function str_contains;
/**
* @internal
*/
final class PluginList
{
- private ?ConfigFile $config_file = null;
-
- private ComposerLock $composer_lock;
-
/** @var ?array [pluginClass => packageName] */
private ?array $all_plugins = null;
/** @var ?array [pluginClass => ?packageName] */
private ?array $enabled_plugins = null;
- public function __construct(?ConfigFile $config_file, ComposerLock $composer_lock)
- {
- $this->config_file = $config_file;
- $this->composer_lock = $composer_lock;
+ public function __construct(
+ private readonly ?ConfigFile $config_file,
+ private readonly ComposerLock $composer_lock,
+ ) {
}
/**
@@ -74,7 +70,7 @@ public function getAll(): array
public function resolvePluginClass(string $class_or_package): string
{
- if (false === strpos($class_or_package, '/')) {
+ if (!str_contains($class_or_package, '/')) {
return $class_or_package; // must be a class then
}
diff --git a/src/Psalm/Internal/PluginManager/PluginListFactory.php b/src/Psalm/Internal/PluginManager/PluginListFactory.php
index 659f72dcebc..f3fab24774c 100644
--- a/src/Psalm/Internal/PluginManager/PluginListFactory.php
+++ b/src/Psalm/Internal/PluginManager/PluginListFactory.php
@@ -20,21 +20,17 @@
*/
final class PluginListFactory
{
- private string $project_root;
-
- private string $psalm_root;
-
- public function __construct(string $project_root, string $psalm_root)
- {
- $this->project_root = $project_root;
- $this->psalm_root = $psalm_root;
+ public function __construct(
+ private readonly string $project_root,
+ private readonly string $psalm_root,
+ ) {
}
public function __invoke(string $current_dir, ?string $config_file_path = null): PluginList
{
try {
$config_file = new ConfigFile($current_dir, $config_file_path);
- } catch (RuntimeException $exception) {
+ } catch (RuntimeException) {
$config_file = null;
}
$composer_lock = new ComposerLock($this->findLockFiles());
diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php
index a6b5033990e..266284ee376 100644
--- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php
+++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php
@@ -14,7 +14,6 @@
use function dirname;
use function file_exists;
use function filemtime;
-use function get_class;
use function hash;
use function is_dir;
use function is_null;
@@ -22,14 +21,13 @@
use function strtolower;
use const DIRECTORY_SEPARATOR;
-use const PHP_VERSION_ID;
/**
* @internal
*/
class ClassLikeStorageCacheProvider
{
- private Cache $cache;
+ private readonly Cache $cache;
private string $modified_timestamps = '';
@@ -96,7 +94,7 @@ public function getLatestFromCache(
$cache_hash = $this->getCacheHash($file_path, $file_contents);
/** @psalm-suppress TypeDoesNotContainType */
- if (@get_class($cached_value) === '__PHP_Incomplete_Class'
+ if (@$cached_value::class === '__PHP_Incomplete_Class'
|| $cache_hash !== $cached_value->hash
) {
$this->cache->deleteItem($this->getCacheLocationForClass($fq_classlike_name_lc, $file_path));
@@ -109,8 +107,8 @@ public function getLatestFromCache(
private function getCacheHash(?string $_unused_file_path, ?string $file_contents): string
{
- $data = $file_contents ? $file_contents : $this->modified_timestamps;
- return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data);
+ $data = $file_contents ?: $this->modified_timestamps;
+ return hash('xxh128', $data);
}
/**
@@ -162,7 +160,7 @@ private function getCacheLocationForClass(
$data = $file_path ? strtolower($file_path) . ' ' : '';
$data .= $fq_classlike_name_lc;
- $file_path_sha = PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data);
+ $file_path_sha = hash('xxh128', $data);
return $parser_cache_directory
. DIRECTORY_SEPARATOR
diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php
index 8260b207d18..1be26f84398 100644
--- a/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php
+++ b/src/Psalm/Internal/Provider/ClassLikeStorageProvider.php
@@ -8,7 +8,6 @@
use LogicException;
use Psalm\Storage\ClassLikeStorage;
-use function array_merge;
use function strtolower;
/**
@@ -28,11 +27,8 @@ final class ClassLikeStorageProvider
*/
private static array $new_storage = [];
- public ?ClassLikeStorageCacheProvider $cache = null;
-
- public function __construct(?ClassLikeStorageCacheProvider $cache = null)
+ public function __construct(public ?ClassLikeStorageCacheProvider $cache = null)
{
- $this->cache = $cache;
}
/**
@@ -103,8 +99,8 @@ public function getNew(): array
*/
public function addMore(array $more): void
{
- self::$new_storage = array_merge(self::$new_storage, $more);
- self::$storage = array_merge(self::$storage, $more);
+ self::$new_storage = [...self::$new_storage, ...$more];
+ self::$storage = [...self::$storage, ...$more];
}
public function makeNew(string $fq_classlike_name_lc): void
diff --git a/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php b/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php
index 3f0aca0ec03..9babb09b7c4 100644
--- a/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php
+++ b/src/Psalm/Internal/Provider/DynamicFunctionStorageProvider.php
@@ -37,7 +37,7 @@ final class DynamicFunctionStorageProvider
*/
public function registerClass(string $class): void
{
- $callable = Closure::fromCallable([$class, 'getFunctionStorage']);
+ $callable = $class::getFunctionStorage(...);
foreach ($class::getFunctionIds() as $function_id) {
$this->registerClosure($function_id, $callable);
diff --git a/src/Psalm/Internal/Provider/FakeFileProvider.php b/src/Psalm/Internal/Provider/FakeFileProvider.php
index a5e18fd7bb8..134243213b7 100644
--- a/src/Psalm/Internal/Provider/FakeFileProvider.php
+++ b/src/Psalm/Internal/Provider/FakeFileProvider.php
@@ -5,7 +5,7 @@
namespace Psalm\Internal\Provider;
use function microtime;
-use function strpos;
+use function str_starts_with;
/**
* @internal
@@ -86,7 +86,7 @@ public function getFilesInDir(string $dir_path, array $file_extensions, callable
$file_paths = parent::getFilesInDir($dir_path, $file_extensions, $filter);
foreach ($this->fake_files as $file_path => $_) {
- if (strpos($file_path, $dir_path) === 0) {
+ if (str_starts_with($file_path, $dir_path)) {
$file_paths[] = $file_path;
}
}
diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php
index b070f7f3625..2f26b8e32b7 100644
--- a/src/Psalm/Internal/Provider/FileProvider.php
+++ b/src/Psalm/Internal/Provider/FileProvider.php
@@ -170,7 +170,7 @@ public function getFilesInDir(string $dir_path, array $file_extensions, callable
$iterator = new RecursiveCallbackFilterIterator(
$iterator,
/** @param mixed $_ */
- static function (string $current, $_, RecursiveIterator $iterator) use ($filter): bool {
+ static function (string $current, mixed $_, RecursiveIterator $iterator) use ($filter): bool {
if ($iterator->hasChildren()) {
$path = $current . DIRECTORY_SEPARATOR;
} else {
diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php
index e91df5dde67..fefb161bce5 100644
--- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php
+++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php
@@ -285,7 +285,8 @@ public function setTypeCoverage(array $mixed_counts): void
$this->saveCacheItem(self::TYPE_COVERAGE_CACHE_NAME, $mixed_counts);
}
- public function getConfigHashCache(): string|false
+ /** @return string|false */
+ public function getConfigHashCache(): string|bool
{
$cache_directory = $this->config->getCacheDirectory();
diff --git a/src/Psalm/Internal/Provider/FileReferenceProvider.php b/src/Psalm/Internal/Provider/FileReferenceProvider.php
index 0e74f56e687..77f161b61e5 100644
--- a/src/Psalm/Internal/Provider/FileReferenceProvider.php
+++ b/src/Psalm/Internal/Provider/FileReferenceProvider.php
@@ -165,13 +165,10 @@ final class FileReferenceProvider
*/
private static array $method_param_uses = [];
- private FileProvider $file_provider;
- public ?FileReferenceCacheProvider $cache = null;
-
- public function __construct(FileProvider $file_provider, ?FileReferenceCacheProvider $cache = null)
- {
- $this->file_provider = $file_provider;
- $this->cache = $cache;
+ public function __construct(
+ private readonly FileProvider $file_provider,
+ public ?FileReferenceCacheProvider $cache = null,
+ ) {
}
/**
@@ -386,7 +383,7 @@ private function calculateFilesReferencingFile(Codebase $codebase, string $file)
try {
$referenced_files[] = $codebase->scanner->getClassLikeFilePath($fq_class_name_lc);
- } catch (UnexpectedValueException $e) {
+ } catch (UnexpectedValueException) {
if (isset(self::$classlike_files[$fq_class_name_lc])) {
$referenced_files[] = self::$classlike_files[$fq_class_name_lc];
}
@@ -1233,7 +1230,7 @@ public function getTypeCoverage(): array
*/
public function setTypeCoverage(array $mixed_counts): void
{
- self::$mixed_counts = array_merge(self::$mixed_counts, $mixed_counts);
+ self::$mixed_counts = [...self::$mixed_counts, ...$mixed_counts];
}
/**
diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php
index c660e0c8324..a8b6f83a539 100644
--- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php
+++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php
@@ -14,14 +14,12 @@
use function dirname;
use function file_exists;
use function filemtime;
-use function get_class;
use function hash;
use function is_dir;
use function mkdir;
use function strtolower;
use const DIRECTORY_SEPARATOR;
-use const PHP_VERSION_ID;
/**
* @internal
@@ -30,7 +28,7 @@ class FileStorageCacheProvider
{
private string $modified_timestamps = '';
- private Cache $cache;
+ private readonly Cache $cache;
private const FILE_STORAGE_CACHE_DIRECTORY = 'file_cache';
@@ -92,7 +90,7 @@ public function getLatestFromCache(string $file_path, string $file_contents): ?F
$cache_hash = $this->getCacheHash($file_path, $file_contents);
/** @psalm-suppress TypeDoesNotContainType */
- if (@get_class($cached_value) === '__PHP_Incomplete_Class'
+ if (@$cached_value::class === '__PHP_Incomplete_Class'
|| $cache_hash !== $cached_value->hash
) {
$this->removeCacheForFile($file_path);
@@ -113,8 +111,8 @@ private function getCacheHash(string $_unused_file_path, string $file_contents):
// do not concatenate, as $file_contents can be big and performance will be bad
// the timestamp is only needed if we don't have file contents
// as same contents should give same results, independent of when file was modified
- $data = $file_contents ? $file_contents : $this->modified_timestamps;
- return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data);
+ $data = $file_contents ?: $this->modified_timestamps;
+ return hash('xxh128', $data);
}
/**
@@ -158,11 +156,7 @@ private function getCacheLocationForPath(string $file_path, bool $create_directo
}
}
- if (PHP_VERSION_ID >= 8_01_00) {
- $hash = hash('xxh128', $file_path);
- } else {
- $hash = hash('md4', $file_path);
- }
+ $hash = hash('xxh128', $file_path);
return $parser_cache_directory
. DIRECTORY_SEPARATOR
diff --git a/src/Psalm/Internal/Provider/FileStorageProvider.php b/src/Psalm/Internal/Provider/FileStorageProvider.php
index 059f6d7fabd..747a11e6d60 100644
--- a/src/Psalm/Internal/Provider/FileStorageProvider.php
+++ b/src/Psalm/Internal/Provider/FileStorageProvider.php
@@ -7,7 +7,6 @@
use InvalidArgumentException;
use Psalm\Storage\FileStorage;
-use function array_merge;
use function strtolower;
/**
@@ -31,11 +30,8 @@ final class FileStorageProvider
*/
private static array $new_storage = [];
- public ?FileStorageCacheProvider $cache = null;
-
- public function __construct(?FileStorageCacheProvider $cache = null)
+ public function __construct(public ?FileStorageCacheProvider $cache = null)
{
- $this->cache = $cache;
}
public function get(string $file_path): FileStorage
@@ -103,8 +99,8 @@ public function getNew(): array
*/
public function addMore(array $more): void
{
- self::$new_storage = array_merge(self::$new_storage, $more);
- self::$storage = array_merge(self::$storage, $more);
+ self::$new_storage = [...self::$new_storage, ...$more];
+ self::$storage = [...self::$storage, ...$more];
}
public function create(string $file_path): FileStorage
diff --git a/src/Psalm/Internal/Provider/FunctionExistenceProvider.php b/src/Psalm/Internal/Provider/FunctionExistenceProvider.php
index f5527c98075..7e84726d4e5 100644
--- a/src/Psalm/Internal/Provider/FunctionExistenceProvider.php
+++ b/src/Psalm/Internal/Provider/FunctionExistenceProvider.php
@@ -36,7 +36,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, FunctionExistenceProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'doesFunctionExist']);
+ $callable = $class::doesFunctionExist(...);
foreach ($class::getFunctionIds() as $function_id) {
$this->registerClosure($function_id, $callable);
diff --git a/src/Psalm/Internal/Provider/FunctionParamsProvider.php b/src/Psalm/Internal/Provider/FunctionParamsProvider.php
index f7ce8e60e2a..9a627bdb3c7 100644
--- a/src/Psalm/Internal/Provider/FunctionParamsProvider.php
+++ b/src/Psalm/Internal/Provider/FunctionParamsProvider.php
@@ -43,7 +43,7 @@ public function __construct()
*/
public function registerClass(string $class): void
{
- $callable = Closure::fromCallable([$class, 'getFunctionParams']);
+ $callable = $class::getFunctionParams(...);
foreach ($class::getFunctionIds() as $function_id) {
$this->registerClosure($function_id, $callable);
diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
index de3a11462b6..665be17edda 100644
--- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
@@ -27,6 +27,7 @@
use Psalm\Internal\Provider\ReturnTypeProvider\BasenameReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DateReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DirnameReturnTypeProvider;
+use Psalm\Internal\Provider\ReturnTypeProvider\FilterInputReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\GetClassMethodsReturnTypeProvider;
@@ -87,6 +88,7 @@ public function __construct()
$this->registerClass(ArrayReverseReturnTypeProvider::class);
$this->registerClass(ArrayFillReturnTypeProvider::class);
$this->registerClass(ArrayFillKeysReturnTypeProvider::class);
+ $this->registerClass(FilterInputReturnTypeProvider::class);
$this->registerClass(FilterVarReturnTypeProvider::class);
$this->registerClass(IteratorToArrayReturnTypeProvider::class);
$this->registerClass(ParseUrlReturnTypeProvider::class);
@@ -117,7 +119,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, FunctionReturnTypeProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'getFunctionReturnType']);
+ $callable = $class::getFunctionReturnType(...);
foreach ($class::getFunctionIds() as $function_id) {
$this->registerClosure($function_id, $callable);
diff --git a/src/Psalm/Internal/Provider/MethodExistenceProvider.php b/src/Psalm/Internal/Provider/MethodExistenceProvider.php
index 657fddd0de5..af345f77e3e 100644
--- a/src/Psalm/Internal/Provider/MethodExistenceProvider.php
+++ b/src/Psalm/Internal/Provider/MethodExistenceProvider.php
@@ -35,7 +35,7 @@ public function __construct()
*/
public function registerClass(string $class): void
{
- $callable = Closure::fromCallable([$class, 'doesMethodExist']);
+ $callable = $class::doesMethodExist(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/MethodParamsProvider.php b/src/Psalm/Internal/Provider/MethodParamsProvider.php
index 58e225b787a..8599c4b2916 100644
--- a/src/Psalm/Internal/Provider/MethodParamsProvider.php
+++ b/src/Psalm/Internal/Provider/MethodParamsProvider.php
@@ -44,7 +44,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, MethodParamsProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'getMethodParams']);
+ $callable = $class::getMethodParams(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php
index 575b257caa3..1e57683a2ea 100644
--- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php
@@ -51,7 +51,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, MethodReturnTypeProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'getMethodReturnType']);
+ $callable = $class::getMethodReturnType(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/MethodVisibilityProvider.php b/src/Psalm/Internal/Provider/MethodVisibilityProvider.php
index fcda0291d9c..a9d88825641 100644
--- a/src/Psalm/Internal/Provider/MethodVisibilityProvider.php
+++ b/src/Psalm/Internal/Provider/MethodVisibilityProvider.php
@@ -39,7 +39,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, MethodVisibilityProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'isMethodVisible']);
+ $callable = $class::isMethodVisible(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php
index 739fc54d852..0c6bba2af47 100644
--- a/src/Psalm/Internal/Provider/ParserCacheProvider.php
+++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php
@@ -32,7 +32,6 @@
use const DIRECTORY_SEPARATOR;
use const JSON_THROW_ON_ERROR;
use const LOCK_EX;
-use const PHP_VERSION_ID;
use const SCANDIR_SORT_NONE;
/**
@@ -44,7 +43,7 @@ class ParserCacheProvider
private const PARSER_CACHE_DIRECTORY = 'php-parser';
private const FILE_CONTENTS_CACHE_DIRECTORY = 'file-caches';
- private Cache $cache;
+ private readonly Cache $cache;
/**
* A map of filename hashes to contents hashes
@@ -60,12 +59,11 @@ class ParserCacheProvider
*/
protected array $new_file_content_hashes = [];
- private bool $use_file_cache;
-
- public function __construct(Config $config, bool $use_file_cache = true)
- {
+ public function __construct(
+ Config $config,
+ private readonly bool $use_file_cache = true,
+ ) {
$this->cache = new Cache($config);
- $this->use_file_cache = $use_file_cache;
}
/**
@@ -202,7 +200,7 @@ private function getExistingFileContentHashes(): array
}
/** @psalm-suppress MixedAssignment */
- $hashes_decoded = json_decode($hashes_encoded, true);
+ $hashes_decoded = json_decode($hashes_encoded, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($hashes_decoded)) {
throw new UnexpectedValueException(
@@ -333,11 +331,7 @@ public function deleteOldParserCaches(float $time_before): int
private function getParserCacheKey(string $file_path): string
{
- if (PHP_VERSION_ID >= 8_01_00) {
- $hash = hash('xxh128', $file_path);
- } else {
- $hash = hash('md4', $file_path);
- }
+ $hash = hash('xxh128', $file_path);
return $hash . ($this->cache->use_igbinary ? '-igbinary' : '') . '-r';
}
diff --git a/src/Psalm/Internal/Provider/ProjectCacheProvider.php b/src/Psalm/Internal/Provider/ProjectCacheProvider.php
index 8bfe3c41e24..6200430f8dd 100644
--- a/src/Psalm/Internal/Provider/ProjectCacheProvider.php
+++ b/src/Psalm/Internal/Provider/ProjectCacheProvider.php
@@ -14,7 +14,6 @@
use function touch;
use const DIRECTORY_SEPARATOR;
-use const PHP_VERSION_ID;
/**
* Used to determine which files reference other files, necessary for using the --diff
@@ -31,11 +30,9 @@ class ProjectCacheProvider
private ?string $composer_lock_hash = null;
- private string $composer_lock_location;
-
- public function __construct(string $composer_lock_location)
- {
- $this->composer_lock_location = $composer_lock_location;
+ public function __construct(
+ private readonly string $composer_lock_location,
+ ) {
}
public function canDiffFiles(): bool
@@ -86,11 +83,7 @@ public function hasLockfileChanged(): bool
return true;
}
- if (PHP_VERSION_ID >= 8_01_00) {
- $hash = hash('xxh128', $lockfile_contents);
- } else {
- $hash = hash('md4', $lockfile_contents);
- }
+ $hash = hash('xxh128', $lockfile_contents);
} else {
$hash = '';
}
@@ -119,7 +112,7 @@ public function updateComposerLockHash(): void
file_put_contents($lock_hash_location, $this->composer_lock_hash);
}
- protected function getComposerLockHash(): string
+ private function getComposerLockHash(): string
{
if ($this->composer_lock_hash === null) {
$cache_directory = Config::getInstance()->getCacheDirectory();
diff --git a/src/Psalm/Internal/Provider/PropertyExistenceProvider.php b/src/Psalm/Internal/Provider/PropertyExistenceProvider.php
index d10e6f2479d..9f6e7b1a5e9 100644
--- a/src/Psalm/Internal/Provider/PropertyExistenceProvider.php
+++ b/src/Psalm/Internal/Provider/PropertyExistenceProvider.php
@@ -39,7 +39,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, PropertyExistenceProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'doesPropertyExist']);
+ $callable = $class::doesPropertyExist(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/PropertyTypeProvider.php b/src/Psalm/Internal/Provider/PropertyTypeProvider.php
index 71b9231a7f9..19687569af1 100644
--- a/src/Psalm/Internal/Provider/PropertyTypeProvider.php
+++ b/src/Psalm/Internal/Provider/PropertyTypeProvider.php
@@ -41,7 +41,7 @@ public function __construct()
public function registerClass(string $class): void
{
if (is_subclass_of($class, PropertyTypeProviderInterface::class, true)) {
- $callable = Closure::fromCallable([$class, 'getPropertyType']);
+ $callable = $class::getPropertyType(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php b/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php
index 1af11782fa5..a9176a4fec2 100644
--- a/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php
+++ b/src/Psalm/Internal/Provider/PropertyVisibilityProvider.php
@@ -36,7 +36,7 @@ public function __construct()
*/
public function registerClass(string $class): void
{
- $callable = Closure::fromCallable([$class, 'isPropertyVisible']);
+ $callable = $class::isPropertyVisible(...);
foreach ($class::getClassLikeNames() as $fq_classlike_name) {
$this->registerClosure($fq_classlike_name, $callable);
diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php
index 673a6ab2e2b..e6d5707dfd7 100644
--- a/src/Psalm/Internal/Provider/Providers.php
+++ b/src/Psalm/Internal/Provider/Providers.php
@@ -20,10 +20,6 @@
*/
final class Providers
{
- public FileProvider $file_provider;
-
- public ?ParserCacheProvider $parser_cache_provider = null;
-
public FileStorageProvider $file_storage_provider;
public ClassLikeStorageProvider $classlike_storage_provider;
@@ -32,20 +28,14 @@ final class Providers
public FileReferenceProvider $file_reference_provider;
- public ?ProjectCacheProvider $project_cache_provider = null;
-
public function __construct(
- FileProvider $file_provider,
- ?ParserCacheProvider $parser_cache_provider = null,
+ public FileProvider $file_provider,
+ public ?ParserCacheProvider $parser_cache_provider = null,
?FileStorageCacheProvider $file_storage_cache_provider = null,
?ClassLikeStorageCacheProvider $classlike_storage_cache_provider = null,
?FileReferenceCacheProvider $file_reference_cache_provider = null,
- ?ProjectCacheProvider $project_cache_provider = null,
+ public ?ProjectCacheProvider $project_cache_provider = null,
) {
- $this->file_provider = $file_provider;
- $this->parser_cache_provider = $parser_cache_provider;
- $this->project_cache_provider = $project_cache_provider;
-
$this->file_storage_provider = new FileStorageProvider($file_storage_cache_provider);
$this->classlike_storage_provider = new ClassLikeStorageProvider($classlike_storage_cache_provider);
$this->statements_provider = new StatementsProvider(
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php
index f8230115991..69cb75a24ed 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFillReturnTypeProvider.php
@@ -39,7 +39,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$second_arg_type = isset($call_args[1]) ? $statements_source->node_data->getType($call_args[1]->value) : null;
$third_arg_type = isset($call_args[2]) ? $statements_source->node_data->getType($call_args[2]->value) : null;
- $value_type_from_third_arg = $third_arg_type ? $third_arg_type : Type::getMixed();
+ $value_type_from_third_arg = $third_arg_type ?: Type::getMixed();
if ($first_arg_type && $second_arg_type && $third_arg_type
&& $first_arg_type->isSingleIntLiteral()
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php
index baca7ce6579..edf35f8b169 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php
@@ -293,7 +293,7 @@ static function ($keyed_type) use ($statements_source, $context) {
$statements_source,
$codebase,
);
- } catch (ComplicatedExpressionException $e) {
+ } catch (ComplicatedExpressionException) {
$filter_clauses = [];
}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php
index 4905aba9ccf..795615b983f 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php
@@ -44,7 +44,7 @@
use function in_array;
use function mt_rand;
use function reset;
-use function strpos;
+use function str_contains;
use function substr;
/**
@@ -417,7 +417,7 @@ public static function getReturnTypeFromMappingIds(
);
}
- if (strpos($mapping_function_id_part, '::') !== false) {
+ if (str_contains($mapping_function_id_part, '::')) {
$is_instance = false;
if ($mapping_function_id_part[0] === '$') {
@@ -527,7 +527,7 @@ public static function getReturnTypeFromMappingIds(
public static function cleanContext(Context $context, int $fake_var_discriminator): void
{
foreach ($context->vars_in_scope as $var_in_scope => $_) {
- if (strpos($var_in_scope, "__fake_{$fake_var_discriminator}_") !== false) {
+ if (str_contains($var_in_scope, "__fake_{$fake_var_discriminator}_")) {
unset($context->vars_in_scope[$var_in_scope]);
}
}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php
index 5a62a189743..42e146c36cc 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php
@@ -29,7 +29,7 @@ final class ArrayPointerAdjustmentReturnTypeProvider implements FunctionReturnTy
/**
* These functions are already handled by the CoreGenericFunctions stub
*/
- const IGNORE_FUNCTION_IDS_FOR_FALSE_RETURN_TYPE = [
+ public const IGNORE_FUNCTION_IDS_FOR_FALSE_RETURN_TYPE = [
'reset',
'end',
'current',
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php
index c316e7debb1..cd29cf75535 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php
@@ -25,7 +25,7 @@
use function explode;
use function in_array;
use function reset;
-use function strpos;
+use function str_contains;
use function strtolower;
use function substr;
@@ -218,7 +218,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$part_match_found = true;
}
} elseif ($mapping_function_id_part) {
- if (strpos($mapping_function_id_part, '::') !== false) {
+ if (str_contains($mapping_function_id_part, '::')) {
if ($mapping_function_id_part[0] === '$') {
$mapping_function_id_part = substr($mapping_function_id_part, 1);
}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php
index 8e11092326e..487f1dcffb4 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/BasenameReturnTypeProvider.php
@@ -44,7 +44,48 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
);
if ($evaled_path === null) {
- return Type::getString();
+ $union = $statements_source->getNodeTypeProvider()->getType($call_args[0]->value);
+ $generic = false;
+ $non_empty = false;
+ if ($union !== null) {
+ foreach ($union->getAtomicTypes() as $atomic) {
+ if ($atomic instanceof Type\Atomic\TNonFalsyString) {
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TLiteralString) {
+ if ($atomic->value === '') {
+ $generic = true;
+ break;
+ }
+
+ if ($atomic->value === '0') {
+ $non_empty = true;
+ continue;
+ }
+
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TNonEmptyString) {
+ $non_empty = true;
+ continue;
+ }
+
+ $generic = true;
+ break;
+ }
+ }
+
+ if ($union === null || $generic) {
+ return Type::getString();
+ }
+
+ if ($non_empty) {
+ return Type::getNonEmptyString();
+ }
+
+ return Type::getNonFalsyString();
}
$basename = basename($evaled_path);
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php
index f6c63c47ac4..ff4fbc54fed 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DirnameReturnTypeProvider.php
@@ -9,7 +9,6 @@
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralInt;
-use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Union;
use function array_values;
@@ -39,6 +38,41 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$statements_source = $event->getStatementsSource();
$node_type_provider = $statements_source->getNodeTypeProvider();
+ $union = $node_type_provider->getType($call_args[0]->value);
+ $generic = false;
+ if ($union !== null) {
+ foreach ($union->getAtomicTypes() as $atomic) {
+ if ($atomic instanceof Type\Atomic\TNonFalsyString) {
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TLiteralString) {
+ if ($atomic->value === '') {
+ $generic = true;
+ break;
+ }
+
+ // 0 will be non-falsy too (.)
+ continue;
+ }
+
+ if ($atomic instanceof Type\Atomic\TNonEmptyString
+ || $atomic instanceof Type\Atomic\TEmptyNumeric) {
+ continue;
+ }
+
+ // generic string is the only other possible case of empty string
+ // which would result in a generic string
+ $generic = true;
+ break;
+ }
+ }
+
+ $fallback_type = Type::getNonFalsyString();
+ if ($union === null || $generic) {
+ $fallback_type = Type::getString();
+ }
+
$dir_level = 1;
if (isset($call_args[1])) {
$type = $node_type_provider->getType($call_args[1]->value);
@@ -49,7 +83,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$atomic_type->value > 0) {
$dir_level = $atomic_type->value;
} else {
- return Type::getString();
+ return $fallback_type;
}
}
}
@@ -63,17 +97,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
);
if ($evaled_path === null) {
- $type = $node_type_provider->getType($call_args[0]->value);
- if ($type !== null && $type->isSingle()) {
- $atomic_type = array_values($type->getAtomicTypes())[0];
- if ($atomic_type instanceof TNonEmptyString) {
- return Type::getNonEmptyString();
- }
- }
- }
-
- if ($evaled_path === null) {
- return Type::getString();
+ return $fallback_type;
}
$path_to_file = dirname($evaled_path, $dir_level);
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php
new file mode 100644
index 00000000000..b3bc3ad48b4
--- /dev/null
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php
@@ -0,0 +1,253 @@
+
+ */
+ public static function getFunctionIds(): array
+ {
+ return ['filter_input'];
+ }
+
+ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
+ {
+ $statements_analyzer = $event->getStatementsSource();
+ if (! $statements_analyzer instanceof StatementsAnalyzer) {
+ throw new UnexpectedValueException('Expected StatementsAnalyzer not StatementsSource');
+ }
+
+ $call_args = $event->getCallArgs();
+ $function_id = $event->getFunctionId();
+ $code_location = $event->getCodeLocation();
+ $codebase = $statements_analyzer->getCodebase();
+
+ if (! isset($call_args[0]) || ! isset($call_args[1])) {
+ return FilterUtils::missingFirstArg($codebase);
+ }
+
+ $first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value);
+ if ($first_arg_type && ! $first_arg_type->isInt()) {
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // default option won't be used in this case
+ return Type::getNull();
+ }
+
+ $filter_int_used = FILTER_DEFAULT;
+ if (isset($call_args[2])) {
+ $filter_int_used = FilterUtils::getFilterArgValueOrError(
+ $call_args[2],
+ $statements_analyzer,
+ $codebase,
+ );
+
+ if (!is_int($filter_int_used)) {
+ return $filter_int_used;
+ }
+ }
+
+ $options = null;
+ $flags_int_used = FILTER_FLAG_NONE;
+ if (isset($call_args[3])) {
+ $helper = FilterUtils::getOptionsArgValueOrError(
+ $call_args[3],
+ $statements_analyzer,
+ $codebase,
+ $code_location,
+ $function_id,
+ $filter_int_used,
+ );
+
+ if (!is_array($helper)) {
+ return $helper;
+ }
+
+ $flags_int_used = $helper['flags_int_used'];
+ $options = $helper['options'];
+ }
+
+ // if we reach this point with callback, the callback is missing
+ if ($filter_int_used === FILTER_CALLBACK) {
+ return FilterUtils::missingFilterCallbackCallable(
+ $function_id,
+ $code_location,
+ $statements_analyzer,
+ $codebase,
+ );
+ }
+
+ [$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions(
+ $filter_int_used,
+ $flags_int_used,
+ $options,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ );
+
+ // only return now, as we still want to report errors above
+ if (!$first_arg_type) {
+ return null;
+ }
+
+ if (! $first_arg_type->isSingleIntLiteral()) {
+ // eventually complex cases can be handled too, however practically this is irrelevant
+ return null;
+ }
+
+ if (!$default) {
+ [$fails_type, $not_set_type, $fails_or_not_set_type] = FilterUtils::getFailsNotSetType($flags_int_used);
+ } else {
+ $fails_type = $default;
+ $not_set_type = $default;
+ $fails_or_not_set_type = $default;
+ }
+
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) {
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // any "array" flags are ignored by this filter!
+ return $fails_or_not_set_type;
+ }
+
+ $possible_types = array(
+ '$_GET' => INPUT_GET,
+ '$_POST' => INPUT_POST,
+ '$_COOKIE' => INPUT_COOKIE,
+ '$_SERVER' => INPUT_SERVER,
+ '$_ENV' => INPUT_ENV,
+ );
+
+ $first_arg_type_type = $first_arg_type->getSingleIntLiteral();
+ $global_name = array_search($first_arg_type_type->value, $possible_types);
+ if (!$global_name) {
+ // invalid
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // the "not set type" is never in an array, even if FILTER_FORCE_ARRAY is set!
+ return $not_set_type;
+ }
+
+ $second_arg_type = $statements_analyzer->node_data->getType($call_args[1]->value);
+ if (!$second_arg_type) {
+ return null;
+ }
+
+ if (! $second_arg_type->hasString()) {
+ // for filter_input there can only be string array keys
+ return $not_set_type;
+ }
+
+ if (! $second_arg_type->isString()) {
+ // already reports an error by default
+ return null;
+ }
+
+ // in all these cases it can fail or be not set, depending on whether the variable is set or not
+ $redundant_error_return_type = FilterUtils::checkRedundantFlags(
+ $filter_int_used,
+ $flags_int_used,
+ $fails_or_not_set_type,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ );
+ if ($redundant_error_return_type !== null) {
+ return $redundant_error_return_type;
+ }
+
+ if (FilterUtils::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && in_array($first_arg_type_type->value, array(INPUT_COOKIE, INPUT_SERVER, INPUT_ENV), true)) {
+ // these globals can never be an array
+ return $fails_or_not_set_type;
+ }
+
+ // @todo eventually this needs to be changed when we fully support filter_has_var
+ $global_type = VariableFetchAnalyzer::getGlobalType($global_name, $codebase->analysis_php_version_id);
+
+ $input_type = null;
+ if ($global_type->isArray() && $global_type->getArray() instanceof TKeyedArray) {
+ $array_instance = $global_type->getArray();
+ if ($second_arg_type->isSingleStringLiteral()) {
+ $key = $second_arg_type->getSingleStringLiteral()->value;
+
+ if (isset($array_instance->properties[ $key ])) {
+ $input_type = $array_instance->properties[ $key ];
+ }
+ }
+
+ if ($input_type === null) {
+ $input_type = $array_instance->getGenericValueType();
+ $input_type = $input_type->setPossiblyUndefined(true);
+ }
+ } elseif ($global_type->isArray()
+ && ($array_atomic = $global_type->getArray())
+ && $array_atomic instanceof TArray) {
+ [$_, $input_type] = $array_atomic->type_params;
+ $input_type = $input_type->setPossiblyUndefined(true);
+ } else {
+ // this is impossible
+ throw new UnexpectedValueException('This should not happen');
+ }
+
+ return FilterUtils::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $input_type,
+ $fails_type,
+ $not_set_type,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ );
+ }
+}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
new file mode 100644
index 00000000000..c026134f434
--- /dev/null
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterUtils.php
@@ -0,0 +1,1728 @@
+analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ return Type::getNull();
+ }
+
+ public static function getFilterArgValueOrError(
+ Arg $filter_arg,
+ StatementsAnalyzer $statements_analyzer,
+ Codebase $codebase,
+ ): int|Union|null {
+ $filter_arg_type = $statements_analyzer->node_data->getType($filter_arg->value);
+ if (!$filter_arg_type) {
+ return null;
+ }
+
+ if (! $filter_arg_type->isInt()) {
+ // invalid
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // will return null independent of FILTER_NULL_ON_FAILURE or default option
+ return Type::getNull();
+ }
+
+ if (! $filter_arg_type->isSingleIntLiteral()) {
+ // too complex for now
+ return null;
+ }
+
+ $all_filters = self::getFilters($codebase);
+ $filter_int_used = $filter_arg_type->getSingleIntLiteral()->value;
+ if (! isset($all_filters[ $filter_int_used ])) {
+ // inconsistently, this will always return false, even when FILTER_NULL_ON_FAILURE
+ // or a default option is set
+ // and will also not use any default set
+ return Type::getFalse();
+ }
+
+ return $filter_int_used;
+ }
+
+ /** @return array{flags_int_used: int, options: TKeyedArray|null}|Union|null */
+ public static function getOptionsArgValueOrError(
+ Arg $options_arg,
+ StatementsAnalyzer $statements_analyzer,
+ Codebase $codebase,
+ CodeLocation $code_location,
+ string $function_id,
+ int $filter_int_used,
+ ): array|Union|null {
+ $options_arg_type = $statements_analyzer->node_data->getType($options_arg->value);
+ if (!$options_arg_type) {
+ return null;
+ }
+
+ if ($options_arg_type->isArray()) {
+ $return_null = false;
+ $defaults = array(
+ 'flags_int_used' => FILTER_FLAG_NONE,
+ 'options' => null,
+ );
+
+ $atomic_type = $options_arg_type->getArray();
+ if ($atomic_type instanceof TKeyedArray) {
+ $redundant_keys = array_diff(array_keys($atomic_type->properties), array('flags', 'options'));
+ if ($redundant_keys !== array()) {
+ // reported as it's usually an oversight/misunderstanding of how the function works
+ // it's silently ignored by the function though
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'The options array contains unused keys '
+ . implode(', ', $redundant_keys),
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (isset($atomic_type->properties['options'])) {
+ if ($filter_int_used === FILTER_CALLBACK) {
+ $only_callables = true;
+ foreach ($atomic_type->properties['options']->getAtomicTypes() as $option_atomic) {
+ if ($option_atomic->isCallableType()) {
+ continue;
+ }
+
+ if (CallableTypeComparator::getCallableFromAtomic(
+ $codebase,
+ $option_atomic,
+ null,
+ $statements_analyzer,
+ )) {
+ continue;
+ }
+
+ $only_callables = false;
+ }
+ if ($atomic_type->properties['options']->possibly_undefined) {
+ $only_callables = false;
+ }
+
+ if (!$only_callables) {
+ return self::missingFilterCallbackCallable(
+ $function_id,
+ $code_location,
+ $statements_analyzer,
+ $codebase,
+ );
+ }
+
+ // eventually can improve it to return the type from the callback
+ // there are no flags or other options/flags, so it can be handled here directly
+ // @todo
+ $return_type = Type::getMixed();
+ return self::addReturnTaint(
+ $statements_analyzer,
+ $code_location,
+ $return_type,
+ $function_id,
+ );
+ }
+
+ if (! $atomic_type->properties['options']->isArray()) {
+ // silently ignored by the function, but this usually indicates a bug
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The "options" key in ' . $function_id
+ . ' must be a an array',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ } elseif (($options_array = $atomic_type->properties['options']->getArray())
+ && $options_array instanceof TKeyedArray) {
+ $defaults['options'] = $options_array;
+ } else {
+ // cannot infer a 100% correct specific return type
+ $return_null = true;
+ }
+ }
+
+ if (isset($atomic_type->properties['flags'])) {
+ if ($atomic_type->properties['flags']->isSingleIntLiteral()) {
+ $defaults['flags_int_used'] = $atomic_type->properties['flags']->getSingleIntLiteral()->value;
+ } elseif ($atomic_type->properties['flags']->isInt()) {
+ // cannot infer a 100% correct specific return type
+ $return_null = true;
+ } else {
+ // silently ignored by the function, but this usually indicates a bug
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The "flags" key in ' .
+ $function_id . ' must be a valid flag',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ $defaults['flags_int_used'] = FILTER_FLAG_NONE;
+ }
+ }
+
+ return $return_null ? null : $defaults;
+ }
+
+ // cannot infer a 100% correct specific return type
+ return null;
+ }
+
+ if ($filter_int_used === FILTER_CALLBACK) {
+ return self::missingFilterCallbackCallable(
+ $function_id,
+ $code_location,
+ $statements_analyzer,
+ $codebase,
+ );
+ }
+
+ if ($options_arg_type->isSingleIntLiteral()) {
+ return array(
+ 'flags_int_used' => $options_arg_type->getSingleIntLiteral()->value,
+ 'options' => null,
+ );
+ }
+
+ if ($options_arg_type->isInt()) {
+ // in most cases we cannot infer a 100% correct specific return type though
+ // unless all int are literal
+ // @todo could handle all literal int cases
+ return null;
+ }
+
+ foreach ($options_arg_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TArray) {
+ continue;
+ }
+
+ if ($atomic_type instanceof TInt) {
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat) {
+ // ignored
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ // ignored
+ continue;
+ }
+
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws for the invalid type
+ // for the other types it will still work correctly
+ // however "never" is a bottom type
+ // and will be lost, therefore it's better to return it here
+ // to identify hard to find bugs in the code
+ return Type::getNever();
+ }
+ // before PHP 8, it's ignored but gives a PHP notice
+ }
+
+ // array|int type which is too complex for now
+ // or any other invalid type
+ return null;
+ }
+
+ public static function missingFilterCallbackCallable(
+ string $function_id,
+ CodeLocation $code_location,
+ StatementsAnalyzer $statements_analyzer,
+ Codebase $codebase,
+ ): Union {
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The "options" key in ' . $function_id
+ . ' must be a callable for FILTER_CALLBACK',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // flags are ignored here
+ return Type::getNull();
+ }
+
+ /** @return array{Union, Union, Union} */
+ public static function getFailsNotSetType(int $flags_int_used): array
+ {
+ $fails_type = Type::getFalse();
+ $not_set_type = Type::getNull();
+ if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)) {
+ $fails_type = Type::getNull();
+ $not_set_type = Type::getFalse();
+ }
+
+ $fails_or_not_set_type = new Union([new TNull(), new TFalse()]);
+ return array(
+ $fails_type,
+ $not_set_type,
+ $fails_or_not_set_type,
+ );
+ }
+
+ public static function hasFlag(int $flags, int $flag): bool
+ {
+ if ($flags === 0) {
+ return false;
+ }
+
+ if (($flags & $flag) === $flag) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function checkRedundantFlags(
+ int $filter_int_used,
+ int $flags_int_used,
+ Union $fails_type,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Codebase $codebase,
+ ): ?Union {
+ $all_filters = self::getFilters($codebase);
+ $flags_int_used_rest = $flags_int_used;
+ foreach ($all_filters[ $filter_int_used ]['flags'] as $flag) {
+ if ($flags_int_used_rest === 0) {
+ break;
+ }
+
+ if (self::hasFlag($flags_int_used_rest, $flag)) {
+ $flags_int_used_rest = $flags_int_used_rest ^ $flag;
+ }
+ }
+
+ if ($flags_int_used_rest !== 0) {
+ // invalid flags used
+ // while they are silently ignored
+ // usually it means there's a mistake and the filter doesn't actually do what one expects
+ // as otherwise the flag wouldn't have been provided
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Not all flags used are supported by the filter used',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Flag FILTER_FORCE_ARRAY is ignored when using FILTER_REQUIRE_ARRAY',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP
+ && (
+ self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ || self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)
+ || self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR))
+ ) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'FILTER_VALIDATE_REGEXP will ignore ' .
+ 'FILTER_REQUIRE_ARRAY/FILTER_FORCE_ARRAY/FILTER_REQUIRE_SCALAR ' .
+ 'as it only works on scalar types',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_LOW)
+ && self::hasFlag($flags_int_used, FILTER_FLAG_ENCODE_LOW)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Using flag FILTER_FLAG_ENCODE_LOW is redundant when using FILTER_FLAG_STRIP_LOW',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_HIGH)
+ && self::hasFlag($flags_int_used, FILTER_FLAG_ENCODE_HIGH)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Using flag FILTER_FLAG_ENCODE_HIGH is redundant when using FILTER_FLAG_STRIP_HIGH',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'You cannot use FILTER_REQUIRE_ARRAY together with FILTER_REQUIRE_SCALAR flag',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ // FILTER_REQUIRE_ARRAY will make PHP ignore FILTER_FORCE_ARRAY
+ return $fails_type;
+ }
+
+ return null;
+ }
+
+ /** @return array{Union|null, float|int|null, float|int|null, bool, non-falsy-string|true|null} */
+ public static function getOptions(
+ int $filter_int_used,
+ int $flags_int_used,
+ ?TKeyedArray $options,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Codebase $codebase,
+ string $function_id,
+ ): array {
+ $default = null;
+ $min_range = null;
+ $max_range = null;
+ $has_range = false;
+ $regexp = null;
+
+ if (!$options) {
+ return [$default, $min_range, $max_range, $has_range, $regexp];
+ }
+
+ $all_filters = self::getFilters($codebase);
+ foreach ($options->properties as $option => $option_value) {
+ if (! isset($all_filters[ $filter_int_used ]['options'][ $option ])) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'The option ' . $option . ' is not valid for the filter used',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ continue;
+ }
+
+ if (! UnionTypeComparator::isContainedBy(
+ $codebase,
+ $option_value,
+ $all_filters[ $filter_int_used ]['options'][ $option ],
+ )) {
+ // silently ignored by the function, but it's a bug in the code
+ // since the filtering/option will not do what you expect
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'The option "' . $option . '" of ' . $function_id . ' expects '
+ . $all_filters[ $filter_int_used ]['options'][ $option ]->getId()
+ . ', but ' . $option_value->getId() . ' provided',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ continue;
+ }
+
+ if ($option === 'default') {
+ $default = $option_value;
+
+ if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)) {
+ IssueBuffer::maybeAdd(
+ new RedundantFlag(
+ 'Redundant flag FILTER_NULL_ON_FAILURE when using the "default" option',
+ $code_location,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+ }
+
+ continue;
+ }
+
+ // currently only int ranges are supported
+ // must be numeric, otherwise we would have continued above already
+ if ($option === 'min_range' && $option_value->isSingleLiteral()) {
+ if ($filter_int_used === FILTER_VALIDATE_INT) {
+ $min_range = (int) $option_value->getSingleLiteral()->value;
+ } elseif ($filter_int_used === FILTER_VALIDATE_FLOAT) {
+ $min_range = (float) $option_value->getSingleLiteral()->value;
+ }
+ }
+
+ if ($option === 'max_range' && $option_value->isSingleLiteral()) {
+ if ($filter_int_used === FILTER_VALIDATE_INT) {
+ $max_range = (int) $option_value->getSingleLiteral()->value;
+ } elseif ($filter_int_used === FILTER_VALIDATE_FLOAT) {
+ $max_range = (float) $option_value->getSingleLiteral()->value;
+ }
+ }
+
+ if (($filter_int_used === FILTER_VALIDATE_INT || $filter_int_used === FILTER_VALIDATE_FLOAT)
+ && ($option === 'min_range' || $option === 'max_range')
+ ) {
+ $has_range = true;
+ }
+
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP
+ && $option === 'regexp'
+ ) {
+ if ($option_value->isSingleStringLiteral()) {
+ /**
+ * if it's another type, we would have reported an error above already
+ * @var non-falsy-string $regexp
+ */
+ $regexp = $option_value->getSingleStringLiteral()->value;
+ } elseif ($option_value->isString()) {
+ $regexp = true;
+ }
+ }
+ }
+
+ return [$default, $min_range, $max_range, $has_range, $regexp];
+ }
+
+ protected static function isRangeValid(
+ float|int|null $min_range,
+ float|int|null $max_range,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ string $function_id,
+ ): bool {
+ if ($min_range !== null && $max_range !== null && $min_range > $max_range) {
+ IssueBuffer::maybeAdd(
+ new InvalidArgument(
+ 'min_range cannot be larger than max_range',
+ $code_location,
+ $function_id,
+ ),
+ $statements_analyzer->getSuppressedIssues(),
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * can't split this because the switch is complex since there are too many possibilities
+ *
+ * @psalm-suppress ComplexMethod
+ * @param Union|null $not_set_type null if undefined filtered variable will return $fails_type
+ * @param non-falsy-string|true|null $regexp
+ */
+ public static function getReturnType(
+ int $filter_int_used,
+ int $flags_int_used,
+ Union $input_type,
+ Union $fails_type,
+ ?Union $not_set_type,
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Codebase $codebase,
+ string $function_id,
+ bool $has_range,
+ float|int|null $min_range,
+ float|int|null $max_range,
+ string|bool|null $regexp,
+ bool $in_array_recursion = false,
+ ): Union {
+ // if we are inside a recursion of e.g. array
+ // it will never fail or change the type, so we can immediately return
+ if ($in_array_recursion && $input_type->isNever()) {
+ return $input_type;
+ }
+
+ $from_array = [];
+ // will only handle arrays correctly if either flag is set, otherwise always error
+ // regexp doesn't work on arrays
+ if ((self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY) || self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY))
+ && $filter_int_used !== FILTER_VALIDATE_REGEXP
+ && !self::hasFlag($flags_int_used, FILTER_REQUIRE_SCALAR)
+ ) {
+ foreach ($input_type->getAtomicTypes() as $key => $atomic_type) {
+ if ($atomic_type instanceof TKeyedArray) {
+ $input_type = $input_type->getBuilder();
+ $input_type->removeType($key);
+ $input_type = $input_type->freeze();
+
+ $new = [];
+ foreach ($atomic_type->properties as $k => $property) {
+ if ($property->isNever()) {
+ $new[$k] = $property;
+ continue;
+ }
+
+ $new[$k] = self::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $property,
+ $fails_type,
+ // irrelevant in nested elements
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ true,
+ );
+ }
+
+ // false positive error in psalm when we loop over a non-empty array
+ if ($new === array()) {
+ throw new UnexpectedValueException('This is impossible');
+ }
+
+ $fallback_params = null;
+ if ($atomic_type->fallback_params) {
+ [$keys_union, $values_union] = $atomic_type->fallback_params;
+ $values_union = self::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $values_union,
+ $fails_type,
+ // irrelevant in nested elements
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ true,
+ );
+ $fallback_params = [$keys_union, $values_union];
+ }
+
+ $from_array[] = new TKeyedArray(
+ $new,
+ $atomic_type->class_strings,
+ $fallback_params,
+ $atomic_type->is_list,
+ );
+
+ continue;
+ }
+
+ if ($atomic_type instanceof TArray) {
+ $input_type = $input_type->getBuilder();
+ $input_type->removeType($key);
+ $input_type = $input_type->freeze();
+
+ [$keys_union, $values_union] = $atomic_type->type_params;
+ $values_union = self::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $values_union,
+ $fails_type,
+ // irrelevant in nested elements
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ true,
+ );
+
+ if ($atomic_type instanceof TNonEmptyArray) {
+ $from_array[] = new TNonEmptyArray([$keys_union, $values_union]);
+ } else {
+ $from_array[] = new TArray([$keys_union, $values_union]);
+ }
+
+ continue;
+ }
+
+ // can be an array too
+ if ($atomic_type instanceof TMixed) {
+ $from_array[] = new TArray(
+ [
+ new Union([new TArrayKey]),
+ new Union([new TMixed]),
+ ],
+ );
+ }
+ }
+ }
+
+ $can_fail = false;
+ $filter_types = array();
+ switch ($filter_int_used) {
+ case FILTER_VALIDATE_FLOAT:
+ if (!self::isRangeValid(
+ $min_range,
+ $max_range,
+ $statements_analyzer,
+ $code_location,
+ $function_id,
+ )) {
+ $can_fail = true;
+ break;
+ }
+
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TLiteralFloat) {
+ if ($min_range !== null && $min_range > $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ // float ranges aren't supported yet
+ $filter_types[] = new TFloat();
+ } elseif ($atomic_type instanceof TFloat) {
+ if ($has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ // float ranges aren't supported yet
+ $filter_types[] = new TFloat();
+ }
+
+ if ($atomic_type instanceof TLiteralInt) {
+ if ($min_range !== null && $min_range > $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralFloat((float) $atomic_type->value);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TFloat();
+ } elseif ($atomic_type instanceof TInt) {
+ $filter_types[] = new TFloat();
+
+ if ($has_range === false) {
+ continue;
+ }
+ }
+
+ if ($atomic_type instanceof TLiteralString) {
+ if (($string_to_float = filter_var($atomic_type->value, FILTER_VALIDATE_FLOAT)) === false) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null && $min_range > $string_to_float) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $string_to_float) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralFloat($string_to_float);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TFloat();
+ } elseif ($atomic_type instanceof TString) {
+ $filter_types[] = new TFloat();
+ }
+
+ if ($atomic_type instanceof TBool) {
+ if ($min_range !== null && $min_range > 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralFloat(1.0);
+
+ if ($atomic_type instanceof TTrue) {
+ continue;
+ }
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TFloat();
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TFloat();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_BOOLEAN:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if (($atomic_type instanceof TLiteralInt && $atomic_type->value === 1)
+ || ($atomic_type instanceof TLiteralFloat && $atomic_type->value === 1.0)
+ || ($atomic_type instanceof TLiteralString
+ && in_array(strtolower($atomic_type->value), ['1', 'true', 'on', 'yes'], true))
+ ) {
+ $filter_types[] = new TTrue();
+ continue;
+ }
+
+ if (self::hasFlag($flags_int_used, FILTER_NULL_ON_FAILURE)
+ && (
+ ($atomic_type instanceof TLiteralInt && $atomic_type->value === 0)
+ || ($atomic_type instanceof TLiteralFloat && $atomic_type->value === 0.0)
+ || ($atomic_type instanceof TLiteralString
+ && in_array(strtolower($atomic_type->value), ['0', 'false', 'off', 'no', ''], true)
+ )
+ )
+ ) {
+ $filter_types[] = new TFalse();
+ continue;
+ }
+
+ if ($atomic_type instanceof TLiteralInt
+ || $atomic_type instanceof TLiteralFloat
+ || $atomic_type instanceof TLiteralString
+ ) {
+ // all other literals will fail
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed
+ || $atomic_type instanceof TString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat) {
+ $filter_types[] = new TBool();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_INT:
+ if (!self::isRangeValid(
+ $min_range,
+ $max_range,
+ $statements_analyzer,
+ $code_location,
+ $function_id,
+ )) {
+ $can_fail = true;
+ break;
+ }
+
+ $min_range = $min_range !== null ? (int) $min_range : null;
+ $max_range = $max_range !== null ? (int) $max_range : null;
+
+ if ($min_range !== null || $max_range !== null) {
+ $int_type = new TIntRange($min_range, $max_range);
+ } 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) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = new TInt();
+ } elseif ($atomic_type instanceof TInt) {
+ if ($has_range === false) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TLiteralFloat) {
+ if ((float) (int) $atomic_type->value !== $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null && $min_range > $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $atomic_type->value) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralInt((int) $atomic_type->value);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = $int_type;
+ } elseif ($atomic_type instanceof TFloat) {
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TLiteralString) {
+ if (($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) === false) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null && $min_range > $string_to_int) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < $string_to_int) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralInt($string_to_int);
+ continue;
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = $int_type;
+ } elseif ($atomic_type instanceof TString) {
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ if ($min_range !== null && $min_range > 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($max_range !== null && $max_range < 1) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($min_range !== null || $max_range !== null || $has_range === false) {
+ $filter_types[] = new TLiteralInt(1);
+
+ if ($atomic_type instanceof TTrue) {
+ continue;
+ }
+ }
+
+ // we don't know what the min/max of the range are
+ // and it might be out of the range too
+ $filter_types[] = $int_type;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = $int_type;
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_IP:
+ case FILTER_VALIDATE_MAC:
+ case FILTER_VALIDATE_URL:
+ case FILTER_VALIDATE_EMAIL:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNumericString) {
+ $can_fail = true;
+ continue;
+ }
+
+ if ($atomic_type instanceof TNonFalsyString) {
+ $filter_types[] = $atomic_type;
+ } elseif ($atomic_type instanceof TString) {
+ $filter_types[] = new TNonFalsyString();
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TNonFalsyString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_VALIDATE_REGEXP:
+ // the regexp key is mandatory for this filter
+ // it will only fail if the value exists, therefore it's after the checks above
+ // this must be (and is) handled BEFORE calling this function though
+ // since PHP 8+ throws instead of returning the fails case
+ if ($regexp === null) {
+ $can_fail = true;
+ break;
+ }
+
+ // invalid regex
+ if ($regexp !== true && @preg_match($regexp, 'placeholder') === false) {
+ $can_fail = true;
+ break;
+ }
+
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat
+ || $atomic_type instanceof TNumeric
+ || $atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+
+ break;
+ case FILTER_VALIDATE_DOMAIN:
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_HOSTNAME)) {
+ $string_type = new TNonEmptyString();
+ } else {
+ $string_type = new TString();
+ }
+
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNonEmptyString) {
+ $filter_types[] = $atomic_type;
+ } elseif ($atomic_type instanceof TString) {
+ if (self::hasFlag($flags_int_used, FILTER_FLAG_HOSTNAME)) {
+ $filter_types[] = $string_type;
+ } else {
+ $filter_types[] = $atomic_type;
+ }
+ }
+
+ if ($atomic_type instanceof TMixed
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat) {
+ $filter_types[] = $string_type;
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_SANITIZE_EMAIL:
+ case FILTER_SANITIZE_URL:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TNumericString) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if ($atomic_type instanceof TString) {
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TNumeric) {
+ $filter_types[] = new TNumericString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TTrue) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_SANITIZE_ENCODED:
+ case FILTER_SANITIZE_ADD_SLASHES:
+ case 521: // 8.0.0 FILTER_SANITIZE_MAGIC_QUOTES has been removed.
+ case FILTER_SANITIZE_SPECIAL_CHARS:
+ case FILTER_SANITIZE_FULL_SPECIAL_CHARS:
+ case FILTER_DEFAULT:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($filter_int_used === FILTER_DEFAULT
+ && $flags_int_used === 0
+ && $atomic_type instanceof TString
+ ) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if ($atomic_type instanceof TNumericString) {
+ $filter_types[] = $atomic_type;
+ continue;
+ }
+
+ if (in_array(
+ $filter_int_used,
+ [FILTER_SANITIZE_ENCODED, FILTER_SANITIZE_SPECIAL_CHARS, FILTER_DEFAULT],
+ true,
+ )
+ && $atomic_type instanceof TNonEmptyString
+ && (self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_LOW)
+ || self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_HIGH)
+ || self::hasFlag($flags_int_used, FILTER_FLAG_STRIP_BACKTICK)
+ )
+ ) {
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TNonFalsyString) {
+ $filter_types[] = new TNonFalsyString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TNonEmptyString) {
+ $filter_types[] = new TNonEmptyString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TString) {
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TNumeric) {
+ $filter_types[] = new TNumericString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TTrue) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case 513: // 8.1.0 FILTER_SANITIZE_STRING and FILTER_SANITIZE_STRIPPED (alias) have been deprecated.
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TBool
+ || $atomic_type instanceof TString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TFloat
+ || $atomic_type instanceof TNumeric) {
+ // only basic checking since it's deprecated anyway and not worth the time
+ $filter_types[] = new TString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TString();
+ }
+
+ $can_fail = true;
+ }
+ break;
+ case FILTER_SANITIZE_NUMBER_INT:
+ case FILTER_SANITIZE_NUMBER_FLOAT:
+ foreach ($input_type->getAtomicTypes() as $atomic_type) {
+ if ($atomic_type instanceof TLiteralString
+ || $atomic_type instanceof TLiteralInt
+ || $atomic_type instanceof TLiteralFloat
+ ) {
+ /** @var string|false $literal */
+ $literal = filter_var($atomic_type->value, $filter_int_used);
+ if ($literal === false) {
+ $can_fail = true;
+ } else {
+ $filter_types[] = Type::getAtomicStringFromLiteral($literal);
+ }
+
+ continue;
+ }
+
+ if ($atomic_type instanceof TFloat
+ || $atomic_type instanceof TNumericString
+ || $atomic_type instanceof TInt
+ || $atomic_type instanceof TNumeric) {
+ $filter_types[] = new TNumericString();
+ continue;
+ }
+
+ if ($atomic_type instanceof TString) {
+ $filter_types[] = new TNumericString();
+ // for numeric-string it won't collapse since https://github.com/vimeo/psalm/pull/10459
+ // therefore we can add both
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TTrue) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ continue;
+ }
+
+ if ($atomic_type instanceof TFalse) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TBool) {
+ $filter_types[] = Type::getAtomicStringFromLiteral('1');
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ continue;
+ }
+
+ if ($atomic_type instanceof TMixed) {
+ $filter_types[] = new TNumericString();
+ $filter_types[] = Type::getAtomicStringFromLiteral('');
+ }
+
+ $can_fail = true;
+ }
+ break;
+ }
+
+ if ($input_type->hasMixed()) {
+ // can always fail if we have mixed
+ // only for redundancy in case there's a mistake in the switch above
+ $can_fail = true;
+ }
+
+ // if an array is required, ignore all types we created from non-array on first level
+ if (!$in_array_recursion && self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)) {
+ $filter_types = array();
+ }
+
+ $return_type = $fails_type;
+ if ($filter_types !== array()
+ && ($can_fail === true ||
+ (!$in_array_recursion && !$not_set_type && $input_type->possibly_undefined)
+ )) {
+ $return_type = Type::combineUnionTypes(
+ $return_type,
+ TypeCombiner::combine($filter_types, $codebase),
+ $codebase,
+ );
+ } elseif ($filter_types !== array()) {
+ $return_type = TypeCombiner::combine($filter_types, $codebase);
+ }
+
+ if (!$in_array_recursion
+ && !self::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
+ && self::hasFlag($flags_int_used, FILTER_FORCE_ARRAY)) {
+ $return_type = new Union([new TKeyedArray(
+ [$return_type],
+ null,
+ null,
+ true,
+ )]);
+ }
+
+ if ($from_array !== array()) {
+ $from_array_union = TypeCombiner::combine($from_array, $codebase);
+
+ $return_type = Type::combineUnionTypes(
+ $return_type,
+ $from_array_union,
+ $codebase,
+ );
+ }
+
+ if ($in_array_recursion && $input_type->possibly_undefined) {
+ $return_type = $return_type->setPossiblyUndefined(true);
+ } elseif (!$in_array_recursion && $not_set_type && $input_type->possibly_undefined) {
+ // in case of PHP CLI it will always fail for all filter_input even when they're set
+ // to fix this we would have to add support for environments in Context
+ // e.g. if php_sapi_name() === 'cli'
+ $return_type = Type::combineUnionTypes(
+ $return_type,
+ // the not set type is not coerced into an array when FILTER_FORCE_ARRAY is used
+ $not_set_type,
+ $codebase,
+ );
+ }
+
+ if (!$in_array_recursion) {
+ $return_type = self::addReturnTaint(
+ $statements_analyzer,
+ $code_location,
+ $return_type,
+ $function_id,
+ );
+ }
+
+ return $return_type;
+ }
+
+ private static function addReturnTaint(
+ StatementsAnalyzer $statements_analyzer,
+ CodeLocation $code_location,
+ Union $return_type,
+ string $function_id,
+ ): Union {
+ if ($statements_analyzer->data_flow_graph
+ && !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
+ ) {
+ $function_return_sink = DataFlowNode::getForMethodReturn(
+ $function_id,
+ $function_id,
+ null,
+ $code_location,
+ );
+
+ $statements_analyzer->data_flow_graph->addNode($function_return_sink);
+
+ $function_param_sink = DataFlowNode::getForMethodArgument(
+ $function_id,
+ $function_id,
+ 0,
+ null,
+ $code_location,
+ );
+
+ $statements_analyzer->data_flow_graph->addNode($function_param_sink);
+
+ $statements_analyzer->data_flow_graph->addPath(
+ $function_param_sink,
+ $function_return_sink,
+ 'arg',
+ );
+
+ $return_type = $return_type->setParentNodes([$function_return_sink->id => $function_return_sink]);
+ }
+
+ return $return_type;
+ }
+
+ /** @return array, options: array}> */
+ public static function getFilters(Codebase $codebase): array
+ {
+ $general_filter_flags = array(
+ FILTER_REQUIRE_SCALAR,
+ FILTER_REQUIRE_ARRAY,
+ FILTER_FORCE_ARRAY,
+ FILTER_FLAG_NONE, // does nothing, default
+ );
+
+ // https://www.php.net/manual/en/filter.filters.sanitize.php
+ $sanitize_filters = array(
+ FILTER_SANITIZE_EMAIL => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_ENCODED => array(
+ 'flags' => array(
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_LOW,
+ FILTER_FLAG_ENCODE_HIGH,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_NUMBER_FLOAT => array(
+ 'flags' => array(
+ FILTER_FLAG_ALLOW_FRACTION,
+ FILTER_FLAG_ALLOW_THOUSAND,
+ FILTER_FLAG_ALLOW_SCIENTIFIC,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_NUMBER_INT => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_SPECIAL_CHARS => array(
+ 'flags' => array(
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_HIGH,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_FULL_SPECIAL_CHARS => array(
+ 'flags' => array(
+ FILTER_FLAG_NO_ENCODE_QUOTES,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_SANITIZE_URL => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_UNSAFE_RAW => array(
+ 'flags' => array(
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_LOW,
+ FILTER_FLAG_ENCODE_HIGH,
+ FILTER_FLAG_ENCODE_AMP,
+ ),
+ 'options' => array(),
+ ),
+
+ );
+
+ if ($codebase->analysis_php_version_id <= 7_03_00) {
+ // FILTER_SANITIZE_MAGIC_QUOTES
+ $sanitize_filters[521] = array(
+ 'flags' => array(),
+ 'options' => array(),
+ );
+ }
+
+ if ($codebase->analysis_php_version_id <= 8_01_00) {
+ // FILTER_SANITIZE_STRING
+ $sanitize_filters[513] = array(
+ 'flags' => array(
+ FILTER_FLAG_NO_ENCODE_QUOTES,
+ FILTER_FLAG_STRIP_LOW,
+ FILTER_FLAG_STRIP_HIGH,
+ FILTER_FLAG_STRIP_BACKTICK,
+ FILTER_FLAG_ENCODE_LOW,
+ FILTER_FLAG_ENCODE_HIGH,
+ FILTER_FLAG_ENCODE_AMP,
+ ),
+ 'options' => array(),
+ );
+ }
+
+ if ($codebase->analysis_php_version_id >= 7_03_00) {
+ // was added as a replacement for FILTER_SANITIZE_MAGIC_QUOTES
+ $sanitize_filters[FILTER_SANITIZE_ADD_SLASHES] = array(
+ 'flags' => array(),
+ 'options' => array(),
+ );
+ }
+
+ foreach ($sanitize_filters as $filter_int => $filter_data) {
+ $sanitize_filters[$filter_int]['flags'] = array_merge($filter_data['flags'], $general_filter_flags);
+ }
+
+ // https://www.php.net/manual/en/filter.filters.validate.php
+ // validation filters all match bitmask 0x100
+ // all support FILTER_NULL_ON_FAILURE flag https://www.php.net/manual/en/filter.filters.flags.php
+ $general_filter_flags_validate = array_merge($general_filter_flags, array(FILTER_NULL_ON_FAILURE));
+
+ $validate_filters = array(
+ FILTER_VALIDATE_BOOLEAN => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_EMAIL => array(
+ 'flags' => array(
+ FILTER_FLAG_EMAIL_UNICODE,
+ ),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_FLOAT => array(
+ 'flags' => array(
+ FILTER_FLAG_ALLOW_THOUSAND,
+ ),
+ 'options' => array(
+ 'decimal' => new Union([
+ Type::getAtomicStringFromLiteral('.'),
+ Type::getAtomicStringFromLiteral(','),
+ ]),
+ ),
+ ),
+ FILTER_VALIDATE_INT => array(
+ 'flags' => array(
+ FILTER_FLAG_ALLOW_OCTAL,
+ FILTER_FLAG_ALLOW_HEX,
+ ),
+ 'options' => array(
+ 'min_range' => Type::getNumeric(),
+ 'max_range' => Type::getNumeric(),
+ ),
+ ),
+ FILTER_VALIDATE_IP => array(
+ 'flags' => array(
+ FILTER_FLAG_IPV4,
+ FILTER_FLAG_IPV6,
+ FILTER_FLAG_NO_PRIV_RANGE,
+ FILTER_FLAG_NO_RES_RANGE,
+
+ ),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_MAC => array(
+ 'flags' => array(),
+ 'options' => array(),
+ ),
+ FILTER_VALIDATE_REGEXP => array(
+ 'flags' => array(),
+ 'options' => array(
+ 'regexp' => Type::getNonFalsyString(),
+ ),
+ ),
+ FILTER_VALIDATE_URL => array(
+ 'flags' => array(
+ FILTER_FLAG_PATH_REQUIRED,
+ FILTER_FLAG_QUERY_REQUIRED,
+ ),
+ 'options' => array(),
+ ),
+
+ );
+
+ if ($codebase->analysis_php_version_id >= 7_04_00) {
+ $validate_filters[FILTER_VALIDATE_FLOAT]['options']['min_range'] = Type::getNumeric();
+ $validate_filters[FILTER_VALIDATE_FLOAT]['options']['max_range'] = Type::getNumeric();
+ }
+
+ if ($codebase->analysis_php_version_id < 8_00_00) {
+ // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator
+ $validate_filters[FILTER_VALIDATE_URL]['flags'][] = 65536; // FILTER_FLAG_SCHEME_REQUIRED
+ // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator
+ $validate_filters[FILTER_VALIDATE_URL]['flags'][] = 131072; // FILTER_FLAG_HOST_REQUIRED
+ }
+
+ if ($codebase->analysis_php_version_id >= 8_02_00) {
+ // phpcs:ignore SlevomatCodingStandard.Numbers.RequireNumericLiteralSeparator.RequiredNumericLiteralSeparator
+ $validate_filters[FILTER_VALIDATE_IP]['flags'][] = 268435456; // FILTER_FLAG_GLOBAL_RANGE
+ }
+
+ if ($codebase->analysis_php_version_id >= 7_00_00) {
+ $validate_filters[FILTER_VALIDATE_DOMAIN] = array(
+ 'flags' => array(
+ FILTER_FLAG_HOSTNAME,
+ ),
+ 'options' => array(),
+ );
+ }
+
+ foreach ($validate_filters as $filter_int => $filter_data) {
+ $validate_filters[$filter_int]['flags'] = array_merge(
+ $filter_data['flags'],
+ $general_filter_flags_validate,
+ );
+
+ $default_options = array(
+ 'default' => Type::getMixed(),
+ );
+ $validate_filters[$filter_int]['options'] = array_merge($filter_data['options'], $default_options);
+ }
+
+ // https://www.php.net/manual/en/filter.filters.misc.php
+ $other_filters = array(
+ FILTER_CALLBACK => array(
+ // the docs say that all flags are ignored
+ // however this seems to be incorrect https://github.com/php/doc-en/issues/2708
+ // however they can only be used in the options array, not as a param directly
+ 'flags' => $general_filter_flags_validate,
+ // the options array is required for this filter
+ // and must be a valid callback instead of an array like in other cases
+ 'options' => array(),
+ ),
+ );
+
+ return $sanitize_filters + $validate_filters + $other_filters;
+ }
+}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php
index 40598d4a1e4..7d56d35cce6 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php
@@ -5,30 +5,19 @@
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
-use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
-use Psalm\Type\Atomic\TFalse;
-use Psalm\Type\Atomic\TKeyedArray;
-use Psalm\Type\Atomic\TLiteralInt;
-use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
use UnexpectedValueException;
-use function in_array;
-
-use const FILTER_NULL_ON_FAILURE;
-use const FILTER_SANITIZE_URL;
-use const FILTER_VALIDATE_BOOLEAN;
-use const FILTER_VALIDATE_DOMAIN;
-use const FILTER_VALIDATE_EMAIL;
-use const FILTER_VALIDATE_FLOAT;
-use const FILTER_VALIDATE_INT;
-use const FILTER_VALIDATE_IP;
-use const FILTER_VALIDATE_MAC;
+use function is_array;
+use function is_int;
+
+use const FILTER_CALLBACK;
+use const FILTER_DEFAULT;
+use const FILTER_FLAG_NONE;
use const FILTER_VALIDATE_REGEXP;
-use const FILTER_VALIDATE_URL;
/**
* @internal
@@ -43,135 +32,124 @@ public static function getFunctionIds(): array
return ['filter_var'];
}
- public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
+ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
- $statements_source = $event->getStatementsSource();
+ $statements_analyzer = $event->getStatementsSource();
+ if (!$statements_analyzer instanceof StatementsAnalyzer) {
+ throw new UnexpectedValueException();
+ }
+
$call_args = $event->getCallArgs();
$function_id = $event->getFunctionId();
$code_location = $event->getCodeLocation();
- if (!$statements_source instanceof StatementsAnalyzer) {
- throw new UnexpectedValueException();
+ $codebase = $statements_analyzer->getCodebase();
+
+ if (! isset($call_args[0])) {
+ return FilterUtils::missingFirstArg($codebase);
}
- $filter_type = null;
-
- if (isset($call_args[1])
- && ($second_arg_type = $statements_source->node_data->getType($call_args[1]->value))
- && $second_arg_type->isSingleIntLiteral()
- ) {
- $filter_type_type = $second_arg_type->getSingleIntLiteral();
-
- switch ($filter_type_type->value) {
- case FILTER_VALIDATE_INT:
- $filter_type = Type::getInt();
- break;
-
- case FILTER_VALIDATE_FLOAT:
- $filter_type = Type::getFloat();
- break;
-
- case FILTER_VALIDATE_BOOLEAN:
- $filter_type = Type::getBool();
-
- break;
-
- case FILTER_VALIDATE_IP:
- case FILTER_VALIDATE_MAC:
- case FILTER_VALIDATE_REGEXP:
- case FILTER_VALIDATE_URL:
- case FILTER_VALIDATE_EMAIL:
- case FILTER_VALIDATE_DOMAIN:
- case FILTER_SANITIZE_URL:
- $filter_type = Type::getString();
- break;
- }
+ $filter_int_used = FILTER_DEFAULT;
+ if (isset($call_args[1])) {
+ $filter_int_used = FilterUtils::getFilterArgValueOrError(
+ $call_args[1],
+ $statements_analyzer,
+ $codebase,
+ );
- $has_object_like = false;
- $filter_null = false;
-
- if (isset($call_args[2])
- && ($third_arg_type = $statements_source->node_data->getType($call_args[2]->value))
- && $filter_type
- ) {
- foreach ($third_arg_type->getAtomicTypes() as $atomic_type) {
- if ($atomic_type instanceof TKeyedArray) {
- $has_object_like = true;
-
- if (isset($atomic_type->properties['options'])
- && $atomic_type->properties['options']->hasArray()
- && ($options_array = $atomic_type->properties['options']->getArray())
- && $options_array instanceof TKeyedArray
- && isset($options_array->properties['default'])
- ) {
- $filter_type = Type::combineUnionTypes(
- $filter_type,
- $options_array->properties['default'],
- );
- } else {
- $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze();
- }
-
- if (isset($atomic_type->properties['flags'])
- && $atomic_type->properties['flags']->isSingleIntLiteral()
- ) {
- $filter_flag_type =
- $atomic_type->properties['flags']->getSingleIntLiteral();
-
- if ($filter_type->hasBool()
- && $filter_flag_type->value === FILTER_NULL_ON_FAILURE
- ) {
- $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze();
- }
- }
- } elseif ($atomic_type instanceof TLiteralInt) {
- if ($atomic_type->value === FILTER_NULL_ON_FAILURE) {
- $filter_null = true;
- $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze();
- }
- }
- }
+ if (!is_int($filter_int_used)) {
+ return $filter_int_used;
}
+ }
+
+ $options = null;
+ $flags_int_used = FILTER_FLAG_NONE;
+ if (isset($call_args[2])) {
+ $helper = FilterUtils::getOptionsArgValueOrError(
+ $call_args[2],
+ $statements_analyzer,
+ $codebase,
+ $code_location,
+ $function_id,
+ $filter_int_used,
+ );
- if (!$has_object_like && !$filter_null && $filter_type) {
- $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze();
+ if (!is_array($helper)) {
+ return $helper;
}
- }
- if (!$filter_type) {
- $filter_type = Type::getMixed();
+ $flags_int_used = $helper['flags_int_used'];
+ $options = $helper['options'];
}
- if ($statements_source->data_flow_graph
- && !in_array('TaintedInput', $statements_source->getSuppressedIssues())
- ) {
- $function_return_sink = DataFlowNode::getForMethodReturn(
- $function_id,
+ // if we reach this point with callback, the callback is missing
+ if ($filter_int_used === FILTER_CALLBACK) {
+ return FilterUtils::missingFilterCallbackCallable(
$function_id,
- null,
$code_location,
+ $statements_analyzer,
+ $codebase,
);
+ }
- $statements_source->data_flow_graph->addNode($function_return_sink);
+ [$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions(
+ $filter_int_used,
+ $flags_int_used,
+ $options,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ );
+
+ if (!$default) {
+ [$fails_type] = FilterUtils::getFailsNotSetType($flags_int_used);
+ } else {
+ $fails_type = $default;
+ }
- $function_param_sink = DataFlowNode::getForMethodArgument(
- $function_id,
- $function_id,
- 0,
- null,
- $code_location,
- );
+ if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) {
+ if ($codebase->analysis_php_version_id >= 8_00_00) {
+ // throws
+ return Type::getNever();
+ }
+
+ // any "array" flags are ignored by this filter!
+ return $fails_type;
+ }
- $statements_source->data_flow_graph->addNode($function_param_sink);
+ $input_type = $statements_analyzer->node_data->getType($call_args[0]->value);
- $statements_source->data_flow_graph->addPath(
- $function_param_sink,
- $function_return_sink,
- 'arg',
- );
+ // only return now, as we still want to report errors above
+ if (!$input_type) {
+ return null;
+ }
- return $filter_type->setParentNodes([$function_return_sink->id => $function_return_sink]);
+ $redundant_error_return_type = FilterUtils::checkRedundantFlags(
+ $filter_int_used,
+ $flags_int_used,
+ $fails_type,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ );
+ if ($redundant_error_return_type !== null) {
+ return $redundant_error_return_type;
}
- return $filter_type;
+ return FilterUtils::getReturnType(
+ $filter_int_used,
+ $flags_int_used,
+ $input_type,
+ $fails_type,
+ null,
+ $statements_analyzer,
+ $code_location,
+ $codebase,
+ $function_id,
+ $has_range,
+ $min_range,
+ $max_range,
+ $regexp,
+ );
}
}
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php
index db7000bc721..cd9f841bcb1 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php
@@ -19,7 +19,6 @@
use function array_filter;
use function assert;
use function count;
-use function get_class;
use function in_array;
use function max;
use function min;
@@ -89,7 +88,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
} elseif ($atomic_type instanceof TIntRange) {
$min_bounds[] = $atomic_type->min_bound;
$max_bounds[] = $atomic_type->max_bound;
- } elseif (get_class($atomic_type) === TInt::class) {
+ } elseif ($atomic_type::class === TInt::class) {
$min_bounds[] = null;
$max_bounds[] = null;
} else {
diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php
index 5e95fdb824f..60cc7bca87b 100644
--- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php
+++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementReturnTypeProvider.php
@@ -62,104 +62,81 @@ private static function handleFetch(MethodReturnTypeProviderEvent $event): ?Unio
break;
}
}
-
- switch ($fetch_mode) {
- case 2: // PDO::FETCH_ASSOC - array|false
- return new Union([
- new TArray([
- Type::getString(),
- new Union([
- new TScalar(),
- new TNull(),
- ]),
+ return match ($fetch_mode) {
+ 2 => new Union([
+ new TArray([
+ Type::getString(),
+ new Union([
+ new TScalar(),
+ new TNull(),
]),
- new TFalse(),
- ]);
-
- case 4: // PDO::FETCH_BOTH - array|false
- return new Union([
- new TArray([
- Type::getArrayKey(),
- new Union([
- new TScalar(),
- new TNull(),
- ]),
+ ]),
+ new TFalse(),
+ ]),
+ 4 => new Union([
+ new TArray([
+ Type::getArrayKey(),
+ new Union([
+ new TScalar(),
+ new TNull(),
]),
- new TFalse(),
- ]);
-
- case 6: // PDO::FETCH_BOUND - bool
- return Type::getBool();
-
- case 7: // PDO::FETCH_COLUMN - scalar|null|false
- return new Union([
- new TScalar(),
- new TNull(),
- new TFalse(),
- ]);
-
- case 8: // PDO::FETCH_CLASS - object|false
- return new Union([
- new TObject(),
- new TFalse(),
- ]);
-
- case 1: // PDO::FETCH_LAZY - object|false
- // This actually returns a PDORow object, but that class is
- // undocumented, and its attributes are all dynamic anyway
- return new Union([
- new TObject(),
- new TFalse(),
- ]);
-
- case 11: // PDO::FETCH_NAMED - array>|false
- return new Union([
- new TArray([
- Type::getString(),
- new Union([
- new TScalar(),
- new TNull(),
- Type::getListAtomic(
- new Union([
- new TScalar(),
- new TNull(),
- ]),
- ),
- ]),
+ ]),
+ new TFalse(),
+ ]),
+ 6 => Type::getBool(),
+ 7 => new Union([
+ new TScalar(),
+ new TNull(),
+ new TFalse(),
+ ]),
+ 8 => new Union([
+ new TObject(),
+ new TFalse(),
+ ]),
+ 1 => new Union([
+ new TObject(),
+ new TFalse(),
+ ]),
+ 11 => new Union([
+ new TArray([
+ Type::getString(),
+ new Union([
+ new TScalar(),
+ new TNull(),
+ Type::getListAtomic(
+ new Union([
+ new TScalar(),
+ new TNull(),
+ ]),
+ ),
]),
- new TFalse(),
- ]);
-
- case 12: // PDO::FETCH_KEY_PAIR - array
- return new Union([
- new TArray([
- Type::getArrayKey(),
- new Union([
- new TScalar(),
- new TNull(),
- ]),
+ ]),
+ new TFalse(),
+ ]),
+ 12 => new Union([
+ new TArray([
+ Type::getArrayKey(),
+ new Union([
+ new TScalar(),
+ new TNull(),
]),
- ]);
-
- case 3: // PDO::FETCH_NUM - list|false
- return new Union([
- Type::getListAtomic(
- new Union([
- new TScalar(),
- new TNull(),
- ]),
- ),
- new TFalse(),
- ]);
-
- case 5: // PDO::FETCH_OBJ - stdClass|false
- return new Union([
- new TNamedObject('stdClass'),
- new TFalse(),
- ]);
- }
-
- return null;
+ ]),
+ ]),
+ 3 => new Union([
+ Type::getListAtomic(
+ new Union([
+ new TScalar(),
+ new TNull(),
+ ]),
+ ),
+ new TFalse(),
+ ]),
+ 5 => new Union([
+ new TNamedObject('stdClass'),
+ new TFalse(),
+ ]),
+ default => null,
+ };
}
private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?Union
@@ -183,120 +160,101 @@ private static function handleFetchAll(MethodReturnTypeProviderEvent $event): ?U
) {
$fetch_class_name = $second_arg_type->getSingleStringLiteral()->value;
}
-
- switch ($fetch_mode) {
- case 2: // PDO::FETCH_ASSOC - list>
- return new Union([
- Type::getListAtomic(
- new Union([
- new TArray([
- Type::getString(),
- new Union([
- new TScalar(),
- new TNull(),
- ]),
+ return match ($fetch_mode) {
+ 2 => new Union([
+ Type::getListAtomic(
+ new Union([
+ new TArray([
+ Type::getString(),
+ new Union([
+ new TScalar(),
+ new TNull(),
]),
]),
- ),
- ]);
-
- case 4: // PDO::FETCH_BOTH - list>
- return new Union([
- Type::getListAtomic(
- new Union([
- new TArray([
- Type::getArrayKey(),
- new Union([
- new TScalar(),
- new TNull(),
- ]),
+ ]),
+ ),
+ ]),
+ 4 => new Union([
+ Type::getListAtomic(
+ new Union([
+ new TArray([
+ Type::getArrayKey(),
+ new Union([
+ new TScalar(),
+ new TNull(),
]),
]),
- ),
- ]);
-
- case 6: // PDO::FETCH_BOUND - list
- return new Union([
- Type::getListAtomic(
- Type::getBool(),
- ),
- ]);
-
- case 7: // PDO::FETCH_COLUMN - list
- return new Union([
- Type::getListAtomic(
- new Union([
- new TScalar(),
- new TNull(),
- ]),
- ),
- ]);
-
- case 8: // PDO::FETCH_CLASS - list