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 - return new Union([ - Type::getListAtomic( - new Union([ - $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), - ]), - ), - ]); - - case 11: // PDO::FETCH_NAMED - list>> - return new Union([ - Type::getListAtomic( - new Union([ - new TArray([ - Type::getString(), - new Union([ - new TScalar(), - new TNull(), - Type::getListAtomic( - new Union([ - new TScalar(), - new TNull(), - ]), - ), - ]), + ]), + ), + ]), + 6 => new Union([ + Type::getListAtomic( + Type::getBool(), + ), + ]), + 7 => new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + 8 => new Union([ + Type::getListAtomic( + new Union([ + $fetch_class_name ? new TNamedObject($fetch_class_name) : new TObject(), + ]), + ), + ]), + 11 => new Union([ + Type::getListAtomic( + new Union([ + new TArray([ + Type::getString(), + new Union([ + new TScalar(), + new TNull(), + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), ]), ]), - ), - ]); - - case 12: // PDO::FETCH_KEY_PAIR - array - return new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TScalar(), - new TNull(), - ]), ]), - ]); - - case 3: // PDO::FETCH_NUM - list> - return new Union([ - Type::getListAtomic( - new Union([ - Type::getListAtomic( - new Union([ - new TScalar(), - new TNull(), - ]), - ), - ]), - ), - ]); - - case 5: // PDO::FETCH_OBJ - list - return new Union([ - Type::getListAtomic( - new Union([ - new TNamedObject('stdClass'), - ]), - ), - ]); - } - - return null; + ), + ]), + 12 => new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TScalar(), + new TNull(), + ]), + ]), + ]), + 3 => new Union([ + Type::getListAtomic( + new Union([ + Type::getListAtomic( + new Union([ + new TScalar(), + new TNull(), + ]), + ), + ]), + ), + ]), + 5 => new Union([ + Type::getListAtomic( + new Union([ + new TNamedObject('stdClass'), + ]), + ), + ]), + default => null, + }; } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php index f74d619fc10..25ba7c18ca2 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php @@ -239,7 +239,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev ); break 2; - } catch (ArgumentCountError $error) { + } catch (ArgumentCountError) { // PHP 8 if (count($dummy) === $provided_placeholders_count) { IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index 90a2a0a143f..b5bdbef8ac1 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -33,23 +33,16 @@ use function filemtime; use function hash; use function md5; +use function str_starts_with; use function strlen; use function strpos; -use const PHP_VERSION_ID; - /** * @internal */ final class StatementsProvider { - private FileProvider $file_provider; - - public ?ParserCacheProvider $parser_cache_provider = null; - - private int|bool $this_modified_time; - - private ?FileStorageCacheProvider $file_storage_cache_provider = null; + private readonly int|bool $this_modified_time; /** * @var array> @@ -86,14 +79,11 @@ final class StatementsProvider private static ?Parser $parser = null; public function __construct( - FileProvider $file_provider, - ?ParserCacheProvider $parser_cache_provider = null, - ?FileStorageCacheProvider $file_storage_cache_provider = null, + private readonly FileProvider $file_provider, + public ?ParserCacheProvider $parser_cache_provider = null, + private readonly ?FileStorageCacheProvider $file_storage_cache_provider = null, ) { - $this->file_provider = $file_provider; - $this->parser_cache_provider = $parser_cache_provider; $this->this_modified_time = filemtime(__FILE__); - $this->file_storage_cache_provider = $file_storage_cache_provider; } /** @@ -119,11 +109,7 @@ public function getStatementsForFile( $config = Config::getInstance(); - if (PHP_VERSION_ID >= 8_01_00) { - $file_content_hash = hash('xxh128', $version . $file_contents); - } else { - $file_content_hash = hash('md4', $version . $file_contents); - } + $file_content_hash = hash('xxh128', $version . $file_contents); if (!$this->parser_cache_provider || (!$config->isInProjectDirs($file_path) && strpos($file_path, 'vendor')) @@ -211,7 +197,7 @@ public function getStatementsForFile( $changed_members = array_map( static function (string $key) use ($file_path_hash): string { - if (strpos($key, 'use:') === 0) { + if (str_starts_with($key, 'use:')) { return $key . ':' . $file_path_hash; } @@ -288,7 +274,7 @@ public function getChangedMembers(): array */ public function addChangedMembers(array $more_changed_members): void { - $this->changed_members = array_merge($more_changed_members, $this->changed_members); + $this->changed_members = [...$more_changed_members, ...$this->changed_members]; } /** @@ -304,7 +290,7 @@ public function getUnchangedSignatureMembers(): array */ public function addUnchangedSignatureMembers(array $more_unchanged_members): void { - $this->unchanged_signature_members = array_merge($more_unchanged_members, $this->unchanged_signature_members); + $this->unchanged_signature_members = [...$more_unchanged_members, ...$this->unchanged_signature_members]; } /** @@ -355,7 +341,7 @@ public function getDeletionRanges(): array */ public function addDiffMap(array $diff_map): void { - $this->diff_map = array_merge($diff_map, $this->diff_map); + $this->diff_map = [...$diff_map, ...$this->diff_map]; } /** @@ -363,7 +349,7 @@ public function addDiffMap(array $diff_map): void */ public function addDeletionRanges(array $deletion_ranges): void { - $this->deletion_ranges = array_merge($deletion_ranges, $this->deletion_ranges); + $this->deletion_ranges = [...$deletion_ranges, ...$this->deletion_ranges]; } public function resetDiffs(): void @@ -429,7 +415,7 @@ public static function parseStatements( try { /** @var list */ $stmts = self::$parser->parse($file_contents, $error_handler) ?: []; - } catch (Throwable $t) { + } catch (Throwable) { $stmts = []; // hope this got caught below @@ -439,7 +425,7 @@ public static function parseStatements( try { /** @var list */ $stmts = self::$parser->parse($file_contents, $error_handler) ?: []; - } catch (Throwable $t) { + } catch (Throwable) { $stmts = []; // hope this got caught below diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index aea5ef4ea69..3161c0373af 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -19,7 +19,10 @@ use function preg_match; use function preg_replace; use function rtrim; +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 strspn; @@ -44,21 +47,21 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock // Strip off comments. $docblock = trim($docblock); - if (strpos($docblock, '/**') === 0) { + if (str_starts_with($docblock, '/**')) { $docblock = substr($docblock, 3); } - if (substr($docblock, -2) === '*/') { + if (str_ends_with($docblock, '*/')) { $docblock = substr($docblock, 0, -2); - if (substr($docblock, -1) === '*') { + if (str_ends_with($docblock, '*')) { $docblock = substr($docblock, 0, -1); } } // Normalize multi-line @specials. $lines = explode("\n", str_replace("\t", ' ', $docblock)); - $has_r = strpos($docblock, "\r") === false ? false : true; + $has_r = !str_contains($docblock, "\r") ? false : true; $special = []; @@ -66,7 +69,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock $last = false; foreach ($lines as $k => $line) { - if (strpos($line, '@') !== false && preg_match('/^ *\*?\s*@\w/', $line)) { + if (str_contains($line, '@') && preg_match('/^ *\*?\s*@\w/', $line)) { $last = $k; } elseif (trim($line) === '') { $last = false; @@ -103,7 +106,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock [$type] = $type_info; [$data, $data_offset] = $data_info; - if (strpos($data, '*') !== false) { + if (str_contains($data, '*')) { $data = rtrim((string) preg_replace('/^ *\*\s*$/m', '', $data)); } diff --git a/src/Psalm/Internal/Scanner/FileScanner.php b/src/Psalm/Internal/Scanner/FileScanner.php index aaf92dd670f..d8ba100c47f 100644 --- a/src/Psalm/Internal/Scanner/FileScanner.php +++ b/src/Psalm/Internal/Scanner/FileScanner.php @@ -20,17 +20,8 @@ */ class FileScanner implements FileSource { - public string $file_path; - - public string $file_name; - - public bool $will_analyze; - - public function __construct(string $file_path, string $file_name, bool $will_analyze) + public function __construct(public string $file_path, public string $file_name, public bool $will_analyze) { - $this->file_path = $file_path; - $this->file_name = $file_name; - $this->will_analyze = $will_analyze; } public function scan( diff --git a/src/Psalm/Internal/Scanner/ParsedDocblock.php b/src/Psalm/Internal/Scanner/ParsedDocblock.php index e495e749fd5..61e27b076a9 100644 --- a/src/Psalm/Internal/Scanner/ParsedDocblock.php +++ b/src/Psalm/Internal/Scanner/ParsedDocblock.php @@ -12,24 +12,14 @@ */ final class ParsedDocblock { - public string $description; - - public string $first_line_padding; - - /** @var array> */ - public array $tags = []; - /** @var array> */ public array $combined_tags = []; private static bool $shouldAddNewLineBetweenAnnotations = true; /** @param array> $tags */ - public function __construct(string $description, array $tags, string $first_line_padding = '') + public function __construct(public string $description, public array $tags, public string $first_line_padding = '') { - $this->description = $description; - $this->tags = $tags; - $this->first_line_padding = $first_line_padding; } public function render(string $left_padding): string diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index e1518a8a0e6..2ca354dc68f 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -18,8 +18,8 @@ use function count; use function is_string; +use function str_contains; use function str_replace; -use function strpos; use function strtolower; /** @@ -178,10 +178,10 @@ static function ( } if (($mapped_type = $map[''] ?? null) && is_string($mapped_type)) { - if (strpos($mapped_type, '@') !== false) { + if (str_contains($mapped_type, '@')) { $mapped_type = str_replace('@', $offset_arg_value, $mapped_type); - if (strpos($mapped_type, '.') === false) { + if (!str_contains($mapped_type, '.')) { return new Union([ new TNamedObject($mapped_type), ]); @@ -320,10 +320,10 @@ static function ( } if (($mapped_type = $map[''] ?? null) && is_string($mapped_type)) { - if (strpos($mapped_type, '@') !== false) { + if (str_contains($mapped_type, '@')) { $mapped_type = str_replace('@', $offset_arg_value, $mapped_type); - if (strpos($mapped_type, '.') === false) { + if (!str_contains($mapped_type, '.')) { return new Union([ new TNamedObject($mapped_type), ]); diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php index 656a2c9f359..c3b11b3061e 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayOffsetFetch.php @@ -12,13 +12,9 @@ */ final class ArrayOffsetFetch extends UnresolvedConstantComponent { - public UnresolvedConstantComponent $array; - - public UnresolvedConstantComponent $offset; - - public function __construct(UnresolvedConstantComponent $left, UnresolvedConstantComponent $right) - { - $this->array = $left; - $this->offset = $right; + public function __construct( + public readonly UnresolvedConstantComponent $array, + public readonly UnresolvedConstantComponent $offset, + ) { } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php index f0ca5d0b6ca..ca03b3a54db 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArraySpread.php @@ -12,10 +12,7 @@ */ final class ArraySpread extends UnresolvedConstantComponent { - public UnresolvedConstantComponent $array; - - public function __construct(UnresolvedConstantComponent $array) + public function __construct(public readonly UnresolvedConstantComponent $array) { - $this->array = $array; } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php index 7bc11b7d284..6aa4df4008a 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ArrayValue.php @@ -12,12 +12,8 @@ */ final class ArrayValue extends UnresolvedConstantComponent { - /** @var array */ - public array $entries; - /** @param list $entries */ - public function __construct(array $entries) + public function __construct(public readonly array $entries) { - $this->entries = $entries; } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php index 6b4f4ec9eb1..5833c76a45c 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ClassConstant.php @@ -12,13 +12,7 @@ */ final class ClassConstant extends UnresolvedConstantComponent { - public string $fqcln; - - public string $name; - - public function __construct(string $fqcln, string $name) + public function __construct(public readonly string $fqcln, public readonly string $name) { - $this->fqcln = $fqcln; - $this->name = $name; } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php index b3d59fc8834..250f03a1b87 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/Constant.php @@ -12,13 +12,7 @@ */ final class Constant extends UnresolvedConstantComponent { - public string $name; - - public bool $is_fully_qualified; - - public function __construct(string $name, bool $is_fully_qualified) + public function __construct(public readonly string $name, public readonly bool $is_fully_qualified) { - $this->name = $name; - $this->is_fully_qualified = $is_fully_qualified; } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumPropertyFetch.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumPropertyFetch.php index a9c93d66e38..26a2c8844e1 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumPropertyFetch.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/EnumPropertyFetch.php @@ -12,13 +12,7 @@ */ abstract class EnumPropertyFetch extends UnresolvedConstantComponent { - public string $fqcln; - - public string $case; - - public function __construct(string $fqcln, string $case) + public function __construct(public readonly string $fqcln, public readonly string $case) { - $this->fqcln = $fqcln; - $this->case = $case; } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php index 17198114647..9cd222dc822 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/KeyValuePair.php @@ -12,13 +12,9 @@ */ final class KeyValuePair extends UnresolvedConstantComponent { - public ?UnresolvedConstantComponent $key = null; - - public UnresolvedConstantComponent $value; - - public function __construct(?UnresolvedConstantComponent $key, UnresolvedConstantComponent $value) - { - $this->key = $key; - $this->value = $value; + public function __construct( + public readonly ?UnresolvedConstantComponent $key, + public readonly UnresolvedConstantComponent $value, + ) { } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php index 7f6b7589158..73d12f96f7d 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/ScalarValue.php @@ -12,10 +12,7 @@ */ final class ScalarValue extends UnresolvedConstantComponent { - public string|int|float|bool|null $value = null; - - public function __construct(string|int|float|bool|null $value) + public function __construct(public readonly string|int|float|bool|null $value) { - $this->value = $value; } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php index 40acb62ebe9..58bd8cb2f9f 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedBinaryOp.php @@ -15,13 +15,9 @@ abstract class UnresolvedBinaryOp extends UnresolvedConstantComponent { use ImmutableNonCloneableTrait; - public UnresolvedConstantComponent $left; - - public UnresolvedConstantComponent $right; - - public function __construct(UnresolvedConstantComponent $left, UnresolvedConstantComponent $right) - { - $this->left = $left; - $this->right = $right; + public function __construct( + public readonly UnresolvedConstantComponent $left, + public readonly UnresolvedConstantComponent $right, + ) { } } diff --git a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php index f34383287c4..623bae4b6d8 100644 --- a/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php +++ b/src/Psalm/Internal/Scanner/UnresolvedConstant/UnresolvedTernary.php @@ -15,19 +15,10 @@ final class UnresolvedTernary extends UnresolvedConstantComponent { use ImmutableNonCloneableTrait; - public UnresolvedConstantComponent $cond; - - public ?UnresolvedConstantComponent $if = null; - - public UnresolvedConstantComponent $else; - public function __construct( - UnresolvedConstantComponent $cond, - ?UnresolvedConstantComponent $if, - UnresolvedConstantComponent $else, + public readonly UnresolvedConstantComponent $cond, + public readonly ?UnresolvedConstantComponent $if, + public readonly UnresolvedConstantComponent $else, ) { - $this->cond = $cond; - $this->if = $if; - $this->else = $else; } } diff --git a/src/Psalm/Internal/Scope/CaseScope.php b/src/Psalm/Internal/Scope/CaseScope.php index bb93b265589..371f01bc360 100644 --- a/src/Psalm/Internal/Scope/CaseScope.php +++ b/src/Psalm/Internal/Scope/CaseScope.php @@ -12,16 +12,13 @@ */ final class CaseScope { - public Context $parent_context; - /** * @var array|null */ public ?array $break_vars = null; - public function __construct(Context $parent_context) + public function __construct(public Context $parent_context) { - $this->parent_context = $parent_context; } public function __destruct() diff --git a/src/Psalm/Internal/Scope/FinallyScope.php b/src/Psalm/Internal/Scope/FinallyScope.php index d02d2bf01ef..a587c385bf9 100644 --- a/src/Psalm/Internal/Scope/FinallyScope.php +++ b/src/Psalm/Internal/Scope/FinallyScope.php @@ -11,16 +11,10 @@ */ final class FinallyScope { - /** - * @var array - */ - public array $vars_in_scope = []; - /** * @param array $vars_in_scope */ - public function __construct(array $vars_in_scope) + public function __construct(public array $vars_in_scope) { - $this->vars_in_scope = $vars_in_scope; } } diff --git a/src/Psalm/Internal/Scope/IfConditionalScope.php b/src/Psalm/Internal/Scope/IfConditionalScope.php index 386d8daf034..4db024f4c22 100644 --- a/src/Psalm/Internal/Scope/IfConditionalScope.php +++ b/src/Psalm/Internal/Scope/IfConditionalScope.php @@ -12,39 +12,17 @@ */ final class IfConditionalScope { - public Context $if_context; - - public Context $post_if_context; - - /** - * @var array - */ - public array $cond_referenced_var_ids; - - /** - * @var array - */ - public array $assigned_in_conditional_var_ids; - - /** @var list */ - public array $entry_clauses; - /** * @param array $cond_referenced_var_ids * @param array $assigned_in_conditional_var_ids * @param list $entry_clauses */ public function __construct( - Context $if_context, - Context $post_if_context, - array $cond_referenced_var_ids, - array $assigned_in_conditional_var_ids, - array $entry_clauses, + public Context $if_context, + public Context $post_if_context, + public array $cond_referenced_var_ids, + public array $assigned_in_conditional_var_ids, + public array $entry_clauses, ) { - $this->if_context = $if_context; - $this->post_if_context = $post_if_context; - $this->cond_referenced_var_ids = $cond_referenced_var_ids; - $this->assigned_in_conditional_var_ids = $assigned_in_conditional_var_ids; - $this->entry_clauses = $entry_clauses; } } diff --git a/src/Psalm/Internal/Scope/LoopScope.php b/src/Psalm/Internal/Scope/LoopScope.php index dbd45a88217..d599737e81e 100644 --- a/src/Psalm/Internal/Scope/LoopScope.php +++ b/src/Psalm/Internal/Scope/LoopScope.php @@ -14,10 +14,6 @@ final class LoopScope { public int $iteration_count = 0; - public Context $loop_context; - - public Context $loop_parent_context; - /** * @var array */ @@ -53,10 +49,8 @@ final class LoopScope */ public array $final_actions = []; - public function __construct(Context $loop_context, Context $parent_context) + public function __construct(public Context $loop_context, public Context $loop_parent_context) { - $this->loop_context = $loop_context; - $this->loop_parent_context = $parent_context; } public function __destruct() diff --git a/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php b/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php index dfbbc10c5e1..7a110f6a32e 100644 --- a/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/ClassLikeStubGenerator.php @@ -162,17 +162,11 @@ private static function getPropertyNodes(ClassLikeStorage $storage): array $property_nodes = []; foreach ($storage->properties as $property_name => $property_storage) { - switch ($property_storage->visibility) { - case ClassLikeAnalyzer::VISIBILITY_PRIVATE: - $flag = PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE; - break; - case ClassLikeAnalyzer::VISIBILITY_PROTECTED: - $flag = PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED; - break; - default: - $flag = PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC; - break; - } + $flag = match ($property_storage->visibility) { + ClassLikeAnalyzer::VISIBILITY_PRIVATE => PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE, + ClassLikeAnalyzer::VISIBILITY_PROTECTED => PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED, + default => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + }; $docblock = new ParsedDocblock('', []); @@ -227,17 +221,11 @@ private static function getMethodNodes(ClassLikeStorage $storage): array { throw new UnexpectedValueException('very bad'); } - switch ($method_storage->visibility) { - case ReflectionProperty::IS_PRIVATE: - $flag = PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE; - break; - case ReflectionProperty::IS_PROTECTED: - $flag = PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED; - break; - default: - $flag = PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC; - break; - } + $flag = match ($method_storage->visibility) { + ReflectionProperty::IS_PRIVATE => PhpParser\Node\Stmt\Class_::MODIFIER_PRIVATE, + ReflectionProperty::IS_PROTECTED => PhpParser\Node\Stmt\Class_::MODIFIER_PROTECTED, + default => PhpParser\Node\Stmt\Class_::MODIFIER_PUBLIC, + }; $docblock = new ParsedDocblock('', []); diff --git a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index 528c2e92ed8..301c2979d0b 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -63,12 +63,12 @@ public static function getAll( $psalm_base = dirname(__DIR__, 5); foreach ($class_provider->getAll() as $storage) { - if (strpos($storage->name, 'Psalm\\') === 0) { + if (str_starts_with($storage->name, 'Psalm\\')) { continue; } if ($storage->location - && strpos($storage->location->file_path, $psalm_base) === 0 + && str_starts_with($storage->location->file_path, $psalm_base) ) { continue; } @@ -97,7 +97,7 @@ public static function getAll( foreach ($codebase->functions->getAllStubbedFunctions() as $function_storage) { if ($function_storage->location - && strpos($function_storage->location->file_path, $psalm_base) === 0 + && str_starts_with($function_storage->location->file_path, $psalm_base) ) { continue; } @@ -143,7 +143,7 @@ public static function getAll( } foreach ($file_provider->getAll() as $file_storage) { - if (strpos($file_storage->file_path, $psalm_base) === 0) { + if (str_starts_with($file_storage->file_path, $psalm_base)) { continue; } diff --git a/src/Psalm/Internal/Type/ArrayType.php b/src/Psalm/Internal/Type/ArrayType.php index f115fcc9879..a0a9cebb2c9 100644 --- a/src/Psalm/Internal/Type/ArrayType.php +++ b/src/Psalm/Internal/Type/ArrayType.php @@ -17,20 +17,8 @@ */ final class ArrayType { - public Union $key; - - public Union $value; - - public bool $is_list; - - public ?int $count = null; - - public function __construct(Union $key, Union $value, bool $is_list, ?int $count) + public function __construct(public Union $key, public Union $value, public bool $is_list, public ?int $count) { - $this->key = $key; - $this->value = $value; - $this->is_list = $is_list; - $this->count = $count; } /** diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 1cc7af0733c..851dc29c1c6 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -64,7 +64,6 @@ use function array_intersect_key; use function array_merge; use function count; -use function get_class; use function is_string; /** @@ -830,7 +829,7 @@ private static function refineContainedAtomicWithAnother( bool $type_coerced, ): ?Atomic { if ($type_coerced - && get_class($type_2_atomic) === TNamedObject::class + && $type_2_atomic::class === TNamedObject::class && $type_1_atomic instanceof TGenericObject ) { // this is a hack - it's not actually rigorous, as the params may be different @@ -952,7 +951,7 @@ private static function handleLiteralEquality( $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_atomic_types as $atomic_key => $atomic_type) { - if (get_class($atomic_type) === TNamedObject::class + if ($atomic_type::class === TNamedObject::class && $atomic_type->value === $fq_enum_name ) { $can_be_equal = true; @@ -1547,7 +1546,7 @@ private static function handleIsA( if ($assertion_type instanceof TTemplateParamClass) { return [new TTemplateParam( $assertion_type->param_name, - new Union([$assertion_type->as_type ? $assertion_type->as_type : new TObject()]), + new Union([$assertion_type->as_type ?: new TObject()]), $assertion_type->defining_class, )]; } diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 11974afe85c..e024bc95fc0 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -43,7 +43,6 @@ use function array_values; use function assert; use function count; -use function get_class; use function strtolower; /** @@ -112,10 +111,10 @@ public static function isContainedBy( && !$container_type_part->extra_types && $input_type_part instanceof TMixed) ) { - if (get_class($input_type_part) === TMixed::class + if ($input_type_part::class === TMixed::class && ( - get_class($container_type_part) === TEmptyMixed::class - || get_class($container_type_part) === TNonEmptyMixed::class + $container_type_part::class === TEmptyMixed::class + || $container_type_part::class === TNonEmptyMixed::class ) ) { if ($atomic_comparison_result) { @@ -311,14 +310,14 @@ public static function isContainedBy( ); } - if (get_class($container_type_part) === TNamedObject::class + if ($container_type_part::class === TNamedObject::class && $input_type_part instanceof TEnumCase && $input_type_part->value === $container_type_part->value ) { return true; } - if (get_class($input_type_part) === TNamedObject::class + if ($input_type_part::class === TNamedObject::class && $container_type_part instanceof TEnumCase && $input_type_part->value === $container_type_part->value ) { @@ -381,8 +380,8 @@ public static function isContainedBy( return true; } - if (get_class($input_type_part) === TObject::class - && get_class($container_type_part) === TObject::class + if ($input_type_part::class === TObject::class + && $container_type_part::class === TObject::class ) { return true; } @@ -819,9 +818,9 @@ public static function canBeIdentical( ); } - if ((get_class($type1_part) === TArray::class + if (($type1_part::class === TArray::class && $type2_part instanceof TNonEmptyArray) - || (get_class($type2_part) === TArray::class + || ($type2_part::class === TArray::class && $type1_part instanceof TNonEmptyArray) ) { return UnionTypeComparator::canExpressionTypesBeIdentical( diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index ed65d78276d..047ddd08bda 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -211,7 +211,7 @@ public static function isNotExplicitlyCallableTypeCallable( if (!$codebase->methods->hasStorage($method_id)) { return false; } - } catch (Exception $e) { + } catch (Exception) { return false; } } @@ -306,7 +306,7 @@ public static function getCallableFromAtomic( $return_type, $function_storage->pure, ); - } catch (UnexpectedValueException $e) { + } catch (UnexpectedValueException) { if (InternalCallMapHandler::inCallMap($input_type_part->value)) { $args = []; @@ -371,7 +371,7 @@ public static function getCallableFromAtomic( $converted_return_type, $method_storage->pure, ); - } catch (UnexpectedValueException $e) { + } catch (UnexpectedValueException) { // do nothing } } diff --git a/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php b/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php index e63dd39a0f6..1c9b4ed1ab9 100644 --- a/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php @@ -11,8 +11,6 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParamClass; -use function get_class; - /** * @internal */ @@ -36,7 +34,7 @@ public static function isContainedBy( } if ($container_type_part instanceof TTemplateParamClass - && get_class($input_type_part) === TClassString::class + && $input_type_part::class === TClassString::class ) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index ea0ff25f0a8..e9d150d0d21 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -13,7 +13,6 @@ use UnexpectedValueException; use function count; -use function get_class; /** * @internal @@ -59,11 +58,11 @@ public static function isContainedByUnion( ); if (isset($container_atomic_types['int'])) { - if (get_class($container_atomic_types['int']) === TInt::class) { + if ($container_atomic_types['int']::class === TInt::class) { return true; } - if (get_class($container_atomic_types['int']) === TNonspecificLiteralInt::class) { + if ($container_atomic_types['int']::class === TNonspecificLiteralInt::class) { return true; } diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5689350663b..73454ae8a88 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -18,6 +18,7 @@ use function count; use function current; use function in_array; +use function str_starts_with; use function strpos; use function strtolower; use function substr; @@ -43,8 +44,8 @@ public static function isShallowlyContainedBy( && $container_type_part->defining_class != $input_type_part->defining_class && 1 == count($container_type_part->as->getAtomicTypes()) && 1 == count($input_type_part->as->getAtomicTypes())) { - $containerDefinedInFunction = strpos($container_type_part->defining_class, 'fn-') === 0; - $inputDefinedInFunction = strpos($input_type_part->defining_class, 'fn-') === 0; + $containerDefinedInFunction = str_starts_with($container_type_part->defining_class, 'fn-'); + $inputDefinedInFunction = str_starts_with($input_type_part->defining_class, 'fn-'); if ($inputDefinedInFunction) { $separatorPos = strpos($input_type_part->defining_class, '::'); if ($separatorPos === false) { @@ -187,11 +188,11 @@ private static function isIntersectionShallowlyContainedBy( && $intersection_input_type instanceof TTemplateParam ) { if (!$allow_interface_equality) { - if (strpos($intersection_container_type->defining_class, 'fn-') === 0 - || strpos($intersection_input_type->defining_class, 'fn-') === 0 + if (str_starts_with($intersection_container_type->defining_class, 'fn-') + || str_starts_with($intersection_input_type->defining_class, 'fn-') ) { - if (strpos($intersection_input_type->defining_class, 'fn-') === 0 - && strpos($intersection_container_type->defining_class, 'fn-') === 0 + if (str_starts_with($intersection_input_type->defining_class, 'fn-') + && str_starts_with($intersection_container_type->defining_class, 'fn-') && $intersection_input_type->defining_class !== $intersection_container_type->defining_class ) { @@ -215,11 +216,11 @@ private static function isIntersectionShallowlyContainedBy( if ($intersection_container_type->param_name !== $intersection_input_type->param_name || ($intersection_container_type->defining_class !== $intersection_input_type->defining_class - && strpos($intersection_input_type->defining_class, 'fn-') !== 0 - && strpos($intersection_container_type->defining_class, 'fn-') !== 0) + && !str_starts_with($intersection_input_type->defining_class, 'fn-') + && !str_starts_with($intersection_container_type->defining_class, 'fn-')) ) { - if (strpos($intersection_input_type->defining_class, 'fn-') === 0 - || strpos($intersection_container_type->defining_class, 'fn-') === 0 + if (str_starts_with($intersection_input_type->defining_class, 'fn-') + || str_starts_with($intersection_container_type->defining_class, 'fn-') ) { return false; } diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index fdef3c46fc7..f6ba06538c7 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -40,7 +40,6 @@ use Psalm\Type\Atomic\TTraitString; use Psalm\Type\Atomic\TTrue; -use function get_class; use function is_numeric; use function strtolower; @@ -57,26 +56,26 @@ public static function isContainedBy( bool $allow_float_int_equality = true, ?TypeComparisonResult $atomic_comparison_result = null, ): bool { - if (get_class($container_type_part) === TString::class + if ($container_type_part::class === TString::class && $input_type_part instanceof TString ) { return true; } - if (get_class($container_type_part) === TInt::class + if ($container_type_part::class === TInt::class && $input_type_part instanceof TInt ) { return true; } - if (get_class($container_type_part) === TFloat::class + if ($container_type_part::class === TFloat::class && $input_type_part instanceof TFloat ) { return true; } if ($container_type_part instanceof TNonEmptyString - && get_class($input_type_part) === TString::class + && $input_type_part::class === TString::class ) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; @@ -119,10 +118,10 @@ public static function isContainedBy( } if ($input_type_part instanceof TCallableString - && (get_class($container_type_part) === TSingleLetter::class - || get_class($container_type_part) === TNonEmptyString::class - || get_class($container_type_part) === TNonFalsyString::class - || get_class($container_type_part) === TLowercaseString::class) + && ($container_type_part::class === TSingleLetter::class + || $container_type_part::class === TNonEmptyString::class + || $container_type_part::class === TNonFalsyString::class + || $container_type_part::class === TLowercaseString::class) ) { return true; } @@ -281,12 +280,12 @@ public static function isContainedBy( return true; } - if (get_class($container_type_part) === TFloat::class && $input_type_part instanceof TLiteralFloat) { + if ($container_type_part::class === TFloat::class && $input_type_part instanceof TLiteralFloat) { return true; } - if ((get_class($container_type_part) === TNonEmptyString::class - || get_class($container_type_part) === TNonEmptyNonspecificLiteralString::class) + if (($container_type_part::class === TNonEmptyString::class + || $container_type_part::class === TNonEmptyNonspecificLiteralString::class) && $input_type_part instanceof TNonFalsyString ) { return true; @@ -323,15 +322,15 @@ public static function isContainedBy( return false; } - if ((get_class($container_type_part) === TNonEmptyString::class - || get_class($container_type_part) === TNonFalsyString::class - || get_class($container_type_part) === TSingleLetter::class) + if (($container_type_part::class === TNonEmptyString::class + || $container_type_part::class === TNonFalsyString::class + || $container_type_part::class === TSingleLetter::class) && $input_type_part instanceof TLiteralString ) { return true; } - if (get_class($input_type_part) === TInt::class && $container_type_part instanceof TLiteralInt) { + if ($input_type_part::class === TInt::class && $container_type_part instanceof TLiteralInt) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; $atomic_comparison_result->type_coerced_from_scalar = true; @@ -368,7 +367,7 @@ public static function isContainedBy( return false; } - if (get_class($input_type_part) === TFloat::class && $container_type_part instanceof TLiteralFloat) { + if ($input_type_part::class === TFloat::class && $container_type_part instanceof TLiteralFloat) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; $atomic_comparison_result->type_coerced_from_scalar = true; @@ -377,8 +376,8 @@ public static function isContainedBy( return false; } - if ((get_class($input_type_part) === TString::class - || get_class($input_type_part) === TSingleLetter::class + if (($input_type_part::class === TString::class + || $input_type_part::class === TSingleLetter::class || $input_type_part instanceof TNonEmptyString || $input_type_part instanceof TNonspecificLiteralString) && $container_type_part instanceof TLiteralString @@ -423,7 +422,7 @@ public static function isContainedBy( } if ($container_type_part instanceof TTraitString - && (get_class($input_type_part) === TString::class + && ($input_type_part::class === TString::class || $input_type_part instanceof TNonEmptyString || $input_type_part instanceof TNonEmptyNonspecificLiteralString) ) { @@ -436,15 +435,15 @@ public static function isContainedBy( if (($input_type_part instanceof TClassString || $input_type_part instanceof TLiteralClassString) - && (get_class($container_type_part) === TSingleLetter::class - || get_class($container_type_part) === TNonEmptyString::class - || get_class($container_type_part) === TNonFalsyString::class) + && ($container_type_part::class === TSingleLetter::class + || $container_type_part::class === TNonEmptyString::class + || $container_type_part::class === TNonFalsyString::class) ) { return true; } if ($input_type_part instanceof TNumericString - && get_class($container_type_part) === TNonEmptyString::class + && $container_type_part::class === TNonEmptyString::class ) { return true; } @@ -503,7 +502,7 @@ public static function isContainedBy( } if ($input_type_part instanceof TLowercaseString - && get_class($container_type_part) === TNonEmptyString::class) { + && $container_type_part::class === TNonEmptyString::class) { return false; } diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index e562f76c7b1..b6e85303f01 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -36,7 +36,6 @@ use function array_merge; use function array_values; use function count; -use function get_class; use function strtolower; /** @@ -187,7 +186,7 @@ public static function reconcile( $iterable->type_params[1], ], )); - } elseif ($assertion_type !== null && get_class($assertion_type) === TInt::class + } elseif ($assertion_type !== null && $assertion_type::class === TInt::class && isset($existing_var_type->getAtomicTypes()['array-key']) && !$is_equality ) { @@ -371,7 +370,7 @@ private static function handleLiteralNegatedEquality( } if (isset($existing_var_type->getAtomicTypes()['int']) - && get_class($existing_var_type->getAtomicTypes()['int']) === Type\Atomic\TInt::class + && $existing_var_type->getAtomicTypes()['int']::class === Type\Atomic\TInt::class ) { $redundant = false; //this may be used to generate a range containing any int except the one that was asserted against @@ -394,7 +393,7 @@ private static function handleLiteralNegatedEquality( } elseif ($assertion_type->value === "") { $existing_var_type->addType(new TNonEmptyString()); } - } elseif (get_class($assertion_type) === TLiteralString::class) { + } elseif ($assertion_type::class === TLiteralString::class) { $scalar_var_type = $assertion_type; } } elseif ($assertion_type instanceof TLiteralFloat) { @@ -414,7 +413,7 @@ private static function handleLiteralNegatedEquality( $case_name = $assertion_type->case_name; foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $atomic_type) { - if (get_class($atomic_type) === TNamedObject::class + if ($atomic_type::class === TNamedObject::class && $atomic_type->value === $fq_enum_name ) { $codebase = $statements_analyzer->getCodebase(); diff --git a/src/Psalm/Internal/Type/ParseTree.php b/src/Psalm/Internal/Type/ParseTree.php index 5693203547a..d9dd689d29d 100644 --- a/src/Psalm/Internal/Type/ParseTree.php +++ b/src/Psalm/Internal/Type/ParseTree.php @@ -14,13 +14,10 @@ class ParseTree */ public array $children = []; - public ?ParseTree $parent = null; - public bool $possibly_undefined = false; - public function __construct(?ParseTree $parent = null) + public function __construct(public ?ParseTree $parent = null) { - $this->parent = $parent; } public function __destruct() diff --git a/src/Psalm/Internal/Type/ParseTree/CallableTree.php b/src/Psalm/Internal/Type/ParseTree/CallableTree.php index 6196c72d760..96c050e77bc 100644 --- a/src/Psalm/Internal/Type/ParseTree/CallableTree.php +++ b/src/Psalm/Internal/Type/ParseTree/CallableTree.php @@ -11,13 +11,10 @@ */ final class CallableTree extends ParseTree { - public string $value; - public bool $terminated = false; - public function __construct(string $value, ?ParseTree $parent = null) + public function __construct(public string $value, ?ParseTree $parent = null) { - $this->value = $value; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php b/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php index 6cac672cdb2..7c5d7d3ff21 100644 --- a/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php +++ b/src/Psalm/Internal/Type/ParseTree/ConditionalTree.php @@ -11,11 +11,8 @@ */ final class ConditionalTree extends ParseTree { - public TemplateIsTree $condition; - - public function __construct(TemplateIsTree $condition, ?ParseTree $parent = null) + public function __construct(public TemplateIsTree $condition, ?ParseTree $parent = null) { - $this->condition = $condition; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/GenericTree.php b/src/Psalm/Internal/Type/ParseTree/GenericTree.php index 966e28fb23f..d65bbcf0367 100644 --- a/src/Psalm/Internal/Type/ParseTree/GenericTree.php +++ b/src/Psalm/Internal/Type/ParseTree/GenericTree.php @@ -11,13 +11,10 @@ */ final class GenericTree extends ParseTree { - public string $value; - public bool $terminated = false; - public function __construct(string $value, ?ParseTree $parent = null) + public function __construct(public string $value, ?ParseTree $parent = null) { - $this->value = $value; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php b/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php index 39891f6f23e..59043c04c4b 100644 --- a/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php +++ b/src/Psalm/Internal/Type/ParseTree/IndexedAccessTree.php @@ -11,11 +11,8 @@ */ final class IndexedAccessTree extends ParseTree { - public string $value; - - public function __construct(string $value, ?ParseTree $parent = null) + public function __construct(public string $value, ?ParseTree $parent = null) { - $this->value = $value; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php b/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php index 1c4793306f0..4b1ff376554 100644 --- a/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php +++ b/src/Psalm/Internal/Type/ParseTree/KeyedArrayPropertyTree.php @@ -11,11 +11,8 @@ */ final class KeyedArrayPropertyTree extends ParseTree { - public string $value; - - public function __construct(string $value, ?ParseTree $parent = null) + public function __construct(public string $value, ?ParseTree $parent = null) { - $this->value = $value; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php b/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php index 9a8e2a69b67..2008fb90300 100644 --- a/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php +++ b/src/Psalm/Internal/Type/ParseTree/KeyedArrayTree.php @@ -11,13 +11,10 @@ */ final class KeyedArrayTree extends ParseTree { - public string $value; - public bool $terminated = false; - public function __construct(string $value, ?ParseTree $parent = null) + public function __construct(public string $value, ?ParseTree $parent = null) { - $this->value = $value; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php b/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php index 833f33109fa..9a0a70937e1 100644 --- a/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php +++ b/src/Psalm/Internal/Type/ParseTree/MethodParamTree.php @@ -11,23 +11,14 @@ */ final class MethodParamTree extends ParseTree { - public bool $variadic; - public string $default = ''; - public bool $byref; - - public string $name; - public function __construct( - string $name, - bool $byref, - bool $variadic, + public string $name, + public bool $byref, + public bool $variadic, ?ParseTree $parent = null, ) { - $this->name = $name; - $this->byref = $byref; - $this->variadic = $variadic; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/MethodTree.php b/src/Psalm/Internal/Type/ParseTree/MethodTree.php index 595e3cdfcd7..5e1da1ec6c8 100644 --- a/src/Psalm/Internal/Type/ParseTree/MethodTree.php +++ b/src/Psalm/Internal/Type/ParseTree/MethodTree.php @@ -11,11 +11,8 @@ */ final class MethodTree extends ParseTree { - public string $value; - - public function __construct(string $value, ?ParseTree $parent = null) + public function __construct(public string $value, ?ParseTree $parent = null) { - $this->value = $value; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php b/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php index b7d79634880..b27acf72bae 100644 --- a/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php +++ b/src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php @@ -11,14 +11,8 @@ */ final class TemplateAsTree extends ParseTree { - public string $param_name; - - public string $as; - - public function __construct(string $param_name, string $as, ?ParseTree $parent = null) + public function __construct(public string $param_name, public string $as, ?ParseTree $parent = null) { - $this->param_name = $param_name; - $this->as = $as; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php b/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php index 1bff424cacf..07e29c9b149 100644 --- a/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php +++ b/src/Psalm/Internal/Type/ParseTree/TemplateIsTree.php @@ -11,11 +11,8 @@ */ final class TemplateIsTree extends ParseTree { - public string $param_name; - - public function __construct(string $param_name, ?ParseTree $parent = null) + public function __construct(public string $param_name, ?ParseTree $parent = null) { - $this->param_name = $param_name; $this->parent = $parent; } } diff --git a/src/Psalm/Internal/Type/ParseTree/Value.php b/src/Psalm/Internal/Type/ParseTree/Value.php index 4367469d151..04036e7b9f2 100644 --- a/src/Psalm/Internal/Type/ParseTree/Value.php +++ b/src/Psalm/Internal/Type/ParseTree/Value.php @@ -11,24 +11,15 @@ */ final class Value extends ParseTree { - public string $value; - - public int $offset_start; - - public int $offset_end; - public ?string $text = null; public function __construct( - string $value, - int $offset_start, - int $offset_end, + public string $value, + public int $offset_start, + public int $offset_end, ?string $text, ParseTree $parent = null, ) { - $this->offset_start = $offset_start; - $this->offset_end = $offset_end; - $this->value = $value; $this->parent = $parent; $this->text = $text === $value ? null : $text; } diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 737b5ac3f1b..59df5512ed2 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -43,19 +43,15 @@ final class ParseTreeCreator private ParseTree $current_leaf; - /** @var array */ - private array $type_tokens; - - private int $type_token_count; + private readonly int $type_token_count; private int $t = 0; /** * @param list $type_tokens */ - public function __construct(array $type_tokens) + public function __construct(private array $type_tokens) { - $this->type_tokens = $type_tokens; $this->type_token_count = count($type_tokens); $this->parse_tree = new Root(); $this->current_leaf = $this->parse_tree; @@ -830,7 +826,11 @@ private function handleValue(array $type_token): void $nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; - if ($nexter_token && strpos($nexter_token[0], '@') !== false) { + if ($nexter_token + && strpos($nexter_token[0], '@') !== false + && $type_token[0] !== 'list' + && $type_token[0] !== 'array' + ) { $this->t = $this->type_token_count; if ($type_token[0] === '$this') { $type_token[0] = 'static'; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 5686a31b5ee..cec91f27f89 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -82,7 +82,6 @@ use function assert; use function count; use function explode; -use function get_class; use function in_array; use function is_int; use function min; @@ -428,7 +427,7 @@ public static function reconcile( ); } - if ($assertion_type && get_class($assertion_type) === TBool::class) { + if ($assertion_type && $assertion_type::class === TBool::class) { return self::reconcileBool( $assertion, $existing_var_type, @@ -467,7 +466,7 @@ public static function reconcile( ); } - if ($assertion_type && get_class($assertion_type) === TString::class) { + if ($assertion_type && $assertion_type::class === TString::class) { return self::reconcileString( $assertion, $existing_var_type, @@ -480,7 +479,7 @@ public static function reconcile( ); } - if ($assertion_type && get_class($assertion_type) === TInt::class) { + if ($assertion_type && $assertion_type::class === TInt::class) { return self::reconcileInt( $assertion, $existing_var_type, @@ -1007,7 +1006,7 @@ private static function reconcileString( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TString) { - if (get_class($type) === TString::class) { + if ($type::class === TString::class) { $type = $type->setFromDocblock(false); } $string_types[] = $type; @@ -1099,7 +1098,7 @@ private static function reconcileInt( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TInt) { - if (get_class($type) === TInt::class) { + if ($type::class === TInt::class) { $type = $type->setFromDocblock(false); } @@ -2477,7 +2476,7 @@ private static function reconcileStringArrayAccess( foreach ($existing_var_atomic_types as $type) { if ($type->isArrayAccessibleWithStringKey($codebase)) { - if (get_class($type) === TArray::class) { + if ($type::class === TArray::class) { $array_types[] = new TNonEmptyArray($type->type_params); } elseif ($type instanceof TKeyedArray && $type->is_list) { $properties = $type->properties; @@ -2542,7 +2541,7 @@ private static function reconcileIntArrayAccess( foreach ($existing_var_atomic_types as $type) { if ($type->isArrayAccessibleWithIntOrStringKey($codebase)) { - if (get_class($type) === TArray::class) { + if ($type::class === TArray::class) { $array_types[] = new TNonEmptyArray($type->type_params); } else { $array_types[] = $type; @@ -2613,13 +2612,13 @@ private static function reconcileCallable( && $codebase->methodExists($type->value . '::__invoke') ) { $callable_types[] = $type; - } elseif (get_class($type) === TString::class - || get_class($type) === TNonEmptyString::class - || get_class($type) === TNonFalsyString::class + } elseif ($type::class === TString::class + || $type::class === TNonEmptyString::class + || $type::class === TNonFalsyString::class ) { $callable_types[] = new TCallableString(); $redundant = false; - } elseif (get_class($type) === TLiteralString::class + } elseif ($type::class === TLiteralString::class && InternalCallMapHandler::inCallMap($type->value) ) { $callable_types[] = $type; @@ -2794,7 +2793,7 @@ private static function reconcileTruthyOrNonEmpty( if (isset($types['mixed'])) { $mixed_atomic_type = $types['mixed']; - if (get_class($mixed_atomic_type) === TMixed::class) { + if ($mixed_atomic_type::class === TMixed::class) { unset($types['mixed']); $types []= new TNonEmptyMixed(); } @@ -2803,7 +2802,7 @@ private static function reconcileTruthyOrNonEmpty( if (isset($types['scalar'])) { $scalar_atomic_type = $types['scalar']; - if (get_class($scalar_atomic_type) === TScalar::class) { + if ($scalar_atomic_type::class === TScalar::class) { unset($types['scalar']); $types []= new TNonEmptyScalar(); } @@ -2812,16 +2811,16 @@ private static function reconcileTruthyOrNonEmpty( if (isset($types['string'])) { $string_atomic_type = $types['string']; - if (get_class($string_atomic_type) === TString::class) { + if ($string_atomic_type::class === TString::class) { unset($types['string']); $types []= new TNonFalsyString(); - } elseif (get_class($string_atomic_type) === TLowercaseString::class) { + } elseif ($string_atomic_type::class === TLowercaseString::class) { unset($types['string']); $types []= new TNonEmptyLowercaseString(); - } elseif (get_class($string_atomic_type) === TNonspecificLiteralString::class) { + } elseif ($string_atomic_type::class === TNonspecificLiteralString::class) { unset($types['string']); $types []= new TNonEmptyNonspecificLiteralString(); - } elseif (get_class($string_atomic_type) === TNonEmptyString::class) { + } elseif ($string_atomic_type::class === TNonEmptyString::class) { unset($types['string']); $types []= new TNonFalsyString(); } diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index e9bd56c7587..daedd9307fb 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -64,9 +64,8 @@ use Psalm\Type\Union; use function assert; -use function get_class; use function max; -use function strpos; +use function str_contains; /** * @internal @@ -98,7 +97,7 @@ public static function reconcile( if (!$existing_var_type->isNullable() && $key - && strpos($key, '[') === false + && !str_contains($key, '[') && (!$existing_var_type->hasMixed() || $existing_var_type->isAlwaysTruthy()) ) { if ($code_location) { @@ -293,7 +292,7 @@ public static function reconcile( ); } - if ($assertion_type && get_class($assertion_type) === TBool::class && !$existing_var_type->hasMixed()) { + if ($assertion_type && $assertion_type::class === TBool::class && !$existing_var_type->hasMixed()) { return self::reconcileBool( $assertion, $existing_var_type, @@ -332,7 +331,7 @@ public static function reconcile( ); } - if ($assertion_type && get_class($assertion_type) === TInt::class && !$existing_var_type->hasMixed()) { + if ($assertion_type && $assertion_type::class === TInt::class && !$existing_var_type->hasMixed()) { return self::reconcileInt( $assertion, $existing_var_type, @@ -345,7 +344,7 @@ public static function reconcile( ); } - if ($assertion_type && get_class($assertion_type) === TString::class && !$existing_var_type->hasMixed()) { + if ($assertion_type && $assertion_type::class === TString::class && !$existing_var_type->hasMixed()) { return self::reconcileString( $assertion, $existing_var_type, @@ -484,7 +483,7 @@ private static function reconcileBool( $redundant = false; } elseif (!$type instanceof TBool - || ($is_equality && get_class($type) === TBool::class) + || ($is_equality && $type::class === TBool::class) ) { if ($type instanceof TScalar) { $redundant = false; @@ -974,7 +973,7 @@ private static function reconcileFalsyOrEmpty( if ($existing_var_type->hasMixed()) { $mixed_atomic_type = $existing_var_type->getAtomicTypes()['mixed']; - if (get_class($mixed_atomic_type) === TMixed::class) { + if ($mixed_atomic_type::class === TMixed::class) { $existing_var_type->removeType('mixed'); $existing_var_type->addType(new TEmptyMixed()); } @@ -983,7 +982,7 @@ private static function reconcileFalsyOrEmpty( if ($existing_var_type->hasScalar()) { $scalar_atomic_type = $existing_var_type->getAtomicTypes()['scalar']; - if (get_class($scalar_atomic_type) === TScalar::class) { + if ($scalar_atomic_type::class === TScalar::class) { $existing_var_type->removeType('scalar'); $existing_var_type->addType(new TEmptyScalar()); } @@ -992,17 +991,17 @@ private static function reconcileFalsyOrEmpty( if ($existing_var_type->hasType('string')) { $string_atomic_type = $existing_var_type->getAtomicTypes()['string']; - if (get_class($string_atomic_type) === TString::class) { + if ($string_atomic_type::class === TString::class) { $existing_var_type->removeType('string'); $existing_var_type->addType(Type::getAtomicStringFromLiteral('')); $existing_var_type->addType(Type::getAtomicStringFromLiteral('0')); - } elseif (get_class($string_atomic_type) === TNonEmptyString::class) { + } elseif ($string_atomic_type::class === TNonEmptyString::class) { $existing_var_type->removeType('string'); $existing_var_type->addType(Type::getAtomicStringFromLiteral('0')); - } elseif (get_class($string_atomic_type) === TNonEmptyLowercaseString::class) { + } elseif ($string_atomic_type::class === TNonEmptyLowercaseString::class) { $existing_var_type->removeType('string'); $existing_var_type->addType(Type::getAtomicStringFromLiteral('0')); - } elseif (get_class($string_atomic_type) === TNonEmptyNonspecificLiteralString::class) { + } elseif ($string_atomic_type::class === TNonEmptyNonspecificLiteralString::class) { $existing_var_type->removeType('string'); $existing_var_type->addType(Type::getAtomicStringFromLiteral('0')); } diff --git a/src/Psalm/Internal/Type/TemplateBound.php b/src/Psalm/Internal/Type/TemplateBound.php index b67c79cc211..1e827651ebc 100644 --- a/src/Psalm/Internal/Type/TemplateBound.php +++ b/src/Psalm/Internal/Type/TemplateBound.php @@ -11,38 +11,26 @@ */ final class TemplateBound { - public Union $type; - - /** - * This is the depth at which the template appears in a given type. - * - * In the type Foo>> the type T appears at three different depths. - * - * The shallowest-appearance of the template takes prominence when inferring the type of T. - */ - public int $appearance_depth; - - /** - * The argument offset where this template was set - * - * In the type Foo the type appears at argument offsets 0 and 2 - */ - public ?int $arg_offset = null; - - /** - * When non-null, indicates an equality template bound (vs a lower or upper bound) - */ - public ?string $equality_bound_classlike = null; - public function __construct( - Union $type, - int $appearance_depth = 0, - ?int $arg_offset = null, - ?string $equality_bound_classlike = null, + public Union $type, + /** + * This is the depth at which the template appears in a given type. + * + * In the type Foo>> the type T appears at three different depths. + * + * The shallowest-appearance of the template takes prominence when inferring the type of T. + */ + public int $appearance_depth = 0, + /** + * The argument offset where this template was set + * + * In the type Foo the type appears at argument offsets 0 and 2 + */ + public ?int $arg_offset = null, + /** + * When non-null, indicates an equality template bound (vs a lower or upper bound) + */ + public ?string $equality_bound_classlike = null, ) { - $this->type = $type; - $this->appearance_depth = $appearance_depth; - $this->arg_offset = $arg_offset; - $this->equality_bound_classlike = $equality_bound_classlike; } } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 36f8b6c8fe7..4f3c192e919 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -36,7 +36,7 @@ use function array_shift; use function array_values; use function assert; -use function strpos; +use function str_starts_with; /** * @internal @@ -298,7 +298,7 @@ private static function replaceTemplateParam( } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { foreach ($template_type_map as $template_class => $_) { - if (strpos($template_class, 'fn-') === 0) { + if (str_starts_with($template_class, 'fn-')) { continue; } @@ -323,7 +323,7 @@ private static function replaceTemplateParam( } } } - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { } } } diff --git a/src/Psalm/Internal/Type/TemplateResult.php b/src/Psalm/Internal/Type/TemplateResult.php index 1ad9dc2399f..31663158288 100644 --- a/src/Psalm/Internal/Type/TemplateResult.php +++ b/src/Psalm/Internal/Type/TemplateResult.php @@ -6,7 +6,6 @@ use Psalm\Type\Union; -use function array_merge; use function array_replace_recursive; /** @@ -28,15 +27,10 @@ */ final class TemplateResult { - /** - * @var array> - */ - public array $template_types; - /** * @var array>> */ - public array $lower_bounds; + public array $lower_bounds = []; /** * @var array> @@ -57,11 +51,8 @@ final class TemplateResult * @param array> $template_types * @param array> $lower_bounds */ - public function __construct(array $template_types, array $lower_bounds) + public function __construct(public array $template_types, array $lower_bounds) { - $this->template_types = $template_types; - $this->lower_bounds = []; - foreach ($lower_bounds as $key1 => $boundSet) { foreach ($boundSet as $key2 => $bound) { $this->lower_bounds[$key1][$key2] = [new TemplateBound($bound)]; @@ -79,7 +70,7 @@ public function merge(TemplateResult $result): TemplateResult /** @var array>> $lower_bounds */ $lower_bounds = array_replace_recursive($instance->lower_bounds, $result->lower_bounds); $instance->lower_bounds = $lower_bounds; - $instance->template_types = array_merge($instance->template_types, $result->template_types); + $instance->template_types = [...$instance->template_types, ...$result->template_types]; return $instance; } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 62b9bfbe068..aa01a7ae93e 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -46,6 +46,7 @@ use function count; use function in_array; use function reset; +use function str_starts_with; use function strpos; use function strtolower; use function substr; @@ -505,7 +506,7 @@ private static function findMatchingAtomicTypesForTemplate( continue; } - if (strpos($input_key, $key . '&') === 0) { + if (str_starts_with($input_key, $key . '&')) { $matching_atomic_types[$atomic_input_type->getId()] = $atomic_input_type; continue; } @@ -530,7 +531,7 @@ private static function findMatchingAtomicTypesForTemplate( $matching_atomic_types[$atomic_input_type->getId()] = $atomic_input_type; continue; } - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // do nothing } } @@ -592,7 +593,7 @@ private static function findMatchingAtomicTypesForTemplate( $matching_atomic_types[$atomic_input_type->getId()] = $atomic_input_type; continue; } - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // do nothing } } diff --git a/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php index 945fbc0f103..d9b247ca687 100644 --- a/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/ClassTypeAlias.php @@ -12,16 +12,10 @@ */ final class ClassTypeAlias implements TypeAlias { - /** - * @var list - */ - public array $replacement_atomic_types; - /** * @param list $replacement_atomic_types */ - public function __construct(array $replacement_atomic_types) + public function __construct(public array $replacement_atomic_types) { - $this->replacement_atomic_types = $replacement_atomic_types; } } diff --git a/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php index 35d6893ee96..4a6bd27037c 100644 --- a/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/InlineTypeAlias.php @@ -15,16 +15,10 @@ final class InlineTypeAlias implements TypeAlias { use ImmutableNonCloneableTrait; - /** - * @var list - */ - public array $replacement_tokens; - /** * @param list $replacement_tokens */ - public function __construct(array $replacement_tokens) + public function __construct(public readonly array $replacement_tokens) { - $this->replacement_tokens = $replacement_tokens; } } diff --git a/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php b/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php index c8bfacbf0d4..e3a35026991 100644 --- a/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php +++ b/src/Psalm/Internal/Type/TypeAlias/LinkableTypeAlias.php @@ -15,27 +15,12 @@ final class LinkableTypeAlias implements TypeAlias { use ImmutableNonCloneableTrait; - public string $declaring_fq_classlike_name; - - public string $alias_name; - - public int $line_number; - - public int $start_offset; - - public int $end_offset; - public function __construct( - string $declaring_fq_classlike_name, - string $alias_name, - int $line_number, - int $start_offset, - int $end_offset, + public readonly string $declaring_fq_classlike_name, + public readonly string $alias_name, + public readonly int $line_number, + public readonly int $start_offset, + public readonly int $end_offset, ) { - $this->declaring_fq_classlike_name = $declaring_fq_classlike_name; - $this->alias_name = $alias_name; - $this->line_number = $line_number; - $this->start_offset = $start_offset; - $this->end_offset = $end_offset; } } diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0bc11918e7a..c80c79acc51 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -57,6 +57,9 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; + /** @var array */ + public array $objectlike_class_string_keys = []; + public bool $objectlike_sealed = true; public ?Union $objectlike_key_type = null; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 67850a2846b..ecda4b05ed3 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -63,7 +63,6 @@ use function array_values; use function assert; use function count; -use function get_class; use function is_int; use function is_numeric; use function min; @@ -289,7 +288,7 @@ public static function combine( } $has_non_specific_string = isset($combination->value_types['string']) - && get_class($combination->value_types['string']) === TString::class; + && $combination->value_types['string']::class === TString::class; if (!$has_non_specific_string) { $object_type = self::combine( @@ -439,11 +438,11 @@ private static function scrapeTypeProperties( return null; } - if (get_class($type) === TBool::class && isset($combination->value_types['false'])) { + if ($type::class === TBool::class && isset($combination->value_types['false'])) { unset($combination->value_types['false']); } - if (get_class($type) === TBool::class && isset($combination->value_types['true'])) { + if ($type::class === TBool::class && isset($combination->value_types['true'])) { unset($combination->value_types['true']); } @@ -663,6 +662,7 @@ private static function scrapeTypeProperties( $has_defined_keys = false; + $class_strings = $type->class_strings ?? []; foreach ($type->properties as $candidate_property_name => $candidate_property_type) { $value_type = $combination->objectlike_entries[$candidate_property_name] ?? null; @@ -702,6 +702,19 @@ private static function scrapeTypeProperties( } unset($missing_entries[$candidate_property_name]); + + if (is_int($candidate_property_name)) { + continue; + } + + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { + $combination->objectlike_class_string_keys[$candidate_property_name] = + $combination->objectlike_class_string_keys[$candidate_property_name] + && ($class_strings[$candidate_property_name] ?? false); + } else { + $combination->objectlike_class_string_keys[$candidate_property_name] = + ($class_strings[$candidate_property_name] ?? false); + } } if ($type->fallback_params) { @@ -1062,6 +1075,9 @@ private static function scrapeStringProperties( if ($has_only_numeric_strings) { $combination->value_types['string'] = $type; + } elseif (count($combination->strings) === 1 && !$has_only_non_empty_strings) { + $combination->value_types['string'] = $type; + return; } elseif ($has_only_non_empty_strings) { $combination->value_types['string'] = new TNonEmptyString(); } else { @@ -1105,52 +1121,52 @@ private static function scrapeStringProperties( } else { $combination->value_types[$type_key] = $type; } - } elseif (get_class($combination->value_types['string']) !== TString::class) { - if (get_class($type) === TString::class) { + } elseif ($combination->value_types['string']::class !== TString::class) { + if ($type::class === TString::class) { $combination->value_types['string'] = $type; - } elseif (get_class($combination->value_types['string']) !== get_class($type)) { - if (get_class($type) === TNonEmptyString::class - && get_class($combination->value_types['string']) === TNumericString::class + } elseif ($combination->value_types['string']::class !== $type::class) { + if ($type::class === TNonEmptyString::class + && $combination->value_types['string']::class === TNumericString::class ) { $combination->value_types['string'] = $type; - } elseif (get_class($type) === TNumericString::class - && get_class($combination->value_types['string']) === TNonEmptyString::class + } elseif ($type::class === TNumericString::class + && $combination->value_types['string']::class === TNonEmptyString::class ) { // do nothing - } elseif ((get_class($type) === TNonEmptyString::class - || get_class($type) === TNumericString::class) - && get_class($combination->value_types['string']) === TNonFalsyString::class + } elseif (($type::class === TNonEmptyString::class + || $type::class === TNumericString::class) + && $combination->value_types['string']::class === TNonFalsyString::class ) { $combination->value_types['string'] = $type; - } elseif (get_class($type) === TNonFalsyString::class - && (get_class($combination->value_types['string']) === TNonEmptyString::class - || get_class($combination->value_types['string']) === TNumericString::class) + } elseif ($type::class === TNonFalsyString::class + && ($combination->value_types['string']::class === TNonEmptyString::class + || $combination->value_types['string']::class === TNumericString::class) ) { // do nothing - } elseif ((get_class($type) === TNonEmptyString::class - || get_class($type) === TNonFalsyString::class) - && get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class + } elseif (($type::class === TNonEmptyString::class + || $type::class === TNonFalsyString::class) + && $combination->value_types['string']::class === TNonEmptyLowercaseString::class ) { $combination->value_types['string'] = new TNonEmptyString(); - } elseif ((get_class($combination->value_types['string']) === TNonEmptyString::class - || get_class($combination->value_types['string']) === TNonFalsyString::class) - && get_class($type) === TNonEmptyLowercaseString::class + } elseif (($combination->value_types['string']::class === TNonEmptyString::class + || $combination->value_types['string']::class === TNonFalsyString::class) + && $type::class === TNonEmptyLowercaseString::class ) { $combination->value_types['string'] = new TNonEmptyString(); - } elseif (get_class($type) === TLowercaseString::class - && get_class($combination->value_types['string']) === TNonEmptyLowercaseString::class + } elseif ($type::class === TLowercaseString::class + && $combination->value_types['string']::class === TNonEmptyLowercaseString::class ) { $combination->value_types['string'] = $type; - } elseif (get_class($combination->value_types['string']) === TLowercaseString::class - && get_class($type) === TNonEmptyLowercaseString::class + } elseif ($combination->value_types['string']::class === TLowercaseString::class + && $type::class === TNonEmptyLowercaseString::class ) { //no-change - } elseif (get_class($combination->value_types['string']) + } elseif ($combination->value_types['string']::class === TNonEmptyNonspecificLiteralString::class && $type instanceof TNonEmptyString ) { $combination->value_types['string'] = new TNonEmptyString(); - } elseif (get_class($type) === TNonEmptyNonspecificLiteralString::class + } elseif ($type::class === TNonEmptyNonspecificLiteralString::class && $combination->value_types['string'] instanceof TNonEmptyString ) { // do nothing @@ -1226,8 +1242,8 @@ private static function scrapeIntProperties( if ($combination->ints || !isset($combination->value_types['int'])) { $combination->value_types['int'] = $type; } elseif (isset($combination->value_types['int']) - && get_class($combination->value_types['int']) - !== get_class($type) + && $combination->value_types['int']::class + !== $type::class ) { $combination->value_types['int'] = new TInt(); } @@ -1309,7 +1325,7 @@ private static function getClassLikes(Codebase $codebase, string $fq_classlike_n { try { $class_storage = $codebase->classlike_storage_provider->get($fq_classlike_name); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { return []; } @@ -1416,7 +1432,7 @@ private static function handleKeyedArrayEntries( } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - null, + array_filter($combination->objectlike_class_string_keys), $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 9c858d4af72..884455268b2 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -44,7 +44,6 @@ use function array_merge; use function array_values; use function count; -use function get_class; use function is_string; use function reset; use function strtolower; @@ -156,10 +155,7 @@ public static function expandAtomic( ); if ($extra_type instanceof TNamedObject && $extra_type->extra_types) { - $new_intersection_types = array_merge( - $new_intersection_types, - $extra_type->extra_types, - ); + $new_intersection_types = [...$new_intersection_types, ...$extra_type->extra_types]; $extra_type = $extra_type->setIntersectionTypes([]); } $extra_types[$extra_type->getKey()] = $extra_type; @@ -254,7 +250,7 @@ public static function expandAtomic( $return_type->const_name, ReflectionProperty::IS_PRIVATE, ); - } catch (CircularReferenceException $e) { + } catch (CircularReferenceException) { $class_constant = null; } @@ -593,7 +589,7 @@ private static function expandNamedObject( bool &$expand_generic = false, ): TNamedObject|TTemplateParam { if ($expand_generic - && get_class($return_type) === TNamedObject::class + && $return_type::class === TNamedObject::class && !$return_type->extra_types && $codebase->classOrInterfaceExists($return_type->value) ) { @@ -1043,7 +1039,7 @@ private static function expandKeyOfValueOf( false, $return_type instanceof TValueOf, ); - } catch (CircularReferenceException $e) { + } catch (CircularReferenceException) { return [$return_type]; } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 5b3bb664fe7..3f344916582 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -50,13 +50,16 @@ use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPropertiesOf; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateIndexedAccess; use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; @@ -85,19 +88,24 @@ use function defined; use function end; use function explode; -use function get_class; +use function filter_var; use function in_array; use function is_int; use function is_numeric; use function preg_match; use function preg_replace; use function reset; +use function str_contains; +use function str_starts_with; use function stripslashes; use function strlen; use function strpos; use function strtolower; use function strtr; use function substr; +use function trim; + +use const FILTER_VALIDATE_INT; /** * @psalm-suppress InaccessibleProperty Allowed during construction @@ -125,8 +133,8 @@ public static function parseTokens( // Note: valid identifiers can include class names or $this if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $only_token[0])) { if (!is_numeric($only_token[0]) - && strpos($only_token[0], '\'') !== false - && strpos($only_token[0], '"') !== false + && str_contains($only_token[0], '\'') + && str_contains($only_token[0], '"') ) { throw new TypeParseTreeException("Invalid type '$only_token[0]'"); } @@ -394,7 +402,7 @@ public static function getTypeFromTree( } if (!$parse_tree instanceof Value) { - throw new InvalidArgumentException('Unrecognised parse tree type ' . get_class($parse_tree)); + throw new InvalidArgumentException('Unrecognised parse tree type ' . $parse_tree::class); } if ($parse_tree->value[0] === '"' || $parse_tree->value[0] === '\'') { @@ -635,16 +643,78 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('No generic params provided for type'); } - if ($generic_type_value === 'array' || $generic_type_value === 'associative-array') { + if ($generic_type_value === 'array' + || $generic_type_value === 'associative-array' + || $generic_type_value === 'non-empty-array' + ) { + if ($generic_type_value !== 'non-empty-array') { + $generic_type_value = 'array'; + } + if ($generic_params[0]->isMixed()) { $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { - throw new TypeParseTreeException('Too many template parameters for array'); + throw new TypeParseTreeException('Too many template parameters for '.$generic_type_value); + } + + if ($type_aliases !== []) { + $intersection_types = self::resolveTypeAliases( + $codebase, + $generic_params[0]->getAtomicTypes(), + ); + + if ($intersection_types !== []) { + $generic_params[0] = $generic_params[0]->setTypes($intersection_types); + } + } + + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + || !$from_docblock + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); } - return new TArray($generic_params, $from_docblock); + return $generic_type_value === 'array' + ? new TArray($generic_params, $from_docblock) + : new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock) + ; } if ($generic_type_value === 'arraylike-object') { @@ -663,18 +733,6 @@ private static function getTypeFromGenericTree( ); } - if ($generic_type_value === 'non-empty-array') { - if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey($from_docblock); - } - - if (count($generic_params) !== 2) { - throw new TypeParseTreeException('Too many template parameters for non-empty-array'); - } - - return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); - } - if ($generic_type_value === 'iterable') { if (count($generic_params) > 2) { throw new TypeParseTreeException('Too many template parameters for iterable'); @@ -903,7 +961,7 @@ private static function getTypeFromGenericTree( if (!$atomic_type instanceof TLiteralInt && !($atomic_type instanceof TClassConstant - && strpos($atomic_type->const_name, '*') === false) + && !str_contains($atomic_type->const_name, '*')) ) { throw new TypeParseTreeException( 'int-mask types must all be integer values or scalar class constants', @@ -943,7 +1001,7 @@ private static function getTypeFromGenericTree( 'Invalid reference passed to int-mask-of', ); } elseif ($param_type instanceof TClassConstant - && strpos($param_type->const_name, '*') === false + && !str_contains($param_type->const_name, '*') ) { throw new TypeParseTreeException( 'Class constant passed to int-mask-of must be a wildcard type', @@ -1261,7 +1319,7 @@ private static function getTypeFromCallableTree( $params[] = $param; } - $pure = strpos($parse_tree->value, 'pure-') === 0 ? true : null; + $pure = str_starts_with($parse_tree->value, 'pure-') ? true : null; if (in_array(strtolower($parse_tree->value), ['closure', '\closure', 'pure-closure'], true)) { return new TClosure('Closure', $params, null, $pure, [], [], $from_docblock); @@ -1312,7 +1370,7 @@ private static function getTypeFromIndexAccessTree( $array_defining_class = array_keys($template_type_map[$array_param_name])[0]; if ($offset_defining_class !== $array_defining_class - && strpos($offset_defining_class, 'fn-') !== 0 + && !str_starts_with($offset_defining_class, 'fn-') ) { throw new TypeParseTreeException('Template params are defined in different locations'); } @@ -1459,7 +1517,7 @@ private static function getTypeFromKeyedArrayTree( return new TObjectWithProperties($properties, [], [], $from_docblock); } - $callable = strpos($type, 'callable-') === 0; + $callable = str_starts_with($type, 'callable-'); $class = TKeyedArray::class; if ($callable) { $class = TCallableKeyedArray::class; @@ -1572,7 +1630,7 @@ private static function extractKeyedIntersectionTypes( continue; } - if (get_class($intersection_type) === TObject::class) { + if ($intersection_type::class === TObject::class) { continue; } @@ -1588,7 +1646,7 @@ private static function extractKeyedIntersectionTypes( throw new TypeParseTreeException( 'Intersection types must be all objects, ' - . get_class($intersection_type) . ' provided', + . $intersection_type::class . ' provided', ); } diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 9b4c6fdefda..1b2dafe7031 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -317,37 +317,17 @@ public static function fixScalarTerms( ?int $analysis_php_version_id = null, ): string { $type_string_lc = strtolower($type_string); - - switch ($type_string_lc) { - case 'int': - case 'void': - case 'float': - case 'string': - case 'bool': - case 'callable': - case 'iterable': - case 'array': - case 'object': - case 'true': - case 'false': - case 'null': - case 'mixed': - return $type_string_lc; - } - - switch ($type_string) { - case 'boolean': - return $analysis_php_version_id !== null ? $type_string : 'bool'; - - case 'integer': - return $analysis_php_version_id !== null ? $type_string : 'int'; - - case 'double': - case 'real': - return $analysis_php_version_id !== null ? $type_string : 'float'; - } - - return $type_string; + return match ($type_string_lc) { + 'int', 'void', 'float', 'string', 'bool', + 'callable', 'iterable', 'array', 'object', + 'true', 'false', 'null', 'mixed' => $type_string_lc, + default => match ($type_string) { + 'boolean' => $analysis_php_version_id !== null ? $type_string : 'bool', + 'integer' => $analysis_php_version_id !== null ? $type_string : 'int', + 'double', 'real' => $analysis_php_version_id !== null ? $type_string : 'float', + default => $type_string, + }, + }; } /** diff --git a/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php b/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php index 833f6d3e4bf..b1a1d8de366 100644 --- a/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/CanContainObjectTypeVisitor.php @@ -16,11 +16,9 @@ final class CanContainObjectTypeVisitor extends TypeVisitor { private bool $contains_object_type = false; - private Codebase $codebase; - - public function __construct(Codebase $codebase) - { - $this->codebase = $codebase; + public function __construct( + private readonly Codebase $codebase, + ) { } protected function enterNode(TypeNode $type): ?int diff --git a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php index e46d68c45e2..5ae357812d8 100644 --- a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php +++ b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php @@ -18,15 +18,13 @@ */ final class ClasslikeReplacer extends MutableTypeVisitor { - private string $old; - private string $new; + private readonly string $old; public function __construct( string $old, - string $new, + private readonly string $new, ) { $this->old = strtolower($old); - $this->new = $new; } protected function enterNode(TypeNode &$type): ?int diff --git a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php index 4fc9df15929..434b696a215 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php @@ -17,20 +17,15 @@ */ final class ContainsClassLikeVisitor extends TypeVisitor { - /** - * @var lowercase-string - */ - private string $fq_classlike_name; - private bool $contains_classlike = false; /** * @psalm-external-mutation-free * @param lowercase-string $fq_classlike_name */ - public function __construct(string $fq_classlike_name) - { - $this->fq_classlike_name = $fq_classlike_name; + public function __construct( + private readonly string $fq_classlike_name, + ) { } /** diff --git a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php index b7e8720bedd..a2a202c525d 100644 --- a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php +++ b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php @@ -16,10 +16,9 @@ */ final class FromDocblockSetter extends MutableTypeVisitor { - private bool $from_docblock; - public function __construct(bool $from_docblock) - { - $this->from_docblock = $from_docblock; + public function __construct( + private readonly bool $from_docblock, + ) { } /** * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index 31847f19ced..7f0122ff6e6 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -41,7 +41,8 @@ use function array_search; use function count; use function md5; -use function strpos; +use function str_contains; +use function str_starts_with; use function strtolower; /** @@ -49,52 +50,22 @@ */ final class TypeChecker extends TypeVisitor { - private StatementsSource $source; - - private CodeLocation $code_location; - - /** - * @var array - */ - private array $suppressed_issues; - - /** - * @var array - */ - private array $phantom_classes; - - private bool $inferred; - - private bool $inherited; - - private bool $prevent_template_covariance; - private bool $has_errors = false; - private ?string $calling_method_id = null; - /** * @param array $suppressed_issues * @param array $phantom_classes */ public function __construct( - StatementsSource $source, - CodeLocation $code_location, - array $suppressed_issues, - array $phantom_classes = [], - bool $inferred = true, - bool $inherited = false, - bool $prevent_template_covariance = false, - ?string $calling_method_id = null, + private readonly StatementsSource $source, + private readonly CodeLocation $code_location, + private readonly array $suppressed_issues, + private array $phantom_classes = [], + private readonly bool $inferred = true, + private readonly bool $inherited = false, + private bool $prevent_template_covariance = false, + private readonly ?string $calling_method_id = null, ) { - $this->source = $source; - $this->code_location = $code_location; - $this->suppressed_issues = $suppressed_issues; - $this->phantom_classes = $phantom_classes; - $this->inferred = $inferred; - $this->inherited = $inherited; - $this->prevent_template_covariance = $prevent_template_covariance; - $this->calling_method_id = $calling_method_id; } /** @@ -215,7 +186,7 @@ private function checkGenericParams(TGenericObject $atomic): void try { $class_storage = $codebase->classlike_storage_provider->get(strtolower($atomic->value)); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { return; } @@ -323,7 +294,7 @@ public function checkScalarClassConstant(TClassConstant $atomic): void } $const_name = $atomic->const_name; - if (strpos($const_name, '*') !== false) { + if (str_contains($const_name, '*')) { TypeExpander::expandAtomic( $this->source->getCodebase(), $atomic, @@ -360,7 +331,7 @@ public function checkScalarClassConstant(TClassConstant $atomic): void public function checkTemplateParam(TTemplateParam $atomic): void { if ($this->prevent_template_covariance - && strpos($atomic->defining_class, 'fn-') !== 0 + && !str_starts_with($atomic->defining_class, 'fn-') && $atomic->defining_class !== 'class-string-map' ) { $codebase = $this->source->getCodebase(); diff --git a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php index 17a710d8821..dcc40b5451c 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php +++ b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php @@ -21,21 +21,13 @@ */ final class TypeLocalizer extends MutableTypeVisitor { - /** - * @var array> - */ - private array $extends; - private string $base_fq_class_name; - /** * @param array> $extends */ public function __construct( - array $extends, - string $base_fq_class_name, + private array $extends, + private readonly string $base_fq_class_name, ) { - $this->extends = $extends; - $this->base_fq_class_name = $base_fq_class_name; } protected function enterNode(TypeNode &$type): ?int diff --git a/src/Psalm/Internal/TypeVisitor/TypeScanner.php b/src/Psalm/Internal/TypeVisitor/TypeScanner.php index e610f249d0b..dd480f45b85 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeScanner.php +++ b/src/Psalm/Internal/TypeVisitor/TypeScanner.php @@ -19,26 +19,14 @@ */ final class TypeScanner extends TypeVisitor { - private Scanner $scanner; - - private ?FileStorage $file_storage = null; - - /** - * @var array - */ - private array $phantom_classes; - /** * @param array $phantom_classes */ public function __construct( - Scanner $scanner, - ?FileStorage $file_storage, - array $phantom_classes, + private readonly Scanner $scanner, + private readonly ?FileStorage $file_storage, + private array $phantom_classes, ) { - $this->scanner = $scanner; - $this->file_storage = $file_storage; - $this->phantom_classes = $phantom_classes; } protected function enterNode(TypeNode $type): ?int diff --git a/src/Psalm/Internal/VersionUtils.php b/src/Psalm/Internal/VersionUtils.php index 72e8be62720..fcf7f279f86 100644 --- a/src/Psalm/Internal/VersionUtils.php +++ b/src/Psalm/Internal/VersionUtils.php @@ -94,7 +94,7 @@ private static function loadComposerVersions(): ?array self::PSALM_PACKAGE => self::getVersion(self::PSALM_PACKAGE), self::PHP_PARSER_PACKAGE => self::getVersion(self::PHP_PARSER_PACKAGE), ]; - } catch (OutOfBoundsException $ex) { + } catch (OutOfBoundsException) { } return null; } diff --git a/src/Psalm/Issue/ClassConstantIssue.php b/src/Psalm/Issue/ClassConstantIssue.php index cb6dac913cd..0527f6c20e0 100644 --- a/src/Psalm/Issue/ClassConstantIssue.php +++ b/src/Psalm/Issue/ClassConstantIssue.php @@ -8,14 +8,11 @@ abstract class ClassConstantIssue extends CodeIssue { - public string $const_id; - public function __construct( string $message, CodeLocation $code_location, - string $const_id, + public string $const_id, ) { parent::__construct($message, $code_location); - $this->const_id = $const_id; } } diff --git a/src/Psalm/Issue/ClassIssue.php b/src/Psalm/Issue/ClassIssue.php index 959f625cf42..d88d50d5178 100644 --- a/src/Psalm/Issue/ClassIssue.php +++ b/src/Psalm/Issue/ClassIssue.php @@ -8,14 +8,11 @@ abstract class ClassIssue extends CodeIssue { - public string $fq_classlike_name; - public function __construct( string $message, CodeLocation $code_location, - string $fq_classlike_name, + public string $fq_classlike_name, ) { parent::__construct($message, $code_location); - $this->fq_classlike_name = $fq_classlike_name; } } diff --git a/src/Psalm/Issue/CodeIssue.php b/src/Psalm/Issue/CodeIssue.php index e4b7791caa1..5a321fd1d9e 100644 --- a/src/Psalm/Issue/CodeIssue.php +++ b/src/Psalm/Issue/CodeIssue.php @@ -17,24 +17,12 @@ abstract class CodeIssue /** @var int<0, max> */ public const SHORTCODE = 0; - /** - * @readonly - */ - public CodeLocation $code_location; - - /** - * @readonly - */ - public string $message; - public ?string $dupe_key = null; public function __construct( - string $message, - CodeLocation $code_location, + public readonly string $message, + public readonly CodeLocation $code_location, ) { - $this->code_location = $code_location; - $this->message = $message; } public function getShortLocationWithPrevious(): string diff --git a/src/Psalm/Issue/InvalidInterfaceImplementation.php b/src/Psalm/Issue/InvalidInterfaceImplementation.php index 4993a72e4e9..ddaa2a62c3d 100644 --- a/src/Psalm/Issue/InvalidInterfaceImplementation.php +++ b/src/Psalm/Issue/InvalidInterfaceImplementation.php @@ -4,8 +4,8 @@ namespace Psalm\Issue; -class InvalidInterfaceImplementation extends ClassIssue +final class InvalidInterfaceImplementation extends ClassIssue { - const ERROR_LEVEL = -1; - const SHORTCODE = 317; + final public const ERROR_LEVEL = -1; + final public const SHORTCODE = 317; } diff --git a/src/Psalm/Issue/MixedArgument.php b/src/Psalm/Issue/MixedArgument.php index dde5c5af001..12f50357a33 100644 --- a/src/Psalm/Issue/MixedArgument.php +++ b/src/Psalm/Issue/MixedArgument.php @@ -21,8 +21,7 @@ public function __construct( ?string $function_id = null, ?CodeLocation $origin_location = null, ) { - $this->code_location = $code_location; - $this->message = $message; + parent::__construct($message, $code_location); $this->function_id = $function_id ? strtolower($function_id) : null; $this->origin_location = $origin_location; } diff --git a/src/Psalm/Issue/MixedArgumentTypeCoercion.php b/src/Psalm/Issue/MixedArgumentTypeCoercion.php index a7bd8f43d92..078a4e916b2 100644 --- a/src/Psalm/Issue/MixedArgumentTypeCoercion.php +++ b/src/Psalm/Issue/MixedArgumentTypeCoercion.php @@ -21,8 +21,7 @@ public function __construct( ?string $function_id = null, ?CodeLocation $origin_location = null, ) { - $this->code_location = $code_location; - $this->message = $message; + parent::__construct($message, $code_location); $this->function_id = $function_id ? strtolower($function_id) : null; $this->origin_location = $origin_location; } diff --git a/src/Psalm/Issue/MixedIssueTrait.php b/src/Psalm/Issue/MixedIssueTrait.php index 10dda63d98a..31f371de6f2 100644 --- a/src/Psalm/Issue/MixedIssueTrait.php +++ b/src/Psalm/Issue/MixedIssueTrait.php @@ -18,8 +18,7 @@ public function __construct( CodeLocation $code_location, ?CodeLocation $origin_location = null, ) { - $this->code_location = $code_location; - $this->message = $message; + parent::__construct($message, $code_location); $this->origin_location = $origin_location; } diff --git a/src/Psalm/Issue/PrivateFinalMethod.php b/src/Psalm/Issue/PrivateFinalMethod.php index 8884c29ad0a..889fbd4ef2a 100644 --- a/src/Psalm/Issue/PrivateFinalMethod.php +++ b/src/Psalm/Issue/PrivateFinalMethod.php @@ -4,8 +4,8 @@ namespace Psalm\Issue; -class PrivateFinalMethod extends MethodIssue +final class PrivateFinalMethod extends MethodIssue { - public const ERROR_LEVEL = 2; - public const SHORTCODE = 320; + final public const ERROR_LEVEL = 2; + final public const SHORTCODE = 320; } diff --git a/src/Psalm/Issue/PropertyIssue.php b/src/Psalm/Issue/PropertyIssue.php index 6be211689e7..9e1a24d3eff 100644 --- a/src/Psalm/Issue/PropertyIssue.php +++ b/src/Psalm/Issue/PropertyIssue.php @@ -8,14 +8,11 @@ abstract class PropertyIssue extends CodeIssue { - public string $property_id; - public function __construct( string $message, CodeLocation $code_location, - string $property_id, + public string $property_id, ) { parent::__construct($message, $code_location); - $this->property_id = $property_id; } } diff --git a/src/Psalm/Issue/RedundantFlag.php b/src/Psalm/Issue/RedundantFlag.php new file mode 100644 index 00000000000..7734803ac06 --- /dev/null +++ b/src/Psalm/Issue/RedundantFlag.php @@ -0,0 +1,11 @@ +dupe_key = $dupe_key; + } +} diff --git a/src/Psalm/Issue/TaintedExtract.php b/src/Psalm/Issue/TaintedExtract.php new file mode 100644 index 00000000000..60eef6b9271 --- /dev/null +++ b/src/Psalm/Issue/TaintedExtract.php @@ -0,0 +1,10 @@ + */ public const SHORTCODE = 205; - /** - * @readonly - */ - public string $journey_text; - - /** - * @var list - * @readonly - */ - public array $journey = []; - /** * @param list $journey */ public function __construct( string $message, CodeLocation $code_location, - array $journey, - string $journey_text, + public readonly array $journey, + public readonly string $journey_text, ) { parent::__construct($message, $code_location); - - $this->journey = $journey; - $this->journey_text = $journey_text; } /** diff --git a/src/Psalm/Issue/TaintedXpath.php b/src/Psalm/Issue/TaintedXpath.php index 5f0ab07b769..527fe0ee107 100644 --- a/src/Psalm/Issue/TaintedXpath.php +++ b/src/Psalm/Issue/TaintedXpath.php @@ -6,5 +6,5 @@ final class TaintedXpath extends TaintedInput { - public const SHORTCODE = 322; + public const SHORTCODE = 328; } diff --git a/src/Psalm/Issue/UnusedBaselineEntry.php b/src/Psalm/Issue/UnusedBaselineEntry.php index 7397a33d3f3..dd74a2239d1 100644 --- a/src/Psalm/Issue/UnusedBaselineEntry.php +++ b/src/Psalm/Issue/UnusedBaselineEntry.php @@ -4,8 +4,8 @@ namespace Psalm\Issue; -class UnusedBaselineEntry extends ClassIssue +final class UnusedBaselineEntry extends ClassIssue { - public const ERROR_LEVEL = -1; - public const SHORTCODE = 316; + final public const ERROR_LEVEL = -1; + final public const SHORTCODE = 316; } diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 61d2f0fb53c..dd35480c48f 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -74,8 +74,8 @@ use function sprintf; use function str_repeat; use function str_replace; +use function str_starts_with; use function strlen; -use function strpos; use function trim; use function usort; @@ -88,39 +88,39 @@ final class IssueBuffer /** * @var array> */ - protected static array $issues_data = []; + private static array $issues_data = []; /** * @var array */ - protected static array $console_issues = []; + private static array $console_issues = []; /** * @var array */ - protected static array $fixable_issue_counts = []; + private static array $fixable_issue_counts = []; - protected static int $error_count = 0; + private static int $error_count = 0; /** * @var array */ - protected static array $emitted = []; + private static array $emitted = []; - protected static int $recording_level = 0; + private static int $recording_level = 0; /** @var array> */ - protected static array $recorded_issues = []; + private static array $recorded_issues = []; /** * @var array> */ - protected static array $unused_suppressions = []; + private static array $unused_suppressions = []; /** * @var array> */ - protected static array $used_suppressions = []; + private static array $used_suppressions = []; /** @var array */ private static array $server = []; @@ -132,6 +132,14 @@ final class IssueBuffer */ public static function accepts(CodeIssue $e, array $suppressed_issues = [], bool $is_fixable = false): bool { + $config = Config::getInstance(); + $project_analyzer = ProjectAnalyzer::getInstance(); + $codebase = $project_analyzer->getCodebase(); + $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); + if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { + return false; + } + if (self::isSuppressed($e, $suppressed_issues)) { return false; } @@ -154,7 +162,7 @@ public static function maybeAdd(CodeIssue $e, array $suppressed_issues = [], boo */ public static function addUnusedSuppression(string $file_path, int $offset, string $issue_type): void { - if (strpos($issue_type, 'Tainted') === 0) { + if (str_starts_with($issue_type, 'Tainted')) { return; } @@ -181,7 +189,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) { $config = Config::getInstance(); - $fqcn_parts = explode('\\', get_class($e)); + $fqcn_parts = explode('\\', $e::class); $issue_type = array_pop($fqcn_parts); $file_path = $e->getFilePath(); @@ -257,11 +265,6 @@ public static function add(CodeIssue $e, bool $is_fixable = false): bool $project_analyzer = ProjectAnalyzer::getInstance(); $codebase = $project_analyzer->getCodebase(); - $event = new BeforeAddIssueEvent($e, $is_fixable, $codebase); - if ($config->eventDispatcher->dispatchBeforeAddIssue($event) === false) { - return false; - } - $fqcn_parts = explode('\\', get_class($e)); $issue_type = array_pop($fqcn_parts); @@ -269,7 +272,7 @@ public static function add(CodeIssue $e, bool $is_fixable = false): bool return false; } - $is_tainted = strpos($issue_type, 'Tainted') === 0; + $is_tainted = str_starts_with($issue_type, 'Tainted'); if ($codebase->taint_flow_graph && !$is_tainted) { return false; @@ -706,7 +709,7 @@ public static function finish( try { $source_control_info = (new GitInfoCollector())->collect(); - } catch (RuntimeException $e) { + } catch (RuntimeException) { // do nothing } @@ -896,89 +899,100 @@ public static function getOutput( $format = $report_options->format; - switch ($format) { - case Report::TYPE_COMPACT: - $output = new CompactReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_EMACS: - $output = new EmacsReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_TEXT: - $output = new TextReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_JSON: - $output = new JsonReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_BY_ISSUE_LEVEL: - $output = new ByIssueLevelAndTypeReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_JSON_SUMMARY: - $output = new JsonSummaryReport( - $normalized_data, - self::$fixable_issue_counts, - $report_options, - $mixed_expression_count, - $total_expression_count, - ); - break; - - case Report::TYPE_SONARQUBE: - $output = new SonarqubeReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_PYLINT: - $output = new PylintReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_CHECKSTYLE: - $output = new CheckstyleReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_XML: - $output = new XmlReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_JUNIT: - $output = new JunitReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_CONSOLE: - $output = new ConsoleReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_GITHUB_ACTIONS: - $output = new GithubActionsReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_PHP_STORM: - $output = new PhpStormReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_SARIF: - $output = new SarifReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_CODECLIMATE: - $output = new CodeClimateReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - case Report::TYPE_COUNT: - $output = new CountReport($normalized_data, self::$fixable_issue_counts, $report_options); - break; - - default: - throw new RuntimeException('Unexpected report format: ' . $report_options->format); - } + $output = match ($format) { + Report::TYPE_COMPACT => new CompactReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_EMACS => new EmacsReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_TEXT => new TextReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_JSON => new JsonReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_BY_ISSUE_LEVEL => new ByIssueLevelAndTypeReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_JSON_SUMMARY => new JsonSummaryReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + $mixed_expression_count, + $total_expression_count, + ), + Report::TYPE_SONARQUBE => new SonarqubeReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_PYLINT => new PylintReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_CHECKSTYLE => new CheckstyleReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_XML => new XmlReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_JUNIT => new JunitReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_CONSOLE => new ConsoleReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_GITHUB_ACTIONS => new GithubActionsReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_PHP_STORM => new PhpStormReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_SARIF => new SarifReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_CODECLIMATE => new CodeClimateReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + Report::TYPE_COUNT => new CountReport( + $normalized_data, + self::$fixable_issue_counts, + $report_options, + ), + }; return $output->create(); } - protected static function alreadyEmitted(string $message): bool + public static function alreadyEmitted(string $message): bool { $sham = sha1($message); diff --git a/src/Psalm/Plugin/DynamicTemplateProvider.php b/src/Psalm/Plugin/DynamicTemplateProvider.php index fbbcb3a36fd..c89821f0612 100644 --- a/src/Psalm/Plugin/DynamicTemplateProvider.php +++ b/src/Psalm/Plugin/DynamicTemplateProvider.php @@ -10,14 +10,12 @@ final class DynamicTemplateProvider { - private string $defining_class; - /** * @internal */ - public function __construct(string $defining_class) - { - $this->defining_class = $defining_class; + public function __construct( + private readonly string $defining_class, + ) { } /** diff --git a/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php b/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php index b9b5567b1d5..ebcc464615b 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AddRemoveTaintsEvent.php @@ -11,26 +11,17 @@ final class AddRemoveTaintsEvent { - private Expr $expr; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - /** * Called after an expression has been checked * * @internal */ public function __construct( - Expr $expr, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, + private readonly Expr $expr, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, ) { - $this->expr = $expr; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; } public function getExpr(): Expr diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterAnalysisEvent.php index 6c95a0c7394..977616588bd 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterAnalysisEvent.php @@ -10,14 +10,6 @@ final class AfterAnalysisEvent { - private Codebase $codebase; - /** - * @var array> where string key is a filepath - */ - private array $issues; - private array $build_info; - private ?SourceControlInfo $source_control_info; - /** * Called after analysis is complete * @@ -25,15 +17,11 @@ final class AfterAnalysisEvent * @internal */ public function __construct( - Codebase $codebase, - array $issues, - array $build_info, - ?SourceControlInfo $source_control_info = null, + private readonly Codebase $codebase, + private readonly array $issues, + private readonly array $build_info, + private readonly ?SourceControlInfo $source_control_info = null, ) { - $this->codebase = $codebase; - $this->issues = $issues; - $this->build_info = $build_info; - $this->source_control_info = $source_control_info; } public function getCodebase(): Codebase diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeAnalysisEvent.php index 7b627ab9ffa..20839c14d41 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeAnalysisEvent.php @@ -12,15 +12,6 @@ final class AfterClassLikeAnalysisEvent { - private Node\Stmt\ClassLike $stmt; - private ClassLikeStorage $classlike_storage; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - /** * Called after a statement has been checked * @@ -28,17 +19,12 @@ final class AfterClassLikeAnalysisEvent * @internal */ public function __construct( - Node\Stmt\ClassLike $stmt, - ClassLikeStorage $classlike_storage, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private readonly Node\Stmt\ClassLike $stmt, + private readonly ClassLikeStorage $classlike_storage, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->stmt = $stmt; - $this->classlike_storage = $classlike_storage; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getStmt(): Node\Stmt\ClassLike diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeExistenceCheckEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeExistenceCheckEvent.php index 1971f150266..7e38a0c5a9e 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeExistenceCheckEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeExistenceCheckEvent.php @@ -11,31 +11,17 @@ final class AfterClassLikeExistenceCheckEvent { - private string $fq_class_name; - private CodeLocation $code_location; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - /** * @param FileManipulation[] $file_replacements * @internal */ public function __construct( - string $fq_class_name, - CodeLocation $code_location, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private readonly string $fq_class_name, + private readonly CodeLocation $code_location, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->fq_class_name = $fq_class_name; - $this->code_location = $code_location; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getFqClassName(): string diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeVisitEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeVisitEvent.php index 709708cee26..e6630ff4bc4 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeVisitEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterClassLikeVisitEvent.php @@ -12,31 +12,17 @@ final class AfterClassLikeVisitEvent { - private ClassLike $stmt; - private ClassLikeStorage $storage; - private FileSource $statements_source; - private Codebase $codebase; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - /** * @param FileManipulation[] $file_replacements * @internal */ public function __construct( - ClassLike $stmt, - ClassLikeStorage $storage, - FileSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private readonly ClassLike $stmt, + private readonly ClassLikeStorage $storage, + private readonly FileSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->stmt = $stmt; - $this->storage = $storage; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getStmt(): ClassLike diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterCodebasePopulatedEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterCodebasePopulatedEvent.php index 6082be9a1a0..f2bdd176483 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterCodebasePopulatedEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterCodebasePopulatedEvent.php @@ -8,16 +8,14 @@ final class AfterCodebasePopulatedEvent { - private Codebase $codebase; - /** * Called after codebase has been populated * * @internal */ - public function __construct(Codebase $codebase) - { - $this->codebase = $codebase; + public function __construct( + private readonly Codebase $codebase, + ) { } public function getCodebase(): Codebase diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterEveryFunctionCallAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterEveryFunctionCallAnalysisEvent.php index 4d8b8dc013f..e8b098c4d61 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterEveryFunctionCallAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterEveryFunctionCallAnalysisEvent.php @@ -11,25 +11,14 @@ final class AfterEveryFunctionCallAnalysisEvent { - private FuncCall $expr; - private string $function_id; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - /** @internal */ public function __construct( - FuncCall $expr, - string $function_id, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, + private readonly FuncCall $expr, + private readonly string $function_id, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, ) { - $this->expr = $expr; - $this->function_id = $function_id; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; } public function getExpr(): FuncCall diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterExpressionAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterExpressionAnalysisEvent.php index 9828b39b70a..a14af785993 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterExpressionAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterExpressionAnalysisEvent.php @@ -12,15 +12,6 @@ final class AfterExpressionAnalysisEvent { - private Expr $expr; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - /** * Called after an expression has been checked * @@ -28,17 +19,12 @@ final class AfterExpressionAnalysisEvent * @internal */ public function __construct( - Expr $expr, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private readonly Expr $expr, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->expr = $expr; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getExpr(): Expr diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterFileAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterFileAnalysisEvent.php index da204223da5..915d2efd0bd 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterFileAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterFileAnalysisEvent.php @@ -12,15 +12,6 @@ final class AfterFileAnalysisEvent { - private StatementsSource $statements_source; - private Context $file_context; - private FileStorage $file_storage; - private Codebase $codebase; - /** - * @var Stmt[] - */ - private array $stmts; - /** * Called after a file has been checked * @@ -28,17 +19,12 @@ final class AfterFileAnalysisEvent * @internal */ public function __construct( - StatementsSource $statements_source, - Context $file_context, - FileStorage $file_storage, - Codebase $codebase, - array $stmts, + private readonly StatementsSource $statements_source, + private readonly Context $file_context, + private readonly FileStorage $file_storage, + private readonly Codebase $codebase, + private readonly array $stmts, ) { - $this->statements_source = $statements_source; - $this->file_context = $file_context; - $this->file_storage = $file_storage; - $this->codebase = $codebase; - $this->stmts = $stmts; } public function getStatementsSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterFunctionCallAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterFunctionCallAnalysisEvent.php index 673ad559beb..97f9b1471ec 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterFunctionCallAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterFunctionCallAnalysisEvent.php @@ -13,41 +13,20 @@ final class AfterFunctionCallAnalysisEvent { - private FuncCall $expr; - /** - * @var non-empty-string - */ - private string $function_id; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - private Union $return_type_candidate; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - /** * @param non-empty-string $function_id * @param FileManipulation[] $file_replacements * @internal */ public function __construct( - FuncCall $expr, - string $function_id, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, - Union $return_type_candidate, - array $file_replacements, + private readonly FuncCall $expr, + private readonly string $function_id, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private readonly Union $return_type_candidate, + private array $file_replacements, ) { - $this->expr = $expr; - $this->function_id = $function_id; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->return_type_candidate = $return_type_candidate; - $this->file_replacements = $file_replacements; } public function getExpr(): FuncCall diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterFunctionLikeAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterFunctionLikeAnalysisEvent.php index 6a624c22dc3..527a13dae51 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterFunctionLikeAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterFunctionLikeAnalysisEvent.php @@ -14,17 +14,6 @@ final class AfterFunctionLikeAnalysisEvent { - private Node\FunctionLike $stmt; - private FunctionLikeStorage $functionlike_storage; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - private NodeTypeProvider $node_type_provider; - private Context $context; - /** * Called after a statement has been checked * @@ -32,21 +21,14 @@ final class AfterFunctionLikeAnalysisEvent * @internal */ public function __construct( - Node\FunctionLike $stmt, - FunctionLikeStorage $functionlike_storage, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements, - NodeTypeProvider $node_type_provider, - Context $context, + private readonly Node\FunctionLike $stmt, + private readonly FunctionLikeStorage $functionlike_storage, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements, + private readonly NodeTypeProvider $node_type_provider, + private readonly Context $context, ) { - $this->stmt = $stmt; - $this->functionlike_storage = $functionlike_storage; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; - $this->node_type_provider = $node_type_provider; - $this->context = $context; } public function getStmt(): Node\FunctionLike diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php index 829f0dc15bc..179e2fb575e 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php @@ -15,44 +15,21 @@ final class AfterMethodCallAnalysisEvent { - private MethodCall|StaticCall $expr; - private string $method_id; - private string $appearing_method_id; - private string $declaring_method_id; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; /** - * @var FileManipulation[] - */ - private array $file_replacements; - private ?Union $return_type_candidate; - - /** - * @param MethodCall|StaticCall $expr * @param FileManipulation[] $file_replacements * @internal */ public function __construct( - Expr $expr, - string $method_id, - string $appearing_method_id, - string $declaring_method_id, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], - Union $return_type_candidate = null, + private readonly MethodCall|StaticCall $expr, + private readonly string $method_id, + private readonly string $appearing_method_id, + private readonly string $declaring_method_id, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], + private ?Union $return_type_candidate = null, ) { - $this->expr = $expr; - $this->method_id = $method_id; - $this->appearing_method_id = $appearing_method_id; - $this->declaring_method_id = $declaring_method_id; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; - $this->return_type_candidate = $return_type_candidate; } /** diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterStatementAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterStatementAnalysisEvent.php index a74edefc191..9c96b69a59e 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterStatementAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterStatementAnalysisEvent.php @@ -12,15 +12,6 @@ final class AfterStatementAnalysisEvent { - private Stmt $stmt; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var FileManipulation[] - */ - private array $file_replacements; - /** * Called after a statement has been checked * @@ -28,17 +19,12 @@ final class AfterStatementAnalysisEvent * @internal */ public function __construct( - Stmt $stmt, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private readonly Stmt $stmt, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->stmt = $stmt; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getStmt(): Stmt diff --git a/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php b/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php index 9e354ed900d..d5664a18dd2 100644 --- a/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/BeforeAddIssueEvent.php @@ -9,16 +9,12 @@ final class BeforeAddIssueEvent { - private CodeIssue $issue; - private bool $fixable; - private Codebase $codebase; - /** @internal */ - public function __construct(CodeIssue $issue, bool $fixable, Codebase $codebase) - { - $this->issue = $issue; - $this->fixable = $fixable; - $this->codebase = $codebase; + public function __construct( + private readonly CodeIssue $issue, + private readonly bool $fixable, + private readonly Codebase $codebase, + ) { } public function getIssue(): CodeIssue diff --git a/src/Psalm/Plugin/EventHandler/Event/BeforeExpressionAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/BeforeExpressionAnalysisEvent.php index 63357ee4f7b..f3eb1294652 100644 --- a/src/Psalm/Plugin/EventHandler/Event/BeforeExpressionAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/BeforeExpressionAnalysisEvent.php @@ -12,15 +12,6 @@ final class BeforeExpressionAnalysisEvent { - private Expr $expr; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var list - */ - private array $file_replacements; - /** * Called before an expression is checked * @@ -28,17 +19,12 @@ final class BeforeExpressionAnalysisEvent * @internal */ public function __construct( - Expr $expr, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private readonly Expr $expr, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->expr = $expr; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getExpr(): Expr diff --git a/src/Psalm/Plugin/EventHandler/Event/BeforeFileAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/BeforeFileAnalysisEvent.php index 0dbd4441c89..3a2cd512fc7 100644 --- a/src/Psalm/Plugin/EventHandler/Event/BeforeFileAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/BeforeFileAnalysisEvent.php @@ -11,26 +11,17 @@ final class BeforeFileAnalysisEvent { - private StatementsSource $statements_source; - private Context $file_context; - private FileStorage $file_storage; - private Codebase $codebase; - /** * Called before a file has been checked * * @internal */ public function __construct( - StatementsSource $statements_source, - Context $file_context, - FileStorage $file_storage, - Codebase $codebase, + private readonly StatementsSource $statements_source, + private readonly Context $file_context, + private readonly FileStorage $file_storage, + private readonly Codebase $codebase, ) { - $this->statements_source = $statements_source; - $this->file_context = $file_context; - $this->file_storage = $file_storage; - $this->codebase = $codebase; } public function getStatementsSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/BeforeStatementAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/BeforeStatementAnalysisEvent.php index 4a37bd1b5d8..7792ec4668a 100644 --- a/src/Psalm/Plugin/EventHandler/Event/BeforeStatementAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/BeforeStatementAnalysisEvent.php @@ -12,15 +12,6 @@ final class BeforeStatementAnalysisEvent { - private Stmt $stmt; - private Context $context; - private StatementsSource $statements_source; - private Codebase $codebase; - /** - * @var list - */ - private array $file_replacements; - /** * Called after a statement has been checked * @@ -28,17 +19,12 @@ final class BeforeStatementAnalysisEvent * @internal */ public function __construct( - Stmt $stmt, - Context $context, - StatementsSource $statements_source, - Codebase $codebase, - array $file_replacements = [], + private Stmt $stmt, + private readonly Context $context, + private readonly StatementsSource $statements_source, + private readonly Codebase $codebase, + private array $file_replacements = [], ) { - $this->stmt = $stmt; - $this->context = $context; - $this->statements_source = $statements_source; - $this->codebase = $codebase; - $this->file_replacements = $file_replacements; } public function getStmt(): Stmt diff --git a/src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php index de9fc4c17b5..32246d49e96 100644 --- a/src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/DynamicFunctionStorageProviderEvent.php @@ -14,33 +14,18 @@ final class DynamicFunctionStorageProviderEvent { - private ArgTypeInferer $arg_type_inferer; - private DynamicTemplateProvider $template_provider; - private StatementsSource $statement_source; - private string $function_id; - private PhpParser\Node\Expr\FuncCall $func_call; - private Context $context; - private CodeLocation $code_location; - /** * @internal */ public function __construct( - ArgTypeInferer $arg_type_inferer, - DynamicTemplateProvider $template_provider, - StatementsSource $statements_source, - string $function_id, - PhpParser\Node\Expr\FuncCall $func_call, - Context $context, - CodeLocation $code_location, + private readonly ArgTypeInferer $arg_type_inferer, + private readonly DynamicTemplateProvider $template_provider, + private readonly StatementsSource $statement_source, + private readonly string $function_id, + private readonly PhpParser\Node\Expr\FuncCall $func_call, + private readonly Context $context, + private readonly CodeLocation $code_location, ) { - $this->statement_source = $statements_source; - $this->function_id = $function_id; - $this->func_call = $func_call; - $this->context = $context; - $this->code_location = $code_location; - $this->arg_type_inferer = $arg_type_inferer; - $this->template_provider = $template_provider; } public function getArgTypeInferer(): ArgTypeInferer diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionExistenceProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionExistenceProviderEvent.php index 87389cf2b1f..77448542420 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionExistenceProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionExistenceProviderEvent.php @@ -8,9 +8,6 @@ final class FunctionExistenceProviderEvent { - private StatementsSource $statements_source; - private string $function_id; - /** * Use this hook for informing whether or not a global function exists. If you know the function does * not exist, return false. If you aren't sure if it exists or not, return null and the default analysis @@ -19,11 +16,9 @@ final class FunctionExistenceProviderEvent * @internal */ public function __construct( - StatementsSource $statements_source, - string $function_id, + private readonly StatementsSource $statements_source, + private readonly string $function_id, ) { - $this->statements_source = $statements_source; - $this->function_id = $function_id; } public function getStatementsSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php index e1659f2e0b0..46f1163f062 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionParamsProviderEvent.php @@ -11,31 +11,17 @@ final class FunctionParamsProviderEvent { - private StatementsSource $statements_source; - private string $function_id; - /** - * @var list - */ - private array $call_args; - private ?Context $context; - private ?CodeLocation $code_location; - /** * @param list $call_args * @internal */ public function __construct( - StatementsSource $statements_source, - string $function_id, - array $call_args, - ?Context $context = null, - ?CodeLocation $code_location = null, + private readonly StatementsSource $statements_source, + private readonly string $function_id, + private readonly array $call_args, + private readonly ?Context $context = null, + private readonly ?CodeLocation $code_location = null, ) { - $this->statements_source = $statements_source; - $this->function_id = $function_id; - $this->call_args = $call_args; - $this->context = $context; - $this->code_location = $code_location; } public function getStatementsSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/FunctionReturnTypeProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/FunctionReturnTypeProviderEvent.php index e34e333e943..4b108f6d3f8 100644 --- a/src/Psalm/Plugin/EventHandler/Event/FunctionReturnTypeProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/FunctionReturnTypeProviderEvent.php @@ -12,15 +12,6 @@ final class FunctionReturnTypeProviderEvent { - private StatementsSource $statements_source; - /** - * @var non-empty-string - */ - private string $function_id; - private FuncCall $stmt; - private Context $context; - private CodeLocation $code_location; - /** * Use this hook for providing custom return type logic. If this plugin does not know what a function should * return but another plugin may be able to determine the type, return null. Otherwise return a mixed union type @@ -30,17 +21,12 @@ final class FunctionReturnTypeProviderEvent * @internal */ public function __construct( - StatementsSource $statements_source, - string $function_id, - FuncCall $stmt, - Context $context, - CodeLocation $code_location, + private readonly StatementsSource $statements_source, + private readonly string $function_id, + private readonly FuncCall $stmt, + private readonly Context $context, + private readonly CodeLocation $code_location, ) { - $this->statements_source = $statements_source; - $this->function_id = $function_id; - $this->stmt = $stmt; - $this->context = $context; - $this->code_location = $code_location; } public function getStatementsSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/MethodExistenceProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/MethodExistenceProviderEvent.php index 8c86c5e2e33..5d87ecba720 100644 --- a/src/Psalm/Plugin/EventHandler/Event/MethodExistenceProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/MethodExistenceProviderEvent.php @@ -9,11 +9,6 @@ final class MethodExistenceProviderEvent { - private string $fq_classlike_name; - private string $method_name_lowercase; - private ?StatementsSource $source; - private ?CodeLocation $code_location; - /** * Use this hook for informing whether or not a method exists on a given object. If you know the method does * not exist, return false. If you aren't sure if it exists or not, return null and the default analysis will @@ -22,15 +17,11 @@ final class MethodExistenceProviderEvent * @internal */ public function __construct( - string $fq_classlike_name, - string $method_name_lowercase, - ?StatementsSource $source = null, - ?CodeLocation $code_location = null, + private readonly string $fq_classlike_name, + private readonly string $method_name_lowercase, + private readonly ?StatementsSource $source = null, + private readonly ?CodeLocation $code_location = null, ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->method_name_lowercase = $method_name_lowercase; - $this->source = $source; - $this->code_location = $code_location; } public function getFqClasslikeName(): string diff --git a/src/Psalm/Plugin/EventHandler/Event/MethodParamsProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/MethodParamsProviderEvent.php index babc8ffa379..bcf7d0256a1 100644 --- a/src/Psalm/Plugin/EventHandler/Event/MethodParamsProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/MethodParamsProviderEvent.php @@ -11,34 +11,18 @@ final class MethodParamsProviderEvent { - private string $fq_classlike_name; - private string $method_name_lowercase; - /** - * @var list|null - */ - private ?array $call_args; - private ?StatementsSource $statements_source; - private ?Context $context; - private ?CodeLocation $code_location; - /** * @param list $call_args * @internal */ public function __construct( - string $fq_classlike_name, - string $method_name_lowercase, - ?array $call_args = null, - ?StatementsSource $statements_source = null, - ?Context $context = null, - ?CodeLocation $code_location = null, + private readonly string $fq_classlike_name, + private readonly string $method_name_lowercase, + private readonly ?array $call_args = null, + private readonly ?StatementsSource $statements_source = null, + private readonly ?Context $context = null, + private readonly ?CodeLocation $code_location = null, ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->method_name_lowercase = $method_name_lowercase; - $this->call_args = $call_args; - $this->statements_source = $statements_source; - $this->context = $context; - $this->code_location = $code_location; } public function getFqClasslikeName(): string diff --git a/src/Psalm/Plugin/EventHandler/Event/MethodReturnTypeProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/MethodReturnTypeProviderEvent.php index e6a96aba9b3..0338dec8a0c 100644 --- a/src/Psalm/Plugin/EventHandler/Event/MethodReturnTypeProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/MethodReturnTypeProviderEvent.php @@ -12,23 +12,6 @@ final class MethodReturnTypeProviderEvent { - private StatementsSource $source; - private string $fq_classlike_name; - /** - * @var lowercase-string - */ - private string $method_name_lowercase; - private Context $context; - private CodeLocation $code_location; - private PhpParser\Node\Expr\MethodCall|PhpParser\Node\Expr\StaticCall $stmt; - /** @var non-empty-list|null */ - private ?array $template_type_parameters; - private ?string $called_fq_classlike_name; - /** - * @var lowercase-string|null - */ - private ?string $called_method_name_lowercase; - /** * Use this hook for providing custom return type logic. If this plugin does not know what a method should return * but another plugin may be able to determine the type, return null. Otherwise return a mixed union type if @@ -40,25 +23,16 @@ final class MethodReturnTypeProviderEvent * @internal */ public function __construct( - StatementsSource $source, - string $fq_classlike_name, - string $method_name_lowercase, - PhpParser\Node\Expr\MethodCall|PhpParser\Node\Expr\StaticCall $stmt, - Context $context, - CodeLocation $code_location, - ?array $template_type_parameters = null, - ?string $called_fq_classlike_name = null, - ?string $called_method_name_lowercase = null, + private readonly StatementsSource $source, + private readonly string $fq_classlike_name, + private readonly string $method_name_lowercase, + private readonly PhpParser\Node\Expr\MethodCall|PhpParser\Node\Expr\StaticCall $stmt, + private readonly Context $context, + private readonly CodeLocation $code_location, + private readonly ?array $template_type_parameters = null, + private readonly ?string $called_fq_classlike_name = null, + private readonly ?string $called_method_name_lowercase = null, ) { - $this->source = $source; - $this->fq_classlike_name = $fq_classlike_name; - $this->method_name_lowercase = $method_name_lowercase; - $this->context = $context; - $this->code_location = $code_location; - $this->stmt = $stmt; - $this->template_type_parameters = $template_type_parameters; - $this->called_fq_classlike_name = $called_fq_classlike_name; - $this->called_method_name_lowercase = $called_method_name_lowercase; } public function getSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/MethodVisibilityProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/MethodVisibilityProviderEvent.php index b6b46458c7a..17abcadd3b9 100644 --- a/src/Psalm/Plugin/EventHandler/Event/MethodVisibilityProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/MethodVisibilityProviderEvent.php @@ -10,25 +10,14 @@ final class MethodVisibilityProviderEvent { - private StatementsSource $source; - private string $fq_classlike_name; - private string $method_name_lowercase; - private Context $context; - private ?CodeLocation $code_location; - /** @internal */ public function __construct( - StatementsSource $source, - string $fq_classlike_name, - string $method_name_lowercase, - Context $context, - ?CodeLocation $code_location = null, + private readonly StatementsSource $source, + private readonly string $fq_classlike_name, + private readonly string $method_name_lowercase, + private readonly Context $context, + private readonly ?CodeLocation $code_location = null, ) { - $this->source = $source; - $this->fq_classlike_name = $fq_classlike_name; - $this->method_name_lowercase = $method_name_lowercase; - $this->context = $context; - $this->code_location = $code_location; } public function getSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/PropertyExistenceProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/PropertyExistenceProviderEvent.php index c71b544f535..a2df1d1774e 100644 --- a/src/Psalm/Plugin/EventHandler/Event/PropertyExistenceProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/PropertyExistenceProviderEvent.php @@ -10,13 +10,6 @@ final class PropertyExistenceProviderEvent { - private string $fq_classlike_name; - private string $property_name; - private bool $read_mode; - private ?StatementsSource $source; - private ?Context $context; - private ?CodeLocation $code_location; - /** * Use this hook for informing whether or not a property exists on a given object. If you know the property does * not exist, return false. If you aren't sure if it exists or not, return null and the default analysis will @@ -25,19 +18,13 @@ final class PropertyExistenceProviderEvent * @internal */ public function __construct( - string $fq_classlike_name, - string $property_name, - bool $read_mode, - ?StatementsSource $source = null, - ?Context $context = null, - ?CodeLocation $code_location = null, + private readonly string $fq_classlike_name, + private readonly string $property_name, + private readonly bool $read_mode, + private readonly ?StatementsSource $source = null, + private readonly ?Context $context = null, + private readonly ?CodeLocation $code_location = null, ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->property_name = $property_name; - $this->read_mode = $read_mode; - $this->source = $source; - $this->context = $context; - $this->code_location = $code_location; } public function getFqClasslikeName(): string diff --git a/src/Psalm/Plugin/EventHandler/Event/PropertyTypeProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/PropertyTypeProviderEvent.php index 035ac12dc77..fd647070031 100644 --- a/src/Psalm/Plugin/EventHandler/Event/PropertyTypeProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/PropertyTypeProviderEvent.php @@ -9,25 +9,14 @@ final class PropertyTypeProviderEvent { - private string $fq_classlike_name; - private string $property_name; - private bool $read_mode; - private ?StatementsSource $source; - private ?Context $context; - /** @internal */ public function __construct( - string $fq_classlike_name, - string $property_name, - bool $read_mode, - ?StatementsSource $source = null, - ?Context $context = null, + private readonly string $fq_classlike_name, + private readonly string $property_name, + private readonly bool $read_mode, + private readonly ?StatementsSource $source = null, + private readonly ?Context $context = null, ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->property_name = $property_name; - $this->read_mode = $read_mode; - $this->source = $source; - $this->context = $context; } public function getFqClasslikeName(): string diff --git a/src/Psalm/Plugin/EventHandler/Event/PropertyVisibilityProviderEvent.php b/src/Psalm/Plugin/EventHandler/Event/PropertyVisibilityProviderEvent.php index eaccc482f22..a566ec8c86c 100644 --- a/src/Psalm/Plugin/EventHandler/Event/PropertyVisibilityProviderEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/PropertyVisibilityProviderEvent.php @@ -10,28 +10,15 @@ final class PropertyVisibilityProviderEvent { - private StatementsSource $source; - private string $fq_classlike_name; - private string $property_name; - private bool $read_mode; - private Context $context; - private CodeLocation $code_location; - /** @internal */ public function __construct( - StatementsSource $source, - string $fq_classlike_name, - string $property_name, - bool $read_mode, - Context $context, - CodeLocation $code_location, + private readonly StatementsSource $source, + private readonly string $fq_classlike_name, + private readonly string $property_name, + private readonly bool $read_mode, + private readonly Context $context, + private readonly CodeLocation $code_location, ) { - $this->source = $source; - $this->fq_classlike_name = $fq_classlike_name; - $this->property_name = $property_name; - $this->read_mode = $read_mode; - $this->context = $context; - $this->code_location = $code_location; } public function getSource(): StatementsSource diff --git a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php index 968d955af0d..2013ee4a55d 100644 --- a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php @@ -8,19 +8,16 @@ final class StringInterpreterEvent { - private string $value; - private Codebase $codebase; - /** * Called after a statement has been checked * * @psalm-external-mutation-free * @internal */ - public function __construct(string $value, Codebase $codebase) - { - $this->value = $value; - $this->codebase = $codebase; + public function __construct( + private readonly string $value, + private readonly Codebase $codebase, + ) { } public function getValue(): string diff --git a/src/Psalm/PluginFileExtensionsSocket.php b/src/Psalm/PluginFileExtensionsSocket.php index 6c6694e3788..bf8bc8f7876 100644 --- a/src/Psalm/PluginFileExtensionsSocket.php +++ b/src/Psalm/PluginFileExtensionsSocket.php @@ -16,8 +16,6 @@ final class PluginFileExtensionsSocket implements FileExtensionsInterface { - private Config $config; - /** * @var array> */ @@ -36,9 +34,9 @@ final class PluginFileExtensionsSocket implements FileExtensionsInterface /** * @internal */ - public function __construct(Config $config) - { - $this->config = $config; + public function __construct( + private readonly Config $config, + ) { } /** @@ -57,8 +55,8 @@ public function addFileTypeScanner(string $fileExtension, string $className): vo 1_622_727_271, ); } - if (!empty($this->config->getFiletypeScanners()[$fileExtension]) - || !empty($this->additionalFileTypeScanners[$fileExtension]) + if (isset($this->config->getFiletypeScanners()[$fileExtension]) + || isset($this->additionalFileTypeScanners[$fileExtension]) ) { throw new LogicException( sprintf('Cannot redeclare scanner for file-type %s', $fileExtension), @@ -93,8 +91,8 @@ public function addFileTypeAnalyzer(string $fileExtension, string $className): v 1_622_727_281, ); } - if (!empty($this->config->getFiletypeAnalyzers()[$fileExtension]) - || !empty($this->additionalFileTypeAnalyzers[$fileExtension]) + if (isset($this->config->getFiletypeAnalyzers()[$fileExtension]) + || isset($this->additionalFileTypeAnalyzers[$fileExtension]) ) { throw new LogicException( sprintf('Cannot redeclare analyzer for file-type %s', $fileExtension), diff --git a/src/Psalm/PluginRegistrationSocket.php b/src/Psalm/PluginRegistrationSocket.php index de44e774e4b..9523d21d8df 100644 --- a/src/Psalm/PluginRegistrationSocket.php +++ b/src/Psalm/PluginRegistrationSocket.php @@ -23,17 +23,13 @@ final class PluginRegistrationSocket implements RegistrationInterface { - private Config $config; - - private Codebase $codebase; - /** * @internal */ - public function __construct(Config $config, Codebase $codebase) - { - $this->config = $config; - $this->codebase = $codebase; + public function __construct( + private readonly Config $config, + private readonly Codebase $codebase, + ) { } public function addStubFile(string $file_name): void diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index 8ec228b79da..dac1d228535 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -15,22 +15,16 @@ class LongProgress extends Progress { - public const NUMBER_OF_COLUMNS = 60; + final public const NUMBER_OF_COLUMNS = 60; protected ?int $number_of_tasks = null; protected int $progress = 0; - protected bool $print_errors = false; - - protected bool $print_infos = false; - protected bool $fixed_size = false; - public function __construct(bool $print_errors = true, bool $print_infos = true) + public function __construct(protected bool $print_errors = true, protected bool $print_infos = true) { - $this->print_errors = $print_errors; - $this->print_infos = $print_infos; } public function startScanningFiles(): void diff --git a/src/Psalm/Report.php b/src/Psalm/Report.php index c2a86945672..675f87968d3 100644 --- a/src/Psalm/Report.php +++ b/src/Psalm/Report.php @@ -15,32 +15,29 @@ abstract class Report { - public const TYPE_COMPACT = 'compact'; - public const TYPE_CONSOLE = 'console'; - public const TYPE_PYLINT = 'pylint'; - public const TYPE_JSON = 'json'; - public const TYPE_JSON_SUMMARY = 'json-summary'; - public const TYPE_SONARQUBE = 'sonarqube'; - public const TYPE_EMACS = 'emacs'; - public const TYPE_XML = 'xml'; - public const TYPE_JUNIT = 'junit'; - public const TYPE_CHECKSTYLE = 'checkstyle'; - public const TYPE_TEXT = 'text'; - public const TYPE_GITHUB_ACTIONS = 'github'; - public const TYPE_PHP_STORM = 'phpstorm'; - public const TYPE_SARIF = 'sarif'; - public const TYPE_CODECLIMATE = 'codeclimate'; - public const TYPE_COUNT = 'count'; - public const TYPE_BY_ISSUE_LEVEL = 'by-issue-level'; + final public const TYPE_COMPACT = 'compact'; + final public const TYPE_CONSOLE = 'console'; + final public const TYPE_PYLINT = 'pylint'; + final public const TYPE_JSON = 'json'; + final public const TYPE_JSON_SUMMARY = 'json-summary'; + final public const TYPE_SONARQUBE = 'sonarqube'; + final public const TYPE_EMACS = 'emacs'; + final public const TYPE_XML = 'xml'; + final public const TYPE_JUNIT = 'junit'; + final public const TYPE_CHECKSTYLE = 'checkstyle'; + final public const TYPE_TEXT = 'text'; + final public const TYPE_GITHUB_ACTIONS = 'github'; + final public const TYPE_PHP_STORM = 'phpstorm'; + final public const TYPE_SARIF = 'sarif'; + final public const TYPE_CODECLIMATE = 'codeclimate'; + final public const TYPE_COUNT = 'count'; + final public const TYPE_BY_ISSUE_LEVEL = 'by-issue-level'; /** * @var array */ protected array $issues_data; - /** @var array */ - protected array $fixable_issue_counts; - protected bool $use_color; protected bool $show_snippet; @@ -51,20 +48,16 @@ abstract class Report protected bool $in_ci; - protected int $mixed_expression_count; - - protected int $total_expression_count; - /** * @param array $issues_data * @param array $fixable_issue_counts */ public function __construct( array $issues_data, - array $fixable_issue_counts, + protected array $fixable_issue_counts, ReportOptions $report_options, - int $mixed_expression_count = 1, - int $total_expression_count = 1, + protected int $mixed_expression_count = 1, + protected int $total_expression_count = 1, ) { if (!$report_options->show_info) { $this->issues_data = array_filter( @@ -74,16 +67,12 @@ public function __construct( } else { $this->issues_data = $issues_data; } - $this->fixable_issue_counts = $fixable_issue_counts; $this->use_color = $report_options->use_color; $this->show_snippet = $report_options->show_snippet; $this->show_info = $report_options->show_info; $this->pretty = $report_options->pretty; $this->in_ci = $report_options->in_ci; - - $this->mixed_expression_count = $mixed_expression_count; - $this->total_expression_count = $total_expression_count; } protected function xmlEncode(string $data): string diff --git a/src/Psalm/Report/ByIssueLevelAndTypeReport.php b/src/Psalm/Report/ByIssueLevelAndTypeReport.php index 0d821cb4e30..35f21b36dcc 100644 --- a/src/Psalm/Report/ByIssueLevelAndTypeReport.php +++ b/src/Psalm/Report/ByIssueLevelAndTypeReport.php @@ -166,8 +166,9 @@ private function getFileReference(IssueData|DataFlowNodeData $data): string if (null === $this->link_format) { // if xdebug is not enabled, use `get_cfg_var` to get the value directly from php.ini - $this->link_format = ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') - ?: 'file://%f#L%l'; + $this->link_format = ( + ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') + ) ?: 'file://%f#L%l'; } $link = strtr($this->link_format, ['%f' => $data->file_path, '%l' => $data->line_from]); diff --git a/src/Psalm/Report/ConsoleReport.php b/src/Psalm/Report/ConsoleReport.php index ee34a67b864..7499ffcb162 100644 --- a/src/Psalm/Report/ConsoleReport.php +++ b/src/Psalm/Report/ConsoleReport.php @@ -134,8 +134,9 @@ private function getFileReference(IssueData|DataFlowNodeData $data): string if (null === $this->link_format) { // if xdebug is not enabled, use `get_cfg_var` to get the value directly from php.ini - $this->link_format = ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') - ?: 'file://%f#L%l'; + $this->link_format = ( + ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') + ) ?: 'file://%f#L%l'; } $link = strtr($this->link_format, ['%f' => $data->file_path, '%l' => $data->line_from]); diff --git a/src/Psalm/Report/SarifReport.php b/src/Psalm/Report/SarifReport.php index c16680ca5f3..463468ce60b 100644 --- a/src/Psalm/Report/SarifReport.php +++ b/src/Psalm/Report/SarifReport.php @@ -11,7 +11,7 @@ use function file_exists; use function file_get_contents; -use function strpos; +use function str_starts_with; /** * SARIF report format suitable for import into any SARIF compatible solution @@ -50,7 +50,7 @@ public function create(): string ], 'properties' => [ 'tags' => [ - (strpos($issue_data->type, 'Tainted') === 0) ? 'security' : 'maintainability', + (str_starts_with($issue_data->type, 'Tainted')) ? 'security' : 'maintainability', ], ], 'helpUri' => $issue_data->link, diff --git a/src/Psalm/SourceControl/Git/CommitInfo.php b/src/Psalm/SourceControl/Git/CommitInfo.php index d10bbb41c0b..e0e70bad284 100644 --- a/src/Psalm/SourceControl/Git/CommitInfo.php +++ b/src/Psalm/SourceControl/Git/CommitInfo.php @@ -14,37 +14,37 @@ final class CommitInfo /** * Commit ID. */ - protected ?string $id = null; + private ?string $id = null; /** * Author name. */ - protected ?string $author_name = null; + private ?string $author_name = null; /** * Author email. */ - protected ?string $author_email = null; + private ?string $author_email = null; /** * Committer name. */ - protected ?string $committer_name = null; + private ?string $committer_name = null; /** * Committer email. */ - protected ?string $committer_email = null; + private ?string $committer_email = null; /** * Commit message. */ - protected ?string $message = null; + private ?string $message = null; /** * Commit message. */ - protected ?int $date = null; + private ?int $date = null; public function toArray(): array { diff --git a/src/Psalm/SourceControl/Git/GitInfo.php b/src/Psalm/SourceControl/Git/GitInfo.php index dd7927255fa..c937e748cc6 100644 --- a/src/Psalm/SourceControl/Git/GitInfo.php +++ b/src/Psalm/SourceControl/Git/GitInfo.php @@ -31,23 +31,6 @@ */ final class GitInfo extends SourceControlInfo { - /** - * Branch name. - */ - protected string $branch; - - /** - * Head. - */ - protected CommitInfo $head; - - /** - * Remote. - * - * @var RemoteInfo[] - */ - protected array $remotes; - /** * Constructor. * @@ -55,11 +38,14 @@ final class GitInfo extends SourceControlInfo * @param CommitInfo $head HEAD commit * @param RemoteInfo[] $remotes remote repositories */ - public function __construct(string $branch, CommitInfo $head, array $remotes) - { - $this->branch = $branch; - $this->head = $head; - $this->remotes = $remotes; + public function __construct( + protected string $branch, + protected CommitInfo $head, + /** + * Remote. + */ + protected array $remotes, + ) { } public function toArray(): array diff --git a/src/Psalm/SourceControl/Git/RemoteInfo.php b/src/Psalm/SourceControl/Git/RemoteInfo.php index 91bf9a9b44a..ebb551b2186 100644 --- a/src/Psalm/SourceControl/Git/RemoteInfo.php +++ b/src/Psalm/SourceControl/Git/RemoteInfo.php @@ -14,12 +14,12 @@ final class RemoteInfo /** * Remote name. */ - protected ?string $name = null; + private ?string $name = null; /** * Remote URL. */ - protected ?string $url = null; + private ?string $url = null; public function toArray(): array { diff --git a/src/Psalm/Storage/Assertion.php b/src/Psalm/Storage/Assertion.php index 522af5078f6..3a588a527d2 100644 --- a/src/Psalm/Storage/Assertion.php +++ b/src/Psalm/Storage/Assertion.php @@ -5,13 +5,15 @@ namespace Psalm\Storage; use Psalm\Type\Atomic; +use Stringable; /** * @psalm-immutable */ -abstract class Assertion +abstract class Assertion implements Stringable { use ImmutableNonCloneableTrait; + use UnserializeMemoryUsageSuppressionTrait; abstract public function getNegation(): Assertion; diff --git a/src/Psalm/Storage/Assertion/Any.php b/src/Psalm/Storage/Assertion/Any.php index ad9e4e88c13..b8e3e3e584f 100644 --- a/src/Psalm/Storage/Assertion/Any.php +++ b/src/Psalm/Storage/Assertion/Any.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Any extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return $this; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php index 73f0e3e9cea..53e7cdffb72 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class ArrayKeyDoesNotExist extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new ArrayKeyExists(); diff --git a/src/Psalm/Storage/Assertion/ArrayKeyExists.php b/src/Psalm/Storage/Assertion/ArrayKeyExists.php index aab0e3f01db..c21d4428e47 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyExists.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyExists.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class ArrayKeyExists extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new ArrayKeyDoesNotExist(); diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php index 83076312120..b37c997134a 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php @@ -5,19 +5,17 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class DoesNotHaveAtLeastCount extends Assertion { - /** @var positive-int */ - public int $count; - + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ - public function __construct(int $count) + public function __construct(public readonly int $count) { - $this->count = $count; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php index cc96639ddf0..e557085cd3f 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php @@ -5,19 +5,17 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class DoesNotHaveExactCount extends Assertion { - /** @var positive-int */ - public int $count; - + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ - public function __construct(int $count) + public function __construct(public readonly int $count) { - $this->count = $count; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php index fbd25e22ae6..4a6a891c46e 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class DoesNotHaveMethod extends Assertion { - public string $method; - - public function __construct(string $method) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly string $method) { - $this->method = $method; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/Empty_.php b/src/Psalm/Storage/Assertion/Empty_.php index 344b99d6b08..53bca41fada 100644 --- a/src/Psalm/Storage/Assertion/Empty_.php +++ b/src/Psalm/Storage/Assertion/Empty_.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Empty_ extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new NonEmpty(); diff --git a/src/Psalm/Storage/Assertion/Falsy.php b/src/Psalm/Storage/Assertion/Falsy.php index d758b8352ee..5ac93ba421e 100644 --- a/src/Psalm/Storage/Assertion/Falsy.php +++ b/src/Psalm/Storage/Assertion/Falsy.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Falsy extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Truthy(); diff --git a/src/Psalm/Storage/Assertion/HasArrayKey.php b/src/Psalm/Storage/Assertion/HasArrayKey.php index 1326f09510e..8b0bb509651 100644 --- a/src/Psalm/Storage/Assertion/HasArrayKey.php +++ b/src/Psalm/Storage/Assertion/HasArrayKey.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; /** @@ -12,6 +13,7 @@ */ final class HasArrayKey extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly string $key) { } diff --git a/src/Psalm/Storage/Assertion/HasAtLeastCount.php b/src/Psalm/Storage/Assertion/HasAtLeastCount.php index 0c2555b9994..98581348f93 100644 --- a/src/Psalm/Storage/Assertion/HasAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/HasAtLeastCount.php @@ -5,19 +5,17 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class HasAtLeastCount extends Assertion { - /** @var positive-int */ - public int $count; - + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ - public function __construct(int $count) + public function __construct(public readonly int $count) { - $this->count = $count; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/HasExactCount.php b/src/Psalm/Storage/Assertion/HasExactCount.php index 8f468b1eda4..8f28be407fb 100644 --- a/src/Psalm/Storage/Assertion/HasExactCount.php +++ b/src/Psalm/Storage/Assertion/HasExactCount.php @@ -5,19 +5,17 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class HasExactCount extends Assertion { - /** @var positive-int */ - public int $count; - + use UnserializeMemoryUsageSuppressionTrait; /** @param positive-int $count */ - public function __construct(int $count) + public function __construct(public readonly int $count) { - $this->count = $count; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php index db401486ad7..39be9c6d610 100644 --- a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; /** @@ -12,6 +13,7 @@ */ final class HasIntOrStringArrayAccess extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); diff --git a/src/Psalm/Storage/Assertion/HasMethod.php b/src/Psalm/Storage/Assertion/HasMethod.php index c4cb22a506b..ed994ce149e 100644 --- a/src/Psalm/Storage/Assertion/HasMethod.php +++ b/src/Psalm/Storage/Assertion/HasMethod.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class HasMethod extends Assertion { - public string $method; - - public function __construct(string $method) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly string $method) { - $this->method = $method; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php index 1d2b519b72a..3a8e65a2813 100644 --- a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use UnexpectedValueException; /** @@ -12,6 +13,7 @@ */ final class HasStringArrayAccess extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); diff --git a/src/Psalm/Storage/Assertion/InArray.php b/src/Psalm/Storage/Assertion/InArray.php index 2e63d15457a..6c88a4f3d66 100644 --- a/src/Psalm/Storage/Assertion/InArray.php +++ b/src/Psalm/Storage/Assertion/InArray.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Union; /** @@ -12,11 +13,9 @@ */ final class InArray extends Assertion { - public Union $type; - - public function __construct(Union $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Union $type) { - $this->type = $type; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsAClass.php b/src/Psalm/Storage/Assertion/IsAClass.php index c7476bc9532..905302771c0 100644 --- a/src/Psalm/Storage/Assertion/IsAClass.php +++ b/src/Psalm/Storage/Assertion/IsAClass.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,15 +13,10 @@ */ final class IsAClass extends Assertion { - /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ - public Atomic $type; - public bool $allow_string; - + use UnserializeMemoryUsageSuppressionTrait; /** @param Atomic\TTemplateParamClass|Atomic\TNamedObject $type */ - public function __construct(Atomic $type, bool $allow_string) + public function __construct(public readonly Atomic $type, public readonly bool $allow_string) { - $this->type = $type; - $this->allow_string = $allow_string; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsClassEqual.php b/src/Psalm/Storage/Assertion/IsClassEqual.php index 4f51e7b323f..88e8d1527c8 100644 --- a/src/Psalm/Storage/Assertion/IsClassEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassEqual.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsClassEqual extends Assertion { - public string $type; - - public function __construct(string $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly string $type) { - $this->type = $type; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsClassNotEqual.php b/src/Psalm/Storage/Assertion/IsClassNotEqual.php index 8fd8508654e..d94fb6246ef 100644 --- a/src/Psalm/Storage/Assertion/IsClassNotEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassNotEqual.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsClassNotEqual extends Assertion { - public string $type; - - public function __construct(string $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly string $type) { - $this->type = $type; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/IsCountable.php b/src/Psalm/Storage/Assertion/IsCountable.php index 552d7904ca4..f1f213df5b2 100644 --- a/src/Psalm/Storage/Assertion/IsCountable.php +++ b/src/Psalm/Storage/Assertion/IsCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new IsNotCountable(true); diff --git a/src/Psalm/Storage/Assertion/IsEqualIsset.php b/src/Psalm/Storage/Assertion/IsEqualIsset.php index ce2a2f27901..7cc82cacb8c 100644 --- a/src/Psalm/Storage/Assertion/IsEqualIsset.php +++ b/src/Psalm/Storage/Assertion/IsEqualIsset.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsEqualIsset extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Any(); diff --git a/src/Psalm/Storage/Assertion/IsGreaterThan.php b/src/Psalm/Storage/Assertion/IsGreaterThan.php index 175f571a1cc..a4d4fb153f8 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThan.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThan.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsGreaterThan extends Assertion { - public int $value; - - public function __construct(int $value) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly int $value) { - $this->value = $value; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php index ffdb3bbf44f..295ad5eb444 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsGreaterThanOrEqualTo extends Assertion { - public int $value; - - public function __construct(int $value) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly int $value) { - $this->value = $value; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/IsIdentical.php b/src/Psalm/Storage/Assertion/IsIdentical.php index b703c59fce2..86f5212407a 100644 --- a/src/Psalm/Storage/Assertion/IsIdentical.php +++ b/src/Psalm/Storage/Assertion/IsIdentical.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,11 +13,9 @@ */ final class IsIdentical extends Assertion { - public Atomic $type; - - public function __construct(Atomic $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Atomic $type) { - $this->type = $type; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsIsset.php b/src/Psalm/Storage/Assertion/IsIsset.php index be7c11bedb0..01fc40467e7 100644 --- a/src/Psalm/Storage/Assertion/IsIsset.php +++ b/src/Psalm/Storage/Assertion/IsIsset.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsIsset extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new IsNotIsset(); diff --git a/src/Psalm/Storage/Assertion/IsLessThan.php b/src/Psalm/Storage/Assertion/IsLessThan.php index 3708dd08695..5587ad32ce4 100644 --- a/src/Psalm/Storage/Assertion/IsLessThan.php +++ b/src/Psalm/Storage/Assertion/IsLessThan.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsLessThan extends Assertion { - public int $value; - - public function __construct(int $value) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly int $value) { - $this->value = $value; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php index 13aef9d02e8..2ef344bf3c1 100644 --- a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php @@ -5,17 +5,16 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsLessThanOrEqualTo extends Assertion { - public int $value; - - public function __construct(int $value) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly int $value) { - $this->value = $value; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsLooselyEqual.php b/src/Psalm/Storage/Assertion/IsLooselyEqual.php index 7c145edeb9c..4fd2aa367fb 100644 --- a/src/Psalm/Storage/Assertion/IsLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsLooselyEqual.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,11 +13,9 @@ */ final class IsLooselyEqual extends Assertion { - public Atomic $type; - - public function __construct(Atomic $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Atomic $type) { - $this->type = $type; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/IsNotAClass.php b/src/Psalm/Storage/Assertion/IsNotAClass.php index b474b3ad85e..d710eb7eea7 100644 --- a/src/Psalm/Storage/Assertion/IsNotAClass.php +++ b/src/Psalm/Storage/Assertion/IsNotAClass.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,15 +13,10 @@ */ final class IsNotAClass extends Assertion { - /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ - public Atomic $type; - public bool $allow_string; - + use UnserializeMemoryUsageSuppressionTrait; /** @param Atomic\TTemplateParamClass|Atomic\TNamedObject $type */ - public function __construct(Atomic $type, bool $allow_string) + public function __construct(public readonly Atomic $type, public readonly bool $allow_string) { - $this->type = $type; - $this->allow_string = $allow_string; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/IsNotCountable.php b/src/Psalm/Storage/Assertion/IsNotCountable.php index 5f11cf6df6b..bf9b4db9a04 100644 --- a/src/Psalm/Storage/Assertion/IsNotCountable.php +++ b/src/Psalm/Storage/Assertion/IsNotCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsNotCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly bool $is_negatable) { } diff --git a/src/Psalm/Storage/Assertion/IsNotIdentical.php b/src/Psalm/Storage/Assertion/IsNotIdentical.php index f483cef0293..22b6e7c02d1 100644 --- a/src/Psalm/Storage/Assertion/IsNotIdentical.php +++ b/src/Psalm/Storage/Assertion/IsNotIdentical.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,11 +13,9 @@ */ final class IsNotIdentical extends Assertion { - public Atomic $type; - - public function __construct(Atomic $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Atomic $type) { - $this->type = $type; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/IsNotIsset.php b/src/Psalm/Storage/Assertion/IsNotIsset.php index d73aa64b68d..890b3fdb1e1 100644 --- a/src/Psalm/Storage/Assertion/IsNotIsset.php +++ b/src/Psalm/Storage/Assertion/IsNotIsset.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class IsNotIsset extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new IsIsset(); diff --git a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php index 3715553273e..63a5a78f8c4 100644 --- a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,11 +13,9 @@ */ final class IsNotLooselyEqual extends Assertion { - public Atomic $type; - - public function __construct(Atomic $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Atomic $type) { - $this->type = $type; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/IsNotType.php b/src/Psalm/Storage/Assertion/IsNotType.php index 090a8e4abf0..28a769ca26e 100644 --- a/src/Psalm/Storage/Assertion/IsNotType.php +++ b/src/Psalm/Storage/Assertion/IsNotType.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,11 +13,9 @@ */ final class IsNotType extends Assertion { - public Atomic $type; - - public function __construct(Atomic $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Atomic $type) { - $this->type = $type; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/IsType.php b/src/Psalm/Storage/Assertion/IsType.php index 37fc63d66ed..0bc6c63c9a1 100644 --- a/src/Psalm/Storage/Assertion/IsType.php +++ b/src/Psalm/Storage/Assertion/IsType.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -12,11 +13,9 @@ */ final class IsType extends Assertion { - public Atomic $type; - - public function __construct(Atomic $type) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public readonly Atomic $type) { - $this->type = $type; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/NestedAssertions.php b/src/Psalm/Storage/Assertion/NestedAssertions.php index 59010690a8d..7d6f405caa2 100644 --- a/src/Psalm/Storage/Assertion/NestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NestedAssertions.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use function json_encode; @@ -15,13 +16,10 @@ */ final class NestedAssertions extends Assertion { - /** @var array>> */ - public array $assertions; - + use UnserializeMemoryUsageSuppressionTrait; /** @param array>> $assertions */ - public function __construct(array $assertions) + public function __construct(public readonly array $assertions) { - $this->assertions = $assertions; } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/NonEmpty.php b/src/Psalm/Storage/Assertion/NonEmpty.php index ac208f4d2a3..159da72e798 100644 --- a/src/Psalm/Storage/Assertion/NonEmpty.php +++ b/src/Psalm/Storage/Assertion/NonEmpty.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class NonEmpty extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Empty_(); diff --git a/src/Psalm/Storage/Assertion/NonEmptyCountable.php b/src/Psalm/Storage/Assertion/NonEmptyCountable.php index a36bffdd2a6..8ab74c5bfce 100644 --- a/src/Psalm/Storage/Assertion/NonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NonEmptyCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class NonEmptyCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function __construct(public readonly bool $is_negatable) { } diff --git a/src/Psalm/Storage/Assertion/NotInArray.php b/src/Psalm/Storage/Assertion/NotInArray.php index 824ed5db303..fd47839f84e 100644 --- a/src/Psalm/Storage/Assertion/NotInArray.php +++ b/src/Psalm/Storage/Assertion/NotInArray.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Union; /** @@ -12,14 +13,10 @@ */ final class NotInArray extends Assertion { - /** - * @readonly - */ - public Union $type; - - public function __construct(Union $type) - { - $this->type = $type; + use UnserializeMemoryUsageSuppressionTrait; + public function __construct( + public readonly Union $type, + ) { } public function getNegation(): Assertion diff --git a/src/Psalm/Storage/Assertion/NotNestedAssertions.php b/src/Psalm/Storage/Assertion/NotNestedAssertions.php index f3d932ae498..acf8696b1ea 100644 --- a/src/Psalm/Storage/Assertion/NotNestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NotNestedAssertions.php @@ -5,6 +5,7 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use function json_encode; @@ -15,13 +16,10 @@ */ final class NotNestedAssertions extends Assertion { - /** @var array>> */ - public array $assertions; - + use UnserializeMemoryUsageSuppressionTrait; /** @param array>> $assertions */ - public function __construct(array $assertions) + public function __construct(public readonly array $assertions) { - $this->assertions = $assertions; } public function isNegation(): bool diff --git a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php index 1306167789c..b9eb20f26b5 100644 --- a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class NotNonEmptyCountable extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new NonEmptyCountable(true); diff --git a/src/Psalm/Storage/Assertion/Truthy.php b/src/Psalm/Storage/Assertion/Truthy.php index 35ec5d32e3c..c1e62224f2c 100644 --- a/src/Psalm/Storage/Assertion/Truthy.php +++ b/src/Psalm/Storage/Assertion/Truthy.php @@ -5,12 +5,14 @@ namespace Psalm\Storage\Assertion; use Psalm\Storage\Assertion; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; /** * @psalm-immutable */ final class Truthy extends Assertion { + use UnserializeMemoryUsageSuppressionTrait; public function getNegation(): Assertion { return new Falsy(); diff --git a/src/Psalm/Storage/AttributeArg.php b/src/Psalm/Storage/AttributeArg.php index 72fd6080222..70e5ecafc5e 100644 --- a/src/Psalm/Storage/AttributeArg.php +++ b/src/Psalm/Storage/AttributeArg.php @@ -10,29 +10,17 @@ /** * @psalm-immutable + * @api */ final class AttributeArg { use ImmutableNonCloneableTrait; - /** - * @psalm-suppress PossiblyUnusedProperty It's part of the public API for now - */ - public ?string $name = null; - - public Union|UnresolvedConstantComponent $type; - - /** - * @psalm-suppress PossiblyUnusedProperty It's part of the public API for now - */ - public CodeLocation $location; + use UnserializeMemoryUsageSuppressionTrait; public function __construct( - ?string $name, - Union|UnresolvedConstantComponent $type, - CodeLocation $location, + public readonly ?string $name, + public readonly Union|UnresolvedConstantComponent $type, + public readonly CodeLocation $location, ) { - $this->name = $name; - $this->type = $type; - $this->location = $location; } } diff --git a/src/Psalm/Storage/AttributeStorage.php b/src/Psalm/Storage/AttributeStorage.php index 8cbc499a637..6d4f84e3cdd 100644 --- a/src/Psalm/Storage/AttributeStorage.php +++ b/src/Psalm/Storage/AttributeStorage.php @@ -8,39 +8,21 @@ /** * @psalm-immutable + * @api */ final class AttributeStorage { use ImmutableNonCloneableTrait; - public string $fq_class_name; - - /** - * @var list - */ - public array $args; - - /** - * @psalm-suppress PossiblyUnusedProperty part of public API - */ - public CodeLocation $location; - - /** - * @psalm-suppress PossiblyUnusedProperty part of public API - */ - public CodeLocation $name_location; + use UnserializeMemoryUsageSuppressionTrait; /** * @param list $args */ public function __construct( - string $fq_class_name, - array $args, - CodeLocation $location, - CodeLocation $name_location, + public readonly string $fq_class_name, + public readonly array $args, + public readonly CodeLocation $location, + public readonly CodeLocation $name_location, ) { - $this->fq_class_name = $fq_class_name; - $this->args = $args; - $this->location = $location; - $this->name_location = $name_location; } } diff --git a/src/Psalm/Storage/ClassConstantStorage.php b/src/Psalm/Storage/ClassConstantStorage.php index 78488412a81..17cd3928b50 100644 --- a/src/Psalm/Storage/ClassConstantStorage.php +++ b/src/Psalm/Storage/ClassConstantStorage.php @@ -21,45 +21,7 @@ final class ClassConstantStorage /** @psalm-suppress MutableDependency Mutable by design */ use CustomMetadataTrait; use ImmutableNonCloneableTrait; - - public ?CodeLocation $type_location; - - /** - * The type from an annotation, or the inferred type if no annotation exists. - */ - public ?Union $type; - - /** - * The type inferred from the value. - */ - public ?Union $inferred_type; - - /** - * @var ClassLikeAnalyzer::VISIBILITY_* - */ - public int $visibility = ClassLikeAnalyzer::VISIBILITY_PUBLIC; - - public ?CodeLocation $location; - - public ?CodeLocation $stmt_location; - - public ?UnresolvedConstantComponent $unresolved_node; - - public bool $deprecated = false; - - public bool $final = false; - - /** - * @var list - */ - public array $attributes = []; - - /** - * @var array - */ - public array $suppressed_issues = []; - - public ?string $description; + use UnserializeMemoryUsageSuppressionTrait; /** * @param ClassLikeAnalyzer::VISIBILITY_* $visibility @@ -67,31 +29,25 @@ final class ClassConstantStorage * @param array $suppressed_issues */ public function __construct( - ?Union $type, - ?Union $inferred_type, - int $visibility, - ?CodeLocation $location, - ?CodeLocation $type_location = null, - ?CodeLocation $stmt_location = null, - bool $deprecated = false, - bool $final = false, - ?UnresolvedConstantComponent $unresolved_node = null, - array $attributes = [], - array $suppressed_issues = [], - ?string $description = null, + /** + * The type from an annotation, or the inferred type if no annotation exists. + */ + public ?Union $type, + /** + * The type inferred from the value. + */ + public ?Union $inferred_type, + public readonly int $visibility, + public readonly ?CodeLocation $location, + public readonly ?CodeLocation $type_location = null, + public readonly ?CodeLocation $stmt_location = null, + public readonly bool $deprecated = false, + public readonly bool $final = false, + public readonly ?UnresolvedConstantComponent $unresolved_node = null, + public readonly array $attributes = [], + public readonly array $suppressed_issues = [], + public readonly ?string $description = null, ) { - $this->visibility = $visibility; - $this->location = $location; - $this->type = $type; - $this->inferred_type = $inferred_type; - $this->type_location = $type_location; - $this->stmt_location = $stmt_location; - $this->deprecated = $deprecated; - $this->final = $final; - $this->unresolved_node = $unresolved_node; - $this->attributes = $attributes; - $this->suppressed_issues = $suppressed_issues; - $this->description = $description; } /** @@ -99,18 +55,11 @@ public function __construct( */ public function getHoverMarkdown(string $const): string { - switch ($this->visibility) { - case ClassLikeAnalyzer::VISIBILITY_PRIVATE: - $visibility_text = 'private'; - break; - - case ClassLikeAnalyzer::VISIBILITY_PROTECTED: - $visibility_text = 'protected'; - break; - - default: - $visibility_text = 'public'; - } + $visibility_text = match ($this->visibility) { + ClassLikeAnalyzer::VISIBILITY_PRIVATE => 'private', + ClassLikeAnalyzer::VISIBILITY_PROTECTED => 'protected', + default => 'public', + }; $value = ''; if ($this->type) { diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 8fabc2e3edd..04c5fc94e86 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -12,15 +12,18 @@ use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TypeAlias\ClassTypeAlias; use Psalm\Issue\CodeIssue; +use Psalm\Issue\DeprecatedClass; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; use function array_values; +use function in_array; final class ClassLikeStorage implements HasAttributesInterface { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var array @@ -68,8 +71,6 @@ final class ClassLikeStorage implements HasAttributesInterface */ public array $suppressed_issues = []; - public string $name; - /** * Is this class user-defined */ @@ -388,9 +389,8 @@ final class ClassLikeStorage implements HasAttributesInterface public bool $readonly = false; - public function __construct(string $name) + public function __construct(public string $name) { - $this->name = $name; } /** @@ -459,4 +459,22 @@ private function hasAttribute(string $fq_class_name): bool return false; } + + /** + * @return array + */ + public function getSuppressedIssuesForTemplateExtendParams(): array + { + $allowed_issue_types = [ + DeprecatedClass::getIssueType(), + ]; + $suppressed_issues_for_template_extend_params = []; + foreach ($this->suppressed_issues as $offset => $suppressed_issue) { + if (!in_array($suppressed_issue, $allowed_issue_types, true)) { + continue; + } + $suppressed_issues_for_template_extend_params[$offset] = $suppressed_issue; + } + return $suppressed_issues_for_template_extend_params; + } } diff --git a/src/Psalm/Storage/EnumCaseStorage.php b/src/Psalm/Storage/EnumCaseStorage.php index 12cf8b1098e..0e06a59ff64 100644 --- a/src/Psalm/Storage/EnumCaseStorage.php +++ b/src/Psalm/Storage/EnumCaseStorage.php @@ -10,17 +10,11 @@ final class EnumCaseStorage { - public TLiteralString|TLiteralInt|null $value = null; - - public CodeLocation $stmt_location; + use UnserializeMemoryUsageSuppressionTrait; public bool $deprecated = false; - public function __construct( - TLiteralString|TLiteralInt|null $value, - CodeLocation $location, - ) { - $this->value = $value; - $this->stmt_location = $location; + public function __construct(public TLiteralString|TLiteralInt|null $value, public CodeLocation $stmt_location) + { } } diff --git a/src/Psalm/Storage/FileStorage.php b/src/Psalm/Storage/FileStorage.php index ce309152bba..132d5823f5a 100644 --- a/src/Psalm/Storage/FileStorage.php +++ b/src/Psalm/Storage/FileStorage.php @@ -12,6 +12,7 @@ final class FileStorage { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; /** * @var array @@ -33,8 +34,6 @@ final class FileStorage */ public array $required_interfaces = []; - public string $file_path; - /** * @var array */ @@ -87,8 +86,7 @@ final class FileStorage /** @var Aliases[] */ public array $namespace_aliases = []; - public function __construct(string $file_path) + public function __construct(public string $file_path) { - $this->file_path = $file_path; } } diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index cdeaee2d3eb..12736963ee3 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -14,33 +14,12 @@ final class FunctionLikeParameter implements HasAttributesInterface, TypeNode { use CustomMetadataTrait; - - public string $name; - - public bool $by_ref; - - public ?Union $type = null; - - public ?Union $out_type = null; - - public ?Union $signature_type = null; + use UnserializeMemoryUsageSuppressionTrait; public bool $has_docblock_type = false; - public bool $is_optional; - - public bool $is_nullable; - - public Union|UnresolvedConstantComponent|null $default_type = null; - - public ?CodeLocation $location = null; - - public ?CodeLocation $type_location = null; - public ?CodeLocation $signature_type_location = null; - public bool $is_variadic; - /** * @var array|null */ @@ -65,30 +44,19 @@ final class FunctionLikeParameter implements HasAttributesInterface, TypeNode * @psalm-external-mutation-free */ public function __construct( - string $name, - bool $by_ref, - ?Union $type = null, - ?Union $signature_type = null, - ?CodeLocation $location = null, - ?CodeLocation $type_location = null, - bool $is_optional = true, - bool $is_nullable = false, - bool $is_variadic = false, - Union|UnresolvedConstantComponent|null $default_type = null, - ?Union $out_type = null, + public string $name, + public bool $by_ref, + public ?Union $type = null, + public ?Union $signature_type = null, + public ?CodeLocation $location = null, + public ?CodeLocation $type_location = null, + public bool $is_optional = true, + public bool $is_nullable = false, + public bool $is_variadic = false, + public Union|UnresolvedConstantComponent|null $default_type = null, + public ?Union $out_type = null, ) { - $this->name = $name; - $this->by_ref = $by_ref; - $this->type = $type; - $this->signature_type = $signature_type; - $this->is_optional = $is_optional; - $this->is_nullable = $is_nullable; - $this->is_variadic = $is_variadic; - $this->location = $location; - $this->type_location = $type_location; $this->signature_type_location = $type_location; - $this->default_type = $default_type; - $this->out_type = $out_type; } /** @psalm-mutation-free */ diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index bde27fc80de..8a30fe31e97 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -8,6 +8,7 @@ use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Issue\CodeIssue; use Psalm\Type\Union; +use Stringable; use function array_column; use function array_fill_keys; @@ -15,9 +16,10 @@ use function count; use function implode; -abstract class FunctionLikeStorage implements HasAttributesInterface +abstract class FunctionLikeStorage implements HasAttributesInterface, Stringable { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; public ?CodeLocation $location = null; @@ -211,18 +213,11 @@ static function (FunctionLikeParameter $param): string { return $symbol_text; } - switch ($this->visibility) { - case ClassLikeAnalyzer::VISIBILITY_PRIVATE: - $visibility_text = 'private'; - break; - - case ClassLikeAnalyzer::VISIBILITY_PROTECTED: - $visibility_text = 'protected'; - break; - - default: - $visibility_text = 'public'; - } + $visibility_text = match ($this->visibility) { + ClassLikeAnalyzer::VISIBILITY_PRIVATE => 'private', + ClassLikeAnalyzer::VISIBILITY_PROTECTED => 'protected', + default => 'public', + }; return $visibility_text . ' ' . $symbol_text; } @@ -241,18 +236,11 @@ public function getCompletionSignature(): string return $symbol_text; } - switch ($this->visibility) { - case ClassLikeAnalyzer::VISIBILITY_PRIVATE: - $visibility_text = 'private'; - break; - - case ClassLikeAnalyzer::VISIBILITY_PROTECTED: - $visibility_text = 'protected'; - break; - - default: - $visibility_text = 'public'; - } + $visibility_text = match ($this->visibility) { + ClassLikeAnalyzer::VISIBILITY_PRIVATE => 'private', + ClassLikeAnalyzer::VISIBILITY_PROTECTED => 'protected', + default => 'public', + }; return $visibility_text . ' ' . $symbol_text; } diff --git a/src/Psalm/Storage/FunctionStorage.php b/src/Psalm/Storage/FunctionStorage.php index f9c9dcfeeb2..043bb1832ba 100644 --- a/src/Psalm/Storage/FunctionStorage.php +++ b/src/Psalm/Storage/FunctionStorage.php @@ -6,6 +6,7 @@ final class FunctionStorage extends FunctionLikeStorage { + use UnserializeMemoryUsageSuppressionTrait; /** @var array */ public array $byref_uses = []; } diff --git a/src/Psalm/Storage/MethodStorage.php b/src/Psalm/Storage/MethodStorage.php index c6ebc2d8aab..3dd5986db9a 100644 --- a/src/Psalm/Storage/MethodStorage.php +++ b/src/Psalm/Storage/MethodStorage.php @@ -8,6 +8,7 @@ final class MethodStorage extends FunctionLikeStorage { + use UnserializeMemoryUsageSuppressionTrait; public bool $is_static = false; public int $visibility = 0; diff --git a/src/Psalm/Storage/Possibilities.php b/src/Psalm/Storage/Possibilities.php index 62959661725..d9ca584db4d 100644 --- a/src/Psalm/Storage/Possibilities.php +++ b/src/Psalm/Storage/Possibilities.php @@ -14,24 +14,19 @@ final class Possibilities { - /** - * @var list the rule being asserted - */ - public array $rule; - - /** - * @var int|string the id of the property/variable, or - * the parameter offset of the affected arg - */ - public int|string $var_id; + use UnserializeMemoryUsageSuppressionTrait; /** * @param list $rule */ - public function __construct(string|int $var_id, array $rule) - { - $this->rule = $rule; - $this->var_id = $var_id; + public function __construct( + /** + * @var int|string the id of the property/variable, or + * the parameter offset of the affected arg + */ + public int|string $var_id, + public array $rule, + ) { } public function getUntemplatedCopy( diff --git a/src/Psalm/Storage/PropertyStorage.php b/src/Psalm/Storage/PropertyStorage.php index 3eb742c281e..c7427e58aa6 100644 --- a/src/Psalm/Storage/PropertyStorage.php +++ b/src/Psalm/Storage/PropertyStorage.php @@ -11,6 +11,7 @@ final class PropertyStorage implements HasAttributesInterface { use CustomMetadataTrait; + use UnserializeMemoryUsageSuppressionTrait; public ?bool $is_static = null; @@ -67,18 +68,11 @@ final class PropertyStorage implements HasAttributesInterface public function getInfo(): string { - switch ($this->visibility) { - case ClassLikeAnalyzer::VISIBILITY_PRIVATE: - $visibility_text = 'private'; - break; - - case ClassLikeAnalyzer::VISIBILITY_PROTECTED: - $visibility_text = 'protected'; - break; - - default: - $visibility_text = 'public'; - } + $visibility_text = match ($this->visibility) { + ClassLikeAnalyzer::VISIBILITY_PRIVATE => 'private', + ClassLikeAnalyzer::VISIBILITY_PROTECTED => 'protected', + default => 'public', + }; return $visibility_text . ' ' . ($this->type ? $this->type->getId() : 'mixed'); } diff --git a/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php b/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php new file mode 100644 index 00000000000..f60e264da22 --- /dev/null +++ b/src/Psalm/Storage/UnserializeMemoryUsageSuppressionTrait.php @@ -0,0 +1,26 @@ + $value) { + $this->$key = $value; + } + } +} diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 7960ef264b6..acb3f535c83 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -58,11 +58,11 @@ use function array_shift; use function array_values; use function explode; -use function get_class; use function implode; use function is_int; use function preg_quote; use function preg_replace; +use function str_contains; use function stripos; use function strlen; use function strpos; @@ -104,7 +104,7 @@ public static function getFQCLNFromString( $imported_namespaces = $aliases->uses; - if (strpos($class, '\\') !== false) { + if (str_contains($class, '\\')) { $class_parts = explode('\\', $class); $first_namespace = array_shift($class_parts); @@ -155,7 +155,7 @@ public static function getStringFromFQCLN( if (!isset($aliased_classes[strtolower($candidate_parts[0])])) { return $candidate; } - } elseif (!$namespace && strpos($value, '\\') === false) { + } elseif (!$namespace && !str_contains($value, '\\')) { return $value; } @@ -279,7 +279,7 @@ public static function getString(?string $value = null): Union return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]); } - /** @return TLiteralString|TNonEmptyString */ + /** @return TLiteralString|TNonEmptyString|TNonFalsyString */ public static function getAtomicStringFromLiteral(string $value, bool $from_docblock = false): TString { $config = Config::getInstance(); @@ -289,10 +289,12 @@ public static function getAtomicStringFromLiteral(string $value, bool $from_docb $type = $config->eventDispatcher->dispatchStringInterpreter($event); if (!$type) { - if (strlen($value) < $config->max_string_length) { + if ($value === '' || strlen($value) < $config->max_string_length) { $type = new TLiteralString($value, $from_docblock); - } else { + } elseif ($value === '0') { $type = new TNonEmptyString($from_docblock); + } else { + $type = new TNonFalsyString($from_docblock); } } @@ -628,13 +630,13 @@ public static function combineUnionTypes( $both_failed_reconciliation = true; } else { return $type_2->setProperties([ - 'parent_nodes' => array_merge($type_2->parent_nodes, $type_1->parent_nodes), + 'parent_nodes' => [...$type_2->parent_nodes, ...$type_1->parent_nodes], 'possibly_undefined' => $possibly_undefined ?? $type_2->possibly_undefined, ]); } } elseif ($type_2->failed_reconciliation) { return $type_1->setProperties([ - 'parent_nodes' => array_merge($type_1->parent_nodes, $type_2->parent_nodes), + 'parent_nodes' => [...$type_1->parent_nodes, ...$type_2->parent_nodes], 'possibly_undefined' => $possibly_undefined ?? $type_1->possibly_undefined, ]); } @@ -715,6 +717,8 @@ public static function intersectUnionTypes( ?Union $type_1, ?Union $type_2, Codebase $codebase, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true, ): ?Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); @@ -768,6 +772,8 @@ public static function intersectUnionTypes( $type_2_atomic, $codebase, $intersection_performed, + $allow_interface_equality, + $allow_float_int_equality, ); if (null !== $intersection_atomic) { @@ -841,6 +847,8 @@ private static function intersectAtomicTypes( Atomic $type_2_atomic, Codebase $codebase, bool &$intersection_performed, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true, ): ?Atomic { $intersection_atomic = null; $wider_type = null; @@ -848,15 +856,15 @@ private static function intersectAtomicTypes( && $type_2_atomic instanceof TNamedObject ) { if (($type_1_atomic->value === $type_2_atomic->value - && get_class($type_1_atomic) === TNamedObject::class - && get_class($type_2_atomic) !== TNamedObject::class) + && $type_1_atomic::class === TNamedObject::class + && $type_2_atomic::class !== TNamedObject::class) ) { $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; $intersection_performed = true; } elseif (($type_1_atomic->value === $type_2_atomic->value - && get_class($type_2_atomic) === TNamedObject::class - && get_class($type_1_atomic) !== TNamedObject::class) + && $type_2_atomic::class === TNamedObject::class + && $type_1_atomic::class !== TNamedObject::class) ) { $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; @@ -886,6 +894,8 @@ private static function intersectAtomicTypes( $codebase, $type_2_atomic, $type_1_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; @@ -894,6 +904,8 @@ private static function intersectAtomicTypes( $codebase, $type_1_atomic, $type_2_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; @@ -921,7 +933,7 @@ private static function intersectAtomicTypes( if ($first_is_class && $second_is_class) { return $intersection_atomic; } - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // Ignore non-existing classes during initial scan } } @@ -983,7 +995,7 @@ private static function mayHaveIntersection(Atomic $type, Codebase $codebase): b } try { $storage = $codebase->classlike_storage_provider->get($type->value); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // Ignore non-existing classes during initial scan return true; } diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index b80d0af22a0..15bd44a8667 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -13,6 +13,7 @@ use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; use Psalm\Internal\TypeVisitor\ClasslikeReplacer; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; @@ -66,24 +67,30 @@ use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TTypeAlias; use Psalm\Type\Atomic\TVoid; +use Stringable; use function array_filter; use function array_keys; use function count; -use function get_class; use function is_array; use function is_numeric; +use function str_starts_with; use function strpos; use function strtolower; /** * @psalm-immutable */ -abstract class Atomic implements TypeNode +abstract class Atomic implements TypeNode, Stringable { - public function __construct(bool $from_docblock = false) - { - $this->from_docblock = $from_docblock; + use UnserializeMemoryUsageSuppressionTrait; + + public function __construct( + /** + * Whether or not the type comes from a docblock + */ + public bool $from_docblock = false, + ) { } protected function __clone() { @@ -94,11 +101,6 @@ protected function __clone() */ public bool $checked = false; - /** - * Whether or not the type comes from a docblock - */ - public bool $from_docblock = false; - public ?int $offset_start = null; public ?int $offset_end = null; @@ -385,7 +387,7 @@ private static function createInner( return new TClosure('Closure'); } - if (strpos($value, '-') && strpos($value, 'OCI-') !== 0) { + if (strpos($value, '-') && !str_starts_with($value, 'OCI-')) { throw new TypeParseTreeException('Unrecognized type ' . $value); } @@ -767,7 +769,7 @@ public function replaceTemplateTypesWithArgTypes( public function equals(Atomic $other_type, bool $ensure_source_equality): bool { - return get_class($other_type) === get_class($this); + return $other_type::class === static::class; } public function isTruthy(): bool diff --git a/src/Psalm/Type/Atomic/Scalar.php b/src/Psalm/Type/Atomic/Scalar.php index 739bb9bb514..c4597dffbe7 100644 --- a/src/Psalm/Type/Atomic/Scalar.php +++ b/src/Psalm/Type/Atomic/Scalar.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -11,6 +12,7 @@ */ abstract class Scalar extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return true; diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index d92a2f2f0b6..7481dee2554 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -11,8 +11,6 @@ */ final class TAnonymousClassInstance extends TNamedObject { - public ?string $extends = null; - /** * @param string $value the name of the object * @param array $extra_types @@ -20,12 +18,10 @@ final class TAnonymousClassInstance extends TNamedObject public function __construct( string $value, bool $is_static = false, - ?string $extends = null, + public ?string $extends = null, array $extra_types = [], ) { parent::__construct($value, $is_static, false, $extra_types); - - $this->extends = $extends; } public function toPhpString( diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index cf90faf8851..37cf0bc694f 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -7,11 +7,11 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; use function count; -use function get_class; /** * Denotes a simple array of the form `array`. It expects an array with two elements, both union types. @@ -20,6 +20,7 @@ */ class TArray extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; /** * @use GenericTrait */ @@ -67,7 +68,7 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool public function equals(Atomic $other_type, bool $ensure_source_equality): bool { - if (get_class($other_type) !== static::class) { + if ($other_type::class !== static::class) { return false; } @@ -83,7 +84,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index 90fb430a291..cbf8141d1db 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -8,6 +8,7 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Storage\FunctionLikeParameter; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -18,6 +19,7 @@ */ final class TCallable extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; use CallableTrait; public string $value; diff --git a/src/Psalm/Type/Atomic/TCallableObject.php b/src/Psalm/Type/Atomic/TCallableObject.php index 053dbfb3b0a..58452bd5ce9 100644 --- a/src/Psalm/Type/Atomic/TCallableObject.php +++ b/src/Psalm/Type/Atomic/TCallableObject.php @@ -13,12 +13,9 @@ final class TCallableObject extends TObject { use HasIntersectionTrait; - public ?TCallable $callable; - - public function __construct(bool $from_docblock = false, ?TCallable $callable = null) + public function __construct(bool $from_docblock = false, public ?TCallable $callable = null) { parent::__construct($from_docblock); - $this->callable = $callable; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 217877448b7..6acc150a2ec 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; @@ -14,14 +15,12 @@ */ final class TClassConstant extends Atomic { - public string $fq_classlike_name; - - public string $const_name; - - public function __construct(string $fq_classlike_name, string $const_name, bool $from_docblock = false) - { - $this->fq_classlike_name = $fq_classlike_name; - $this->const_name = $const_name; + use UnserializeMemoryUsageSuppressionTrait; + public function __construct( + public string $fq_classlike_name, + public string $const_name, + bool $from_docblock = false, + ) { parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index f994601666a..5a384a3755a 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -15,8 +15,8 @@ use function count; use function preg_quote; use function preg_replace; +use function str_contains; use function stripos; -use function strpos; use function strtolower; /** @@ -27,29 +27,14 @@ */ class TClassString extends TString { - public string $as; - - public ?TNamedObject $as_type; - - public bool $is_loaded = false; - - public bool $is_interface = false; - - public bool $is_enum = false; - public function __construct( - string $as = 'object', - ?TNamedObject $as_type = null, - bool $is_loaded = false, - bool $is_interface = false, - bool $is_enum = false, + public string $as = 'object', + public ?TNamedObject $as_type = null, + public bool $is_loaded = false, + public bool $is_interface = false, + public bool $is_enum = false, bool $from_docblock = false, ) { - $this->as = $as; - $this->as_type = $as_type; - $this->is_loaded = $is_loaded; - $this->is_interface = $is_interface; - $this->is_enum = $is_enum; parent::__construct($from_docblock); } /** @@ -129,7 +114,7 @@ public function toNamespacedString( ) . '>'; } - if (!$namespace && strpos($this->as, '\\') === false) { + if (!$namespace && !str_contains($this->as, '\\')) { return 'class-string<' . $this->as . '>'; } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index 028252540de..e64f0c60313 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -9,12 +9,11 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; -use function get_class; - /** * Represents an array where the type of each value * is a function of its string key value @@ -23,24 +22,16 @@ */ final class TClassStringMap extends Atomic { - public string $param_name; - - public ?TNamedObject $as_type; - - public Union $value_param; - + use UnserializeMemoryUsageSuppressionTrait; /** * Constructs a new instance of a list */ public function __construct( - string $param_name, - ?TNamedObject $as_type, - Union $value_param, + public string $param_name, + public ?TNamedObject $as_type, + public Union $value_param, bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->as_type = $as_type; - $this->value_param = $value_param; parent::__construct($from_docblock); } @@ -203,11 +194,11 @@ protected function getChildNodeKeys(): array public function equals(Atomic $other_type, bool $ensure_source_equality): bool { - if (get_class($other_type) !== static::class) { + if ($other_type::class !== self::class) { return false; } - if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality)) { + if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TClosedResource.php b/src/Psalm/Type/Atomic/TClosedResource.php index 3bde7d5508e..ffdf3697f27 100644 --- a/src/Psalm/Type/Atomic/TClosedResource.php +++ b/src/Psalm/Type/Atomic/TClosedResource.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TClosedResource extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'closed-resource'; diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index d2548508f8d..3f701c776c6 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -20,9 +20,6 @@ final class TClosure extends TNamedObject { use CallableTrait; - /** @var array */ - public array $byref_uses = []; - /** * @param list $params * @param array $byref_uses @@ -33,14 +30,13 @@ public function __construct( ?array $params = null, ?Union $return_type = null, ?bool $is_pure = null, - array $byref_uses = [], + public array $byref_uses = [], array $extra_types = [], bool $from_docblock = false, ) { $this->params = $params; $this->return_type = $return_type; $this->is_pure = $is_pure; - $this->byref_uses = $byref_uses; parent::__construct( $value, false, diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index f15ebdf350e..6509a574724 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,33 +18,16 @@ */ final class TConditional extends Atomic { - public string $param_name; - - public string $defining_class; - - public Union $as_type; - - public Union $conditional_type; - - public Union $if_type; - - public Union $else_type; - + use UnserializeMemoryUsageSuppressionTrait; public function __construct( - string $param_name, - string $defining_class, - Union $as_type, - Union $conditional_type, - Union $if_type, - Union $else_type, + public string $param_name, + public string $defining_class, + public Union $as_type, + public Union $conditional_type, + public Union $if_type, + public Union $else_type, bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->defining_class = $defining_class; - $this->as_type = $as_type; - $this->conditional_type = $conditional_type; - $this->if_type = $if_type; - $this->else_type = $else_type; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TDependentGetClass.php b/src/Psalm/Type/Atomic/TDependentGetClass.php index ebc492233b0..3e6ab07329f 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -13,20 +13,11 @@ */ final class TDependentGetClass extends TString implements DependentType { - /** - * Used to hold information as to what this refers to - */ - public string $typeof; - - public Union $as_type; - /** * @param string $typeof the variable id */ - public function __construct(string $typeof, Union $as_type) + public function __construct(public string $typeof, public Union $as_type) { - $this->typeof = $typeof; - $this->as_type = $as_type; parent::__construct(false); } diff --git a/src/Psalm/Type/Atomic/TDependentGetDebugType.php b/src/Psalm/Type/Atomic/TDependentGetDebugType.php index 5875f911fdc..cddb12cdc6d 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -11,17 +11,11 @@ */ final class TDependentGetDebugType extends TString implements DependentType { - /** - * Used to hold information as to what this refers to - */ - public string $typeof; - /** * @param string $typeof the variable id */ - public function __construct(string $typeof) + public function __construct(public string $typeof) { - $this->typeof = $typeof; parent::__construct(false); } diff --git a/src/Psalm/Type/Atomic/TDependentGetType.php b/src/Psalm/Type/Atomic/TDependentGetType.php index 7f1bfee3877..a0d64d950f0 100644 --- a/src/Psalm/Type/Atomic/TDependentGetType.php +++ b/src/Psalm/Type/Atomic/TDependentGetType.php @@ -11,17 +11,11 @@ */ final class TDependentGetType extends TString { - /** - * Used to hold information as to what this refers to - */ - public string $typeof; - /** * @param string $typeof the variable id */ - public function __construct(string $typeof) + public function __construct(public string $typeof) { - $this->typeof = $typeof; parent::__construct(false); } diff --git a/src/Psalm/Type/Atomic/TEnumCase.php b/src/Psalm/Type/Atomic/TEnumCase.php index 2fcbb1faa69..99b5a8b99b3 100644 --- a/src/Psalm/Type/Atomic/TEnumCase.php +++ b/src/Psalm/Type/Atomic/TEnumCase.php @@ -11,13 +11,9 @@ */ final class TEnumCase extends TNamedObject { - public string $case_name; - - public function __construct(string $fq_enum_name, string $case_name) + public function __construct(string $fq_enum_name, public string $case_name) { parent::__construct($fq_enum_name); - - $this->case_name = $case_name; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index d5f577e8af5..7f6e32ea77a 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -32,9 +32,6 @@ final class TGenericObject extends TNamedObject */ public array $type_params; - /** @var bool if the parameters have been remapped to another class */ - public bool $remapped_params = false; - /** * @param string $value the name of the object * @param non-empty-list $type_params @@ -43,7 +40,8 @@ final class TGenericObject extends TNamedObject public function __construct( string $value, array $type_params, - bool $remapped_params = false, + /** @var bool if the parameters have been remapped to another class */ + public bool $remapped_params = false, bool $is_static = false, array $extra_types = [], bool $from_docblock = false, @@ -53,7 +51,6 @@ public function __construct( } $this->type_params = $type_params; - $this->remapped_params = $remapped_params; parent::__construct( $value, $is_static, @@ -113,7 +110,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TIntMask.php b/src/Psalm/Type/Atomic/TIntMask.php index f19e30ee91d..6e753372c1f 100644 --- a/src/Psalm/Type/Atomic/TIntMask.php +++ b/src/Psalm/Type/Atomic/TIntMask.php @@ -14,13 +14,9 @@ */ final class TIntMask extends TInt { - /** @var non-empty-array */ - public array $values; - /** @param non-empty-array $values */ - public function __construct(array $values, bool $from_docblock = false) + public function __construct(public array $values, bool $from_docblock = false) { - $this->values = $values; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index ace9727db82..398491c922b 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -4,8 +4,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents the type that is the result of a bitmask combination of its parameters. * This is the same concept as TIntMask but TIntMaskOf is used with a reference to constants in code @@ -15,14 +13,8 @@ */ final class TIntMaskOf extends TInt { - public TClassConstant|TKeyOf|TValueOf $value; - - /** - * @param TClassConstant|TKeyOf|TValueOf $value - */ - public function __construct(Atomic $value, bool $from_docblock = false) + public function __construct(public TClassConstant|TKeyOf|TValueOf $value, bool $from_docblock = false) { - $this->value = $value; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TIntRange.php b/src/Psalm/Type/Atomic/TIntRange.php index b3a55d714fd..e7006a1c4f0 100644 --- a/src/Psalm/Type/Atomic/TIntRange.php +++ b/src/Psalm/Type/Atomic/TIntRange.php @@ -17,20 +17,12 @@ final class TIntRange extends TInt public const BOUND_MIN = 'min'; public const BOUND_MAX = 'max'; - public ?int $min_bound = null; - public ?int $max_bound = null; - - public ?string $dependent_list_key = null; - public function __construct( - ?int $min_bound, - ?int $max_bound, + public ?int $min_bound, + public ?int $max_bound, bool $from_docblock = false, - ?string $dependent_list_key = null, + public ?string $dependent_list_key = null, ) { - $this->min_bound = $min_bound; - $this->max_bound = $max_bound; - $this->dependent_list_key = $dependent_list_key; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index f8153751a04..52878f27caa 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -22,6 +23,7 @@ */ final class TIterable extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; use HasIntersectionTrait; /** * @use GenericTrait @@ -111,7 +113,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TKeyOf.php b/src/Psalm/Type/Atomic/TKeyOf.php index 22d9e53d588..f8a259a7c76 100644 --- a/src/Psalm/Type/Atomic/TKeyOf.php +++ b/src/Psalm/Type/Atomic/TKeyOf.php @@ -16,11 +16,8 @@ */ final class TKeyOf extends TArrayKey { - public Union $type; - - public function __construct(Union $type, bool $from_docblock = false) + public function __construct(public Union $type, bool $from_docblock = false) { - $this->type = $type; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 80f9f968319..189784ec3f0 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -10,6 +10,7 @@ use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeCombiner; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,7 +18,6 @@ use function addslashes; use function assert; use function count; -use function get_class; use function implode; use function is_int; use function is_string; @@ -33,16 +33,7 @@ */ class TKeyedArray extends Atomic { - /** - * @var non-empty-array - */ - public array $properties; - - /** - * @var array|null - */ - public ?array $class_strings = null; - + use UnserializeMemoryUsageSuppressionTrait; /** * If the shape has fallback params then they are here * @@ -68,8 +59,8 @@ class TKeyedArray extends Atomic * @param array $class_strings */ public function __construct( - array $properties, - ?array $class_strings = null, + public array $properties, + public ?array $class_strings = null, ?array $fallback_params = null, bool $is_list = false, bool $from_docblock = false, @@ -77,8 +68,6 @@ public function __construct( if ($is_list && $fallback_params) { $fallback_params[0] = Type::getListKey(); } - $this->properties = $properties; - $this->class_strings = $class_strings; $this->fallback_params = $fallback_params; $this->is_list = $is_list; if ($this->is_list) { @@ -113,6 +102,8 @@ public function setProperties(array $properties): self if ($cloned->is_list) { $last_k = -1; $had_possibly_undefined = false; + + /** @psalm-suppress InaccessibleProperty */ ksort($cloned->properties); foreach ($cloned->properties as $k => $v) { if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) { @@ -416,7 +407,7 @@ public function getGenericArrayType(?string $list_var_id = null): TArray $value_type = $value_type->setPossiblyUndefined(false); - if ($has_defined_keys || $this->fallback_params !== null) { + if ($has_defined_keys) { return new TNonEmptyArray([$key_type, $value_type]); } return new TArray([$key_type, $value_type]); @@ -646,7 +637,7 @@ protected function getChildNodeKeys(): array public function equals(Atomic $other_type, bool $ensure_source_equality): bool { - if (get_class($other_type) !== static::class) { + if ($other_type::class !== static::class) { return false; } @@ -659,11 +650,11 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } if ($this->fallback_params !== null && $other_type->fallback_params !== null) { - if (!$this->fallback_params[0]->equals($other_type->fallback_params[0])) { + if (!$this->fallback_params[0]->equals($other_type->fallback_params[0], false, false)) { return false; } - if (!$this->fallback_params[1]->equals($other_type->fallback_params[1])) { + if (!$this->fallback_params[1]->equals($other_type->fallback_params[1], false, false)) { return false; } } @@ -673,7 +664,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index a3e14fa2d12..85bd43f2189 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -6,8 +6,8 @@ use function preg_quote; use function preg_replace; +use function str_contains; use function stripos; -use function strpos; use function strtolower; /** @@ -17,15 +17,14 @@ */ final class TLiteralClassString extends TLiteralString { - /** - * Whether or not this type can represent a child of the class named in $value - */ - public bool $definite_class = false; - - public function __construct(string $value, bool $definite_class = false, bool $from_docblock = false) - { + public function __construct( + string $value, /** + * Whether or not this type can represent a child of the class named in $value + */ + public bool $definite_class = false, + bool $from_docblock = false, + ) { parent::__construct($value, $from_docblock); - $this->definite_class = $definite_class; } public function getKey(bool $include_extra = true): string @@ -93,7 +92,7 @@ public function toNamespacedString( ) . '::class'; } - if (!$namespace && strpos($this->value, '\\') === false) { + if (!$namespace && !str_contains($this->value, '\\')) { return $this->value . '::class'; } diff --git a/src/Psalm/Type/Atomic/TLiteralFloat.php b/src/Psalm/Type/Atomic/TLiteralFloat.php index 3fe2a407269..1565183108b 100644 --- a/src/Psalm/Type/Atomic/TLiteralFloat.php +++ b/src/Psalm/Type/Atomic/TLiteralFloat.php @@ -11,11 +11,8 @@ */ final class TLiteralFloat extends TFloat { - public float $value; - - public function __construct(float $value, bool $from_docblock = false) + public function __construct(public float $value, bool $from_docblock = false) { - $this->value = $value; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TLiteralInt.php b/src/Psalm/Type/Atomic/TLiteralInt.php index 72706da25bc..ae28af3df30 100644 --- a/src/Psalm/Type/Atomic/TLiteralInt.php +++ b/src/Psalm/Type/Atomic/TLiteralInt.php @@ -11,11 +11,8 @@ */ final class TLiteralInt extends TInt { - public int $value; - - public function __construct(int $value, bool $from_docblock = false) + public function __construct(public int $value, bool $from_docblock = false) { - $this->value = $value; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index b7f24fcb6c5..a1019583718 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,11 +14,9 @@ */ class TMixed extends Atomic { - public bool $from_loop_isset = false; - - public function __construct(bool $from_loop_isset = false, bool $from_docblock = false) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public bool $from_loop_isset = false, bool $from_docblock = false) { - $this->from_loop_isset = $from_loop_isset; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 7a0a2e602fe..7d7e9a68c0f 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -26,23 +26,19 @@ class TNamedObject extends Atomic public string $value; - public bool $is_static = false; - public bool $is_static_resolved = false; - /** - * Whether or not this type can represent a child of the class named in $value - */ - public bool $definite_class = false; - /** * @param string $value the name of the object * @param array $extra_types */ public function __construct( string $value, - bool $is_static = false, - bool $definite_class = false, + public bool $is_static = false, + /** + * Whether or not this type can represent a child of the class named in $value + */ + public bool $definite_class = false, array $extra_types = [], bool $from_docblock = false, ) { @@ -51,8 +47,6 @@ public function __construct( } $this->value = $value; - $this->is_static = $is_static; - $this->definite_class = $definite_class; $this->extra_types = $extra_types; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TNever.php b/src/Psalm/Type/Atomic/TNever.php index a9918aeb8e2..6121a752b05 100644 --- a/src/Psalm/Type/Atomic/TNever.php +++ b/src/Psalm/Type/Atomic/TNever.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -14,6 +15,7 @@ */ final class TNever extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'never'; diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index b71dd5d5f3b..51767f30db1 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -14,18 +14,6 @@ */ final class TNonEmptyArray extends TArray { - /** - * @var positive-int|null - */ - public ?int $count = null; - - /** - * @var positive-int|null - */ - public ?int $min_count = null; - - public string $value = 'non-empty-array'; - /** * @param array{Union, Union} $type_params * @param positive-int|null $count @@ -33,14 +21,11 @@ final class TNonEmptyArray extends TArray */ public function __construct( array $type_params, - ?int $count = null, - ?int $min_count = null, - string $value = 'non-empty-array', + public ?int $count = null, + public ?int $min_count = null, + public string $value = 'non-empty-array', bool $from_docblock = false, ) { - $this->count = $count; - $this->min_count = $min_count; - $this->value = $value; parent::__construct($type_params, $from_docblock); } diff --git a/src/Psalm/Type/Atomic/TNull.php b/src/Psalm/Type/Atomic/TNull.php index 4f8df9abe72..e8ee44fef9a 100644 --- a/src/Psalm/Type/Atomic/TNull.php +++ b/src/Psalm/Type/Atomic/TNull.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TNull extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'null'; diff --git a/src/Psalm/Type/Atomic/TObject.php b/src/Psalm/Type/Atomic/TObject.php index 14b5036a75e..e6c4556aa85 100644 --- a/src/Psalm/Type/Atomic/TObject.php +++ b/src/Psalm/Type/Atomic/TObject.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ class TObject extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'object'; diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 1f54c63c8a8..93c60e99dd1 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -26,16 +26,6 @@ final class TObjectWithProperties extends TObject { use HasIntersectionTrait; - /** - * @var array - */ - public array $properties; - - /** - * @var array - */ - public array $methods; - public bool $is_stringable_object_only = false; /** @@ -46,13 +36,11 @@ final class TObjectWithProperties extends TObject * @param array $extra_types */ public function __construct( - array $properties, - array $methods = [], + public array $properties, + public array $methods = [], array $extra_types = [], bool $from_docblock = false, ) { - $this->properties = $properties; - $this->methods = $methods; $this->extra_types = $extra_types; $this->is_stringable_object_only = @@ -208,7 +196,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index c5c4a92b6fe..c9df9d0bd32 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -15,6 +16,7 @@ */ final class TPropertiesOf extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are // used to compared against properties visibililty. @@ -22,12 +24,6 @@ final class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - public TNamedObject $classlike_type; - /** - * @var self::VISIBILITY_*|null - */ - public ?int $visibility_filter; - /** * @return list */ @@ -45,12 +41,10 @@ public static function tokenNames(): array * @param self::VISIBILITY_*|null $visibility_filter */ public function __construct( - TNamedObject $classlike_type, - ?int $visibility_filter, + public TNamedObject $classlike_type, + public ?int $visibility_filter, bool $from_docblock = false, ) { - $this->classlike_type = $classlike_type; - $this->visibility_filter = $visibility_filter; parent::__construct($from_docblock); } @@ -59,16 +53,12 @@ public function __construct( */ public static function filterForTokenName(string $token_name): ?int { - switch ($token_name) { - case 'public-properties-of': - return self::VISIBILITY_PUBLIC; - case 'protected-properties-of': - return self::VISIBILITY_PROTECTED; - case 'private-properties-of': - return self::VISIBILITY_PRIVATE; - default: - return null; - } + return match ($token_name) { + 'public-properties-of' => self::VISIBILITY_PUBLIC, + 'protected-properties-of' => self::VISIBILITY_PROTECTED, + 'private-properties-of' => self::VISIBILITY_PRIVATE, + default => null, + }; } /** @@ -77,16 +67,12 @@ public static function filterForTokenName(string $token_name): ?int */ public static function tokenNameForFilter(?int $visibility_filter): string { - switch ($visibility_filter) { - case self::VISIBILITY_PUBLIC: - return 'public-properties-of'; - case self::VISIBILITY_PROTECTED: - return 'protected-properties-of'; - case self::VISIBILITY_PRIVATE: - return 'private-properties-of'; - default: - return 'properties-of'; - } + return match ($visibility_filter) { + self::VISIBILITY_PUBLIC => 'public-properties-of', + self::VISIBILITY_PROTECTED => 'protected-properties-of', + self::VISIBILITY_PRIVATE => 'private-properties-of', + default => 'properties-of', + }; } protected function getChildNodeKeys(): array diff --git a/src/Psalm/Type/Atomic/TResource.php b/src/Psalm/Type/Atomic/TResource.php index b01a0987bde..f67527df27a 100644 --- a/src/Psalm/Type/Atomic/TResource.php +++ b/src/Psalm/Type/Atomic/TResource.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TResource extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'resource'; diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index bba86e66e29..102b8c0e5af 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -11,21 +12,13 @@ */ final class TTemplateIndexedAccess extends Atomic { - public string $array_param_name; - - public string $offset_param_name; - - public string $defining_class; - + use UnserializeMemoryUsageSuppressionTrait; public function __construct( - string $array_param_name, - string $offset_param_name, - string $defining_class, + public string $array_param_name, + public string $offset_param_name, + public string $defining_class, bool $from_docblock = false, ) { - $this->array_param_name = $array_param_name; - $this->offset_param_name = $offset_param_name; - $this->defining_class = $defining_class; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 48c61dff5ec..c6a728c88a2 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,21 +18,13 @@ */ final class TTemplateKeyOf extends Atomic { - public string $param_name; - - public string $defining_class; - - public Union $as; - + use UnserializeMemoryUsageSuppressionTrait; public function __construct( - string $param_name, - string $defining_class, - Union $as, + public string $param_name, + public string $defining_class, + public Union $as, bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->defining_class = $defining_class; - $this->as = $as; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index 8a95ab2bdb8..84169899a52 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -19,27 +20,19 @@ */ final class TTemplateParam extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; use HasIntersectionTrait; - public string $param_name; - - public Union $as; - - public string $defining_class; - /** * @param array $extra_types */ public function __construct( - string $param_name, - Union $extends, - string $defining_class, + public string $param_name, + public Union $as, + public string $defining_class, array $extra_types = [], bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->as = $extends; - $this->defining_class = $defining_class; $this->extra_types = $extra_types; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TTemplateParamClass.php b/src/Psalm/Type/Atomic/TTemplateParamClass.php index 00b370a68b8..8bbbf7f3769 100644 --- a/src/Psalm/Type/Atomic/TTemplateParamClass.php +++ b/src/Psalm/Type/Atomic/TTemplateParamClass.php @@ -11,19 +11,13 @@ */ final class TTemplateParamClass extends TClassString { - public string $param_name; - - public string $defining_class; - public function __construct( - string $param_name, + public string $param_name, string $as, ?TNamedObject $as_type, - string $defining_class, + public string $defining_class, bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->defining_class = $defining_class; parent::__construct( $as, $as_type, diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 31f6ef3b3ec..48cd32f857d 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,28 +18,17 @@ */ final class TTemplatePropertiesOf extends Atomic { - public string $param_name; - public string $defining_class; - public TTemplateParam $as; - /** - * @var TPropertiesOf::VISIBILITY_*|null - */ - public ?int $visibility_filter; - + use UnserializeMemoryUsageSuppressionTrait; /** * @param TPropertiesOf::VISIBILITY_*|null $visibility_filter */ public function __construct( - string $param_name, - string $defining_class, - TTemplateParam $as, - ?int $visibility_filter, + public string $param_name, + public string $defining_class, + public TTemplateParam $as, + public ?int $visibility_filter, bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->defining_class = $defining_class; - $this->as = $as; - $this->visibility_filter = $visibility_filter; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index 98be9d1e85c..23d33af5de0 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -7,6 +7,7 @@ use Psalm\Codebase; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -17,21 +18,13 @@ */ final class TTemplateValueOf extends Atomic { - public string $param_name; - - public string $defining_class; - - public Union $as; - + use UnserializeMemoryUsageSuppressionTrait; public function __construct( - string $param_name, - string $defining_class, - Union $as, + public string $param_name, + public string $defining_class, + public Union $as, bool $from_docblock = false, ) { - $this->param_name = $param_name; - $this->defining_class = $defining_class; - $this->as = $as; parent::__construct($from_docblock); } diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index ebda8e4118b..9a76d80405e 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -11,6 +12,7 @@ */ final class TTypeAlias extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function __construct( public string $declaring_fq_classlike_name, public string $alias_name, diff --git a/src/Psalm/Type/Atomic/TUnknownClassString.php b/src/Psalm/Type/Atomic/TUnknownClassString.php index a8d2811e0cc..c46cf16c2b5 100644 --- a/src/Psalm/Type/Atomic/TUnknownClassString.php +++ b/src/Psalm/Type/Atomic/TUnknownClassString.php @@ -12,10 +12,8 @@ */ final class TUnknownClassString extends TClassString { - public ?TObject $as_unknown_type; - public function __construct( - ?TObject $as_unknown_type, + public ?TObject $as_unknown_type, bool $is_loaded = false, bool $from_docblock = false, ) { @@ -27,6 +25,5 @@ public function __construct( false, $from_docblock, ); - $this->as_unknown_type = $as_unknown_type; } } diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 3a86ec720a7..55a9d8d6ab0 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -6,6 +6,7 @@ use Psalm\Codebase; use Psalm\Storage\EnumCaseStorage; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -20,11 +21,9 @@ */ final class TValueOf extends Atomic { - public Union $type; - - public function __construct(Union $type, bool $from_docblock = false) + use UnserializeMemoryUsageSuppressionTrait; + public function __construct(public Union $type, bool $from_docblock = false) { - $this->type = $type; parent::__construct($from_docblock); } @@ -43,8 +42,8 @@ private static function getValueTypeForNamedObject(array $cases, TNamedObject $a return new Union(array_map( static function (EnumCaseStorage $case): Atomic { - assert($case->value !== null); // Backed enum must have a value - + assert($case->value !== null); + // Backed enum must have a value return $case->value; }, array_values($cases), diff --git a/src/Psalm/Type/Atomic/TVoid.php b/src/Psalm/Type/Atomic/TVoid.php index cac95d62293..d1cd2faf60c 100644 --- a/src/Psalm/Type/Atomic/TVoid.php +++ b/src/Psalm/Type/Atomic/TVoid.php @@ -4,6 +4,7 @@ namespace Psalm\Type\Atomic; +use Psalm\Storage\UnserializeMemoryUsageSuppressionTrait; use Psalm\Type\Atomic; /** @@ -13,6 +14,7 @@ */ final class TVoid extends Atomic { + use UnserializeMemoryUsageSuppressionTrait; public function getKey(bool $include_extra = true): string { return 'void'; diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php index 70b6377ee69..b5f42645fe7 100644 --- a/src/Psalm/Type/MutableUnion.php +++ b/src/Psalm/Type/MutableUnion.php @@ -26,7 +26,6 @@ use Psalm\Type\Atomic\TTrue; use function count; -use function get_class; use function get_object_vars; use function strpos; @@ -409,7 +408,7 @@ public function substitute(Union|MutableUnion $old_type, Union|MutableUnion|null foreach ($new_type->types as $key => $new_type_part) { if (!isset($this->types[$key]) || ($new_type_part instanceof Scalar - && get_class($new_type_part) === get_class($this->types[$key])) + && $new_type_part::class === $this->types[$key]::class) ) { $this->types[$key] = $new_type_part; } else { diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 6cc30cdc79f..1e1bd54c8b9 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -22,6 +22,7 @@ use Psalm\Issue\TypeDoesNotContainType; use Psalm\IssueBuffer; use Psalm\Storage\Assertion; +use Psalm\Storage\Assertion\ArrayKeyDoesNotExist; use Psalm\Storage\Assertion\ArrayKeyExists; use Psalm\Storage\Assertion\Empty_; use Psalm\Storage\Assertion\Falsy; @@ -46,6 +47,8 @@ use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; +use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; @@ -69,6 +72,8 @@ use function ksort; use function preg_match; use function preg_quote; +use function str_contains; +use function str_ends_with; use function str_replace; use function str_split; use function strlen; @@ -169,6 +174,7 @@ public static function reconcileKeyedTypes( $has_negation = false; $has_isset = false; $has_inverted_isset = false; + $has_inverted_key_exists = false; $has_truthy_or_falsy_or_empty = false; $has_empty = false; $has_count_check = false; @@ -198,7 +204,11 @@ public static function reconcileKeyedTypes( $is_equality = $is_equality && $new_type_part_part instanceof IsIdentical; - $has_inverted_isset = $has_inverted_isset || $new_type_part_part instanceof IsNotIsset; + $has_inverted_isset = $has_inverted_isset + || $new_type_part_part instanceof IsNotIsset; + + $has_inverted_key_exists = $has_inverted_key_exists + || $new_type_part_part instanceof ArrayKeyDoesNotExist; $has_count_check = $has_count_check || $new_type_part_part instanceof NonEmptyCountable; @@ -217,6 +227,7 @@ public static function reconcileKeyedTypes( $code_location, $has_isset, $has_inverted_isset, + $has_inverted_key_exists, $has_empty, $inside_loop, $has_object_array_access, @@ -330,7 +341,12 @@ public static function reconcileKeyedTypes( if ($type_changed || $failed_reconciliation) { $changed_var_ids[$key] = true; - if (substr($key, -1) === ']' && !$has_inverted_isset && !$has_empty && !$is_equality) { + if (str_ends_with($key, ']') + && !$has_inverted_isset + && !$has_inverted_key_exists + && !$has_empty + && !$is_equality + ) { self::adjustTKeyedArrayType( $key_parts, $existing_types, @@ -459,7 +475,7 @@ private static function addNestedAssertions(array $new_types, array $existing_ty $new_base_key = $base_key . '[' . $array_key . ']'; - if (strpos($array_key, '\'') !== false) { + if (str_contains($array_key, '\'')) { $new_types[$base_key][] = [new HasStringArrayAccess()]; } else { $new_types[$base_key][] = [new HasIntOrStringArrayAccess()]; @@ -644,6 +660,7 @@ private static function getValueForKey( ?CodeLocation $code_location, bool $has_isset, bool $has_inverted_isset, + bool $has_inverted_key_exists, bool $has_empty, bool $inside_loop, bool &$has_object_array_access, @@ -717,11 +734,17 @@ private static function getValueForKey( $new_base_type_candidate = $existing_key_type_part->type_params[1]; - if ($new_base_type_candidate->isMixed() && !$has_isset && !$has_inverted_isset) { + if ($new_base_type_candidate->isMixed() + && !$has_isset + && !$has_inverted_isset + && !$has_inverted_key_exists + ) { return $new_base_type_candidate; } - if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { + if (($has_isset || $has_inverted_isset || $has_inverted_key_exists) + && isset($new_assertions[$new_base_key]) + ) { if ($has_inverted_isset && $new_base_key === $key) { $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); @@ -750,7 +773,7 @@ private static function getValueForKey( } elseif ($existing_key_type_part instanceof TString) { $new_base_type_candidate = Type::getString(); } elseif ($existing_key_type_part instanceof TNamedObject - && ($has_isset || $has_inverted_isset) + && ($has_isset || $has_inverted_isset || $has_inverted_key_exists) ) { $has_object_array_access = true; @@ -822,7 +845,7 @@ private static function getValueForKey( if (!$codebase->classOrInterfaceExists($existing_key_type_part->value)) { $class_property_type = Type::getMixed(); } else { - if (substr($property_name, -2) === '()') { + if (str_ends_with($property_name, '()')) { $method_id = new MethodIdentifier( $existing_key_type_part->value, strtolower(substr($property_name, 0, -2)), @@ -920,11 +943,7 @@ private static function getPropertyType( $fq_class_name, ); - if (isset($declaring_class_storage->pseudo_property_get_types['$' . $property_name])) { - return $declaring_class_storage->pseudo_property_get_types['$' . $property_name]; - } - - return null; + return $declaring_class_storage->pseudo_property_get_types['$' . $property_name] ?? null; } $declaring_property_class = $codebase->properties->getDeclaringClassForProperty( @@ -1103,85 +1122,105 @@ private static function adjustTKeyedArrayType( throw new UnexpectedValueException('Not expecting null array key'); } + $array_key_offsets = []; if ($array_key[0] === '$') { - return; + if (!isset($existing_types[$array_key])) { + return; + } + $t = $existing_types[$array_key]; + foreach ($t->getAtomicTypes() as $lit) { + if ($lit instanceof TLiteralInt || $lit instanceof TLiteralString) { + $array_key_offsets []= $lit->value; + continue; + } + return; + } + } else { + $array_key_offsets []= $array_key[0] === '\'' || $array_key[0] === '"' + ? substr($array_key, 1, -1) + : $array_key + ; } - $array_key_offset = $array_key[0] === '\'' || $array_key[0] === '"' ? substr($array_key, 1, -1) : $array_key; - $base_key = implode($key_parts); - if (isset($existing_types[$base_key]) && $array_key_offset !== '') { - foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { - if ($base_atomic_type instanceof TKeyedArray + $result_type = $result_type->setPossiblyUndefined( + $result_type->possibly_undefined || count($array_key_offsets) > 1, + ); + + foreach ($array_key_offsets as $array_key_offset) { + if (isset($existing_types[$base_key])) { + foreach ($existing_types[$base_key]->getAtomicTypes() as $base_atomic_type) { + if ($base_atomic_type instanceof TKeyedArray || ($base_atomic_type instanceof TArray && !$base_atomic_type->isEmptyArray()) || $base_atomic_type instanceof TClassStringMap - ) { - $new_base_type = $existing_types[$base_key]; + ) { + $new_base_type = $existing_types[$base_key]; - if ($base_atomic_type instanceof TArray) { - $fallback_key_type = $base_atomic_type->type_params[0]; - $fallback_value_type = $base_atomic_type->type_params[1]; + if ($base_atomic_type instanceof TArray) { + $fallback_key_type = $base_atomic_type->type_params[0]; + $fallback_value_type = $base_atomic_type->type_params[1]; - $base_atomic_type = new TKeyedArray( - [ + $base_atomic_type = new TKeyedArray( + [ $array_key_offset => $result_type, - ], - null, - $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], - ); - } elseif ($base_atomic_type instanceof TClassStringMap) { - // do nothing - } else { - $properties = $base_atomic_type->properties; - $properties[$array_key_offset] = $result_type; - if ($base_atomic_type->is_list + ], + null, + $fallback_key_type->isNever() ? null : [$fallback_key_type, $fallback_value_type], + ); + } elseif ($base_atomic_type instanceof TClassStringMap) { + // do nothing + } else { + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = $result_type; + if ($base_atomic_type->is_list && (!is_numeric($array_key_offset) || ($array_key_offset && !isset($properties[$array_key_offset-1]) ) ) - ) { - if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { - $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( - $result_type->isNever(), - ); - for ($x = 0; $x < $array_key_offset; $x++) { - $properties[$x] ??= $fallback; + ) { + if ($base_atomic_type->fallback_params && is_numeric($array_key_offset)) { + $fallback = $base_atomic_type->fallback_params[1]->setPossiblyUndefined( + $result_type->isNever(), + ); + for ($x = 0; $x < $array_key_offset; $x++) { + $properties[$x] ??= $fallback; + } + ksort($properties); + $base_atomic_type = $base_atomic_type->setProperties($properties); + } else { + // This should actually be a paradox + $base_atomic_type = new TKeyedArray( + $properties, + null, + $base_atomic_type->fallback_params, + false, + $base_atomic_type->from_docblock, + ); } - ksort($properties); - $base_atomic_type = $base_atomic_type->setProperties($properties); } else { - // This should actually be a paradox - $base_atomic_type = new TKeyedArray( - $properties, - null, - $base_atomic_type->fallback_params, - false, - $base_atomic_type->from_docblock, - ); + $base_atomic_type = $base_atomic_type->setProperties($properties); } - } else { - $base_atomic_type = $base_atomic_type->setProperties($properties); } - } - $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); - $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; + $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; - if ($key_parts[count($key_parts) - 1] === ']') { - self::adjustTKeyedArrayType( - $key_parts, - $existing_types, - $changed_var_ids, - $new_base_type, - ); - } + if ($key_parts[count($key_parts) - 1] === ']') { + self::adjustTKeyedArrayType( + $key_parts, + $existing_types, + $changed_var_ids, + $new_base_type, + ); + } - $existing_types[$base_key] = $new_base_type; - break; + $existing_types[$base_key] = $new_base_type; + break; + } } } } diff --git a/src/Psalm/Type/TaintKind.php b/src/Psalm/Type/TaintKind.php index ecd355d3532..5b2ac02a8ae 100644 --- a/src/Psalm/Type/TaintKind.php +++ b/src/Psalm/Type/TaintKind.php @@ -24,6 +24,7 @@ final class TaintKind public const INPUT_HEADER = 'header'; public const INPUT_XPATH = 'xpath'; public const INPUT_SLEEP = 'sleep'; + public const INPUT_EXTRACT = 'extract'; public const USER_SECRET = 'user_secret'; public const SYSTEM_SECRET = 'system_secret'; } diff --git a/src/Psalm/Type/TaintKindGroup.php b/src/Psalm/Type/TaintKindGroup.php index 19c8bb63030..cbe04e8ee21 100644 --- a/src/Psalm/Type/TaintKindGroup.php +++ b/src/Psalm/Type/TaintKindGroup.php @@ -25,5 +25,6 @@ final class TaintKindGroup TaintKind::INPUT_COOKIE, TaintKind::INPUT_XPATH, TaintKind::INPUT_SLEEP, + TaintKind::INPUT_EXTRACT, ]; } diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 1d0b7fd0748..f108ed717eb 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -176,7 +176,7 @@ final class Union implements TypeNode /** * This is a cache of getId on exact mode */ - private ?string $exact_id; + private ?string $exact_id = null; /** @@ -188,6 +188,52 @@ final class Union implements TypeNode public bool $different = false; + private const PROPERTY_KEYS_FOR_UNSERIALIZE = [ + "\0" . self::class . "\0" . 'types' => 'types', + 'from_docblock' => 'from_docblock', + 'from_calculation' => 'from_calculation', + 'from_property' => 'from_property', + 'from_static_property' => 'from_static_property', + 'initialized' => 'initialized', + 'initialized_class' => 'initialized_class', + 'checked' => 'checked', + 'failed_reconciliation' => 'failed_reconciliation', + 'ignore_nullable_issues' => 'ignore_nullable_issues', + 'ignore_falsable_issues' => 'ignore_falsable_issues', + 'ignore_isset' => 'ignore_isset', + 'possibly_undefined' => 'possibly_undefined', + 'possibly_undefined_from_try' => 'possibly_undefined_from_try', + 'explicit_never' => 'explicit_never', + 'had_template' => 'had_template', + 'from_template_default' => 'from_template_default', + "\0" . self::class . "\0" . 'literal_string_types' => 'literal_string_types', + "\0" . self::class . "\0" . 'typed_class_strings' => 'typed_class_strings', + "\0" . self::class . "\0" . 'literal_int_types' => 'literal_int_types', + "\0" . self::class . "\0" . 'literal_float_types' => 'literal_float_types', + 'by_ref' => 'by_ref', + 'reference_free' => 'reference_free', + 'allow_mutations' => 'allow_mutations', + 'has_mutations' => 'has_mutations', + "\0" . self::class . "\0" . 'id' => 'id', + "\0" . self::class . "\0" . 'exact_id' => 'exact_id', + 'parent_nodes' => 'parent_nodes', + 'propagate_parent_nodes' => 'propagate_parent_nodes', + 'different' => 'different', + ]; + + /** + * 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]; + } + } + /** * @param TProperties $properties * @return static diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index 114b09cc35c..9506aaa5049 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -46,11 +46,11 @@ use function array_filter; use function array_unique; use function count; -use function get_class; use function implode; use function ksort; use function reset; use function sort; +use function str_contains; use function strpos; use const ARRAY_FILTER_USE_BOTH; @@ -219,7 +219,7 @@ public function getId(bool $exact = true): string if (count($types) > 1) { foreach ($types as $i => $type) { - if (strpos($type, ' as ') && strpos($type, '(') === false) { + if (strpos($type, ' as ') && !str_contains($type, '(')) { $types[$i] = '(' . $type . ')'; } } @@ -263,9 +263,9 @@ public function toNamespacedString( } elseif ($type instanceof TLiteralString) { $literal_strings[] = $type_string; } else { - if (get_class($type) === TString::class) { + if ($type::class === TString::class) { $has_non_literal_string = true; - } elseif (get_class($type) === TInt::class) { + } elseif ($type::class === TInt::class) { $has_non_literal_int = true; } $other_types[] = $type_string; @@ -827,7 +827,7 @@ public function isEmptyMixed(): bool public function isVanillaMixed(): bool { return isset($this->types['mixed']) - && get_class($this->types['mixed']) === TMixed::class + && $this->types['mixed']::class === TMixed::class && !$this->types['mixed']->from_loop_isset && count($this->types) === 1; } diff --git a/stubs/CoreGenericAttributes.phpstub b/stubs/CoreGenericAttributes.phpstub index 7aa6400df06..92abe9542f8 100644 --- a/stubs/CoreGenericAttributes.phpstub +++ b/stubs/CoreGenericAttributes.phpstub @@ -6,6 +6,12 @@ final class AllowDynamicProperties public function __construct() {} } +#[Attribute(Attribute::TARGET_METHOD)] +final class Override +{ + public function __construct() {} +} + #[Attribute(Attribute::TARGET_PARAMETER)] final class SensitiveParameter { diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 2b7b76da7e4..3b43831e6ff 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -158,46 +158,46 @@ class ArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Count * Returns whether the requested index exists * @link http://php.net/manual/en/arrayobject.offsetexists.php * - * @param TKey $index The index being checked. + * @param TKey $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.0.0 */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** * Returns the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetget.php * - * @param TKey $index The index with the value. + * @param TKey $offset The index with the value. * @return TValue The value at the specified index or false. * * @since 5.0.0 */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** * Sets the value at the specified index to newval * @link http://php.net/manual/en/arrayobject.offsetset.php * - * @param TKey $index The index being set. - * @param TValue $newval The new value for the index. + * @param TKey $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.0.0 */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** * Unsets the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetunset.php * - * @param TKey $index The index being unset. + * @param TKey $offset The index being unset. * @return void * * @since 5.0.0 */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * Appends the value @@ -492,6 +492,16 @@ final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traver * @return void */ public function offsetUnset($offset) {} + + /** + * Create a new iterator from an ArrayObject instance + * @link http://php.net/manual/en/arrayobject.getiterator.php + * + * @return \Traversable An iterator from an ArrayObject. + * + * @since 5.0.0 + */ + public function getIterator() { } } class mysqli diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 735953ed454..a5f996beca3 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1825,3 +1825,8 @@ function time_sleep_until(float $timestamp): bool {} * @psalm-ignore-falsable-return */ function get_browser(?string $user_agent = null, bool $return_array = false): object|array|false {} + +/** + * @psalm-taint-sink extract $array + */ +function extract(array &$array, int $flags = EXTR_OVERWRITE, string $prefix = ""): int {} diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 0e3a935bd78..2f719131f78 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -185,30 +185,30 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Serializable, Coun public function __construct($array = array(), $flags = 0) { } /** - * @param TKey $index The offset being checked. + * @param TKey $offset The offset being checked. * @return bool true if the offset exists, otherwise false */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** - * @param TKey $index The offset to get the value from. + * @param TKey $offset The offset to get the value from. * @return TValue|null The value at offset index, null when accessing invalid indexes * @psalm-ignore-nullable-return */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** - * @param TKey $index The index to set for. - * @param TValue $newval The new value to store at the index. + * @param TKey $offset The index to set for. + * @param TValue $value The new value to store at the index. * @return void */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** - * @param TKey $index The offset to unset. + * @param TKey $offset The offset to unset. * @return void */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * @param TValue $value The value to append. diff --git a/stubs/SPL.phpstub b/stubs/SPL.phpstub index 288ceba770a..7a623c933a0 100644 --- a/stubs/SPL.phpstub +++ b/stubs/SPL.phpstub @@ -15,14 +15,14 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa /** * Add/insert a new value at the specified index * - * @param int $index The index where the new value is to be inserted. - * @param TValue $newval The new value for the index. + * @param int $offset The index where the new value is to be inserted. + * @param TValue $value The new value for the index. * @return void * * @link https://php.net/spldoublylinkedlist.add * @since 5.5.0 */ - public function add($index, $newval) {} + public function add($offset, $value) {} /** * Pops a node from the end of the doubly linked list @@ -107,49 +107,49 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa public function isEmpty() {} /** - * Returns whether the requested $index exists + * Returns whether the requested $offset exists * @link https://php.net/manual/en/spldoublylinkedlist.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.3.0 */ - public function offsetExists($index) {} + public function offsetExists($offset) {} /** - * Returns the value at the specified $index + * Returns the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetget.php * - * @param int $index The index with the value. + * @param int $offset The index with the value. * @return TValue The value at the specified index. * * @since 5.3.0 */ - public function offsetGet($index) {} + public function offsetGet($offset) {} /** - * Sets the value at the specified $index to $newval + * Sets the value at the specified $offset to $value * @link https://php.net/manual/en/spldoublylinkedlist.offsetset.php * - * @param int $index The index being set. - * @param TValue $newval The new value for the index. + * @param int $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.3.0 */ - public function offsetSet($index, $newval) {} + public function offsetSet($offset, $value) {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetunset.php * - * @param int $index The index being unset. + * @param int $offset The index being unset. * @return void * * @since 5.3.0 */ - public function offsetUnset($index) {} + public function offsetUnset($offset) {} /** * Return current array entry @@ -297,46 +297,46 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * Returns whether the specified index exists * @link https://php.net/manual/en/splfixedarray.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, and false otherwise. * * @since 5.3.0 */ - public function offsetExists(int $index): bool {} + public function offsetExists(int $offset): bool {} /** * Sets a new value at a specified index * @link https://php.net/manual/en/splfixedarray.offsetset.php * - * @param int $index The index being sent. - * @param TValue $newval The new value for the index + * @param int $offset The index being sent. + * @param TValue $value The new value for the index * @return void * * @since 5.3.0 */ - public function offsetSet(int $index, $newval): void {} + public function offsetSet(int $offset, $value): void {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/splfixedarray.offsetunset.php * - * @param int $index The index being unset + * @param int $offset The index being unset * @return void * * @since 5.3.0 */ - public function offsetUnset(int $index): void {} + public function offsetUnset(int $offset): void {} /** * Returns the value at the specified index * @link https://php.net/manual/en/splfixedarray.offsetget.php * - * @param int $index The index with the value + * @param int $offset The index with the value * @return TValue The value at the specified index * * @since 5.3.0 */ - public function offsetGet(int $index) {} + public function offsetGet(int $offset) {} } diff --git a/stubs/extensions/dom.phpstub b/stubs/extensions/dom.phpstub index 76563f18f01..866d5dbfa07 100644 --- a/stubs/extensions/dom.phpstub +++ b/stubs/extensions/dom.phpstub @@ -979,8 +979,8 @@ class DOMXPath public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} /** - * @return DOMNodeList|false * @psalm-taint-sink xpath $expression + * @return DOMNodeList|false */ public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {} diff --git a/stubs/extensions/simplexml.phpstub b/stubs/extensions/simplexml.phpstub index d2501f62096..66b67530cf1 100644 --- a/stubs/extensions/simplexml.phpstub +++ b/stubs/extensions/simplexml.phpstub @@ -69,6 +69,8 @@ class SimpleXMLElement implements Traversable, Countable public function count(): int {} public function __get(string $name): SimpleXMLElement|SimpleXMLIterator|null {} + + public function __set(string $name, $value): void {} } /** diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index cdd002de207..3e254899342 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1365,6 +1365,20 @@ function f(): array } EOT, ], + 'validArrayKeyAlias' => [ + 'code' => ' + */ + class Foo {}', + 'assertions' => [], + ], ]; } @@ -1383,7 +1397,15 @@ public function barBar() { }', 'error_message' => 'MissingDocblockType', ], - + 'invalidArrayKeyType' => [ + 'code' => ' $arg + * @return void + */ + function foo($arg) {}', + 'error_message' => 'InvalidDocblock', + ], 'invalidClassMethodReturnBrackets' => [ 'code' => ' "array{b: 'value'}", ], ], + 'possiblyUndefinedArrayOffsetKeyedArray' => [ + 'code' => ' [ + '$x===' => '\'a\'', + ], + 'ignored_issues' => ['PossiblyUndefinedArrayOffset'], + ], 'domNodeListAccessible' => [ 'code' => ' [], 'ignored_issues' => ['MixedArgument', 'MixedArrayOffset', 'MissingParamType'], ], - 'suppressPossiblyUndefinedStringArrayOffet' => [ + 'suppressPossiblyUndefinedStringArrayOffset' => [ 'code' => ' 'InvalidArrayOffset', ], - 'possiblyUndefinedIntArrayOffet' => [ + 'possiblyUndefinedIntArrayOffset' => [ 'code' => ' 'PossiblyUndefinedArrayOffset', ], - 'possiblyUndefinedStringArrayOffet' => [ + 'possiblyUndefinedStringArrayOffset' => [ 'code' => ' 0.5, "b" => 1.5, "c" => new Exception()]);', 'error_message' => 'InvalidArgument', ], + 'possiblyUndefinedArrayOffsetKeyedArray' => [ + 'code' => ' 'PossiblyUndefinedArrayOffset', + ], ]; } } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 990a3bccbe9..343b86506b2 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -36,6 +36,45 @@ public function testConditionalAssignment(): void public function providerValidCodeParse(): iterable { return [ + 'assignUnionOfLiterals' => [ + 'code' => ' [ + '$result===' => 'array{a: true, b: true}', + '$resultOpt===' => 'array{a?: true, b?: true}', + ], + ], + 'assignUnionOfLiteralsClassKeys' => [ + 'code' => ' $v) { + $vv = new $k; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => ' [ '$foo' => 'array{0: string, 1: string, 2: string}', '$bar' => 'list{int, int, int}', - '$bat' => 'non-empty-array', + '$bat' => 'array{a: int, b: int, c: int}', ], ], 'implicitStringArrayCreation' => [ @@ -981,6 +1020,7 @@ function updateArray(array $arr) : array { $a = []; foreach (["one", "two", "three"] as $key) { + $a[$key] ??= 0; $a[$key] += rand(0, 10); } @@ -1260,6 +1300,29 @@ function foo(array $arr) : string { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'constantArraySpreadWithString' => [ + 'code' => ' "a", + "b" => "b", + ]; + } + + class ChildClass extends BaseClass { + public const A = [ + ...parent::KEYS, + "c" => "c", + ]; + } + + $a = ChildClass::A;', + 'assertions' => [ + '$a===' => "array{a: 'a', b: 'b', c: 'c'}", + ], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], 'listPropertyAssignmentAfterIsset' => [ 'code' => ' ' $array - * @return non-empty-array + * @return array */ function getArray(array $array): array { if (rand(0, 1)) { @@ -2063,6 +2126,29 @@ function getQueryParams(): array '$l===' => 'non-empty-array>', ], ], + 'stringIntKeys' => [ + 'code' => ' $arg + * @return bool + */ + function foo($arg) { + foreach ($arg as $k => $v) { + if ( $k === 15 ) { + return true; + } + + if ( $k === 17 ) { + return false; + } + } + + return true; + } + + $x = ["15" => "a", 17 => "b"]; + foo($x);', + ], ]; } @@ -2437,7 +2523,8 @@ public function getThisName($offset, $weird_array): string { return $weird_array[$offset]; } }', - 'error_message' => 'InvalidArrayOffset', + 'error_message' => 'MixedArrayAccess', + 'ignored_issues' => ['InvalidDocblock'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + ], ]; } @@ -126,6 +153,34 @@ function getKeys() { ', 'error_message' => 'InvalidReturnStatement', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + 'error_message' => 'InvalidReturnStatement', + ], ]; } } diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 072779059b0..a9e6335c15a 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -295,6 +295,22 @@ class Foo 'ignored_issues' => [], 'php_version' => '8.2', ], + 'override' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'sensitiveParameter' => [ 'code' => ' ' [ + 'code' => ' [ + '$a===' => 'non-falsy-string', + ], + 'ignored_issues' => ['InvalidReturnType'], + ], 'concatenationWithNumberInWeakMode' => [ 'code' => ' [ - 'code' => ' [ 'code' => ' [ + 'code' => ') $arg + * @return void + */ + function foo($arg) {} + + /** + * @param array{a?: string}&array $cb_arg + * @return void + */ + function bar($cb_arg) {} + + foo("bar");', + ], ]; } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 61ae873cd96..7e3b5b77fde 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -19,28 +19,63 @@ public function providerValidCodeParse(): iterable return [ 'byRefUseVar' => [ 'code' => ' [ + '$testBefore===' => '123', + '$testInsideBefore===' => "'test'|123|null", + '$testInsideAfter===' => "'test'|null", + '$test===' => "'test'|123", + + '$doNotContaminate===' => '123', + ], + ], + 'byRefUseSelf' => [ + 'code' => ' [ + 'code' => ' [ 'code' => ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided', ], - 'closureByRefUseToMixed' => [ - 'code' => ' 'MixedReturnStatement', - ], 'noCrashWhenComparingIllegitimateCallable' => [ 'code' => 'analyzeFile('somefile.php', new Context); self::assertSame(0, IssueBuffer::getErrorCount()); } + /** + * @test + */ + public function addingCodeIssueIsMarkedAsRedundant(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('UnusedPsalmSuppress'); + + $this->addFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + 'getIssue(); + if ($issue->code_location->file_path !== (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php') { + return null; + } + if ($issue instanceof InvalidReturnStatement && $event->isFixable() === false) { + return false; + } elseif ($issue instanceof InvalidReturnType && $event->isFixable() === true) { + return false; + } + return null; + } + }; + + (new PluginRegistrationSocket($this->codebase->config, $this->codebase)) + ->registerHooksFromClass(get_class($eventHandler)); + + $this->analyzeFile( + (string) getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php', + new Context, + ); + } } diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 34a5b06f976..6757b43088f 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -19,6 +19,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\VersionUtils; use Psalm\Issue\TooManyArguments; use Psalm\Issue\UndefinedFunction; use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin; @@ -60,11 +61,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/Config/PluginListTest.php b/tests/Config/PluginListTest.php index 5ca98ab0ef8..7083f0891a2 100644 --- a/tests/Config/PluginListTest.php +++ b/tests/Config/PluginListTest.php @@ -20,11 +20,11 @@ class PluginListTest extends TestCase { use MockeryPHPUnitIntegration; - private ConfigFile&MockInterface $config_file; + private MockInterface $config_file; - private Config&MockInterface $config; + private MockInterface $config; - private ComposerLock&MockInterface $composer_lock; + private MockInterface $composer_lock; public function setUp(): void { diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 93f2eb95bb3..6127135342b 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -14,6 +14,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterEveryFunctionCallAnalysisInterface; @@ -48,11 +49,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/DeprecatedAnnotationTest.php b/tests/DeprecatedAnnotationTest.php index aa3a169cf34..bb376442527 100644 --- a/tests/DeprecatedAnnotationTest.php +++ b/tests/DeprecatedAnnotationTest.php @@ -101,6 +101,24 @@ class A { public $property; } '], + 'suppressDeprecatedClassOnTemplateType' => [ + 'code' => ' + * @psalm-suppress DeprecatedClass + */ + class TheChildClass extends TheParentClass {} + '], ]; } diff --git a/tests/ErrorBaselineTest.php b/tests/ErrorBaselineTest.php index 584748d418e..3f37112e3b4 100644 --- a/tests/ErrorBaselineTest.php +++ b/tests/ErrorBaselineTest.php @@ -23,7 +23,7 @@ class ErrorBaselineTest extends TestCase { use MockeryPHPUnitIntegration; - private FileProvider&MockInterface $fileProvider; + private MockInterface $fileProvider; public function setUp(): void { diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index a9599e5e9e9..cb50eb8675b 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -515,19 +515,41 @@ function foo($s): array { ], 'extractVarCheck' => [ 'code' => ' "bar"]; + $foo = "foo"; + $a = getUnsealedArray(); extract($a); takesString($foo);', 'assertions' => [], 'ignored_issues' => [ - 'MixedAssignment', - 'MixedArrayAccess', 'MixedArgument', ], ], + 'extractVarCheckValid' => [ + 'code' => ' 15]; + extract($a); + takesInt($foo);', + ], + 'extractSkipExtr' => [ + 'code' => ' "x", "b" => "y"], EXTR_SKIP);', + 'assertions' => [ + '$a===' => '1', + '$b===' => '\'y\'', + ], + ], 'compact' => [ 'code' => ' 'string', ], ], + 'filterInput' => [ + 'code' => ' ["default" => null]]); + } + function filterIntWithDefault(string $s) : int { + return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => 5]]); + } + function filterBool(string $s) : bool { + return filter_var($s, FILTER_VALIDATE_BOOLEAN); + } + function filterNullableBool(string $s) : ?bool { + return filter_var($s, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + } + function filterNullableBoolWithFlagsArray(string $s) : ?bool { + return filter_var($s, FILTER_VALIDATE_BOOLEAN, ["flags" => FILTER_NULL_ON_FAILURE]); + } + function filterFloat(string $s) : float { + $filtered = filter_var($s, FILTER_VALIDATE_FLOAT); + if ($filtered === false) { + return 0.0; + } + return $filtered; + } + function filterFloatWithDefault(string $s) : float { + return filter_var($s, FILTER_VALIDATE_FLOAT, ["options" => ["default" => 5.0]]); + }', + ], 'filterVar' => [ 'code' => ' [ 'code' => ' [ @@ -2083,7 +2140,7 @@ function foo(A $a1, A $a2 = null): void 'strposFirstParamAllowClassString' => [ 'code' => ' [ @@ -2354,12 +2411,35 @@ function fooFoo(int $a): void {} fooFoo("string");', 'error_message' => 'InvalidScalarArgument', ], + 'invalidArgumentCallableWithoutArgsUnion' => [ + 'code' => ' 'InvalidArgument', + ], 'invalidArgumentWithDeclareStrictTypes' => [ 'code' => ' 'InvalidArgument', ], + 'invalidArgumentFalseTrueExpected' => [ + 'code' => ' 'InvalidArgument', + ], 'builtinFunctioninvalidArgumentWithWeakTypes' => [ 'code' => ' [], 'php_version' => '8.1', ], + 'extractVarCheckInvalid' => [ + 'code' => ' 15]; + extract($a); + takesInt($foo);', + 'error_message' => 'InvalidScalarArgument', + ], ]; } diff --git a/tests/ImmutableAnnotationTest.php b/tests/ImmutableAnnotationTest.php index 11e8313932c..5755578194f 100644 --- a/tests/ImmutableAnnotationTest.php +++ b/tests/ImmutableAnnotationTest.php @@ -302,7 +302,7 @@ public function getError(): ?string { $dto = new DTO("BOOM!"); - if ($dto->getError()) { + if ($dto->getError() !== null) { takesString($dto->getError()); }', ], @@ -766,6 +766,81 @@ public function getShortMutating() : string { }', 'error_message' => 'ImpurePropertyAssignment', ], + 'readonlyByRefInClass' => [ + 'code' => 'values = $values; + } + + /** + * @return mixed + */ + public function bar() + { + return reset($this->values); + } + }', + 'error_message' => 'InaccessibleProperty', + ], + 'readonlyByRef' => [ + 'code' => 'values = $values; + } + } + + $x = new Foo([]); + reset($x->values);', + 'error_message' => 'InaccessibleProperty', + ], + 'readonlyByRefCustomFunction' => [ + 'code' => 'values = $values; + } + } + + /** + * @param string $a + * @param array $b + * @return void + */ + function bar($a, &$b) {} + + $x = new Foo([]); + bar("hello", $x->values);', + 'error_message' => 'InaccessibleProperty', + ], 'preventUnset' => [ 'code' => ' [ 'code' => ' 1, 'message' => 'Operand of type non-falsy-string is always truthy', - 'line' => 4, + 'line' => 5, 'error' => '$b', ], ]; diff --git a/tests/KeyOfArrayTest.php b/tests/KeyOfArrayTest.php index efd7cf66fff..8ae17ab91c1 100644 --- a/tests/KeyOfArrayTest.php +++ b/tests/KeyOfArrayTest.php @@ -86,11 +86,11 @@ function getKey() { 'keyOfUnionArrayLiteral' => [ 'code' => '|array> + * @return key-of|array> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asString) { + if ($asString) { + return "42"; } return 42; } @@ -194,14 +194,14 @@ public function getKey() { ', 'error_message' => 'InvalidReturnStatement', ], - 'noStringAllowedInKeyOfIntFloatArray' => [ + 'noStringAllowedInKeyOfIntFloatStringArray' => [ 'code' => '|array> + * @return key-of|array<"42.0", string>> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asInt) { + if ($asInt) { + return 42; } return "42"; } diff --git a/tests/Loop/DoTest.php b/tests/Loop/DoTest.php index 536ff35670a..a6d4552fd38 100644 --- a/tests/Loop/DoTest.php +++ b/tests/Loop/DoTest.php @@ -247,7 +247,7 @@ function bar(?string &$i) : void {} $c = null; do { - if (!$c) { + if ($c === null || $c === "" || $c === "0") { foo($c); } else { bar($c); diff --git a/tests/Loop/ForeachTest.php b/tests/Loop/ForeachTest.php index 125be719e4d..cd6d00358d5 100644 --- a/tests/Loop/ForeachTest.php +++ b/tests/Loop/ForeachTest.php @@ -1029,9 +1029,7 @@ function foo() : void { $arr = []; foreach ([1, 2, 3] as $i) { - if (!isset($arr[$i]["a"])) { - $arr[$i]["a"] = 0; - } + $arr[$i]["a"] ??= 0; $arr[$i]["a"] += 5; } diff --git a/tests/Loop/WhileTest.php b/tests/Loop/WhileTest.php index 4976bac05b6..c38420d5a21 100644 --- a/tests/Loop/WhileTest.php +++ b/tests/Loop/WhileTest.php @@ -157,7 +157,7 @@ function foo(): ?A { } while ($a = foo()) { - if ($a->bar) {} + if ($a->bar !== null) {} }', ], 'whileTrueWithBreak' => [ @@ -273,7 +273,7 @@ function bar(?string &$i) : void {} $c = null; while (rand(0, 1)) { - if (!$c) { + if ($c === null || $c === "" || $c === "0") { foo($c); } else { bar($c); diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index ef3bcca14e4..f906f35d35f 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -1235,7 +1235,7 @@ interface B extends A {} ', 'error_message' => 'ImplementedParamTypeMismatch', ], - 'MagicMethodMadeConcreteChecksParams' => [ + 'SKIPPED-MagicMethodMadeConcreteChecksParams' => [ 'code' => ' [], 'php_version' => '8.0', ], + 'nullCoalesce' => [ + 'code' => <<<'PHP' + null, + true => 1, + } ?? 2; + PHP, + 'assertions' => [ + '$match' => 'int', + ], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 4524962d8c0..314f2401f5f 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -151,7 +151,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ); @@ -187,7 +187,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ); @@ -955,7 +955,7 @@ final public function getA() { $a = new A(); - if ($a->getA()) { + if ($a->getA() !== null) { echo strlen($a->getA()); }', ], @@ -1026,7 +1026,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ], @@ -1050,7 +1050,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', ], @@ -1650,7 +1650,7 @@ function getA() { } function foo(A $a) : void { - if ($a->getA()) { + if ($a->getA() !== null) { echo strlen($a->getA()); } } @@ -1716,7 +1716,7 @@ function printInt(int $int): void { $obj = new SomeClass(); - if ($obj->getInt()) { + if ($obj->getInt() !== null) { printInt($obj->getInt()); }', 'error_message' => 'PossiblyNullArgument', diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 2446c5c65c0..970a5d7d8b7 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -313,7 +313,7 @@ public function foo(string $s): ?string { class B extends A { public function foo(?string $s): string { - return $s ?: "hello"; + return $s !== null ? $s : "hello"; } } @@ -329,7 +329,7 @@ public function foo(string $s): string { class B extends A { public function foo(string $s = null): string { - return $s ?: "hello"; + return $s !== null ? $s : "hello"; } } @@ -1058,7 +1058,7 @@ public function fooFoo(int $a, bool $c): void { 'code' => 'getX()) { + if (is_int($x->getX())) { XCollector::modify(); if ($x->getX() === null) {} } @@ -223,7 +223,7 @@ public function getX() : ?int { } function testX(X $x): void { - if ($x->getX()) { + if ($x->getX() !== null) { XCollector::modify(); if ($x->getX() === null) {} } @@ -257,7 +257,7 @@ public function __construct(?int $x) { } function testX(X $x): void { - if ($x->x) { + if ($x->x !== null) { XCollector::modify(); if ($x->x === null) {} } @@ -688,6 +688,8 @@ class A { } echo substr($a->aa, 1);', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'nullableStaticPropertyWithIfCheck' => [ 'code' => ' 'UndefinedPropertyAssignment', ], - 'setPropertiesOfSimpleXMLElement1' => [ - 'code' => '"); - $a->b = "c"; - ', - 'error_message' => 'UndefinedPropertyAssignment', - ], - 'setPropertiesOfSimpleXMLElement2' => [ - 'code' => '"); - if (isset($a->b)) { - $a->b = "c"; - } - ', - 'error_message' => 'UndefinedPropertyAssignment', - ], ]; } } diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index 5ae7048cabe..bf575406a98 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -448,6 +448,81 @@ function gimmeFoo(): MyEnum return MyEnum::FOO(); }', ], + 'pureThroughCallStaticInTrait' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => 'getString());', ], - 'makeByRefUseMixed' => [ - 'code' => ' [], - 'ignored_issues' => ['MixedArgument'], - ], 'assignByRefToMixed' => [ 'code' => ' [ 'name' => 'Psalm', 'informationUri' => 'https://psalm.dev', - 'version' => '4.0.0', + 'version' => VersionUtils::getPsalmVersion(), 'rules' => [ [ 'id' => '246', @@ -712,6 +713,7 @@ public function testJsonReport(): void $issue_data = [ [ + 'link' => 'https://psalm.dev/024', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -727,13 +729,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => -1, 'shortcode' => 24, - 'link' => 'https://psalm.dev/024', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/138', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -749,13 +751,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => 1, 'shortcode' => 138, - 'link' => 'https://psalm.dev/138', + 'error_level' => 1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/020', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, @@ -771,13 +773,13 @@ public function testJsonReport(): void 'snippet_to' => 172, 'column_from' => 6, 'column_to' => 15, - 'error_level' => -1, 'shortcode' => 20, - 'link' => 'https://psalm.dev/020', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/126', 'severity' => 'info', 'line_from' => 17, 'line_to' => 17, @@ -793,9 +795,8 @@ public function testJsonReport(): void 'snippet_to' => 277, 'column_from' => 6, 'column_to' => 8, - 'error_level' => 3, 'shortcode' => 126, - 'link' => 'https://psalm.dev/126', + 'error_level' => 3, 'taint_trace' => null, 'other_references' => null, ], diff --git a/tests/ReturnTypeProvider/BasenameTest.php b/tests/ReturnTypeProvider/BasenameTest.php index 701255887e0..6e61ffea06a 100644 --- a/tests/ReturnTypeProvider/BasenameTest.php +++ b/tests/ReturnTypeProvider/BasenameTest.php @@ -34,5 +34,25 @@ public function providerValidCodeParse(): iterable '$base===' => 'string', ], ]; + + yield 'basenameOfStringPathReturnsNonEmptyString' => [ + 'code' => ' [ + '$base===' => 'non-empty-string', + ], + ]; + + yield 'basenameOfStringPathReturnsNonFalsyString' => [ + 'code' => ' [ + '$base===' => 'non-falsy-string', + ], + ]; } } diff --git a/tests/ReturnTypeProvider/DirnameTest.php b/tests/ReturnTypeProvider/DirnameTest.php index 894bd1c1c6d..029f91d0a8c 100644 --- a/tests/ReturnTypeProvider/DirnameTest.php +++ b/tests/ReturnTypeProvider/DirnameTest.php @@ -51,7 +51,27 @@ public function providerValidCodeParse(): iterable $dir = dirname(uniqid() . "abc", 2); ', 'assertions' => [ - '$dir===' => 'non-empty-string', + '$dir===' => 'non-falsy-string', + ], + ]; + + yield 'dirnameOfNonEmptyShouldBeNonFalsy' => [ + 'code' => ' [ + '$dir===' => 'non-falsy-string', + ], + ]; + + yield 'dirnameOfEmptyShouldBeString' => [ + 'code' => ' [ + '$dir===' => 'string', ], ]; } diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 0cfc5f52c89..842a79bdb37 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -102,6 +102,8 @@ public function barBar($str) { return $str; } }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'returnTypeNotEmptyCheckInElseIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'returnTypeNotEmptyCheckInElse' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'returnTypeAfterIf' => [ 'code' => ' 'TaintedSleep', ], + 'taintedExtract' => [ + 'code' => ' 'TaintedExtract', + ], + 'extractPost' => [ + 'code' => ' 'TaintedExtract', + ], ]; } diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index eb0d6fd9516..597dd15a6be 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -2880,6 +2880,8 @@ private static function test( ): void { } }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'noCrashTemplateInsideGenerator' => [ 'code' => ' 'string', ], ], + 'InheritFuncNumArgs' => [ + 'code' => ' [ 'code' => ' [], 'php_version' => '7.2', ], + 'ineritedreturnTypeBasedOnPhpVersionId' => [ + 'code' => ' ? string : int) + */ + function getSomething() + { + return mt_rand(1, 10) > 5 ? "a value" : 42; + } + + /** + * @psalm-return (PHP_VERSION_ID is int<70100, max> ? string : int) + */ + function getSomethingElse() + { + return mt_rand(1, 10) > 5 ? "a value" : 42; + } + } + + class B extends A {} + + $class = new B(); + $something = $class->getSomething(); + $somethingElse = $class->getSomethingElse(); + ', + 'assertions' => [ + '$something' => 'int', + '$somethingElse' => 'string', + ], + 'ignored_issues' => [], + 'php_version' => '7.2', + ], 'ineritedConditionalTemplatedReturnType' => [ 'code' => ' [ + 'code' => ' $v + */ + function a(array $v): void {}', + ], 'typeAliasBeforeClass' => [ 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + '$x===' => 'non-falsy-string', + ], + ], ]; } diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 6ed17a19da3..65a15193ec7 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -48,6 +48,28 @@ function three(array $a): void { echo $a["a"]; echo $a["b"]; }', + ], + 'arrayKeyExistsNegation' => [ + 'code' => ' */ + } + ', + ], + 'arrayKeyExistsNoSideEffects' => [ + 'code' => ' */ + } + ', ], 'arrayKeyExistsTwice' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'assignmentInIf' => [ 'code' => ' 5) { - } elseif (($a = rand(0, 1) ? new A : null) && $a->foo) {}', + } elseif (($a = rand(0, 1) ? new A : null) && is_string($a->foo)) {}', ], 'noParadoxAfterConditionalAssignment' => [ 'code' => ' 'InvalidReturnStatement', - 'ignored_issues' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], 'php_version' => '8.0', ], 'assignmentInBranchOfAndReferencedAfterIf' => [ diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 3c6b9975432..6de140f62a5 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -40,6 +40,32 @@ function foo($a): void { if ($b === $a) { } }', ], + 'nonStrictConditionTruthyFalsyNoOverlap' => [ + 'code' => ' [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'numericStringAssertion' => [ 'code' => ' [ + 'code' => ' [ + 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => '|false|string */ - function foo() : array { + function foo() { return filter_input(INPUT_POST, "some_var") ?? []; }', ], @@ -1603,7 +1678,7 @@ function foo(string $a, string $b) : void { 'code' => ' $arr - * @return non-empty-array + * @return array */ function foo(array $arr) : array { if (isset($arr["a"])) { @@ -1879,6 +1954,8 @@ function foo(?string $a) : void { if ($a && strlen($a) > 5) {} } }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'arrayUnionTypeSwitching' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'propertySetOnElementInConditional' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'issetAssertionOnStaticProperty' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'nonEmptyStringAfterLiteralCheck' => [ 'code' => ' 'TypeDoesNotContainType', ], + 'nonStrictConditionTruthyFalsy' => [ + 'code' => ' 'RiskyTruthyFalsyComparison', + ], + 'nonStrictConditionTruthyFalsyNegated' => [ + 'code' => ' 'RiskyTruthyFalsyComparison', + ], + 'nonStrictConditionTruthyFalsyFuncCall' => [ + 'code' => ' 'RiskyTruthyFalsyComparison', + ], + 'nonStrictConditionTruthyFalsyFuncCallNegated' => [ + 'code' => ' 'RiskyTruthyFalsyComparison', + ], 'redundantConditionForNonEmptyString' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'emptyExceptionReconciliationAfterIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'alwaysBoolResult' => [ 'code' => ' [], - 'ignored_issues' => ['MixedAssignment', 'MissingParamType', 'MixedArgument'], + 'ignored_issues' => ['MixedAssignment', 'MissingParamType', 'MixedArgument', 'RiskyTruthyFalsyComparison'], ], 'multipleEmptiesInCondition' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'reconcileEmptyTwiceWithoutReturn' => [ 'code' => ' 'string', ], ], + 'emptyLiteralTrueFalse' => [ + 'code' => ' [ + '$x===' => 'true', + ], + ], ]; } @@ -722,6 +736,30 @@ function nonEmptyString(string $str): string { }', 'error_message' => 'LessSpecificReturnStatement', ], + 'impossibleEmptyOnFalsyFunctionCall' => [ + 'code' => ' 'DocblockTypeContradiction', + ], + 'redundantEmptyOnFalsyFunctionCall' => [ + 'code' => ' 'RedundantConditionGivenDocblockType', + ], ]; } } diff --git a/tests/TypeReconciliation/IssetTest.php b/tests/TypeReconciliation/IssetTest.php index e4485c79012..a5ac9393271 100644 --- a/tests/TypeReconciliation/IssetTest.php +++ b/tests/TypeReconciliation/IssetTest.php @@ -43,6 +43,50 @@ public function providerValidCodeParse(): iterable 'assertions' => [], 'ignored_issues' => ['MixedArrayAccess'], ], + 'issetWithArrayAssignment' => [ + 'code'=> ' [ + 'code'=> ' [ + 'code'=> ' [ 'code' => ' [], - 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'], + 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess', 'RiskyTruthyFalsyComparison'], ], 'mixedArrayIssetGetStringVar' => [ 'code' => ' [], + 'ignored_issues' => [ + 'RiskyTruthyFalsyComparison', + ], ], 'noRedundantConditionAfterAssignment' => [ 'code' => 'foo) {} + if ($i->foo !== null) {} break; default: @@ -182,7 +186,7 @@ function makeA() { } if ($a) {}', 'assertions' => [], - 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'], + 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess', 'RiskyTruthyFalsyComparison'], ], 'noComplaintWithIsNumericThenIsEmpty' => [ 'code' => ' [], 'ignored_issues' => ['MixedAssignment', 'MixedArrayAccess'], ], @@ -599,7 +603,7 @@ function bar(string $b) : bool { exit; } - if ($i) {}', + if ($i !== array() && $i !== "" && $i !== "0") {}', ], 'emptyWithoutKnowingArrayType' => [ 'code' => ' ' 'RedundantCondition', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'refineTypeInMethodCall' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithAllPathsReturning' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithAssignmentBeforeReturn' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'invertedTwoVarLogicNotNested' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'invertedTwoVarLogicNotNestedWithAssignmentBeforeReturn' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithElseifAndNoNegations' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'threeVarLogicNotNestedWithNoRedefinitionsWithClasses' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'threeVarLogicNotNestedAndOrWithNoRedefinitions' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithElseifCorrectlyNegatedInElseIf' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'lotsaTruthyStatements' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'cancelOutDifferentStatement' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'moreChecks' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'compareToIntInsideIfCNF' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'ternaryAssertionOnBool' => [ 'code' => ' 'NullableReturnStatement', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'invertedTwoVarLogicNotNestedWithElseif' => [ 'code' => ' 'NullableReturnStatement', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'threeVarLogicWithElseifAndAnd' => [ 'code' => ' 'TypeDoesNotContainType', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'twoVarLogicNotNestedWithElseifIncorrectlyReinforcedInIf' => [ 'code' => ' 'RedundantCondition', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'repeatedIfStatements' => [ 'code' => ' 'TypeDoesNotContainType', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'repeatedConditionals' => [ 'code' => ' 'RedundantCondition', + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'dependentTypeInvalidated' => [ 'code' => ' [ - 'code' => ' [ 'code' => ' [ @@ -130,10 +130,10 @@ function foo(string $a): void { 'dummyByRefVar' => [ 'code' => 'getMessage(); } - if ($s) {} + if ($s !== null) {} }', ], 'throwWithMessageCallAndAssignmentInCatchAndReference' => [ @@ -852,7 +852,6 @@ function returnFoo(): Foo { /** @psalm-suppress UnusedParam */ function foo(callable $c) : void {} $listener = function () use (&$listener) : void { - /** @psalm-suppress MixedArgument */ foo($listener); }; foo($listener);', @@ -876,7 +875,6 @@ function bar(string $type) : ArrayObject { $i = 1; }; $a(); - /** @psalm-suppress MixedArgument */ echo $i;', ], 'regularVariableClosureUseInAddition' => [ @@ -944,7 +942,7 @@ function foo() : void { if ($foo) {} } catch (Exception $e) {} - if ($foo) {}', + if ($foo !== false && $foo !== 0) {}', ], 'useTryAssignedVariableInsideFinally' => [ 'code' => ' [ 'code' => ' [], - 'ignored_issues' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], 'php_version' => '8.0', ], 'concatWithUnknownProperty' => [ @@ -3172,7 +3166,7 @@ function bar() : void { $user = $user_id; } - if ($user) { + if ($user !== null && $user !== 0) { $a = 0; for ($i = 1; $i <= 10; $i++) { $a += $i; @@ -3192,7 +3186,7 @@ function bar() : void { $user = $user_id; } - if ($user) { + if ($user !== null && $user !== 0) { $a = 0; foreach ([1, 2, 3] as $i) { $a += $i;